SpringCloud 源码学习笔记2——Feign声明式http客户端源码分析
一丶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中。
SpringCloud 源码学习笔记2——Feign声明式http客户端源码分析的更多相关文章
- 吴裕雄--天生自然JAVA SPRING框架开发学习笔记:Spring声明式事务管理(基于Annotation注解方式实现)
在 Spring 中,除了使用基于 XML 的方式可以实现声明式事务管理以外,还可以通过 Annotation 注解的方式实现声明式事务管理. 使用 Annotation 的方式非常简单,只需要在项目 ...
- SpringCloud无废话入门03:Feign声明式服务调用
1.Feign概述 在上一篇的HelloService这个类中,我们有这样一行代码: return restTemplate.getForObject("http://hello-servi ...
- Spring Cloud(Dalston.SR5)--Feign 声明式REST客户端
Spring Cloud 对 Feign 进行了封装,集成了 Ribbon 并结合 Eureka 可以实现客户端的负载均衡,Spring Cloud 实现的 Feign 客户端类名为 LoadBala ...
- Qt Creator 源码学习笔记04,多插件实现原理分析
阅读本文大概需要 8 分钟 插件听上去很高大上,实际上就是一个个动态库,动态库在不同平台下后缀名不一样,比如在 Windows下以.dll结尾,Linux 下以.so结尾 开发插件其实就是开发一个动态 ...
- Spring源码学习笔记12——总结篇,IOC,Bean的生命周期,三大扩展点
Spring源码学习笔记12--总结篇,IOC,Bean的生命周期,三大扩展点 参考了Spring 官网文档 https://docs.spring.io/spring-framework/docs/ ...
- Spring Cloud官方文档中文版-声明式Rest客户端:Feign
官方文档地址为:http://cloud.spring.io/spring-cloud-static/Dalston.SR2/#spring-cloud-feign 文中例子我做了一些测试在:http ...
- SpringCloud 源码系列(6)—— 声明式服务调用 Feign
SpringCloud 源码系列(1)-- 注册中心 Eureka(上) SpringCloud 源码系列(2)-- 注册中心 Eureka(中) SpringCloud 源码系列(3)-- 注册中心 ...
- Underscore.js 源码学习笔记(下)
上接 Underscore.js 源码学习笔记(上) === 756 行开始 函数部分. var executeBound = function(sourceFunc, boundFunc, cont ...
- Underscore.js 源码学习笔记(上)
版本 Underscore.js 1.9.1 一共 1693 行.注释我就删了,太长了… 整体是一个 (function() {...}()); 这样的东西,我们应该知道这是一个 IIFE(立即执行 ...
- AXI_LITE源码学习笔记
AXI_LITE源码学习笔记 1. axi_awready信号的产生 准备接收写地址信号 // Implement axi_awready generation // axi_awready is a ...
随机推荐
- 【SQL基础】多表查询:子查询、连接查询(JOIN)、组合查询(UNION集合运算)
〇.概述 1.内容 JOIN表连接(内连接INNER JOIN/JOIN)(外连接LEFT/RIGHT (OUTER) JOIN) 集合运算-UNION联合 2.建表语句 drop table if ...
- 【大数据-课程】高途-天翼云侯圣文-Day3-实时计算原理解析
〇.老师及课程介绍 一.今日内容 二.实时计算理论解析 1.什么是实时计算 微批处理.流式处理.实时计算 水流和车流的例子 spark streaming就是一种微批处理,水满了才处理,进入下一个地方 ...
- Github Actions 学习笔记
Github Actions是什么? Github Actions 官方介绍:GitHub Actions是一个持续集成和持续交付(CI/CD)平台,允许您自动化构建.测试和部署管道.您可以创建构建和 ...
- STM32点亮LED的代码
led.c #include "led.h" void LED_Config(void) { GPIO_InitTypeDef GPIO_InitStruct; RCC_APB2P ...
- JavaScript:变量:如何声明变量?
声明变量可以用下面几种方式: 但是这几种声明方式肯定是有区别的,主要是上面三种方式的区别,这需要结合window对象和作用域来说明,这里不赘述. 声明变量的时候,推荐使用let,这是ES6新推出的更好 ...
- 万万没想到,go的数据库操作,也能像php一样溜了
Hi,各位go的小伙伴. 很多人都是从php转过来的吧,不知道你们有没有发现,go界的orm并没有像php的orm一样好用.这篇文章里,我们认真的讨论下这个问题,并且会在后面提出解决方案. php的方 ...
- [编程基础] C++多线程入门7-条件变量介绍
原始C++标准仅支持单线程编程.新的C++标准(称为C++11或C++0x)于2011年发布.在C++11中,引入了新的线程库.因此运行本文程序需要C++至少符合C++11标准. 文章目录 7 条件变 ...
- ArcGIS插件 - 易至天工影像加载插件
众所周知,谷歌地图由于其分辨率高.更新速度快,且一直免费,受到行业内外各种人士的青睐,也正因如此,有人利用,有人嫉妒,导致它在国内市场无法再继续下去了.各大相关软件产商也主动或被动下架相关服务,可还是 ...
- Flutter异常监控 - 肆 | Rollbar源码赏析
一. Rollbar可以帮你解决哪些问题 无特别说明,文中Rollbar统指Rollbar-flutter 1. 代码复用 Rollbar官方文档说是纯Dart实现,该特征意味着自带"代码复 ...
- 深入Typescript--01-使用roolup编译Typescript
Typescript是什么? TypeScript是Javascript的超集,遵循最新的ES5/ES6规范.Typescript扩展了Javascript语法. 为什么要用Typescript? 1 ...