SpringCloud全系列(一): OpenFeign原理解析

目录

1、背景

2、Spring Cloud OpenFeign用法举例

3、Spring Cloud OpenFeign“魔法”揭秘

1. @FeignClient 如何根据接口生成实现类的?

2. 生成的实现(代理)类是如何适配各种HTTP组件的?

3. 生成的实现(代理)类如何实现HTTP请求应答序列化和反序列化的?

4. 生成的实现(代理)类是如何注入到Spring容器中的?

4、Spring Cloud OpenFeign不仅如此

5、总结


1、背景

之前未曾想过会有全面拥抱Spring Cloud的一天,这是我来腾讯后写的第一篇博客,也是第一篇关于Spring Cloud体系的博客,废话少说,我们进入主题。

我们知道HTTP的重要性,也知道开发Web服务端还是很爽的,Spring MVC 或 Spring WebFlux 一套拳打完直接收工即可,但作为Web服务的调用方(这里主要指服务端调用服务端),由于需要熟悉各种(OkHttp、HttpClient)组件的使用,而且还要显示的序列化和反序列化,很多样板代码,写起来很痛苦,瞬间不香了,还是Dubbo好啊。

于是,Feign诞生了!!!Feign直译过来是“假装、捏造”的意思,而OpenFeign(https://github.com/OpenFeign/feign,本文同Feign)按照官方的解释是受Retrofit,JAXRS-2.0和WebSocket启发的Java版的HTTP客户端绑定程序。Feign的第一个目标是减少HTTP API的复杂性,希望能将HTTP调用做到像RPC一样易用。而Spring Cloud OpenFeign(https://github.com/spring-cloud/spring-cloud-openfeign)将其和Spring Cloud体系打通,让Feign更加方便的在Spring Cloud中使用。

2、Spring Cloud OpenFeign用法举例

这里先给出Spring Cloud OpenFeign官方“栗子”:https://github.com/spring-cloud-samples/feign-eureka ,说实话,这个官方例子写的很简单,服务端和客户端各一个类,所有东西都揉在一起了,反而不好理解,个人推荐腾讯云的TSF(腾讯微服务平台)的demo(https://github.com/tencentyun/tsf-simple-demo/tree/release/1.23.0-greenwich/consumer-demo)来看Spring Cloud OpenFeign的用法,我们只看consumer-demo模块即可,consumer-demo演示了如何像调用本地服务一样调用Web服务,这里我们关注其中的 ProviderDemoService 接口,如下图:

SpringCloud全系列(一): OpenFeign原理解析_第1张图片

我们可以把 ProviderDemoService 当做由Web服务端(后面简称Provider端)定义好的API来提供,HTTP客户端(后面简称Consumer端)只需要依赖该API就可以使用该接口(这里TSF官方例子为了简单起见,ProviderDemoService 直接在Consumer端定义)有了 ProviderDemoService,我们在Consumer端借助Spring容器,即可像使用本地接口一样调用远程HTTP服务,是不是很爽?那 ProviderDemoService 到底特殊在哪里?是因为有 @FeignClient !!!如何做到的后面会讲解,我们来看在Spring容器中如何使用 ProviderDemoService , 这里通过控制器 ConsumerController 来演示 ProviderDemoService 的使用 :

SpringCloud全系列(一): OpenFeign原理解析_第2张图片

没错,我们就像普通的Spring Bean一样使用 ProviderDemoService 即可,就是这么神奇!!!我们发现其实在这个接口中还使用了 @RequestMapping 用于标识该方法所对应的HTTP请求路径,这也是保持了Spring Web的传统用法,而在原生的Feign中使用的是 @RequestLine

对了,这里插一句,我们还需要在启动类中使用 @EnableFeignClients 来给Spring Boot应用开启Feign功能!!!

SpringCloud全系列(一): OpenFeign原理解析_第3张图片

3、Spring Cloud OpenFeign“魔法”揭秘

Feign只是对HTTP调用组件进行了易用性封装,底层还是使用我们常见的OkHttp、HttpClient等组件(我们不生产水,我们只是水的搬运工),你瞧:

Feign的目标之一就让这些HTTP客户端更好用,使用方式更统一(这和Spring出现的目的如出一辙),更像RPC。要想了解Spring Cloud OpenFeign整体实现原理,通过之前使用方式的介绍,我们需要回答如下四个问题:

  1. @FeignClient 如何根据接口生成实现(代理)类的?
  2. 生成的实现(代理)类是如何适配各种HTTP组件的?
  3. 生成的实现(代理)类如何实现HTTP请求应答序列化和反序列化的?
  4. 生成的实现(代理)类是如何注入到Spring容器中的?

接下来,我们通过解读源码方式,逐一解答上述问题。

1. @FeignClient 如何根据接口生成实现类的?

我们先来回顾下常见的Java字节码库类,包括JDK自带的动态代理类、以及Spring中用到的CGLib(依赖ASM)、ASM等,还有Dubbo中使用过的Javassist等,如上所示,Feign使用必须要有接口(该接口),满足JDK动态代理的使用条件,所以Feign使用的就是JDK自带的动态代理技术。这里我们已经给出了答案,但还有一个小问题,我们回看上面提到的 ProviderDemoService 接口,里面有多个方法,每个方法有 @RequestMapping ,意味着这些方法可以映射到不同的远端HTTP路径,所以给整个 ProviderDemoService 接口做代理时,代理类的方法必须知道对应到哪个远端HTTP路径,虽然我们可以在 java.lang.reflect.InvocationHandler#invoke 的方法入参 Method 中去解析 @RequestMapping 拿url(注意,大多数开源框架很忌讳在运行时高频使用JDK的反射,这样影响执行效率,Dubbo的Provider端也不是用反射来调用本地方法的),所以在Feign使用JDK动态代理技术时,需要提前将接口(例如ProviderDemoService)带 @RequestMapping 方法解析出来。为了探究这块的具体实现,我们移步原生Feign的feign-core包(上面已经给出过Feign的GitHub地址)的核心类ReflectiveFeign

package feign;

import feign.InvocationHandlerFactory.MethodHandler;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.LinkedHashMap;
import java.util.Map;

import static feign.Util.checkNotNull;

public class ReflectiveFeign extends Feign {

    private final InvocationHandlerFactory factory;

    /**
     * creates an api binding to the {@code target}. As this invokes reflection, care should be taken
     * to cache the result.
     * 注意:这里我们隐藏了大部分非核心的代码
     */
    @Override
    public  T newInstance(Target target) {
        // 将@FeignClient的接口类所有带@RequestMapping方法解析出来,map的key为方法签名,MethodHander为包装过的方法调用Hander
        Map nameToHandler = targetToHandlersByName.apply(target);
        Map methodToHandler = new LinkedHashMap();

        // 根据nameToHandler来组装methodToHandler
        for (Method method : target.type().getMethods()) {
            methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
        }

        // 这里通过一个InvocationHandler工厂来创建JDK动态代理中的InvocationHandler(既下面的FeignInvocationHandler)
        InvocationHandler handler = factory.create(target, methodToHandler);

        // 创建JDK动态代理生成代理类,这个类在Spring Cloud OpenFeign中会被注册到Spring容器中
        T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
                new Class[]{target.type()}, handler);

        return proxy;
    }

    /**
     * Feign专用FeignInvocationHandler
     */
    static class FeignInvocationHandler implements InvocationHandler {

        private final Target target;
        // 这里保持的是我们在newInstance解析出来的@RequestMapping(在原生Feign中是@RequestLine)方法和方法处理器的映射关系
        private final Map dispatch;

        FeignInvocationHandler(Target target, Map dispatch) {
            this.target = checkNotNull(target, "target");
            this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            // 常规的用于AOP的动态代理会选择调用target的method方法,但我们这里由于没有自定义的接口实现类,所以直接调用我们包装过的对应MethodHandler
            return dispatch.get(method).invoke(args);
        }
    }
}

