目录
1、背景
2、Spring Cloud OpenFeign用法举例
3、Spring Cloud OpenFeign“魔法”揭秘
1. @FeignClient 如何根据接口生成实现类的?
2. 生成的实现(代理)类是如何适配各种HTTP组件的?
3. 生成的实现(代理)类如何实现HTTP请求应答序列化和反序列化的?
4. 生成的实现(代理)类是如何注入到Spring容器中的?
4、Spring Cloud OpenFeign不仅如此
5、总结
之前未曾想过会有全面拥抱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中使用。
这里先给出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 接口,如下图:
我们可以把 ProviderDemoService 当做由Web服务端(后面简称Provider端)定义好的API来提供,HTTP客户端(后面简称Consumer端)只需要依赖该API就可以使用该接口(这里TSF官方例子为了简单起见,ProviderDemoService 直接在Consumer端定义),有了 ProviderDemoService,我们在Consumer端借助Spring容器,即可像使用本地接口一样调用远程HTTP服务,是不是很爽?那 ProviderDemoService 到底特殊在哪里?是因为有 @FeignClient !!!如何做到的后面会讲解,我们来看在Spring容器中如何使用 ProviderDemoService , 这里通过控制器 ConsumerController 来演示 ProviderDemoService 的使用 :
没错,我们就像普通的Spring Bean一样使用 ProviderDemoService 即可,就是这么神奇!!!我们发现其实在这个接口中还使用了 @RequestMapping 用于标识该方法所对应的HTTP请求路径,这也是保持了Spring Web的传统用法,而在原生的Feign中使用的是 @RequestLine。
对了,这里插一句,我们还需要在启动类中使用 @EnableFeignClients 来给Spring Boot应用开启Feign功能!!!
Feign只是对HTTP调用组件进行了易用性封装,底层还是使用我们常见的OkHttp、HttpClient等组件(我们不生产水,我们只是水的搬运工),你瞧:
Feign的目标之一就让这些HTTP客户端更好用,使用方式更统一(这和Spring出现的目的如出一辙),更像RPC。要想了解Spring Cloud OpenFeign整体实现原理,通过之前使用方式的介绍,我们需要回答如下四个问题:
接下来,我们通过解读源码方式,逐一解答上述问题。
我们先来回顾下常见的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调用并将结果反序列化成原接口方法的返回值对象。
这个问题应该由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组件也是类似的方式来判断和加载,这里就不在一一阐述。
原生的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提供了 Decoder 和 Encoder 两个接口(本文我们只重点关注解码部分):
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());
}
}
到此为止,相信你对编解码这块已经有一定的了解。
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来构建代理对象,这里不再阐述,感兴趣的读者可以翻下源码。
Spring Cloud OpenFeign还支持可插拔编码器和解码器。还添加了对Spring MVC注释的支持,并且Spring CloudOpenFeign集成了Ribbon和Eureka以及Spring Cloud LoadBalancer,以在使用时能提供负载平衡的HTTP客户端。精彩内容参见官方文档:Spring Cloud OpenFeign
由于本人接触Feign还没几天,文中难免有不正确或写的不够明白的地方,欢迎交流、吐槽和指正!!