声明式服务调用-Feign 源码分析
文章目录
前言
上一章 《声明式服务调用-Feign 基础篇》 我们介绍了 Feign 的基本概念和使用方式;本章我们主要介绍 Feign 的一些主要组件和整个组件的执行过程,重点讨论 @FeignClient 注解的工作原理,在其处理的过程中如何集成 Hystrix、Ribbon 等分布式组件,最后会简单的看看 Spring Cloud 如何使用 Spring MVC 的注解扩展兼容 Feign。
项目环境
- Java 8
- Spring Cloud Greenwich.SR2
- Spring Boot 2.1.6.RELEASE
- 项目地址:https://github.com/huajiexiewenfeng/deep-in-spring-cloud-netflix
1.Feign 主要组件
Feign 主要组件如图:
相关源码的位置 feign.Feign.Builder 中
- Contract 契约组件
在 Feign 中可以通过定义 API 接口的方式来调用远程的 Http API,在定义调用 Client 的时候需要增加一些注解来描述整个调用 API 的基本信息,比如请求类型,请求 URI 等等。
Contract 允许用户自定义契约去解析注解信息,最典型的应用场景就是在 Spring Cloud 中使用 Feign,我们可以使用 Spring MVC 的注解来定义 Feign 的客户端,这是因为 Spring Cloud OpenFeign 中实现了自己的 SpringMvcContract。
- Encoder 编码组件
通过该组件我们可以将请求信息采用指定的编码方式进行编码后传输。
- Decoder 解码组件
Decoder 将相应数据解码成对象。
- ErrorDecoder 异常解码器
当被调用方发生异常后,我们可以在 ErrorDecoder 中将响应的数据转换成具体的异常返回给调用方,适合内部服务之间调用,但不想通过指定的字段来判断是否成功的场景,直接用自定义异常代替。
- Logger 日志记录
Logger 组件是负责 Feign 中记录日志的,可以指定 Logger 的级别以及自定义日志的输出。
- Client 请求执行组件
Client 是负责 HTTP 请求执行的组件,Feign 将请求信息封装好后会交由 Client 来执行,Feign 中默认的 Client 是通过 JDK 的 HttpURLConnection 来发起请求的,在每次发送请求的时候,都会创建新的 HttpURLConnection 链接,使用默认的方式,Feign 的性能会很差,原因就是使用了 HttpURLConnection。你可以通过扩展该接口,使用 Apache HttpClient 等基于连接池的高性能 HTTP 客户端。
- Retryer 重试组件
Retryer 是负责重试的组件,Feign 内置了重试器,当 HTTP 请求出现 IO 异常时,Feign 会限定一个最大重试次数来进行重试操作。
- InvocationHandler 代理
InvocationHandlerFactory 采用 JDK 的动态代理方式生成代理对象,我们定义的 Feign Client 是接口,当我们调用这个接口中定义的方法时,实际上是要去调用远程的 HTTP API,这里用了动态代理方式,当调用某个方法时,会进入代理中真正的去调用远程 HTTP API。
- RequestInterceptor 请求拦截器
可以为 Feign 添加多个拦截器,在请求执行前设置一些扩展的参数信息。
- QueryMapEncoder 参数查询
QueryMapEncoder 是针对实体类参数查询的编码器,可以基于 QueryMapEncoder 将实体类生成对应的查询参数。
2.Feign 执行过程
在 Spring Cloud 中使用 Feign 时,我们会在接口类上使用 Feign 自带的注解 @FeignClient 来标识需要访问服务的名称,并且在相关的接口方法上标注 URL 信息以及请求参数信息,当调用接口对应的方法时,Feign 内部会基于面向接口的动态代理方式生成实现类,将请求调用委托到动态代理实现类,负责动态代理的组件是 InvocationHandlerFactory。
根据 Contract 规则,解析接口类的注解信息,翻译成 Feign 内部能识别的信息。Feign 默认有一套自己的协议规范,我们也可以自定义其他的规范来进行扩展,在 Spring Cloud OpenFeign 中就扩展了 SpringMvcContract,这样做的目的是为了降低学习和使用成本,客户端和服务端使用同一个接口定义,发布成 SDK 给调用方使用。
MethodHandler 在执行的时候会生成 Request 对象,在构建 Request 对象的时候会为其设置拦截器,交由 Client 执行前记录一些日志,Client 执行完成后也记录一些日志,然后使 Decoder 进行相应结果的解码操作,并返回结果。
3.Feign & Hystrix 集成
3.1 FeignClient 配置类 FeignClientsConfiguration
- FeignClientsConfiguration.HystrixFeignConfiguration#feignHystrixBuilder
@Configuration
@ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class })
protected static class HystrixFeignConfiguration {
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "feign.hystrix.enabled")
public Feign.Builder feignHystrixBuilder() {
return HystrixFeign.builder();
}
}
条件装配
- 设置 feign.hystrix.enabled = true 就会装配 Feign.Builder 类
HystrixFeign 看名称就是集成了 Hystrix 的 Feign 实现,我们进入 HystrixFeign.builder 中可以看到继承了 Feign 的 Builder,对 invocationHandlerFactory 进行了重写,在 create 的时候返回的是 HystrixInvocationHandler,HystrixInvocationHandler 中在 invoke 的时候会将请求包装成 HystrixCommand 去执行,这里就自然的集成了 Hystrix。
3.2 自动装配 FeignAutoConfiguration
- FeignAutoConfiguration.HystrixFeignTargeterConfiguration
@Configuration
@ConditionalOnClass(name = "feign.hystrix.HystrixFeign")
protected static class HystrixFeignTargeterConfiguration {
@Bean
@ConditionalOnMissingBean
public Targeter feignTargeter() {
return new HystrixTargeter();
}
}
条件装配
- feign.hystrix.HystrixFeign 存在 ClassPath 中
- 如果 Targeter 不存在 Spring IoC 容器中
满足上面两个条件,就会自动装配 HystrixTargeter 对象,而在 @FeignClient 处理过程中,封装代理 Feign Client 代理对象时,会通过 ApplicationContext.getBean 依赖查找的方式从 Spring IoC 容器获取 HystrixTargeter 对象来进行封装。
4.Feign & Ribbon 集成
Feign & Ribbon 集成的过程和 Hystrix 集成的过程如出一辙,同样是通过自动装配+依赖查找
- 通过 @Bean + 条件注解方式注入 Client 对象,看名称这是一个 LoadBalancer 也就是集成了 Ribbon 的对象
@Configuration
class DefaultFeignLoadBalancedConfiguration {
@Bean
@ConditionalOnMissingBean
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
SpringClientFactory clientFactory) {
return new LoadBalancerFeignClient(new Client.Default(null, null), cachingFactory,
clientFactory);
}
}
依赖查找的过程封装代理 Feign Client 代理对象的过程下面会具体分析。
代码调试如下:
- org.springframework.cloud.openfeign.FeignClientFactoryBean#loadBalance 236 行
可以看到封装的过程中 getOptional(context, Client.class)
拿到的 Client 就是之前配置好的 LoadBalancerFeignClient 对象。
5.@FeignClient 工作原理
5.1 @FeignClient 处理过程
这一小节,我们来一起看看标注 @FeignClient 注解的接口,如何被替换成代理类,并且注入到 Spring IoC 容器中。
首先第一步 @EnableFeignClients 用来激活 Feign 相关的组件,这部分内容不熟悉的可以参考 《@Enable 模块驱动》 来了解相关的细节,这里我们只关注 feign 相关实现。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
...
FeignClientsRegistrar 类采用的是实现 ImportBeanDefinitionRegistrar 接口的模式,其中的核心代码
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
registerDefaultConfiguration(metadata, registry);
registerFeignClients(metadata, registry);
}
- registerDefaultConfiguration 注册默认的配置
- registerFeignClients 注册 FeignClient 对象
这里我们看 registerFeignClients 方法的调用链路
1.FeignClientsRegistrar#registerBeanDefinitions
-
获取 @FeignClient 注解相关的参数信息,比如 value,fallback 等等
-
registerFeignClient(registry, annotationMetadata, attributes);
2.FeignClientsRegistrar#registerFeignClient 源码如下:
private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientFactoryBean.class);
validate(attributes);
definition.addPropertyValue("url", getUrl(attributes));
definition.addPropertyValue("path", getPath(attributes));
String name = getName(attributes);
definition.addPropertyValue("name", name);
String contextId = getContextId(attributes);
definition.addPropertyValue("contextId", contextId);
definition.addPropertyValue("type", className);
definition.addPropertyValue("decode404", attributes.get("decode404"));
definition.addPropertyValue("fallback", attributes.get("fallback"));
definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
String alias = contextId + "FeignClient";
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
boolean primary = (Boolean) attributes.get("primary"); // has a default, won't be
// null
beanDefinition.setPrimary(primary);
String qualifier = getQualifier(attributes);
if (StringUtils.hasText(qualifier)) {
alias = qualifier;
}
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
new String[] { alias });
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
通过 BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
的方式来构建BeanDefinition,并设置相关的属性信息,最后将 BeanDefinition 注册到 Spring IoC 容器中。
- BeanDefinition 定义和注册方式可以参考 《Spring Bean 定义》
总结 @FeignClient 处理过程
@EnableFeignClients 激活 -> FeignClientsRegistrar -> registerBeanDefinitions -> registerFeignClient,最终将相关的元信息封装成 BeanDefinition,通过 registerBeanDefinition 的方式注册到 Spring IoC 容器。
然后我们在业务代码中使用 FeignClient 对象,只需要通过 @Autowired 依赖注入的方式就可以从 Spring IoC 容器中拿到这个对象。
5.2 代理类如何处理?
上面的处理过程有一个小细节,就是 BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
这个代理类的处理过程并不是通过 Proxy.newProxyInstance 这种传统方式 直接
来构建的,而是通过 FactoryBean 的方式,我们来看看 FeignClientFactoryBean 的实现细节。
第一个是 feign API 的封装
- 可以看到 FeignClientFactoryBean 帮我们进行了 feign 原生 API 的封装
protected Feign.Builder feign(FeignContext context) {
FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
Logger logger = loggerFactory.create(this.type);
// @formatter:off
Feign.Builder builder = get(context, Feign.Builder.class)
// required values
.logger(logger)
.encoder(get(context, Encoder.class))
.decoder(get(context, Decoder.class))
.contract(get(context, Contract.class));
// @formatter:on
configureFeign(context, builder);
return builder;
}
第二个是获取代理对象
- Factorybean 是通过 getObject 的方式来延迟获取对象
@Override
public Object getObject() throws Exception {
return getTarget();
}
<T> T getTarget() {
// 依赖查找的方式获取 FeignContext 对象
FeignContext context = this.applicationContext.getBean(FeignContext.class);
// 封装成 Feign 对象
Feign.Builder builder = feign(context);
// 如果 URL 为空
if (!StringUtils.hasText(this.url)) {
if (!this.name.startsWith("http")) {
this.url = "http://" + this.name;
}
else {
this.url = this.name;
}
this.url += cleanPath();
return (T) loadBalance(builder, context,
new HardCodedTarget<>(this.type, this.name, this.url));
}
...// 本示例启动时不会走这段逻辑
}
启动时我们在 FeignClientFactoryBean#getTarget 259 行打上断点
loadBalance 代码如下:
protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
HardCodedTarget<T> target) {
Client client = getOptional(context, Client.class);
if (client != null) {
builder.client(client);
Targeter targeter = get(context, Targeter.class);
return targeter.target(this, builder, context, target);
}
throw new IllegalStateException(
"No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?");
}
其实通过 getOptional(context, Client.class);
拿到的是一个 LoadBalancerFeignClient,因为 DefaultFeignLoadBalancedConfiguration
默认自动配置集成了 Ribbon,所以这里根据类型获取的就是一个 LoadBalancerFeignClient 的对象,具体的代码如下:
- FeignRibbonClientAutoConfiguration#feignRequestOptions
@Bean
@ConditionalOnMissingBean
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
SpringClientFactory clientFactory) {
return new LoadBalancerFeignClient(new Client.Default(null, null), cachingFactory,
clientFactory);
}
同样的这里的 Targeter 也是通过自动配置的方式注入到了 Spring IoC 容器中
FeignAutoConfiguration.HystrixFeignTargeterConfiguration#feignTargeter
继续 targeter.target
方法
class HystrixTargeter implements Targeter {
@Override
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
FeignContext context, Target.HardCodedTarget<T> target) {
if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) {
return feign.target(target);
}
feign.hystrix.HystrixFeign.Builder builder = (feign.hystrix.HystrixFeign.Builder) feign;
SetterFactory setterFactory = getOptional(factory.getName(), context,
SetterFactory.class);
if (setterFactory != null) {
builder.setterFactory(setterFactory);
}
Class<?> fallback = factory.getFallback();
if (fallback != void.class) {
return targetWithFallback(factory.getName(), context, target, builder,
fallback);
}
Class<?> fallbackFactory = factory.getFallbackFactory();
if (fallbackFactory != void.class) {
return targetWithFallbackFactory(factory.getName(), context, target, builder,
fallbackFactory);
}
return feign.target(target);
}
...
feign.Feign.Builder#target 方法代码如下:
public <T> T target(Target<T> target) {
return this.build().newInstance(target);
}
feign.ReflectiveFeign#newInstance 方法代码如下:
@Override
public <T> T newInstance(Target<T> target) {
Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
for (Method method : target.type().getMethods()) {
if (method.getDeclaringClass() == Object.class) {
continue;
} else if (Util.isDefault(method)) {
DefaultMethodHandler handler = new DefaultMethodHandler(method);
defaultMethodHandlers.add(handler);
methodToHandler.put(method, handler);
} else {
methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
}
}
InvocationHandler handler = factory.create(target, methodToHandler);
T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
new Class<?>[] {target.type()}, handler);
for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
defaultMethodHandler.bindTo(proxy);
}
return proxy;
}
这里我们断点调试一下代码,将断点打在 feign.ReflectiveFeign#newInstance 53 行
可以看到 nameToHandler 对象中存放的是 com.csdn.feign.user.api.service.UserServiceFeignApi 对应的三个方法,继续往下执行代码,最后会将 nameToHandler 对象转化为 methodToHandler 对象
通过 feign.InvocationHandlerFactory 来创建 InvocationHandler 对象,代码如下:
InvocationHandler handler = factory.create(target, methodToHandler);
最后还是通过 Proxy.newProxyInstance 这种方式来创建代理对象,然后返回 proxy 代理对象。
总结代理类如何处理
整个的调用链路如下:
BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class); -> FeignClientFactoryBean -> getObject -> getTarget -> loadBalance -> targeter.target -> feign.Feign.Builder#target -> feign.ReflectiveFeign#newInstance -> feign.InvocationHandlerFactory.create -> Proxy.newProxyInstance
-
首先通过 BeanDefinitionBuilder 来构建 BeanDefinition (每个 FeignClient 对象 Bean 的定义信息);
-
然后在 FeignClientFactoryBean 中来封装 feign 对象以及相关配置;
-
再调用 getObject 方法,在方法中集成 Ribbon、Hystrix 等分布式组件;
-
最后调用 ReflectiveFeign#newInstance 来创建代理对象,通过 feign.InvocationHandlerFactory.create 来创建 InvocationHandler 对象;
-
最终还是通过 Proxy.newProxyInstance 这种 JDK 动态代理的方式来创建的最后的 proxy 代理对象。
补充:FactoryBean#getObject 方法在何时被调用?
在 Spring 的 ApplicationContext 应用上下文启动时,finishBeanFactoryInitialization 方法也就是 BeanFactory 初始化完成阶段,通过 beanDefinitionNames 来遍历我们所有的 BeanDefintion,逐一进行 getBean(beanName) 操作,通过我们的 BeanDefinition 创建 bean 对象,并缓存到 DefaultSingletonBeanRegistry#singletonObjects 中。
其中 getBean 的过程就会触发 FactoryBean#getObject 来获取代理对象。
6.Spring MVC 注解集成
- org.springframework.cloud.openfeign.FeignClientsConfiguration
在这个类中,Spring cloud 默认配置了很多相关的 feign 组件,其中和 Spring Mvc 注解相关的如下:
@Bean
@ConditionalOnMissingBean
public Contract feignContract(ConversionService feignConversionService) {
return new SpringMvcContract(this.parameterProcessors, feignConversionService);
}
SpringMvcContract 继承了 Contract.BaseContract ,我们再看看 Contract.BaseContract,它的实现类只有两个
- SpringMvcContract
- feign.Contract.Default 默认的解析注解信息处理类
我们只需要对比这两个实现类其中几个主要的方法即可
- processAnnotationOnClass
- processAnnotationOnMethod
- processAnnotationsOnParameter
三个方法分别对应类,方法,参数,SpringMvcContract 中通过 @RequestMapping 注解来重新实现相关的功能,并增加了四种对应参数的处理者,分别对应 SpringMvc 中四种参数类型的处理,具体细节这里就不展开了。
private List<AnnotatedParameterProcessor> getDefaultAnnotatedArgumentsProcessors() {
List<AnnotatedParameterProcessor> annotatedArgumentResolvers = new ArrayList<>();
annotatedArgumentResolvers.add(new PathVariableParameterProcessor());
annotatedArgumentResolvers.add(new RequestParamParameterProcessor());
annotatedArgumentResolvers.add(new RequestHeaderParameterProcessor());
annotatedArgumentResolvers.add(new QueryMapParameterProcessor());
return annotatedArgumentResolvers;
}
7.参考
-
《深入理解 Spring Cloud 与微服务架构》 方志朋
-
《300分钟搞懂 Spring Cloud》尹吉欢