Feign远程调用的基础流程

Feign远程调用,核心就是通过一系列的封装和处理,将以Java注解的方式定义的远程调用API接口,最终转换成HTTP的请求形式,然后将HTTP的响应结果,解码成Java Bean,放回给调用者。Feign远程调用的基本流程,大致如下所示。
OpenFeign的核心原理 - 图1

远程调用步骤

1、基于面向接口的动态代理方式生成实现类
2、基于RequestBean,动态生成Request
3、使用Encoder将Bean转换成Http报文正文
4、拦截器负责对请求和返回进行装饰处理
5、发送Http请求

工作原理

OpenFeign的核心原理 - 图2

(1)利用动态代理生成实现类

在使用feign 时,会定义对应的接口类,在接口类上使用Http相关的注解,标识HTTP请求参数信息
在Feign 底层,通过基于面向接口的动态代理方式生成实现类,将请求调用委托到动态代理实现类,基本原理如下所示:
OpenFeign的核心原理 - 图3

(2)根据Contract协议规则,解析接口类的注解信息,解析成内部表现:

OpenFeign的核心原理 - 图4

(3)基于RequestBean,动态生成Request

根据传入的Bean对象和注解信息,从中提取出相应的值,来构造Http Request对象

(4)使用Encoder将Bean转换成Http报文正文(消息解析和转码逻辑)

Feign最终会将请求转换成Http消息发送出去,传入的请求对象最终会解析成消息体,如下所示
OpenFeign的核心原理 - 图5

(5)拦截器负责对请求和返回进行装饰处理

在请求转换的过程汇总,Feign抽象出来了拦截器接口,用于用户自定义对请求的操作,比如,如果希望Http消息传递过程中被压缩,可以定义一个请求拦截器。

(6)日志记录

(7)基于重试器发送HTTP请求

Feign内置了一个重试器,当HTTP请求出现IO异常时,Feign会有一个最大尝试次数发送请求。

(8)发送Http请求

Feign真正发送Http请求是委托给FeignClient来做的。
Feign 默认底层通过JDK 的 java.net.HttpURLConnection 实现了feign.Client接口类,在每次发送请求的时候,都会创建新的HttpURLConnection 链接,这也就是为什么默认情况下Feign的性能很差的原因。可以通过拓展该接口,使用Apache HttpClient 或者OkHttp3等基于连接池的高性能Http客户端。
—————————————————————————————-

Feign远程调用的重要组件

远程接口的本地JDK Proxy代理实例

远程接口的本地JDK Proxy代理实例,有以下特点:
1、Proxy代理实例,实现了一个加@FeignClient注解的远程调用接口;
2、Proxy代理实例,能在内部进行HTTP请求的封装,以及发送HTTP 请求;
3、Proxy代理实例,能处理远程HTTP请求的响应,并且完成结构的解码,然后返回给调用者。

下面以一个简单的远程服务的调用接口DemoClient为例,具体介绍一下远程接口的本地JDK Proxy代理实例的创建过程。
DemoClient接口,有两个非常简单的远程调用抽象方法:一个为hello()抽象方法,用于完成远程URL “/api/demo/hello/v1”的HTTP请求;一个未echo(…)抽象方法,用于完成远程URL”/api/demo/echo/{word}/v1”的HTTP请求,具体如下图所示。
OpenFeign的核心原理 - 图6
代理实例示意图
**
DemoClient 接口代码如下:

  1. package com.crazymaker.springcloud.demo.contract.client;
  2. //…省略import
  3. @FeignClient(
  4. value = "seckill-provider", path = "/api/demo/",
  5. fallback = DemoDefaultFallback.class)
  6. public interface DemoClient {
  7. /**
  8. * 测试远程调用
  9. *
  10. * @return hello
  11. */
  12. @GetMapping("/hello/v1")
  13. Result<JSONObject> hello();
  14. /**
  15. * 非常简单的一个 回显 接口,主要用于远程调用
  16. *
  17. * @return echo 回显消息
  18. */
  19. @RequestMapping(value = "/echo/{word}/v1", method = RequestMethod.GET)
  20. Result<JSONObject> echo(
  21. @PathVariable(value = "word") String word);
  22. }

注意,上面的代码中,在DemoClient 接口上,加有@FeignClient 注解。也即是说,Feign在启动时,会为其创建一个本地JDK Proxy代理实例,并注册到Spring IOC容器。

