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