这里顺便补充下 MethodHandler 接口的定义:

package feign;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;

/**
 * Controls reflective method dispatch.
 */
public interface InvocationHandlerFactory {

    InvocationHandler create(Target target, Map dispatch);

    /**
     * Like {@link InvocationHandler#invoke(Object, java.lang.reflect.Method, Object[])}, except for a
     * single method.
     */
    interface MethodHandler {
        Object invoke(Object[] argv) throws Throwable;
    }

    static final class Default implements InvocationHandlerFactory {
        @Override
        public InvocationHandler create(Target target, Map dispatch) {
            return new ReflectiveFeign.FeignInvocationHandler(target, dispatch);
        }
    }
}

看ReflectiveFeign的类名我们就知道它和反射有关,ReflectiveFeign借助于JDK动态代理,根据我们的业务接口生成对应的代理类,这个代理类会根据调用的方法来直接找到对应已经提前准备好的 MethodHandler,直接调用即可完成Feign的使命,根据上面的使用方法,我们不难猜到 MethodHandler 里面有HTTP调用的相关信息(这些信息之前是在接口方法定义的 @RequestMapping 或 @RequestLine 之中),而且 MethodHandler#invoke 会完成真正的HTTP调用并将结果反序列化成原接口方法的返回值对象。

2. 生成的实现(代理)类是如何适配各种HTTP组件的?