如何使用呢?可以通过@Resource注解,按照类型匹配(这里的类型为DemoClient接口类型),从Spring IOC容器找到这个代理实例,并且装配给需要的成员变量。

DemoClient的 本地JDK Proxy 代理实例的使用的代码如下:

  1. package com.crazymaker.springcloud.user.info.controller;
  2. //…省略import
  3. @Api(value = "用户信息、基础学习DEMO", tags = {"用户信息、基础学习DEMO"})
  4. @RestController
  5. @RequestMapping("/api/user")
  6. public class UserController {
  7. @Resource
  8. DemoClient demoClient; //装配 DemoClient 的本地代理实例
  9. @GetMapping("/say/hello/v1")
  10. @ApiOperation(value = "测试远程调用速度")
  11. public Result<JSONObject> hello() {
  12. Result<JSONObject> result = demoClient.hello();
  13. JSONObject data = new JSONObject();
  14. data.put("others", result);
  15. return Result.success(data).setMsg("操作成功");
  16. }
  17. //…
  18. }

DemoClient的本地JDK Proxy代理实例的创建过程,比较复杂,稍后作为重点介绍。先来看另外两个重要的逻辑组件。

调用处理器InvocationHandler

大家知道,通过JDK Proxy生成动态代理类,核心步骤就是需要定制一个调用处理器,具体来说,就是实现JDK中位于java.lang.reflect 包中的 InvocationHandler 调用处理器接口,并且实现该接口的 invoke(…) 抽象方法。

为了创建Feign的远程接口的代理实现类,Feign提供了自己的一个默认的调用处理器,叫做FeignInvocationHandler类,该类处于Feign-core核心Jar包中,当然,调用处理器可以进行替换,如果Feign与Hystrix结合使用,则会替换成 HystrixInvocationHandler 调用处理器类,类处于 feign-hystrix 的jar包中。
OpenFeign的核心原理 - 图7
图3 Feign中实现的 InvocationHandler 调用处理器

默认的调用处理器FeignInvocationHandler

默认的调用处理器 FeignInvocationHandler 是一个相对简单的类,有一个非常重要Map类型成员dispatch映射,保存着远程接口方法到MethodHandler方法处理器的映射。

以前面示例中Democlient接口为例,其代理实现类的调用处理器 FeignInvocationHandler 的dispatch 成员的内存结构图如图3所示。
OpenFeign的核心原理 - 图8

图4 DemoClient代理实例的调用处理器 FeignInvocationHandler的dispatch 成员
为何在图3中的Map类型成员 dispatch 映射对象中,有两个Key-Value键值对呢?
原因是:默认的调用处理器 FeignInvocationHandle,在处理远程方法调用的时候,会根据Java反射的方法实例,在dispatch 映射对象中,找到对应的MethodHandler 方法处理器,然后交给MethodHandler 完成实际的HTTP请求和结果的处理。前面示例中的 DemoClient 远程调用接口,有两个远程调用方法,所以,其代理实现类的调用处理器 FeignInvocationHandler 的dispatch 成员,有两个有两个Key-Value键值对。

FeignInvocationHandler的关键源码,节选如下:

  1. package feign;
  2. //...省略import
  3. public class ReflectiveFeign extends Feign {
  4. //...
  5. //内部类:默认的Feign调用处理器 FeignInvocationHandler
  6. static class FeignInvocationHandler implements InvocationHandler {
  7. private final Target target;
  8. //方法实例对象和方法处理器的映射
  9. private final Map<Method, MethodHandler> dispatch;
  10. //构造函数
  11. FeignInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) {
  12. this.target = checkNotNull(target, "target");
  13. this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);
  14. }
  15. //默认Feign调用的处理
  16. @Override
  17. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  18. //...
  19. //首先,根据方法实例,从方法实例对象和方法处理器的映射中,
  20. //取得 方法处理器,然后,调用 方法处理器 的 invoke(...) 方法
  21. return dispatch.get(method).invoke(args);
  22. }
  23. //...
  24. }

源码很简单,重点在于invoke(…)方法,虽然核心代码只有一行,但是其功能是复杂的:

  1. 根据Java反射的方法示例,在dispatch映射对象中,找到对应的MethodHandler方法处理器;
  2. 调用MethodHandler方法处理器的invoke(…)方法,完成实际的HTTP请求和结果的处理。

