SpringCloud 源码学习笔记2——Feign声明式http客户端源码分析 - Cuzzz
source link: https://www.cnblogs.com/cuzzz/p/17154932.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
一丶Feign是什么#
Feign是一种声明式、 模板化的HTTP客户端。在Spring Cloud中使用Feign,可以做到使用HTTP请求访问远程服务,就像调用本地方法一一样的, 开发者完全感知不到这是在调用远程方法,更感知不到在访问HTTP请求。接下来介绍一下Feign的特性,具体如下:
- 可插拔的注解支持,和SpringBoot结合后还支持SpringMvc中的注解
- 支持可插拔的HTTP编码器和解码器。
- 支持Hystrix和它的Fallback。
- 支持Ribbon的负载均衡。
- 支持HTTP请求和响应的压缩。
Feign是一个声明式的Web Service客户端,它的目的就是让Web Service 调用更加简单。它整合了Ribbon和Hystrix,从而不需要开发者针对Feign对其进行整合。Feign 还提供了HTTP请求的模板,通过编写简单的接口和注解,就可以定义好HTTP请求的参数、格式、地址等信息。Feign 会完全代理HTTP的请求,在使用过程中我们只需要依赖注人Bean,然后调用对应的方法传递参数即可。
二丶@EnableFeignClients ——Feign Client扫描与注册#
通常这个注解标注在 SpringBoot项目启动类,或者配置类,其本质是@Import(FeignClientsRegistrar.class)
。在 SpringBoot源码学习1——SpringBoot自动装配源码解析+Spring如何处理配置类的 中我们讲到过,spring中的ConfigurationClassPostProcessor
中会使用ConfigurationClassParser
解析配置类,对于@Import
注解根据注解导入的类有如下处理
-
导入的类是
ImportSelector
类型反射实例化ImportSelector
如果此
ImportSelector
实现了BeanClassLoaderAware
,BeanFactoryAware
,EnvironmentAware
,EnvironmentAware
,ResourceLoaderAware
会回调对应的方法调用当前
ImportSelector
的selectImports
,然后递归执行处理@Import
注解的方法,也就是说可以导入一个具备@Import
的类,如果没有``@Import`那么当中配置类解析 -
导入的类是
ImportBeanDefinitionRegistrar
类型反射实例化
ImportBeanDefinitionRegistrar
,然后加入到importBeanDefinitionRegistrars
集合中后续会回调其registerBeanDefinitions
-
既不是
ImportBeanDefinitionRegistrar
也不是ImportSelector
,将导入的类当做配置类处理,后续会判断条件注解是否满足,然后解析导入的类,并且解析其父类
这里导入FeignClientsRegistrar
是一个ImportBeanDefinitionRegistrar
,因而会回调其registerBeanDefinitions
这里我们关注下 registerFeignClients
此方法会扫描标记有@FeignClient
注解的接口,包装成BeanDefinition 注册到BeanDefinitionRegistry
,后续在feignClient被依赖注入的时候,根据此BeanDefinition进行实例化
1.扫描FeignClient#
-
如果我们在
@EnableFeignClients
注解中的clients
指定了类,那么只会将这些FeignClient 包装成AnnotatedGenericBeanDefinition
-
否则使用
ClassPathScanningCandidateComponentProvider
扫描生成BeanDefinitionClassPathScanningCandidateComponentProvider
允许 重写isCandidateComponent
方法自定义什么样的BeanDefinition是我们的候选者,以及添加TypeFilter
来进行限定(其addExcludeFilter
,addIncludeFilter
可以设置排除什么,包含什么)这个
getScanner
方法,对isCandidateComponent
进行了重写,限定不能是内部类且不能是注解 -
哪些包下的类需要扫描
如果
@EnableFeignClients
指定了value
,basePackages
,basePackageClasses
,那么优先扫描指定的包,如果没有,那么扫描@EnableFeignClients
标注配置类所在的包 -
调用
ClassPathScanningCandidateComponentProvider#findCandidateComponents
进行扫描底层还是基于
ClassLoader#getResources
获取资源
2.处理每一个FeignClient 接口的 BeanDefinition#
-
注册每一个
FeignClient
的个新化配置openFeign 支持每一个
FeignClient
接口使用个新化的配置,基于父子容器实现,这点我们在后续进行分析 -
注册
FeignClient
的BeanDefinition
这里非常关键,因为我们的
FeignClient
接口的BeanDefinition
其记录的class 是 一个接口,spring无法实例化,这里要设置为FactoryBean
,然后后续才能调用FactoryBean#getObject
,生成接口的动态代理类,从而让动态代理类对象实现发送Http请求的功能BeanDefinitionBuilder definition = BeanDefinitionBuilder .genericBeanDefinition(FeignClientFactoryBean.class);
其中会生成一个
FeignClientFactoryBean
的BeanDefinition,并且将@FeignClient中的url
,path
,name
,contextId
等都调用BeanDefinition.addPropertyValue
进行设置,这样spring在实例化的使用会据此来对FeignClientFactoryBean
对象的属性进行填充其中最关键的是,记录了原FeignClient接口的类型,因为
FeignClientFactoryBean
使用的是Jdk动态代理,需要接口类型。至此feignClient类型的bean都被加载并注册到
BeanDefinitionRegistry
,后续在Spring容器刷新时便会触发FeignClient的实例化
三丶FeignClient 是如何实例化动态代理对象的#
在其他spring bean需要注入FeignClient
的时候,将触发FeignClient 的实例化。会先实例化FeignClientFactoryBean
,并且进行属性填充(之前将@FeignClient注解中的内容,使用BeanDefinition.addPropertyValue
进行了绑定,后面由spring据此进行属性填充),然后调用getObject
方法实例化出原本FeignClient 接口
实现类
下面我们看下FeignClient是如何生成代理类的(这里设计到编码器,解码器等组件,这部分内容再发送请求的章节进行解释,这一章节关注于FeignClient是如何生成代理对象的)
1.Feign个性化配置上下文#
FeignContext是Feign允许每一个FeignClient进行个性化配置的关键。
FeignContext是Spring上下文中的一个Bean,其内部使用一个Map保存每一个Feign对应的个性化配置ApplicationContext
1.1.何为Feign的个性化配置ApplicationContext#
如上图这种使用方式,可以为一个FeignClient指定特定的配置类,然后再这个配置类中使用@Bean注入特定的Encoder(将FeignClient入参转化Http报文的一部分的一个组件),Decoder(将Http请求解析为接口出参的一个组件)等。
上图中AClientConfig
会被注册到A这个FeignClient的个性化ApplicationContext(下图的黄色部分)
1.2 FeignClient 个性化配置ApplicationContext的父ApplicationContext是Spring容器#
上图中,我们标注了AClient个性化配置ApplicationContext的父容器时Spring上下文(SpringBoot启动后创建的上下文,最大的上下文)。这样设计的目的是,如果当前个性化配置中没有指定Decoder 那么使用默认的容器中的Decoder,如果指定了那么使用个性化的配置。
2.构建Feign创建者,并选择使用的Decoder,Encoder#
2.1 获取个性化配置,或者使用默认配置#
上图中,获取Encoder,Decoder等都使用get方法,get方法内容如下
利用了AnnotationConfigApplicationContext#getBean
会去父容器找的特点,实现个性化配置不存在,使用默认配置,具体逻辑在DefaultListableBeanFactory
中,如下
2.2 configureFeign根据配置文件 进一步进行配置#
feign还支持我们在配置文件中,进行若干配置,下面展示一部分配置
这些配置都将映射FeignClientProperties
中
3.生成动态代理对象#
3.1 对于@FeignClient指定url的特殊处理#
如果@FeignClient注解指定了url,将无法进行负载,比如我们业务系统,指定请求外部系统的API,这个API和我们并不在同一个注册中心,那么便无从进行负载均衡。这里会将原本的LoadBalanceFeignClient
中的delegate
拿出来(这个delegate被LoadBalanceFeignClient
装饰,再请求之前会先根据注册中心和负载均衡选择一个实例,然后重构url,然后再使用delegate发送请求)
最终生成代理对象的逻和指定服务名的FeignClient殊途同归
3.2 对于指定应用名称的FeignClient#
生成动态代理对象最终调用到Feign(实现类ReflectiveFeign)#newInstance
3.2.1 SpringMvcContract 解析方法生成MethodHandler#
其中生成的MethodHandler
这一步将根据SpringMvcContract
(springmvc合约)去解析接口方法上的注解,最关键的是构建出RequestTemplate
对象,它是请求的模板,后续Http请求对象由它转化而来。
这一步还会解析@RequestMapping注解(包括@PostMapping这种复合注解)
- 解析类上和方法上的value,解析出请求的目的地址,存储到RequestTemplate
- 解析@RequestMapping中的heads,会根据环境变量中的内容得到对应的值,在请求的时候自动携带对应的头
- 解析@RequestMapping的生产
produces
,报文Accept携带这部分内容 - 解析@RequestMapping的消费
consumes
,报文头Content-Type携带这部分内容
这一步还会解析以下三个方法上的注解:
- 将@RequestParam标注的参数,添加到RequestTemplate的
Map<String, Collection<String>> queries
,最终会表单的格式加入到Http报文的body - 将@PathVariable标注的参数,添加到
List<String> formParams
,最终会以路径参数的形式加入到Http路径请求中 - 将@RequestHead标注的参数,添加到
Map<String, Collection<String>> headers
,最终会加入到http请求报文的头部
解析的操作交由AnnotatedParameterProcessor#processArgument
处理
3.2.2使用InvocationHandlerFactory
构建出InvocationHandler
并进行jdk动态代理。#
这里产生的InvocationHandler(一般为ReflectiveFeign.FeignInvocationHandler,如果由熔断配置那么是HystrixInvocationHandler,此类会在调用失败的时候,回调FeignClient对应的fallBack)
最后使用JDK动态代理生成代理对象。
至此FeignClient接口的动态代理对象生成,那么如何发送请求呢,如果将入参转化为http请求报文,如何将http响应转换为实体对象呢?
四丶Feign 如何发送请求#
上面我们已经分析了FeignClient是如何被扫描,被包装成BeanDefinition注册到BeanDefinitionRegistry中,也看了FeignClientFactoryBean
是如何生成FeignClient接口代理类的,至此我们可用知道的我们平时依赖注入的接口其实是FeignClientFactoryBean#getObject
生成的动态代理对象。那么这个代理对象是如何发送请求的昵?
1.InvocationHandlerFactory 生成InvocationHandler#
这一步使用工厂模式生成InvocationHandler,如果没有hystrix熔断的配置,那么这里生成的是ReflectiveFeign.FeignInvocationHandler
,反之生成的是HystrixInvocationHandler
2.ReflectiveFeign.FeignInvocationHandler#
这里是从dispatch
根据Method
获取到MethodHandler
(通常是SynchronousMethodHandler
)
3.SynchronousMethodHandler 发现请求#
3.1根据参数构造RequestTemplate#
这里使用RequestTemplate.Factory(请求模板对象)
生成RquestTemplate
,比较关键的点是:
-
将http请求头,表单参数,路径参数,根据参数的值设置到RequestTemplate
3.2.1中,我们知道Feign会使用AnnotatedParameterProcessor解析参数注解内容,并解析@RequestMapping注解的内容,放在对应的数据结构中,然后当真正调用的时候,它会根据之前解析的内容,将参数中的值设置到RequestTemplate中,这部分会填充url,表单参数,请求头等。
-
使用Encoder对@RequestBody注解标注的参数解析到RequestTemplate
Encoder会被回调
encoder
方法,其中最重要的是SpringEncoder
,它负责解析这里并没有说必须标注
@RequestBody
注解,即使不标注,且没有标注@RequestParam
,@RequestHead
,@PathVariable
,都会一股脑,进行序列化写入到body,看来是不支持@RequestPart
这种multipart/form-data格式的参数。
3.2 使用Retryer控制重试#
重试器提供两个方法
- clone:拷贝,注意如果使用浅拷贝,需要考虑多线程情况下的并发问题
- continueOrPropagate:继续,还是传播(即抛出)异常,如果抛出异常,代表不在重试,反之继续重试
我们可以通过在容器中,或者FeignClient个性化配置类中,注入Retryer实现重试逻辑,如果不注入使用的是默认的实现Retryer.Default
。这里需要注意
- Feign默认配置是不走重试策略的,当发生RetryableException异常时直接抛出异常。
- 并非所有的异常都会触发重试策略,只有发送请求的过程中抛出 RetryableException 异常才会触发异常策略。
- 在默认Feign配置情况下,只有在网络调用时发生 IOException 异常时,才会抛出RetryableException,也是就是说链接超时、读超时等不不会触发此异常。
下面是Feign默认的重试策略,总结就是,请求失败后获取间隔多久重试(响应头可指定,或者使用1.5的幂次计算),然后让当前线程休眠,后发起重试
3.3 发送请求并解码#
发送请求并解码的逻辑在executeAndDecoder
方法中,这个方法外层是一个while(true)
的死循环,如果抛出的异常是RetryableExecption
那么交由Retryer
来控制是重试,还是抛出异常结束重试。如果抛出的不是重试异常那么将直接结束,不进行重试。
整个excuteAndDecode 可用分为三步:
-
回调RequestInterceptor,并将RequestTemplate转化为Request
RequestInterceptor
的apply
方法在此被回调,我们可自定义自己的RequestIntereptor
实现token
透传等操作RequestTemplate(请求模板)
转化为Request(请求对象)
,这里可理解为什么叫请求模板,在FeignClient被动态代理前,就对接口中方法进行了扫描,为每一个方法要发送怎样的报文制定了模板(RequestTemplate)后面针对参数的不同来补充模板,然后用模板生成请求对象,这何尝不是一种单一职责的体验!下面是RequestTemplate如何转变为Request对象 -
使用
Client
发送请求Client具备两个重要的实现:
Default
(使用jdk自带的HttpConnection发送http请求,也支持Https)LoadBalancerFeignClient
(基于Ribbon实现负载均衡功能增强的装饰器)LoadBalancerFeignClient
本质是一个装饰器,内部持有了一个Client实现类实例,使用Ribbon根据请求应用名和负载均衡策略选择合适的实例,然后重构url(替换成实际的域名或者ip)然后再使用Client发送http请求。Feign默认使用的就是
LoadBalancerFeignClient
装饰后的Default(没有连接池,对每一个请求都保持一个长连接)
,建议替换成其他的Http组件,如OkHttp,Apache的HttpClient等。 -
使用
Decoder
对响应进行解码- 如果FeignClient接口方法返回值类型为
Response
,那么将直接返回Response
,而不会进行解码。 - 如果请求码为
[200,300)
的范围,那么将使用Decoder
进行解码,解析成接口方法指定的类型 - 如果请求为404,且指定了需要解码404,那么同使用
Decoder
进行解码 - 其余情况使用
ErrorDecoder
进行解码,根据响应信息决定抛出异常(如果抛出RetryException 将由Retryer控制重试,还是结束)
- 如果FeignClient接口方法返回值类型为
3.3.1 Decoder解码#
可看到只要是非FeignException的RuntimeExeption会被包装成DecoderExeption抛出。下面我们看下Decoder
的实现类
-
Default
主要是对Byte数组的支持
-
StringDecoder
主要是将body转成字符串
-
SpringDecoder
底层使用HttpMessageConverter对body进行装换,会从响应头中拿出
Content-Type
决定使用什么策略,通常返回json这里将使用基于Jackson
的MappingJackson2HttpMessageConverter
进行转换。(这部分在springmvc源码中有过介绍,不再赘述) -
ResponeEntityDecoder
一个Decoder装饰器实现对
ResponeEntity
的支持
3.3.2 ErrorDecoder#
ErrorDecoder存在一个实现类Default
,它会根据响应头中的Retry-After
抛出重试异常,反之抛出FeignExeption,如果是重试异常那么,由Retryer控制重试还是结束
但是遗憾的是,这个重试通常是不生效的,它需要服务提供方返回重试时间塞到Retry-After
的头中,且会使用下面这个SimpleDateFormat加锁进行序列化,序列化为Date,咱中国人的服务估计不是这样的时间格式,且现在企业级的服务都是返回code,data,message
这样的响应体,http响应状态码基本上都是200,所以想实现这种重试,需要我们自定义Decoder(不是ErrorDecoder)
去实现
五丶对Feign进行扩展#
可看到Feign是很模块的化的,也提供了很多扩展的接口让我们做自定义,以下是笔者做过(或者见过)的一些扩展。
1.自定义RequestIntercptor
实现认证信息的透传#
class AService{
void process(){
feign.getSomething(xxxx);
}
}
我们服务中,需要AService调用process的时候,将认证信息透传到微服务提供方,我们自定义RequestIntercptor拿到当前的请求信息,然后获取其中的认证信息通过apply
方法写入到RequestTemplate
的head中。
2.SpringMVC 统一返回结果集解包装#
基于SpringBoot的服务,通过使用SpringMVC ResponseAdvice
实现统一包装集,即使业务逻辑抛出异常,也通过ExeptionHandler
进行统一包装,包装形式如下
{
"code":"业务错误码",
"data": "业务数据",
"message":"错误信息"
}
这就导致,我们微服务调用方,使用feign的时候,结果返回值也是这种统一返回结果集形式的对象,需要自己对code进行校验,然后选择抛出异常,还是反序列化为目标对象。
我们可以实现自己的Decoder结果这一问题!在Decoder中对code
进行判断,决定抛出异常,还是序列化data
。但是需要注意Decoder
抛出的异常,都将被包装为FeignExeption
或者DecodeExption
,所以调用方还需要针对这两种异常配置ExeptionHandler
3.自定义RequestIntercptor
实现分布式链路追踪#
原理同一,只不过拿的是调用方请求中的 traceId,将traceId,写到RequestTemplate的head中。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK