4

SpringCloud 源码学习笔记2——Feign声明式http客户端源码分析 - Cuzzz

 1 year ago
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.
neoserver,ios ssh client

系列文章目录和关于我

一丶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扫描与注册#

image-20230116152112543

通常这个注解标注在 SpringBoot项目启动类,或者配置类,其本质是@Import(FeignClientsRegistrar.class) 。在 SpringBoot源码学习1——SpringBoot自动装配源码解析+Spring如何处理配置类的 中我们讲到过,spring中的ConfigurationClassPostProcessor中会使用ConfigurationClassParser解析配置类,对于@Import注解根据注解导入的类有如下处理

  • 导入的类是ImportSelector类型

    反射实例化ImportSelector

    如果此ImportSelector实现了BeanClassLoaderAware,BeanFactoryAwareEnvironmentAware,EnvironmentAware,ResourceLoaderAware会回调对应的方法

    调用当前ImportSelectorselectImports,然后递归执行处理@Import注解的方法,也就是说可以导入一个具备@Import的类,如果没有``@Import`那么当中配置类解析

  • 导入的类是ImportBeanDefinitionRegistrar类型

    反射实例化ImportBeanDefinitionRegistrar,然后加入到importBeanDefinitionRegistrars集合中后续会回调其registerBeanDefinitions

  • 既不是ImportBeanDefinitionRegistrar也不是ImportSelector,将导入的类当做配置类处理,后续会判断条件注解是否满足,然后解析导入的类,并且解析其父类

这里导入FeignClientsRegistrar 是一个ImportBeanDefinitionRegistrar,因而会回调其registerBeanDefinitions

image-20230117143738317

这里我们关注下 registerFeignClients 此方法会扫描标记有@FeignClient注解的接口,包装成BeanDefinition 注册到BeanDefinitionRegistry,后续在feignClient被依赖注入的时候,根据此BeanDefinition进行实例化

1.扫描FeignClient#

  • 如果我们在@EnableFeignClients注解中的clients 指定了类,那么只会将这些FeignClient 包装成AnnotatedGenericBeanDefinition

  • 否则使用ClassPathScanningCandidateComponentProvider 扫描生成BeanDefinition

    ClassPathScanningCandidateComponentProvider 允许 重写isCandidateComponent方法自定义什么样的BeanDefinition是我们的候选者,以及添加TypeFilter来进行限定(其addExcludeFilter,addIncludeFilter可以设置排除什么,包含什么)

    image-20230117151942928

    这个getScanner方法,对isCandidateComponent进行了重写,限定不能是内部类且不能是注解

    image-20230117152057318
  • 哪些包下的类需要扫描

    如果@EnableFeignClients指定了valuebasePackages,basePackageClasses,那么优先扫描指定的包,如果没有,那么扫描@EnableFeignClients标注配置类所在的包

  • 调用ClassPathScanningCandidateComponentProvider#findCandidateComponents进行扫描

    image-20230117160223831

    底层还是基于ClassLoader#getResources获取资源

2.处理每一个FeignClient 接口的 BeanDefinition#

image-20230117162547413
  • 注册每一个FeignClient的个新化配置

    openFeign 支持每一个 FeignClient接口使用个新化的配置,基于父子容器实现,这点我们在后续进行分析

  • 注册FeignClientBeanDefinition

    这里非常关键,因为我们的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 接口实现类

image-20230221185518919

下面我们看下FeignClient是如何生成代理类的(这里设计到编码器,解码器等组件,这部分内容再发送请求的章节进行解释,这一章节关注于FeignClient是如何生成代理对象的)

image-20230221194421921

1.Feign个性化配置上下文#

image-20230221190407434

FeignContext是Feign允许每一个FeignClient进行个性化配置的关键。

image-20230221191414079

FeignContext是Spring上下文中的一个Bean,其内部使用一个Map保存每一个Feign对应的个性化配置ApplicationContext

1.1.何为Feign的个性化配置ApplicationContext#

image-20230221191656203

如上图这种使用方式,可以为一个FeignClient指定特定的配置类,然后再这个配置类中使用@Bean注入特定的Encoder(将FeignClient入参转化Http报文的一部分的一个组件),Decoder(将Http请求解析为接口出参的一个组件)等。

上图中AClientConfig会被注册到A这个FeignClient的个性化ApplicationContext(下图的黄色部分)

image-20230221191414079

1.2 FeignClient 个性化配置ApplicationContext的父ApplicationContext是Spring容器#

上图中,我们标注了AClient个性化配置ApplicationContext的父容器时Spring上下文(SpringBoot启动后创建的上下文,最大的上下文)。这样设计的目的是,如果当前个性化配置中没有指定Decoder 那么使用默认的容器中的Decoder,如果指定了那么使用个性化的配置。

2.构建Feign创建者,并选择使用的Decoder,Encoder#

image-20230221200129618

2.1 获取个性化配置,或者使用默认配置#

上图中,获取Encoder,Decoder等都使用get方法,get方法内容如下

image-20230221200306671

利用了AnnotationConfigApplicationContext#getBean会去父容器找的特点,实现个性化配置不存在,使用默认配置,具体逻辑在DefaultListableBeanFactory中,如下

image-20230221200602940

2.2 configureFeign根据配置文件 进一步进行配置#

feign还支持我们在配置文件中,进行若干配置,下面展示一部分配置

image-20230221201318720

这些配置都将映射FeignClientProperties

image-20230221201436290

3.生成动态代理对象#

3.1 对于@FeignClient指定url的特殊处理#

如果@FeignClient注解指定了url,将无法进行负载,比如我们业务系统,指定请求外部系统的API,这个API和我们并不在同一个注册中心,那么便无从进行负载均衡。这里会将原本的LoadBalanceFeignClient中的delegate拿出来(这个delegate被LoadBalanceFeignClient装饰,再请求之前会先根据注册中心和负载均衡选择一个实例,然后重构url,然后再使用delegate发送请求)

最终生成代理对象的逻和指定服务名的FeignClient殊途同归

image-20230221202016500

3.2 对于指定应用名称的FeignClient#

生成动态代理对象最终调用到Feign(实现类ReflectiveFeign)#newInstance

image-20230221204152053
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处理

image-20230223222306010
3.2.2使用InvocationHandlerFactory构建出InvocationHandler并进行jdk动态代理。#

这里产生的InvocationHandler(一般为ReflectiveFeign.FeignInvocationHandler,如果由熔断配置那么是HystrixInvocationHandler,此类会在调用失败的时候,回调FeignClient对应的fallBack)

最后使用JDK动态代理生成代理对象。

至此FeignClient接口的动态代理对象生成,那么如何发送请求呢,如果将入参转化为http请求报文,如何将http响应转换为实体对象呢?

image-20230225173331201

四丶Feign 如何发送请求#

上面我们已经分析了FeignClient是如何被扫描,被包装成BeanDefinition注册到BeanDefinitionRegistry中,也看了FeignClientFactoryBean是如何生成FeignClient接口代理类的,至此我们可用知道的我们平时依赖注入的接口其实是FeignClientFactoryBean#getObject生成的动态代理对象。那么这个代理对象是如何发送请求的昵?

image-20230222230601698

1.InvocationHandlerFactory 生成InvocationHandler#

这一步使用工厂模式生成InvocationHandler,如果没有hystrix熔断的配置,那么这里生成的是ReflectiveFeign.FeignInvocationHandler,反之生成的是HystrixInvocationHandler

2.ReflectiveFeign.FeignInvocationHandler#

image-20230222231152217

这里是从dispatch根据Method 获取到MethodHandler(通常是SynchronousMethodHandler

3.SynchronousMethodHandler 发现请求#

image-20230222231609284

3.1根据参数构造RequestTemplate#

这里使用RequestTemplate.Factory(请求模板对象)生成RquestTemplate,比较关键的点是:

  1. 将http请求头,表单参数,路径参数,根据参数的值设置到RequestTemplate

    3.2.1中,我们知道Feign会使用AnnotatedParameterProcessor解析参数注解内容,并解析@RequestMapping注解的内容,放在对应的数据结构中,然后当真正调用的时候,它会根据之前解析的内容,将参数中的值设置到RequestTemplate中,这部分会填充url,表单参数,请求头等。

  2. 使用Encoder对@RequestBody注解标注的参数解析到RequestTemplate

    image-20230223224712952

    Encoder会被回调encoder方法,其中最重要的是SpringEncoder,它负责解析

    image-20230223225149861

    这里并没有说必须标注@RequestBody注解,即使不标注,且没有标注@RequestParam@RequestHead,@PathVariable,都会一股脑,进行序列化写入到body,看来是不支持@RequestPart这种multipart/form-data格式的参数。

3.2 使用Retryer控制重试#

image-20230223230015826

重试器提供两个方法

  • clone:拷贝,注意如果使用浅拷贝,需要考虑多线程情况下的并发问题
  • continueOrPropagate:继续,还是传播(即抛出)异常,如果抛出异常,代表不在重试,反之继续重试

我们可以通过在容器中,或者FeignClient个性化配置类中,注入Retryer实现重试逻辑,如果不注入使用的是默认的实现Retryer.Default。这里需要注意

  1. Feign默认配置是不走重试策略的,当发生RetryableException异常时直接抛出异常。
  2. 并非所有的异常都会触发重试策略,只有发送请求的过程中抛出 RetryableException 异常才会触发异常策略。
  3. 在默认Feign配置情况下,只有在网络调用时发生 IOException 异常时,才会抛出RetryableException,也是就是说链接超时、读超时等不不会触发此异常。

下面是Feign默认的重试策略,总结就是,请求失败后获取间隔多久重试(响应头可指定,或者使用1.5的幂次计算),然后让当前线程休眠,后发起重试

image-20230223231747489

3.3 发送请求并解码#

image-20230225152403418

发送请求并解码的逻辑在executeAndDecoder方法中,这个方法外层是一个while(true)的死循环,如果抛出的异常是RetryableExecption那么交由Retryer来控制是重试,还是抛出异常结束重试。如果抛出的不是重试异常那么将直接结束,不进行重试。

整个excuteAndDecode 可用分为三步:

  1. 回调RequestInterceptor,并将RequestTemplate转化为Request

    image-20230225153749871

    RequestInterceptorapply方法在此被回调,我们可自定义自己的RequestIntereptor实现token透传等操作

    image-20230225154445807

    RequestTemplate(请求模板)转化为Request(请求对象),这里可理解为什么叫请求模板,在FeignClient被动态代理前,就对接口中方法进行了扫描,为每一个方法要发送怎样的报文制定了模板(RequestTemplate)后面针对参数的不同来补充模板,然后用模板生成请求对象,这何尝不是一种单一职责的体验!下面是RequestTemplate如何转变为Request对象

    image-20230225154341704
  2. 使用Client发送请求

    image-20230225154932024

    Client具备两个重要的实现:Default(使用jdk自带的HttpConnection发送http请求,也支持Https)LoadBalancerFeignClient(基于Ribbon实现负载均衡功能增强的装饰器)

    LoadBalancerFeignClient本质是一个装饰器,内部持有了一个Client实现类实例,使用Ribbon根据请求应用名和负载均衡策略选择合适的实例,然后重构url(替换成实际的域名或者ip)然后再使用Client发送http请求。

    Feign默认使用的就是 LoadBalancerFeignClient装饰后的Default(没有连接池,对每一个请求都保持一个长连接),建议替换成其他的Http组件,如OkHttp,Apache的HttpClient等。

    image-20230225160204589
  3. 使用Decoder对响应进行解码

    • 如果FeignClient接口方法返回值类型为Response,那么将直接返回Response,而不会进行解码。
    • 如果请求码为[200,300)的范围,那么将使用Decoder进行解码,解析成接口方法指定的类型
    • 如果请求为404,且指定了需要解码404,那么同使用Decoder进行解码
    • 其余情况使用ErrorDecoder进行解码,根据响应信息决定抛出异常(如果抛出RetryException 将由Retryer控制重试,还是结束)

    image-20230225161451870
3.3.1 Decoder解码#

image-20230225161605655

可看到只要是非FeignException的RuntimeExeption会被包装成DecoderExeption抛出。下面我们看下Decoder的实现类

image-20230225162436495
  • Default

    主要是对Byte数组的支持

    image-20230225162651752
  • StringDecoder

    主要是将body转成字符串

    image-20230225162718395
  • SpringDecoder

    底层使用HttpMessageConverter对body进行装换,会从响应头中拿出Content-Type决定使用什么策略,通常返回json这里将使用基于JacksonMappingJackson2HttpMessageConverter进行转换。(这部分在springmvc源码中有过介绍,不再赘述)

  • ResponeEntityDecoder

    一个Decoder装饰器实现对ResponeEntity的支持

    image-20230225163609432
3.3.2 ErrorDecoder#

image-20230225164114565

ErrorDecoder存在一个实现类Default,它会根据响应头中的Retry-After抛出重试异常,反之抛出FeignExeption,如果是重试异常那么,由Retryer控制重试还是结束

image-20230225164651517

但是遗憾的是,这个重试通常是不生效的,它需要服务提供方返回重试时间塞到Retry-After的头中,且会使用下面这个SimpleDateFormat加锁进行序列化,序列化为Date,咱中国人的服务估计不是这样的时间格式,且现在企业级的服务都是返回code,data,message这样的响应体,http响应状态码基本上都是200,所以想实现这种重试,需要我们自定义Decoder(不是ErrorDecoder)去实现

image-20230225165012526

image-20230225175151180

五丶对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中。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK