正所谓麻雀虽小五脏俱全,HTTP 调用看着简单,实则下面隐藏的是一套非常复杂的流程。
从上古时代 jsp+servlet,到后面的 SpringMVC,在 HTTP 请求解析和封装上同样是煞费苦心。
我们在学习中经常会碰到这种 case,有些开源组件不显山来不露水,乍一看功能很简单,配置起来也不麻烦,让人感觉实现起来也不难。实际上我们所看到的只是冰山上的一角,在冰山下面隐藏的巨大基座才是这套技术的全貌。
就像 Feign 一样,往往以一个注解开场的项目,背后的故事都不简单。接下来,我们就潜入深海,看看 Feign 这座冰山的架构全景。
武装到牙齿 - Feign 体系架构
大家有没有看过一部叫做《黑衣人》的电影?这部电影讲述了搞笑特工联手对抗外星生物,维护世界和平的故事。里面有一句经典台词叫做武装到牙齿,意思是几位特工身上任何部位都被武装到位,甚至牙齿也不放过。
Feign
就是这样一位被武装到牙齿的特工,Feign 的每个运作流程都包含了复杂的业务处理,Netflix
对 Feign
更是关爱有加,甚至还给配备了两件重武器:Ribbon
和 Hystrix
。由于Feign 的调用链路比较长,所以我删减了很多支线剧情,只玩主线剧情,我们分为上下半场两张图给大家介绍 Feign 的架构全貌。
如果用一句话来介绍 Feign
,那就是:声明一个代理接口,服务调用者通过调用这个代理接口的方式来调用远程服务。这样一来,调用远程方法就如同调用本地接口一样方便。
上半场 - 构建请求
左右护法:大伙现在看出 Feign 是个什么腕儿了吗?看那身旁站着 Ribbon
和 Hystrix
,左青龙右白虎,给 Feign
保驾护航。没错,Feign
自己兜里就揣着Ribbon
和 Hystrix
两把重武器,引入Feign
依赖的同时这两个组件也会被一同引入。
Ribbon
:利用负载均衡策略选定目标机器。Hystrix
:根据熔断器的开启状态,决定是否发起此次调用
动态代理
:Feign 是通过一个代理接口进行远程调用,这一步就是为了构造接口的动态代理对象,用来代理远程服务的真实调用,这样你就可以像调用本地方法一样发起 HTTP 请求,不需要像Ribbon
或者Eureka
那样在方法调用的地方提供服务名。在Feign
中动态代理是通过Feign.build
返回的构造器来装配相关参数,然后调用ReflectFeign
的newInstance
方法创建的。这里就应用到了Builder
设计模式。Contract:协议
,顾名思义,就像HTTP
协议,RPC
协议一样,Feign
也有自己的一套协议的规范,只不过他解析的不是 HTTP 请求,而是上一步提到的动态代理类。通过解析动态代理接口+Builder
模式,Contract
协议会构造复杂的元数据对象MethodMetadata
,这里面包含了动态代理接口定义的所有特征。接下来,根据这些元数据生成一系列MethodHandler
对象用来处理Request
和Response
请求。Contract
具有高度可扩展性,可以经由对 Contract 的扩展,将 Feign 集成到其他开源组件之中。
番外篇 - 关于 Builder 模式
Builder 是设计模式中的一种,用来简化复杂组件的装配过程,假如用传统方式构建一个House
类,那应该是这样写:
House house = ne House();
house.setWindow("open");
house.setDoor("close");
而 Builder
模式是用链式构造的方式创建复杂对象,比如这种形式House.builder().window("open").door("close").build()
这里教大家一个简单的实现方式,那就是 lombok
小工具的@Builder
注解,只要在 pom
中添加 lombok
依赖,并且在 IDE 中添加 lombok 的插件,就可以用注解的方法,不用写一行代码就能实现 Builder 模式。
下半场 - 发起调用
拦截器
:拦截器是 Spring 处理网络请求的经典方案,Feign 这里也沿用了这个做法,通过一系列的拦截器对 Request 和 Response 对象进行装饰,比如通过RequestInterceptor
给Request
对象构造请求头。整装待发之后,就是正式发起调用的时候了。发起请求
:又到了左右护法的出场镜头了。这哼哈二将绝不放过开头和结尾两处重要镜头,正所谓从头到尾都参与了进来。重试:Feign
这里借助Ribbon
的配置重试器实现了重试操作,可以指定对当前服务节点发起重试,也可以让 Feign 换一个服务节点重试。降级
:Feign
接口在声明时可以指定 Hystrix 的降级策略实现类,如果达到了Hystrix
的超时判定,或得到了异常结果,将执行指定的降级逻辑。
Feign 之动态代理
动态代理 是面试场景里的高频问题,从 Spring 中 AOP 的实现方式,到让自己手写一个动“态代理实例,这个话题仿佛成了面试中很有仪式感的一个问题。面试官开口问到 请说出你对 ” aop ”的理解 ,感觉就像汪峰导师在问 你的梦想是什么 。
可是代理就代理好了,为什么要加个 “动态” 二字呢,难道还有静态代理一说?简单的说,所谓动态是相对于 “静态编译” 来说的。在 java 中,假如我们在编译期不知道这个对象是何方神圣,只能等待程序执行的时候,也就在是运行期才能知道,那么我们就称之为 “动态” 获取对象(比如通过类名+ 反射创建一个实例)。而所谓 “动态代理” ,就是指在运行期指定一个代理对象,以接管的方式执行后续的任务。就是这么简单。
而 Feign
的 动态代理 是个偷天换日的过程。我们把目标服务看做一个新娘子,当服务调用请求发出后,一伙迎亲车队浩浩荡荡地出发去迎亲。这时候,一伙打着 Feign
名号的抢亲小队出现了,他们利用 “动态代理” 的方式,截胡了迎亲车队,自个儿当起了新郎官去接新娘。我们这就来看看这伙抢亲小队是怎么工作的。
抢亲小队 - 截胡方法调用
问:截胡迎亲小队总共分几步?总共分四步:
我们来一起看下Feign
的源码。
- ·GetObject· :原配的迎亲小队出发了,一路喊着 “接对象咯”(getObject),成功吸引到了抢亲小队的注意力。
- 这一步是
FeignClientFactoryBean
的getObject
方法发起的,为了获取一个可以发起远程调用的实体方法,只是这时它还不知道,getObject ()
方法获取到的其实是一个代理对象。 - 我们知道
Feign
实际上是调用了@FeignClient
注解所修饰的接口,FeignClientFactoryBean
封装了这个接口中所包含的配置信息,比如Eureka
服务名称,服务调用的路径,降级逻辑的处理类,等等。
- 创建代理对象:一伙抢亲小队听到风声,立马开始着手准备埋伏。在上一步的
getObject
方法的最后做好了埋伏,开始了偷天换日的过程。
- 上一步中
getObject
最后一行,经由Targeter
类的转发,抢亲小队登场了。 - 下面就是创建代理对象的时候了,Feign 的所有代理实例均通过
ReflectiveFeign.newInstance
创建,他的底层是采用Builder
模式,将@FeignClient
接口的特征,方法名,参数等等一系列信息提取出来,拼装成Java 反射机制中通用的 Method 类。 - 偷天换日:这一步是整个动态代理机制中的核心操作。在
newInstance·
的创建过程中,Feign 通过实现 JDK 的InvocationHandler
接口(所有动态代理方案几乎都和它有关联),将自己的Handler
和上一步组装的Method
进行了关联,这样一来,所有对这个接口方法的调用,都将被 Feign 自定义的InvocationHandler
给接管。这种动态代理的方式,我们叫做 JDK 动态代理 - 所有一切就绪,就等截胡方法调用了
- 拦截请求:这时迎亲车队经过了,因为我们在前一步已经做了埋伏,这个方法调用立马被我们自己人,也就是上一步中自定义的
InvocationHandler
截胡了。
SynchronousMethodHandler
这时接管了invoke
方法。构造Request
请
求,装模作样当起了新郎官。(在构造 Request 请求的同时还会涉及一系列的参数拼装和加密等步骤)
- 发起调用:最后一步,借助
LoadBalancerFeignClient
发起了真正的HTTP
请求。从这个类的名字大家可以看到,似乎和负载均衡有点关系?没错,这个就是 Feign 和 Ribbon组合而成的一个 Client 类,它会利用 Ribbon 实现超时重试等操作。前面讲到过,Feign
是武装到牙齿的组件,每一步的背后都有非常复杂的处理流程。
Spring 的动态代理
Spring 的 AOP 有两种动态代理方式,其中一种就是前面讲到的Feign
采用的方式:JDK 动态代理。在 Spring 中通过 JdkDynamicAopProxy
实现。它有两个特点
- 实现
InvocationHandler
接口,接管invoke
方法实现自己的业务逻辑,所有调用都会被传递到InvocationHandler
的invoke
方法,通过
Proxy.newProxyInstance
获取动态代理对象 - 被代理的对象必须实现了某个接口,不能代理无接口的类。Spring 还有一种动态代理的方式,那就是
CGLIB
,它并不强制代理类实现某个接口。在实际使用中,CGLIB
在代理对象的性能方面比JDKDynamic
要快很多,但是在创建代理对象上的时间花费也相当长。所以,如果你的类并没有实现接口,或者是单例模式的类不需要重复创建,建议使用CGLIB
的方式。
本文已收录至我的个人网站:程序员波特,主要记录Java相关技术系列教程,共享电子书、Java学习路线、视频教程、简历模板和面试题等学习资源,让想要学习的你,不再迷茫。