文章目录
- 一、前言
- 1)FeignContext上下文的获取
- 2)从FeignContext中获取Feign.Builder
- 3)处理配置信息
- 4)使用Feign.Builder构建出一个FeignClient
- 1> LoadBalancerFeignClient在哪里注入到Spring容器?
- 2> HystrixTargeter在哪里注入到Spring容器?
- 3> HystrixTargeter#target()方法
- 4> ReflectiveFeign#newInstance()生成动态代理类
- <1> FeignClient接口和MethodHandler的映射map生成机制流程图
- <2> ParseHandlersByName#apply()解析FeignClient中的方法
一、前言
在前面的文章:
- SpringCloud之Feign实现声明式客户端负载均衡详细案例
- SpringCloud之OpenFeign实现服务间请求头数据传递(OpenFeign拦截器RequestInterceptor的使用)
- SpringCloud之OpenFeign的常用配置(超时、数据压缩、日志)
- SpringCloud之OpenFeign的核心组件(Encoder、Decoder、Contract)
- SpringBoot启动流程中开启OpenFeign的入口(ImportBeanDefinitionRegistrar详解)
- 源码剖析OpenFeign如何扫描所有的FeignClient
我们聊了以下内容:
- OpenFeign的概述、为什么会使用Feign代替Ribbon?
- Feign和OpenFeign的区别?
- 详细的OpenFeign实现声明式客户端负载均衡案例
- OpenFeign中拦截器RequestInterceptor的使用
- OpenFeign的一些常用配置(超时、数据压缩、日志输出)
- SpringCloud之OpenFeign的核心组件(Encoder、Decoder、Contract)
- 在SpringBoot启动流程中开启OpenFeign的入口
- OpenFeign如何扫描 / 注册所有的FeignClient
本文基于OpenFeign低版本(SpringCloud 2020.0.x版本之前
)讨论:OpenFeign如何为FeignClient生成动态代理类
PS:本文基于的SpringCloud版本
在上一篇文章(SpringCloud之深度源码剖析OpenFeign如何扫描所有的FeignClient)我们聊了OpenFeign如何扫描所有的FeignClient,本文接着聊OpenFeign是如何针对FeignClient生成动态代理类的?
0、生成FeignClient的动态代理类整理流程图
二、生成FeignClient的动态代理类
这里依旧是结合OpenFeign案例( SpringCloud之Feign实现声明式客户端负载均衡详细案例)聊,ServiceBController中通过@Autowired注解注入了一个被@FeignClient标注的接口ServiceAClient。
在AbstractApplicationContext#refresh()
方法中最后调用的finishBeanFactoryInitialization(beanFactory)
方法中会将所有类全部注入到Spring容器中;在将ServiceBController注入到Spring容器过程中,会将其成员ServiceAClient也注入到Spring容器中,AbstractAutowireCapableBeanFactory#populateBean()
方法会处理ServiceAClient,进而调用到AutowiredAnnotationBeanPostProcessor#postProcessProperties()
方法对ServiceAClient做一个注入操作,具体执行流程如下:
…中间经过一些过程(买个坑,@AutoWired自动注入实现机制后面特地写一篇文章分析)…走到AbstractBeanFactory#getBean(String)
方法获取ServiceBController依赖的成员类型ServiceAClient;
由于我们在注册FeignClient到Spring容器时,构建的BeanDefinition的beanClas是FeignClientFactoryBean
;
FeignClientFactoryBean是一个工厂,保存了@FeignClient注解的所有属性值,在Spring容器初始化的过程中,其会根据之前扫描出的FeignClient信息构建FeignClient的动态代理类。
从debug的堆栈信息我们可以看到是FeignClientFactoryBean#getObject()
方法负责获取/创建动态代理类。
假如我们不debug,该如何找到哪里负责创建动态代理类?
1、FeignClientFactoryBean创建动态代理类的入口
我们知道要通过注册到Spring容器中的FeignClient的BeanDefinition的beanClass属性是FeignClientFactoryBean,所大概率和FeignClientFactoryBean
是相关的,那怎么找呢?
高工、架构们选择连蒙带猜!!!!
注意到FeignClientFactoryBean的feign(FeignContext)
方法,方法会构造一个Feign.Builder
,Builder、Builder,这不是就是构造器模式嘛;基于Feign.Builder可以构造对应的FeignClient。
再看哪里调用了feign()
方法;找到getTarget()
方法;
对于有一定开发的经验而言,见到Target这一类东西,基本可以确定就是动态代理;
再往上追,看哪里调用了getTarget()
方法?进入到getObject()
方法;
而getObject()方法是FactoryBean接口中定义的方法;
到这里可以确定FeignClientFactoryBean#getObject()
方法,在spring容器初始化时,会被作为入口来调用,进而创建一个ServiceAClient的动态代理,返回给spring容器 并 注册到Spring容器里去。
2、Feign.Builder的构建过程
上面我们得出结论:FeignClient是通过Feign.Builder来构建的,生成FeignClient动态代理的入口是FeignClientFactoryBean#getObject()
,这里我们看一下Feign.Builder是如何构建的?
1)FeignContext上下文的获取
调用FeignClientFactoryBean#getObject()
创建/获取FeignClient动态代理类时,首先要通过
获取Feign的上下文FeignContext
。
这里的applicationContext
是AnnotationConfigServletWebApplicationContext
。
在Ribbon系列中我们聊了ribbon里有一个
SpringClientFactory
,就是对每个服务的调用,都会有一个独立的ILoadBalancer,IoadBalancer里面的IRule、IPing都是独立的组件,也就是说ribbon要调用的每个服务都对应一个独立的spring容器;从那个独立的spring容器中,可以取出某个服务关联的属于自己的LoadBalancer、IRule、IPing等。
FeignContext
我们如果要调用一个服务的话,ServiceA,那么那个服务(ServiceA)就会关联一个独立的spring容器;关联着自己独立的一些组件,比如说独立的Logger组件,独立的Decoder组件,独立的Encoder组件;FeignContext则代表了一个独立的容器工厂,里面记录了每个服务对应的容器
AnnotationConfigApplicationContext
。
- 因此,可以对不同的@FeignClient自定义不同的Configuration。
FeignContext在哪里注入到Spring容器的?
FeignContext位于spring-cloud-openfeign-core
项目,我们在这个项目下结合SpringBoot自动装配的特性找XxxAutoConfiguration 或 XxxConfiguration,最终找到FeignAutoConfiguration
。
FeignAutoConfiguration中使用@Bean方法将FeignContext注入到Spring容器;
FeignContext继承自NamedContextFactory
,内部负责对每个服务都维护一个对应的spring容器(以map存储,一个服务对应一个spring容器);此处和Ribbon一样,可以参考Ribbon的文章(SpringCloud之Ribbon和服务注册中心的集成细节)。
进入到feign()
方法中,以获取FeignLoggerFactory为例:
get(FeignContext context, Class type)方法要做的事情如下:
- 根据服务名称(ServiceA)去FeignContext里面去获取对应的FeignLoggerFactory;
- 其实就是根据ServiceA服务名称,先获取对应的spring容器,然后从那个spring容器中,获取自己独立的一个FeignLoggerFactory;
默认使用的FeignLoggerFactory
是在spring-cloud-openfeign-core
项目的FeignClientsConfiguration类中加载的DefaultFeignLoggerFactory
,而DefaultFeignLoggerFactory
中默认创建的是Slf4jLogger
;
2)从FeignContext中获取Feign.Builder
这里和上面获取FeignLoggerFactory一样,在spring-cloud-openfeign-core
项目的FeignClientsConfiguration类中会找到两个Feign.Builder
(一个和Hystrix相关,另外一个Retryer相关的(请求超时、失败重试))的注册逻辑:
由于默认feign.hystrix.enabled
属性为false,所以默认注入的Feign.Builder是Feign.builder().retryer(retryer)
。
3)处理配置信息
回到feign()
方法,其中调用的configureFeign(context, builder)
方法负责处理Feign的相关配置(即:使用application.yml中配置的参数,来设置Feign.Builder)。
逻辑解析:
-
FeignClientProperties
是针对FeignClient的配置;- 先读取application.yml中的feign.client打头的一些参数,包括了connectionTimeout、readTimeout之类的参数;如果application.yml中没有配置feign.client相关参数,则使用默认配置(Retryer retryer、ErrorDecoder、Request.Options等);
- 然后读取application.yml中针对当前要调用服务的配置;
所以如果在application.yml文件中同时配置了针对全部服务和单个服务的配置,则针对单个服务的配置优先级最高,因为在代码解析中它是放在后面解析的,会覆盖调前面解析的内容。
4)使用Feign.Builder构建出一个FeignClient
如果在@FeignClient上,没有配置url属性
,也就是没有指定服务的url地址;那么Feign就会自动跟ribbon关联起来,采用ribbon来进行负载均衡,直接拿出@FeignClient中配置的name()为Ribbon准备对应的url地址:http://ServiceA
;
此外如果在@FeignClient注解中配置了path属性
,就表示要访问的是这个ServiceA服务的某一类接口,比如:@FeignClient(value = “ServiceA”, path = “/user”),在拼接请求URL地址的时候,就会拼接成:http://ServiceA/user
。
FeignClientFactoryBean#loadBalance()
方法是一个基于ribbon进行负载均衡的FeignClient动态代理生成方法;
其入参包括:
- Feign.Builder builder --> FeignClient构造器
- FeignContext context --> Feign上下文
-
HardCodedTarget<T>
target,target
是一个HardCodedTarget,硬编码的Target,里面包含了接口类型(com.zhss.service.ServiceAClient)、服务名称(ServiceA)、url地址(http://ServiceA)
loadBalance()
方法中首先会获取Client 和 Targeter;
- 通过
Client client = getOptional(context, Client.class)
方法获取Client,返回的是LoadBalancerFeignClient
,(高版本是FeignBlockingLoadBalancerClient); - 通过
Targeter targeter = get(context, Targeter.class)
方法获取Targeter,返回的是HystrixTargeter
;
下面我们来看一下LoadBalancerFeignClient 和 HystrixTargeter是在哪里注入到Spring容器的?
1> LoadBalancerFeignClient在哪里注入到Spring容器?
进入到LoadBalancerFeignClient类中,看哪里调用了它唯一一个构造函数;
找到LoadBalancerFeignClient发现有三个地方调用了它的构造函数,new了一个实例;
- DefaultFeignLoadBalancedConfiguration
- HttpClientFeignLoadBalancedConfiguration
- OkHttpFeignLoadBalancedConfiguration
再结合默认的配置,只有DefaultFeignLoadBalancedConfiguration
中的Client符合条件装配;
可以通过引入Apache HttpClient的maven依赖使用HttpClientFeignLoadBalancedConfiguration,
或引入OkHttpClient的maven依赖并在application.yml文件中指定feign.okhttp.enabled
属性为true使用OkHttpFeignLoadBalancedConfiguration。
2> HystrixTargeter在哪里注入到Spring容器?
在FeignAutoConfiguration
类中可以找到Targeter
注入到Spring容器的逻辑;
默认是创建HystrixTargeter;
- 如果有feign.hystrix.HystrixFeign这个类的话,那么就会构造一个HystrixTargeter出来;
- 如果没有feign.hystrix.HystrixFeign这个类的话,那么就会构造一个DefaultTargeter出来;
HystrixTargeter是用来让feign和hystrix整合使用的,在发送请求的时候可以基于hystrix实现熔断、限流、降级。
- 生产环境如果启用
feign.hystrix.enabled
,则Feign.Builder也会变成HystrixFeign.Builder
,默认还是Feign自己的Feign.Builder
;
3> HystrixTargeter#target()方法
继续往下走,进入到HystrixTargeter#target()
方法,具体代码执行流程如下:
Feign#target()
方法中主要做两件事:
-
build()
方法将Feign.Builder中所有东西集成在一起,构建成一个ReflectiveFeign;-
ReflectiveFeign#newInstance()
方法负责生成动态代理;
4> ReflectiveFeign#newInstance()生成动态代理类
ReflectiveFeign#newInstance()
源代码如下:
方法中有两个Map类型的局部变量:nameToHandler、methodToHandler;
- nameToHandler释义:接口中的每个方法的名称,对应一个处理这个方法的
SynchronousMethodHandler
;由ReflectiveFeign的内部类ParseHandlersByName的apply(target)方法获取。- methodToHandler释义:接口中的每个方法(Method对象),对应一个处理这个方法的
SynchronousMethodHandler
;
上述代码段中,就是JDK动态代理的体现;动态生成一个没有名字的匿名类,这个类实现了ServiceAClient(FeignClient)
接口,基于这个匿名的类创建一个对象(T proxy),这就是所谓的动态代理;后续所有对这个T proxy
对象所有接口方法的调用,都会交给InvocationHandler
来处理,此处的InvocationHandler是ReflectiveFeign的内部类FeignInvocationHandler
。
下面我们继续看ReflectiveFeign的内部类ParseHandlersByName
的apply(target)方法如何解析FeignClient中的方法?
<1> FeignClient接口和MethodHandler的映射map生成机制流程图
<2> ParseHandlersByName#apply()解析FeignClient中的方法
ParseHandlersByName#apply()
方法会对我们定义的ServiceAClient接口进行解析,解析里面有哪些方法,然后为每个方法创建一个SynchronousMethodHandler出来,也就是说某个SynchronousMethodHandler专门用来处理那个方法的请求调用。
apply()方法逻辑如下:
其中 (1)factory.create()
会为所有标注了SpringMvc注解的方法都生成一个对应的SynchronousMethodHandler
。
(2)SpringMvcContract.parseAndValidateMetadata()
方法负责解析FeignClient接口中每个标注了SpringMVC注解的方法;即:Feign依靠Contract组件(SpringMvcContract
)来解析接口上的spring mvc注解;
针对FeignClient接口中的每个标注了SpringMVC注解的方法都会被SpringMvcContract组件解析,针对每个方法最后都生成一个MethodMetadata,代表方法的一些元数据,包括:
- 方法的定义,比如:ServiceAClient#deleteUser(Long)
- 方法的返回类型,比如:class java.lang.String
- 发送HTTP请求的模板,比如:DELETE /user/{id} HTTP/1.1
5> SpringMvcContract组件的工作原理
这里看一下SpringMvcContract.parseAndValidateMetadata()
是如何解析FeignClient中的每个方法。
以如下方法为例:
示例解析逻辑如下:
- 解析@RequestMapping注解,看看里面的method属性是什么?是GET/UPDATE/DELETE,然后在HTTP template里就加上GET/UPDATE/DELETE;(示例为DELETE)
- 找到接口上定义的@RequestMapping注解,解析里面的value值,拿到请求路径(/user),此时HTTP template变成:DELETE /user;
- 再次解析deleteUser()方法上的@RequestMapping注解,找到里面的value,获取到/{id},拼接到HTTP template里去:DELETE /user/{id}
- 接着硬编码拼死一个HTTP协议,http 1.1,HTTP template:DELETE /user/{id} HTTP/1.1
- indexToName:解析@PathVariable注解,第一个占位符(index是0)要替换成方法入参里的id这个参数的值。
- 假如后面来调用这个deleteUser()方法,传递进来的id = 1.那么此时就会拿出之前解析好的HTTP template:DELETE /user/{id} HTTP/1.1。然后用传递进来的id = 1替换掉第一个占位符的值,DELETE /user/1 HTTP/1.1
三、总结和后续文章
本文我们聊了OpenFeign如何为FeignClient生成动态代理类,SpringMvcContract组件如何解析FeignClient中标注了SpringMVC注解的方法。
下篇文章我们接着聊OpenFeign接收到一个请求如何处理?