这个问题应该由Feign来回答,而不是Spring Cloud OpenFeign,Feign的feign-core模块中有一个 Client 接口,专门用来给各个HTTP组件提供接入接口(还记得Slf4j适配各种日志组件的方案吗?——SLF4J漫谈_飞向札幌的班机的博客-CSDN博客_serviceloader slf4j 和这个类似),我们看其定义:

package feign;

import feign.Request.Options;

import java.io.IOException;

/**
 * Submits HTTP {@link Request requests}. Implementations are expected to be thread-safe.
 * 注意,为了展现方便,我们裁剪了部分代码
 */
public interface Client {

    /**
     * Executes a request against its {@link Request#url() url} and returns a response.
     *
     * @param request safe to replay.
     * @param options options to apply to this request.
     * @return connected response, {@link Response.Body} is absent or unread.
     * @throws IOException on a network error connecting to {@link Request#url()}.
     */
    Response execute(Request request, Options options) throws IOException;
}

各个日志组件的适配模块(例如feign-okhttp、feign-httpclient等)只需要实现该接口就可以和Feign打通,而在原生的Feign中,选择何种HTTP组件是自己选择的,比如我们想使用OkHttpClient,在Consumer端可以这样:

public class Example {
  public static void main(String[] args) {
     String response = Feign.builder()
                     .client(new OkHttpClient())
                     .target(ProviderDemoService.class, "https://xxxx");
  }
}

Spring Cloud继承了Spring Boot的“约定优于配置”的原则,通过条件注解,实现了通过当前项目的依赖包决定使用哪个HTTP组件,详见 Spring Cloud OpenFeign中的 org.springframework.cloud.openfeign.FeignAutoConfiguration:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Feign.class)
@EnableConfigurationProperties({FeignClientProperties.class, FeignHttpClientProperties.class})
@Import(DefaultGzipDecoderConfiguration.class)
public class FeignAutoConfiguration {

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnClass(ApacheHttpClient.class)
	@ConditionalOnMissingBean(CloseableHttpClient.class)
	@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
	protected static class HttpClientFeignConfiguration {

		private final Timer connectionManagerTimer = new Timer(
			"FeignApacheHttpClientConfiguration.connectionManagerTimer", true);

		@Autowired(required = false)
		private RegistryBuilder registryBuilder;

		private CloseableHttpClient httpClient;

		@Bean
		@ConditionalOnMissingBean(HttpClientConnectionManager.class)
		public HttpClientConnectionManager connectionManager(
			ApacheHttpClientConnectionManagerFactory connectionManagerFactory,
			FeignHttpClientProperties httpClientProperties) {
			// 略
		}

		@Bean
		public CloseableHttpClient httpClient(ApacheHttpClientFactory httpClientFactory,
											  HttpClientConnectionManager httpClientConnectionManager,
											  FeignHttpClientProperties httpClientProperties) {
			// 略
		}

		@Bean
		@ConditionalOnMissingBean(Client.class)
		public Client feignClient(HttpClient httpClient) {
			return new ApacheHttpClient(httpClient);
		}

	}

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnClass(OkHttpClient.class)
	@ConditionalOnMissingBean(okhttp3.OkHttpClient.class)
	@ConditionalOnProperty("feign.okhttp.enabled")
	protected static class OkHttpFeignConfiguration {

		private okhttp3.OkHttpClient okHttpClient;

		@Bean
		@ConditionalOnMissingBean(ConnectionPool.class)
		public ConnectionPool httpClientConnectionPool(FeignHttpClientProperties httpClientProperties,
													   OkHttpClientConnectionPoolFactory connectionPoolFactory) {
			Integer maxTotalConnections = httpClientProperties.getMaxConnections();
			Long timeToLive = httpClientProperties.getTimeToLive();
			TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit();
			return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit);
		}

		@Bean
		public okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory, ConnectionPool connectionPool,
										   FeignHttpClientProperties httpClientProperties) {
			// 略
		}

		@Bean
		@ConditionalOnMissingBean(Client.class)
		public Client feignClient(okhttp3.OkHttpClient client) {
			return new OkHttpClient(client);
		}
	}
}

