在很久之前,我写过两篇关于OpenFeign和Ribbon这两个SpringCloud核心组件架构原理的文章
但是说实话,从我现在的角度来看,这两篇文章的结构和内容其实还可以更加完善
刚好我最近打算整个SpringCloud各个组件架构原理的小册子
所以趁着这个机会,我就来重新写一下这两篇文章,弥补之前文章的不足
这一篇文章就先来讲一讲OpenFeign的核心架构原理
整篇文章大致分为以下四个部分的内容:
第一部分,脱离于SpringCloud,原始的Feign是什么样的?
第二部分,Feign的核心组件有哪些,整个执行链路是什么样的?
第三部分,SpringCloud是如何把Feign融入到自己的生态的?
第四部分,OpenFeign有几种配置方式,各种配置方式的优先级是什么样的?
好了,话不多说,接下来就直接进入主题,来探秘OpenFeign核心架构原理
原始Feign是什么样的?
在日常开发中,使用Feign很简单,就三步
引入依赖
org.springframework.cloud
spring-cloud-starter-openfeign
2.2.5.RELEASE
在启动引导类加上@EnableFeignClients注解
@SpringBootApplication
@EnableFeignClients
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
写个FeignClient接口
@FeignClient(name = "order")
@RequestMapping("/order")
public interface OrderApiClient {
@GetMapping
Order queryOrder(@RequestParam("orderId") Long orderId);
}
之后当我们要使用时,只需要注入OrderApiClient对象就可以了
虽然使用方便,但这并不是Feign最原始的使用方式,而是SpringCloud整合Feign之后的使用方式
Feign最开始是由Netflix开源的
后来SpringCloud就将Feign进行了一层封装,整合到自己的生态,让Feign使用起来更加简单
并同时也给它起了一个更高级的名字,OpenFeign
接下来文章表述有时可能并没有严格区分Feign和OpenFeign的含义,你知道是这么个意思就行了。
Feign本身有自己的使用方式,也有类似Spring MVC相关的注解,如下所示:
public interface OrderApiClient {
@RequestLine("GET /order/{orderId}")
Order queryOrder(@Param("orderId") Long orderId);
}
OrderApiClient对象需要手动通过Feign.builder()来创建
public class FeignDemo {
public static void main(String[] args) {
OrderApiClient orderApiClient = Feign.builder()
.target(OrderApiClient.class, "http://localhost:8088");
orderApiClient.queryOrder(9527L);
}
}
Feign的本质:动态代理 + 七大核心组件
相信稍微了解Feign的小伙伴都知道,Feign底层其实是基于JDK动态代理来的
所以Feign.builder()最终构造的是一个代理对象
Feign在构建动态代理的时候,会去解析方法上的注解和参数
获取Http请求需要用到基本参数以及和这些参数和方法参数的对应关系
比如Http请求的url、请求体是方法中的第几个参数、请求头是方法中的第几个参数等等
之后在构建Http请求时,就知道请求路径以及方法的第几个参数对应是Http请求的哪部分数据
当调用动态代理方法的时候,Feign就会将上述解析出来的Http请求基本参数和方法入参组装成一个Http请求
然后发送Http请求,获取响应,再根据响应的内容的类型将响应体的内容转换成对应的类型
这就是Feign的大致原理
图片
在整个Feign动态代理生成和调用过程中,需要依靠Feign的一些核心组件来协调完成
如下图所示是Feign的一些核心组件
这些核心组件可以通过Feign.builder()进行替换
由于组件很多,这里我挑几个重要的跟大家讲一讲
1、Contract
图片
前面在说Feign在构建动态代理的时候,会去解析方法上的注解和参数,获取Http请求需要用到基本参数
而这个Contract接口的作用就是用来干解析这件事的
Contract的默认实现是解析Feign自己原生注解的
图片
解析时,会为每个方法生成一个MethodMetadata对象
图片
MethodMetadata就封装了Http请求需要用到基本参数以及这些参数和方法参数的对应关系
SpringCloud在整合Feign的时候,为了让Feign能够识别Spring MVC的注解,所以就自己实现了Contract接口
2、Encoder
通过名字也可以看出来,这个其实用来编码的
具体的作用就是将请求体对应的方法参数序列化成字节数组
Feign默认的Encoder实现只支持请求体对应的方法参数类型为String和字节数组
图片
如果是其它类型,比如说请求体对应的方法参数类型为AddOrderRequest.class类型,此时就无法对AddOrderRequest对象进行序列化
这就导致默认情况下,这个Encoder的实现很难用
于是乎,Spring就实现了Encoder接口
图片
可以将任意请求体对应的方法参数类型对象序列化成字节数组
3、Decoder
Decoder的作用恰恰是跟Encoder相反
Encoder是将请求体对应的方法参数序列化成字节数组
而Decoder其实就是将响应体由字节流反序列化成方法返回值类型的对象
Decoder默认情况下跟Encoder的默认情况是一样的,只支持反序列化成字节数组或者是String
所以,Spring也同样实现了Decoder,扩展它的功能
图片
可以将响应体对应的字节流反序列化成任意返回值类型对象
4、Client
从接口方法的参数和返回值其实可以看出,这其实就是动态代理对象最终用来执行Http请求的组件
默认实现就是通过JDK提供的HttpURLConnection来的
除了这个默认的,Feign还提供了基于HttpClient和OkHttp实现的
在项目中,要想替换默认的实现,只需要引入相应的依赖,在构建Feign.builder()时设置一下就行了
SpringCloud环境底下会根据引入的依赖自动进行设置
除了上述的三个实现,最最重要的当然是属于它基于负载均衡的实现
如下是OpenFeign用来整合Ribbon的核心实现
图片
这个Client会根据服务名,从Ribbon中获取一个服务实例的信息,也就是ip和端口
之后会通过ip和端口向服务实例发送Http请求
5、InvocationHandlerFactory
InvocationHandler我相信大家应该都不陌生
对于JDK动态代理来说,必须得实现InvocationHandler才能创建动态代理
InvocationHandler的invoke方法实现就是动态代理走的核心逻辑
而InvocationHandlerFactory其实就是创建InvocationHandler的工厂
所以,这里就可以猜到,通过InvocationHandlerFactory创建的InvocationHandler应该就是Feign动态代理执行的核心逻辑
InvocationHandlerFactory默认实现是下面这个
SpringCloud环境下默认也是使用它的这个默认实现
所以,我们直接去看看InvocationHandler的实现类FeignInvocationHandler
图片
从实现可以看出,除了Object类的一些方法,最终会调用方法对应的MethodHandler的invoke方法
所以注意注意,这个MethodHandler就封装了Feign执行Http调用的核心逻辑,很重要,后面还会提到
图片
虽然说默认情况下SpringCloud使用是默认实现,最终使用FeignInvocationHandler
但是当其它框架整合SpringCloud生态的时候,为了适配OpenFeign,有时会自己实现InvocationHandler
比如常见的限流熔断框架Hystrix和Sentinel都实现了自己的InvocationHandler
这样就可以对MethodHandler执行前后,也就是Http接口调用前后进行限流降级等操作。
6、RequestInterceptor
图片
RequestInterceptor它其实是一个在发送请求前的一个拦截接口
通过这个接口,在发送Http请求之前再对Http请求的内容进行修改
比如我们可以设置一些接口需要的公共参数,如鉴权token之类的
@Component
public class TokenRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
template.header("token", "token值");
}
}
7、Retryer
这是一个重试的组件,默认实现如下
默认情况下,最大重试5次
在SpringCloud下,并没有使用上面那个实现,而使用的是下面这个实现
图片
所以,SpringCloud下默认是不会进行重试
小总结
这一节主要是介绍了7个Feign的核心组件以及Spring对应的扩展实现
为了方便你查看,我整理了如下表格
接口 | 作用 | Feign默认实现 | Spring实现 |
Contract | 解析方法注解和参数,将Http请求参数和方法参数对应 | Contract.Default | SpringMvcContract |
Encoder | 将请求体对应的方法参数序列化成字节数组 | Encoder.Default | SpringEncoder |
Decoder | 将响应体的字节流反序列化成方法返回值类型对象 | Decoder.Default | SpringDecoder |
Client | 发送Http请求 | Client.Default | LoadBalancerFeignClient |
InvocationHandlerFactory | InvocationHandler工厂,动态代理核心逻辑 | InvocationHandlerFactory.Default | 无 |
RequestInterceptor | 在发送Http请求之前,再对Http请求的内容进行拦截修改 | 无 | 无 |
Retryer | 重试组件 | Retryer.Default | 无 |
除了这些之外,还有一些其它组件这里就没有说了
比如日志级别Logger.Level,日志输出Logger,有兴趣的可以自己查看
Feign核心运行原理分析
上一节说了Feign核心组件,这一节我们来讲一讲Feign核心运行原理,主要分为两部分内容:
- 动态代理生成原理
- 一次Feign的Http调用执行过程
1、动态代理生成原理
这里我先把上面的Feign原始使用方式的Demo代码再拿过来
public class FeignDemo {
public static void main(String[] args) {
OrderApiClient orderApiClient = Feign.builder()
.target(OrderApiClient.class, "http://localhost:8088");
orderApiClient.queryOrder(9527L);
}
}
通过Demo可以看出,最后是通过Feign.builder().target(xx)获取到动态代理的
而上述代码执行逻辑如下所示:
图片
最终会调用ReflectiveFeign的newInstance方法来创建动态代理对象
而ReflectiveFeign内部设置了前面提到的一些核心组件
接下我们来看看newInstance方法
这个方法主要就干两件事:
第一件事首先解析接口,构建每个方法对应的MethodHandler
MethodHandler在前面讲InvocationHandlerFactory特地提醒过
动态代理(FeignInvocationHandler)最终会调用MethodHandler来处理Feign的一次Http调用
在解析接口的时候,就会用到前面提到的Contract来解析方法参数和注解,生成MethodMetadata,这里我代码我就不贴了
第二件事通过InvocationHandlerFactory创建InvocationHandler
然后再构建出接口的动态代理对象
ok,到这其实就走完了动态代理的生成过程
所以动态代理生成逻辑很简单,总共也没几行代码,画个图来总结一下
图片
2、一次Feign的Http调用执行过程
前面说了,调用接口动态代理的方式时,通过InvocationHandler(FeignInvocationHandler),最终交给MethodHandler的invoke方法来执行
MethodHandler是一个接口,最终会走到它的实现类SynchronousMethodHandler的invoke方法实现
SynchronousMethodHandler中的属性就是我们前面提到的一些组件
由于整个代码调用执行链路比较长,这里我就不截代码了,有兴趣的可以自己翻翻
不过这里我画了一张图,可以通过这张图来大致分析整个Feign一次Http调用的过程
图片
- 首先就是前面说的,进入FeignInvocationHandler,找到方法对应的SynchronousMethodHandler,调用invoke方法实现
- 之后根据MethodMetadata和方法的入参,构造出一个RequestTemplate,RequestTemplate封装了Http请求的参数,在这个过程中,如果有请求体,那么会通过Encoder序列化
- 然后调用RequestInterceptor,通过RequestInterceptor对RequestTemplate进行拦截扩展,可以对请求数据再进行修改
- 再然后将RequestTemplate转换成Request,Request其实跟RequestTemplate差不多,也是封装了Http请求的参数
- 接下来通过Client去根据Request中封装的Http请求参数,发送Http请求,得到响应Response
- 最后根据Decoder,将响应体反序列化成方法返回值类型对象,返回
这就是Feign一次Http调用的执行过程
如果有设置重试,那么也是在这个阶段生效的
SpringCloud是如何整合Feign的?
SpringCloud在整合Feign的时候,主要是分为两部分
- 核心组件重新实现,支持更多SpringCloud生态相关的功能
- 将接口动态代理对象注入到Spring容器中
第一部分核心组件重新实现前面已经都说过了,这里就不再重复了
至于第二部分我们就来好好讲一讲,Spring是如何将接口动态代理对象注入到Spring容器中的
1、将FeignClient接口注册到Spring中
使用OpenFeign时,必须加上@EnableFeignClients
这个注解就是OpenFeign的发动机
图片
@EnableFeignClients最后通过@Import注解导入了一个FeignClientsRegistrar
图片
FeignClientsRegistrar实现了ImportBeanDefinitionRegistrar
所以最终Spring在启动的时候会调用registerBeanDefinitions方法实现
之所以会调用registerBeanDefinitions方法,是@Import注解的作用,不清楚的同学可以看一下扒一扒Bean注入到Spring的那些姿势,你会几种?
图片
最终会走到registerFeignClients这个方法
这个方法虽然比较长,主要是干了下面这个2件事:
第一件事,扫描@EnableFeignClients所在类的包及其子包(如果有指定包就扫指定包),找出所有加了@FeignClient注解的接口,生成一堆BeanDefinition
这个BeanDefinition包含了这个接口的信息等信息
第二件事,将扫描到的这些接口注册到Spring容器中
图片
在注册的时候,并非直接注册接口类型,而是FeignClientFactoryBean类型
图片
好了,到这整个@EnableFeignClients启动过程就结束了
虽然上面写的很长,但是整个@EnableFeignClients其实也就只干了一件核心的事
扫描到所有的加了@FeignClient注解的接口
然后为每个接口生成一个Bean类型为FeignClientFactoryBean的BeanDefinition
最终注册到Spring容器中
图片
2、FeignClientFactoryBean的秘密
上一节说到,每个接口都对应一个class类型为FeignClientFactoryBean的BeanDefinition
图片
如上所示,FeignClientFactoryBean是一个FactoryBean
并且FeignClientFactoryBean的这些属性,是在生成BeanDefinition的时候设置的
图片
并且这个type属性就是代表的接口类型
由于实现了FactoryBean,所以Spring启动过程中,一定为会调用getObject方法获取真正的Bean对象
FactoryBean的作用就不说了,不清楚的小伙伴还是可以看看扒一扒Bean注入到Spring的那些姿势,你会几种?这篇文章
getObject最终会走到getTarget()方法
图片
从如上代码其实可以看出来,最终还是会通过Feign.builder()来创建动态代理对象
只不过不同的是,SpringCloud会替换Feign默认的组件,改成自己实现的
总的来说,Spring是通过FactoryBean的这种方式,将Feign动态代理对象添加到Spring容器中
OpenFeign的各种配置方式以及对应优先级
既然Feign核心组件可以替换,那么在SpringCloud环境下,我们该如何去配置自己的组件呢?
不过在说配置之前,先说一下FeignClient配置隔离操作
在SpringCloud环境下,为了让每个不同的FeignClient接口配置相互隔离
在应用启动的时候,会为每个FeignClient接口创建一个Spring容器,接下来我就把这个容器称为FeignClient容器
这些FeignClient的Spring容器有一个相同的父容器,那就是项目启动时创建的容器
图片
SpringCloud会给每个FeignClient容器添加一个默认的配置类FeignClientsConfiguration配置类
图片
这个配置类就声明了各种Feign的组件
图片
所以,默认情况下,OpenFeign就使用这些配置的组件构建代理对象
知道配置隔离之后,接下来看看具体的几种方式配置以及它们之间的优先级关系
1、通过@EnableFeignClients注解的defaultConfiguration属性配置
举个例子,比如我自己手动声明一个Contract对象,类型为MyContract
public class FeignConfiguration {
@Bean
public Contract contract(){
return new MyContract();
}
}
注意注意,这里FeignConfiguration我没加@Configuration注解,原因后面再说
此时配置如下所示:
@EnableFeignClients(defaultConfiguration = FeignConfiguration.class)
之后这个配置类会被加到每个FeignClient容器中,所以这个配置是对所有的FeignClient生效
并且优先级大于默认配置的优先级
比如这个例子就会使得FeignClient使用我声明的MyContract,而不是FeignClientsConfiguration中声明的SpringMvcContract
2、通过@FeignClient注解的configuration属性配置
还以上面的FeignConfiguration配置类举例,可以通过@FeignClient注解配置
@FeignClient(name = "order", configuration = FeignConfiguration.class)
此时这个配置类会被加到自己FeignClient容器中,注意是自己FeignClient容器
所以这种配置的作用范围是自己的这个FeignClient
并且这种配置的优先级是大于@EnableFeignClients注解配置的优先级
3、在项目启动的容器中配置
前面提到,由于所有的FeignClient容器的父容器都是项目启动的容器
所以可以将配置放在这个项目启动的容器中
还以FeignConfiguration为例,加上@Configuration注解,让项目启动的容器的扫描到就成功配置了
这种配置的优先级大于前面提到的所有配置优先级
并且是对所有的FeignClient生效
所以,这就是为什么使用注解配置时为什么配置类不能加@Configuration注解的原因,因为一旦被项目启动的容器扫描到,这个配置就会作用于所有的FeignClient,并且优先级是最高的,就会导致你其它的配置失效,当然你也可以加@Configuration注解,但是一定不能被项目启动的容器扫到
4、配置文件
除了上面3种编码方式配置,OpenFeign也是支持通过配置文件的方式进行配置
并且也同时支持对所有FeignClient生效和对单独某个FeignClient生效
对所有FeignClient生效配置:
feign:
client:
config:
default: # default 代表对全局生效
contract: com.sanyou.feign.MyContract
对单独某个FeignClient生效配置:
feign:
client:
config:
order: # 具体的服务名
contract: com.sanyou.feign.MyContract
在默认情况下,这种配置文件方式优先级最高
但是如果你在配置文件中将配置项feign.client.default-to-properties设置成false的话,配置文件的方式优先级就是最低了
feign:
client:
default-to-properties: false
小总结
这一节,总共总结了4种配置OpenFeign的方式以及它们优先级和作用范围
画张图来总结一下
图片
如果你在具体使用的时候,还是遇到了一些优先级的问题,可以debug这部分源码,看看到底生效的是哪个配置
图片
图片