新来个架构师,用48张图把OpenFeign原理讲的炉火纯青~~
大家好,我是三友~~
在很久之前,我写过两篇关于OpenFeign和Ribbon这两个SpringCloud核心组件架构原理的文章
但是说实话,从我现在的角度来看,这两篇文章的结构和内容其实还可以更加完善
刚好我最近打算整个SpringCloud各个组件架构原理的小册子
所以趁着这个机会,我就来重新写一下这两篇文章,弥补之前文章的不足
这一篇文章就先来讲一讲OpenFeign的核心架构原理
整篇文章大致分为以下四个部分的内容:
第一部分,脱离于SpringCloud,原始的Feign是什么样的?
第二部分,Feign的核心组件有哪些,整个执行链路是什么样的?
第三部分,SpringCloud是如何把Feign融入到自己的生态的?
第四部分,OpenFeign有几种配置方式,各种配置方式的优先级是什么样的?
好了,话不多说,接下来就直接进入主题,来探秘OpenFeign核心架构原理
原始Feign是什么样的?
在日常开发中,使用Feign很简单,就三步
第一步:引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
第二步:在启动引导类加上@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这部分源码,看看到底生效的是哪个配置

总结
到这,总算讲完了OpenFeign的核心架构原理了
这又是一篇洋洋洒洒的万字长文
由于OpenFeign它只是一个框架,并没有什么复杂的机制
所以整篇文章还是更多偏向源码方面
不知道你看起来感觉如何
如果你感觉还不错,欢迎点赞、在看、收藏、转发分享给其他需要的人
你的支持就是我更新的最大动力,感谢感谢!
更多SpringCloud系列的文章,可以在公众号后台菜单栏中查看。
好了,本文就讲到这里,让我们下期再见,拜拜!
往期热门文章推荐
扫码或者搜索关注公众号 三友的java日记 ,及时干货不错过,公众号致力于通过画图加上通俗易懂的语言讲解技术,让技术更加容易学习,回复 面试 即可获得一套面试真题。

【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
2023-02-28 如何实现延迟任务,这11种方式才算优雅!