从上面各种复杂的条件注解来看,如果我们项目中引入了feign-httpclient包(即ApacheHttpClient),并且Spring容器还未加载CloseableHttpClient的话,哪怕没有配置“feign.httpclient.enable”,那么就会使用HttpClient,其他的HTTP组件也是类似的方式来判断和加载,这里就不在一一阐述。

3. 生成的实现(代理)类如何实现HTTP请求应答序列化和反序列化的?

原生的Feign允许你添加额外的解码器,官方给出了Consumer的例子:

public class Example {
  public static void main(String[] args) {
    // 这里假定ProviderDemoService中有一个返回MyResponse的方法
    MyResponse response = Feign.builder()
                     .decoder(new GsonDecoder())
                     .client(new OkHttpClient())
                     .target(ProviderDemoService.class, "https://xxxx");
  }
}

为了能做到这一点,原生Feign提供了 DecoderEncoder 两个接口(本文我们只重点关注解码部分):

public interface Decoder {

    /**
     * Decodes an http response into an object corresponding to its
     * {@link java.lang.reflect.Method#getGenericReturnType() generic return type}. If you need to
     * wrap exceptions, please do so via {@link DecodeException}.
     *
     * @param response the response to decode
     * @param type {@link java.lang.reflect.Method#getGenericReturnType() generic return type} of the
     *        method corresponding to this {@code response}.
     * @return instance of {@code type}
     * @throws IOException will be propagated safely to the caller.
     * @throws DecodeException when decoding failed due to a checked exception besides IOException.
     * @throws FeignException when decoding succeeds, but conveys the operation failed.
     */
    Object decode(Response response, Type type) throws IOException, DecodeException, FeignException;
}


public interface Encoder {

    /**
     * Converts objects to an appropriate representation in the template.
     *
     * @param object what to encode as the request body.
     * @param bodyType the type the object should be encoded as. {@link #MAP_STRING_WILDCARD}
     *        indicates form encoding.
     * @param template the request template to populate.
     * @throws EncodeException when encoding failed due to a checked exception.
     */
    void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException;
}

换成Spring Cloud OpenFeign的话,就得和Spring的Web体系打通了,这里就不得不提一个构造类即 FeignClientsConfiguration :

// 注意:为了演示方便,对其进行了代码裁剪
@Configuration(proxyBeanMethods = false)
public class FeignClientsConfiguration {

	@Autowired
	// 这里将Spring Web的消息转换器机制注入进来
	private ObjectFactory messageConverters;

	@Bean
	@ConditionalOnMissingBean
	// 构造解码Decoder的Spring Bean
	public Decoder feignDecoder() {
		// 这里的SpringDecoder实现了Feign的Decoder接口,并且将Spring Web的消息转换器设置到SpringDecoder来使用
		return new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)));
	}

	@Bean
	@ConditionalOnMissingBean
	// 构造编码Encoder的Spring Bean
	public Encoder feignEncoder(ObjectProvider formWriterProvider) {
		return springEncoder(formWriterProvider);
	}

	private Encoder springEncoder(ObjectProvider formWriterProvider) {
		AbstractFormWriter formWriter = formWriterProvider.getIfAvailable();

		if (formWriter != null) {
			return new SpringEncoder(new SpringPojoFormEncoder(formWriter), this.messageConverters);
		}
		else {
			return new SpringEncoder(new SpringFormEncoder(), this.messageConverters);
		}
	}
}

那我们看看 SpringDecoder 拿到Spring Web的解码器后如何使用:

// 注意:裁剪了部分代码
public class SpringDecoder implements Decoder {

	private ObjectFactory messageConverters;

	public SpringDecoder(ObjectFactory messageConverters) {
		this.messageConverters = messageConverters;
	}

	@Override
	public Object decode(final Response response, Type type) throws IOException, FeignException {
		if (type instanceof Class || type instanceof ParameterizedType || type instanceof WildcardType) {
			HttpMessageConverterExtractor extractor = new HttpMessageConverterExtractor(type,
					this.messageConverters.getObject().getConverters());
			// 直接使用了。。。
			return extractor.extractData(new FeignResponseAdapter(response));
		}
		
		throw new DecodeException(response.status(), "type is not an instance of Class or ParameterizedType: " + type,
				response.request());
	}
}