补充说明一下:MethodHandler 方法处理器,和JDK 动态代理机制中位于 java.lang.reflect 包的 InvocationHandler 调用处理器接口,没有任何的继承和实现关系。MethodHandler 仅仅是Feign自定义的,一个非常简单接口。
、、、、、、、、、、】【

方法处理器MethodHandler

Feign的方法处理器 MethodHandler 是一个独立的接口,定义在 InvocationHandlerFactory 接口中,仅仅拥有一个invoke(…)方法,源码如下:

  1. //定义在InvocationHandlerFactory接口中
  2. public interface InvocationHandlerFactory {
  3. //…
  4. //方法处理器接口,仅仅拥有一个invoke(…)方法
  5. interface MethodHandler {
  6. //完成远程URL请求
  7. Object invoke(Object[] argv) throws Throwable;
  8. }
  9. //...
  10. }

MethodHandler 的invoke(…)方法,主要职责是完成实际远程URL请求,然后返回解码后的远程URL的响应结果。Feign提供了默认的 SynchronousMethodHandler 实现类,提供了基本的远程URL的同步请求处理。有关 SynchronousMethodHandler类以及其与MethodHandler的关系,大致如图4所示。
OpenFeign的核心原理 - 图9
图5 Feign的MethodHandler方法处理器
为了彻底了解方法处理器,来读一下 SynchronousMethodHandler 方法处理器的源码,大致如下:

  1. package feign;
  2. //…..省略import
  3. final class SynchronousMethodHandler implements MethodHandler {
  4. //…
  5. // 执行Handler 的处理
  6. public Object invoke(Object[] argv) throws Throwable {
  7. RequestTemplate requestTemplate = this.buildTemplateFromArgs.create(argv);
  8. Retryer retryer = this.retryer.clone();
  9. while(true) {
  10. try {
  11. return this.executeAndDecode(requestTemplate);
  12. } catch (RetryableException var5) {
  13. //…省略不相干代码
  14. }
  15. }
  16. }
  17. //执行请求,然后解码结果
  18. Object executeAndDecode(RequestTemplate template) throws Throwable {
  19. Request request = this.targetRequest(template);
  20. long start = System.nanoTime();
  21. Response response;
  22. try {
  23. response = this.client.execute(request, this.options);
  24. response.toBuilder().request(request).build();
  25. }
  26. }
  27. }

SynchronousMethodHandler的invoke(…)方法,调用了自己的executeAndDecode(…) 请求执行和结果解码方法。该方法的工作步骤:
1、首先通 RequestTemplate 请求模板实例,生成远程URL请求实例 request;
2、然后用自己的 feign 客户端client成员,excecute(…) 执行请求,并且获取 response 响应;
3、对response响应进行解码

Feign客户端组件Feign.Client

客户端组件是Feign中一个非常重要的组件,负责端到端的执行URL请求。其核心的逻辑:发送request请求到服务器,并接收response响应后进行解码。

feign.Client 类,是代表客户端的顶层接口,只有一个抽象方法,源码如下:

  1. package feign;
  2. /**客户端接口
  3. * Submits HTTP {@link Request requests}.
  4. Implementations are expected to be thread-safe.
  5. */
  6. public interface Client {
  7. //提交HTTP请求,并且接收response响应后进行解码
  8. Response execute(Request request, Options options) throws IOException;
  9. }

由于不同的feign.Client 实现类,内部完成HTTP请求的组件和技术不同,故,feign.Client 有多个不同的实现。这里举出几个例子:

  • Client.Default类:默认的feign.Client 客户端实现类,内部使用HttpURLConnnection 完成URL请求处理;
  • ApacheHttpClient 类:内部使用 Apache httpclient 开源组件完成URL请求处理的feign.Client 客户端实现类;
  • OkHttpClient类:内部使用 OkHttp3 开源组件完成URL请求处理的feign.Client 客户端实现类。
  • LoadBalancerFeignClient 类:内部使用 Ribben 负载均衡技术完成URL请求处理的feign.Client 客户端实现类。

此外,还有一些特殊场景使用的feign.Client客户端实现类,也可以定制自己的feign.Client实现类。下面对上面几个常见的客户端实现类,进行简要介绍。
OpenFeign的核心原理 - 图10
图6 feign.Client客户端实现类

Client.Default
**
作为默认的Client 接口的实现类,在Client.Default内部使用JDK自带的HttpURLConnnection类实现URL网络请求。

OpenFeign的核心原理 - 图11

图7 默认的Client 接口的客户端实现类

