SpringCloud-Feign声明式源码解析

一丶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 扫描生成BeanDefinition

ClassPathScanningCandidateComponentProvider 允许 重写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请求的功能

BeanDefinitionBuilderdefinition=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控制重试,还是结束)

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实现认证信息的透传

classAService{voidprocess(){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中。

                                                                  更多技术资源敬请登录www.ayshuju.com官网

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容