到此为止,相信你对编解码这块已经有一定的了解。

4. 生成的实现(代理)类是如何注入到Spring容器中的?

Spring Cloud OpenFeign如何将动态生成的代理类和Spring容器打通?还记得我们前面说的 @EnableFeignClients 吗?这是需要我们在使用 Spring Cloud OpenFeign 时显示的在一个能被Spring容器扫到并加载的类上使用的,@EnableFeignClients 的定义如下:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
	// 注解内容省略
}

就是这里的 @Import 提前加载Spring Bean的方式,触发了 FeignClientRegistrar 的初始化,而 FeignClientRegistrar 由于实现了 ImportBeanDefinitionRegistrar 接口,我们知道在处理@Configuration类时可以通过Import注册其他Spring Bean定义的能力,而前面说过,我们还不知道哪些接口使用了 @FeignClient,所以在 FeignClientRegistrar 我们需要做的就是扫描某些路径(该路径由配置Spring扫描路径包括@EnableFeignClients中配置的路径)的接口类,识别对应的 @FeignClient ,给这些接口类创建代理对象。而为了把这些代理对象注入到Spring 容器中,所以还得借助 FactoryBean 的能力。我们先看下 ImportBeanDefinitionRegistrar 的实现:

// 注意:裁剪了大量代码
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {

	private ResourceLoader resourceLoader;
	private Environment environment;

	@Override
	public void setResourceLoader(ResourceLoader resourceLoader) {
		this.resourceLoader = resourceLoader;
	}

	@Override
	public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
		// 获取 @EnableFeignClients 上的相关属性并用这些属性做一些基本配置Bean的注册
		registerDefaultConfiguration(metadata, registry);
        // 注册Bean
		registerFeignClients(metadata, registry);
	}

	private void registerDefaultConfiguration(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
		// 略
	}

	public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
		LinkedHashSet candidateComponents = new LinkedHashSet<>();
		Map attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
		final Class[] clients = attrs == null ? null : (Class[]) attrs.get("clients");
		if (clients == null || clients.length == 0) {
			// 获取包路径下的扫描器
			ClassPathScanningCandidateComponentProvider scanner = getScanner();
			scanner.setResourceLoader(this.resourceLoader);
			scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
			Set basePackages = getBasePackages(metadata);
			for (String basePackage : basePackages) {
				// 将所有 @FeignClient 的接口的BeanDefinition拿到
				candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
			}
		} else {
			for (Class clazz : clients) {
				candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
			}
		}

		for (BeanDefinition candidateComponent : candidateComponents) {
			if (candidateComponent instanceof AnnotatedBeanDefinition) {

				AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
				AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();

				// 对,这里要求必须是接口
				Assert.isTrue(annotationMetadata.isInterface(), "@FeignClient can only be specified on an interface");

				Map attributes = annotationMetadata
					.getAnnotationAttributes(FeignClient.class.getCanonicalName());

				// 根据这些属性和接口来注册FeignClient Bean
				registerFeignClient(registry, annotationMetadata, attributes);
			}
		}
	}

	private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata,
									 Map attributes) {
		String className = annotationMetadata.getClassName();
		// 使用FactoryBean,将Bean的具体生成过程收拢到FeignClientFactoryBean之中
		BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
		definition.addPropertyValue("type", className);
		definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);

		AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();

		BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[]{alias});
		// 将这个使用了 @FeignClient 的接口的工厂Bean的 BeanDefinition 注册到Spring容器中
		BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
	}
}

可以看出,关键逻辑又回到 FeignClientFactoryBean 拿到业务接口、@EnableFeignClient 和 @FeignClient的数据后如何去构造代理类了,而 FeignClientFactoryBean 内部其实使用的是原生Feign的API来构建代理对象,这里不再阐述,感兴趣的读者可以翻下源码。

4、Spring Cloud OpenFeign不仅如此

Spring Cloud OpenFeign还支持可插拔编码器和解码器。还添加了对Spring MVC注释的支持,并且Spring CloudOpenFeign集成了Ribbon和Eureka以及Spring Cloud LoadBalancer,以在使用时能提供负载平衡的HTTP客户端。精彩内容参见官方文档:Spring Cloud OpenFeign

5、总结

由于本人接触Feign还没几天,文中难免有不正确或写的不够明白的地方,欢迎交流、吐槽和指正!!

你可能感兴趣的:(Spring,Cloud,SpringCloud全系列,Feign,OpenFeign,OpenFeign原理解析)