在JKD1.8中,虽然在HttpURLConnnection 底层,使用了非常简单的HTTP连接池技术,但是,其HTTP连接的复用能力,实际是非常弱的,性能当然也很低。具体的原因,参见后面的“SpringCloud与长连接的深入剖析”专题内容。

Feigh远程调用的执行流程

由于Feign远程调用接口的JDK Proxy实例的InvokeHandler调用处理器有多种,导致Feign远程调用的执行流程,也稍微有所区别,但是远程调用执行流程的主要步骤,是一致的。这里主要介绍两类JDK Proxy实例的InvokeHandler调用处理器相关的远程调用执行流程:

  • 与默认的调用处理器 FeignInvocationHandler 相关的远程调用执行流程;
  • 与Hystrix调用处理器 HystrixInvocationHandler 相关的远程调用执行流程。

介绍过程中,还是以前面的DemoClient的JDK Proxy远程动态代理实例的执行过程为例,演示分析Feigh远程调用的执行流程。

3.1 与 FeignInvocationHandler 相关的远程调用执行流程

FeignInvocationHandler是默认的调用处理器,如果不对Feign做特殊的配置,则Feign将使用此调用处理器。结合前面的DemoClient的JDK Proxy远程动态代理实例的hello()远程调用执行过程,在这里,详细的介绍一下与 FeignInvocationHandler 相关的远程调用执行流程,大致如下图所示。
OpenFeign的核心原理 - 图12
图6 与 FeignInvocationHandler 相关的远程调用执行流程
整体的远程调用执行流程,大致分为4步,具体如下:

3.1.1 第1步:通过Spring IOC 容器实例,装配代理实例,然后进行远程调用。

前文讲到,Feign在启动时,会为加上了@FeignClient注解的所有远程接口(包括 DemoClient 接口),创建一个本地JDK Proxy代理实例,并注册到Spring IOC容器。在这里,暂且将这个Proxy代理实例,叫做 DemoClientProxy,稍后,会详细介绍这个Proxy代理实例的具体创建过程。
然后,在本实例的UserController 调用代码中,通过@Resource注解,按照类型或者名称进行匹配(这里的类型为DemoClient接口类型),从Spring IOC容器找到这个代理实例,并且装配给@Resource注解所在的成员变量,本实例的成员变量的名称为 demoClient。
在需要代进行hello()远程调用时,直接通过 demoClient 成员变量,调用JDK Proxy动态代理实例的hello()方法。

3.1.2 第2步:执行 InvokeHandler 调用处理器的invoke(…)方法

前面讲到,JDK Proxy动态代理实例的真正的方法调用过程,具体是通过 InvokeHandler 调用处理器完成的。故,这里的DemoClientProxy代理实例,会调用到默认的FeignInvocationHandler 调用处理器实例的invoke(…)方法。
通过前面 FeignInvocationHandler 调用处理器的详细介绍,大家已经知道,默认的调用处理器 FeignInvocationHandle,内部保持了一个远程调用方法实例和方法处理器的一个Key-Value键值对Map映射。FeignInvocationHandle 在其invoke(…)方法中,会根据Java反射的方法实例,在dispatch 映射对象中,找到对应的 MethodHandler 方法处理器,然后由后者完成实际的HTTP请求和结果的处理。
所以在第2步中,FeignInvocationHandle 会从自己的 dispatch映射中,找到hello()方法所对应的MethodHandler 方法处理器,然后调用其 invoke(…)方法。

3.1.3 第3步:执行 MethodHandler 方法处理器的invoke(…)方法

通过前面关于 MethodHandler 方法处理器的非常详细的组件介绍,大家都知道,feign默认的方法处理器为 SynchronousMethodHandler,其invoke(…)方法主要是通过内部成员feign客户端成员 client,完成远程 URL 请求执行和获取远程结果。

.0

3.1.4 第4步:通过 feign.Client 客户端成员,完成远程 URL 请求执行和获取远程结果

如果MethodHandler方法处理器实例中的client客户端,是默认的 feign.Client.Default 实现类性,则使用JDK自带的HttpURLConnnection类,完成远程 URL 请求执行和获取远程结果。
如果MethodHandler方法处理器实例中的client客户端,是 ApacheHttpClient 客户端实现类性,则使用 Apache httpclient 开源组件,完成远程 URL 请求执行和获取远程结果。
通过以上四步,应该可以清晰的了解到了 SpringCloud中的 feign 远程调用执行流程和运行机制。