Feign原理分析
背景
近几年来随着微服务的流行,微服务治理成为了大家越来越关注的重点。目前市面上有很多成熟的方案,其中Spring Cloud Netflix
因为Netflix
成功落地实现而流行起来。本文主要介绍Spring Cloud Netfix
中服务间调用框架Feign
具体如何实现的,通过阅读本文后,希望能让你对Feign
是什么以及如何实现的有一个清楚的认识。
Feign
是一种声明式、模板化的HTTP
客户端,可以让HTTP
的远程调用就像调用本地方法一样,无感知发送远程HTTP
请求。Feign
通过为目标服务接口生成动态代理对象,将HTTP
请求逻辑封装到代理对象中,从而达到简化服务间调用的Java
代码的目的。
接下来会详细Feign
是如何创建代理对象,代理对象方法执行的完整流程了解Feign
的工作原理,同时也会简单说明Feign
与Hystrix
和Eureka
如何协同工作的。
Feign
首先我们需要一个interface
,这个interface
定义了我们需要调用的目标服务中的方法,这里我们用Feign
官方文档中调用GitHub
服务的例子来举例:
1 | interface GitHub { |
创建流程
了解以上信息之后就进入使用案例,案例通过Feign
生成了一个GitHub
的代理对象:
1 | public class MyApp { |
Feign
对象构建使用Builder
模式,将复杂的构建过程封装在Feign.Builider
类中。我们追踪Feign.builder().target()
方法,可以看到:
1 | public <T> T target(Target<T> target) { |
首先会调用到 Feign.builder.build()
方法:
1 | public Feign build() { |
代码中我们只保留了代码主体,可以看到build()
方法最终会返回一个Feign
的实现类ReflectiveFeign
的对象。接下来会调用ReflectiveFeign.newInstance(Target<T> target)
方法,这里我们可以看到非常熟悉的动态代理的代码逻辑:
1 | public <T> T newInstance(Target<T> target) { |
至此代理对象生成就完成了,总体流程逻辑非常简单,下图为生成代理对象的时序图:
接下来我们详细分析下各个类的作用。
FeignInvocationHandler
代理对象生成的核心对象是InvocationHandler
,在里边封装了方法调用时的处理逻辑。代码中的InvocationHandlerFactory
默认实现会创建FeignInvocationHandler
对象。下面我们仔细研究下FeignInvocationHandler
是如何实现的:
1 | static class FeignInvocationHandler implements InvocationHandler { |
可以看到FeignInvocationHandler
里边逻辑很简单,内部包含一个method
到具体执行逻辑对象MehothHandler
的一个map
,然后根据method
获取到MethodHandler
后就可以执行对象。具体的http
调用实现是在MethodHandler
中实现。
SynchronousMethodHandler
在Feign.Builider.build()
方法中,我们知道MethodHandler
使用的是同步实现类SynchronousMethodHandler
,采用同步的方式进行http
调用。我们看下这个类中的方法调用的主要逻辑:
1 | public Object invoke(Object[] argv) throws Throwable { |
成员变量buildTemplateFromArgs
是一个RequestTemplate.Factory
对象,该对象的作用是根据方法的参数 argv
生成对应的RequestTemplate
对象,用于底层发送HTTP
请求。接下来会会执行executeAndDecode
方法进行HTTP
的调用。
那么SynchronousMethodHandler
是怎么生成的呢?
ParseHandlersByName
在Feign.Builder.build()
方法中,我们可以看到一个对象ParseHandlersByName
,他负责生成对应的SynchronousMethodHandler
对象:
1 | public Map<String, MethodHandler> apply(Target target) { |
这个类的核心功能就是根据输入的Target
接口中的为每个方法生成对应的SynchronousMethodHandler
。从上面代码可以看到,首先需要获取接口中每个方法上的元数据信息,接口中的每个方法上的注解表示的元信息通过Contract
规范来解析,比如Feign
默认规范中@RequestLine
注解用来指定HTTP
访问方法以及访问的资源地址。
Contract
Contract
规定了如何通过接口上的annotation
来表示接口要访问的http
地址,使用的http
方法,http
头信息等。Contract
接口定义如下:
1 | public interface Contract { |
通过接口定义可以看到,Contract
会根据输入的接口类生成对应的MethodMetadata
,在这个类中记录了关于如何根据输入参数生成对应的RequestTemplate
的元数据信息。
Feign
的规范主要包含@RequestLine
,@Param
,@Headers
,@QueryMap
,@HeaderMap
,@Body
几个注解来表示请求的方法,url,参数,headers等信息。
当然除了默认规范外Feign
还支持JAX-RS
,SOAP
等等,如果这些规范都不满足业务需求时,你也可以自定义自己的规范。
至此,Feign
生成代理对象的总体流程就介绍完了。
执行流程
有了代理对象后,我们在来看下一个完整的方法调用的流程:
- 访问
GitHub
接口的createIssue
方法 - 执行
FeignInvocationHanlder
的invoke
方法,根据dispatch
获取方法createIssue
对应的SynchronousMethodHandler
对象。 - 调用
SynchronousMethodHandler
的invoke
方法,方法执行时首先会调用RequestTemplate.Factory
来根据访问参数获取HTTP
访问的请求对象RequestTemplate
,然后将请求对象交给底层的Client
完成HTTP
调用。
HystrixFeign
Feign
可以和Hystrix
结合,从而使方法调用增加熔断保护的功能。Hystrix
的使用方式是将需要的方法调用封装成一个HystrixCommand
,经过上文的分析,我们知道所有的方法调用入口都是在FeignInvocationHandler
中,所以很自然可以想到在FeignInvocationHanlder
中将方法执行都封装成HystrixCommand
就可以了,实际上HystrixFeign
也是这样做的。
我们看下HystrixFeign
的使用方式:
1 | public class MyApp { |
和Feign
代码中唯一的不同是HystrixFeign.Builder.build()
方法:
1 | Feign build(final FallbackFactory<?> nullableFallbackFactory) { |
可以看到最终方法调用了父类方法的build()
方法,在这之前只是额外做了两件事情:
- 设置
InvocationHandlerFactory
,最终生成的代理对象中的的InvocationHandler
会是HystrixInvocationHandler
。 - 设置
Contract
,将原始的Contract
用HystrixDelegatingContract
包装了一下
第一点修改很好理解,因为我们需要将方法调用包装成HystrixCommand
,但是为什么还需要修改Contract
呢?
我们看下两个类中具体做了些什么:
HystrixDelegatingContract
:
1 | public List<MethodMetadata> parseAndValidateMetadata(Class<?> targetType) { |
HystrixInvocationHandler
1 | public Object invoke(final Object proxy, final Method method, final Object[] args) |
我们看到HystrixDelegatingContract
只是把返回值为HystrixCommand
,Observable
,Signle
,Completable
,CompletableFuture
,这几种异步执行对象的方法做了特殊处理,同样HystrixInvocationHandler
除了将方法执行包装成HystrixCommand
以外也对返回值为这几种类型的方法做了特殊处理。我们用一个例子来说明:
1 | interface GitHub { |
这个方法返回一个HystrixCommand
,它的目的并不是想要马上同步去执行HTTP
访问,而是想获取一个HystrixCommand
对象在业务需要的地方才去自行决定执行execute()
方法来发起HTTP
调用。
所以为了满足这种需求,所以在HystrixInvocationHandler
中包装成的HystrixCommand
就不能马上直接调用execute()
方法而是直接返回HystrixCommand
对象。
但是只是HystrixInvocationHandler
中做处理是不够的,因为经过Contract
解析后,方法执行的返回值也是HystrixCommand
对象,但是实际底层发起HTTP
请求后的返回值是HystrixCommand<List<Contributor>>
中的泛型List<Contributor>
,所以HystrixDelegatingContract
需要对返回值为异步执行类型的方法修改方法返回值类型为它们内部对象的泛型类,比如上面contributors
方法的返回值类型HystrixCommand<List<Contributor>>
为List<Contributor>
类,这样在HystrixInvocationHandler
返回时会重新返回HystrixCommand<List<Contributor>>
类对象,达到获取一个异步执行对象的目的。
客户端负载均衡
在方法执行流程一节中我们可以看到,最底层发起HTTP
请求是由Client
来完成,Client
是HttpClient
的抽象接口,有多种具体实现类:OkHttpClient
,JAXRSClient
,RibbonClient
等等。其中RibbonClient
可以实现简单的客户端的负载均衡。
下面我们举个例子说明:
1 | public class MyApp { |
这里使用方式上需要设置Client
为RibbonClient
,并且http://service-a
不再是直接可以访问的实际地址,service-a
作为服务A
在客户端的一个clientName
,clientName
会映射到一个服务地址的列表。
我们来看下RibbonClient
的实现:
1 | public class RibbonClient implements Client { |
这里可以看到RibbonClient
将发送HTTP
请求的工作委托给其他类型的Client
,自身只是做了额外的负载均衡的工作。
下面我们看下具体执行逻辑:
1 | public Response execute(Request request, Request.Options options) throws IOException { |
- 从
Uri
中解析出clientName
,比如例子中的service-a
- 生成
service-a
对应的LBClient
对象,然后执行executeWithLoadBalancer
方法。
LBClient
中包含成员对象ILoadBalancer
,这个就是实现复杂均衡的核心类,方法调用时通过ILoadBalancer.choose()
方法来选择一个服务地址发起HTTP
请求。
ILoadBalancer
负责最终选取一个Server
地址,ILoadBalancer
中又会涉及到服务列表如何维护,选择Server
规则这些方面的内容,这里我们只简单介绍RibbonClient
中使用的相关类,更多详细设计可以参考项目Ribbon。
我们追踪lbClient(clientName)
最终会进入到LBClientFactory
文件中:
1 | public static final class Default implements LBClientFactory { |
这里我们会看到会得到一个ILoadBalancer
对象,底层更多信息后可以知道,这里会根据DefaultClientConfigImpl
配置类中获取到默认的ILoadBalancer
类ZoneAwareLoadBalancer
,挖掘它的子类可以获取几个关键的成员:
ServerList
定义了如何去获取服务列表。ServerListFilter
更新服务列表时按需要过滤掉不符合需求的服务。ServerListUpdater
更新ILoadBalancer
中的服务列表的规则类。IRule
方法调用时选择服务的规则。
我们可以通过自定义ServerList
对象来达到控制服务列表的目的,与Eureka
相结合也是通过ServerList
来实现的。通过指定ServerList
为DiscoveryEnabledNIWSServerList
类即可,DiscoveryEnabledNIWSServerList
会从EurekaClient
中来获取相应的服务列表。
但是因为DiscoveryEnabledNIWSServerList
这个类只是作为ZoneAwareLoadBalancer
获取服务列表的方式,只有当ServerListUpdater
发起服务列表更新时才会去重新到EurekaClient
里获取一次服务列表,所以当Eureka
中服务端服务列表有变更时,除了EurekaClient
重新到服务端获取列表的延时外,还要加上ServerListUpdater
更新ILoadBalancer
频率的延时。
总结
文章主要讲解了Feign
的目标以及具体实现的原理以及和Hystrix
与Eureka
是如何结合在一起的,希望能对日常使用Spring Cloud Netflix全家
桶的小伙伴定位日常问题有所帮助。