聊聊 Feign 的实现原理

What is Feign?

Feign 是⼀个 HTTP 请求的轻量级客户端框架。通过 接口 + 注解的方式发起 HTTP 请求调用,面向接口编程,而不是像 Java 中通过封装 HTTP 请求报文的方式直接调用。服务消费方拿到服务提供方的接⼝,然后像调⽤本地接⼝⽅法⼀样去调⽤,实际发出的是远程的请求。让我们更加便捷和优雅的去调⽤基于 HTTP 的 API,被⼴泛应⽤在 Spring Cloud 的解决⽅案中。开源项目地址:Feign,官方描述如下:

Feign is a Java to HTTP client binder inspired by Retrofit, JAXRS-2.0, and WebSocket. Feign's first goal was reducing the complexity of binding Denominator uniformly to HTTP APIs regardless of ReSTfulness.

Why Feign?

Feign 的首要目标就是减少 HTTP 调用的复杂性。在微服务调用的场景中,我们调用很多时候都是基于 HTTP 协议的服务,如果服务调用只使用提供 HTTP 调用服务的 HTTP Client 框架(e.g. Apache HttpComponnets、HttpURLConnection OkHttp 等),我们需要关注哪些问题呢?

feign-1.png

相比这些 HTTP 请求框架,Feign 封装了 HTTP 请求调用的流程,而且会强制使用者去养成面向接口编程的习惯(因为 Feign 本身就是要面向接口)。

Demo

原生使用方式

以获取 Feign 的 GitHub 开源项目的 Contributors 为例,原生方式使用 Feign 步骤有如下三步(这里以使用 Gradle 进行依赖管理的项目为例):
第一步: 引入相关依赖:implementation 'io.github.openfeign:feign-core:11.0'
在项目的 build.gradle 文件的依赖声明处 dependencies 添加该依赖声明即可。

第二步: 声明 HTTP 请求接口
使用 Java 的接口和 Feign 的原生注解 @RequestLine 声明 HTTP 请求接口,从这里就可以看到 Feign 给使用者封装了 HTTP 的调用细节,极大的减少了 HTTP 调用的复杂性,只要定义接口即可。

declare-interface.png

第三步: 配置初始化 Feign 客户端
最后一步配置初始化客户端,这一步主要是设置请求地址、编码(Encoder)、解码(Decoder)等。

config-feign-client.png

通过定义接口,使用注解的方式描述接口的信息,就可以发起接口调用。最后请求结果如下:

native-use-feign-result.png

结合 Spring Cloud 使用方式

同样还是以获取 Feign 的 GitHub 开源项目的 Contributors 为例,结合 Spring Cloud 的使用方式有如下三步:
第一步: 引入相关 starter 依赖:org.springframework.cloud:spring-cloud-starter-openfeign
在项目的 build.gradle 文件的依赖声明处 dependencies 添加该依赖声明即可。

第二步: 在项目的启动类 XXXApplication 上添加 @EnableFeignClients 注解启用 Feign 客户端功能。

feign-6.png

第三步: 创建 HTTP 调用接口,并添加声明 @FeignClient 注解。
最后一步配置初始化客户端,这一步主要是设置请求地址(url)、编码(Encoder)、解码(Decoder)等,与原生使用方式不同的是,现在我们是通过 @FeignClient 注解配置的 Feign 客户端属性,同时请求的 URL 也是使用的 Spring MVC 提供的注解。

feign-7.png

测试类如下所示:

feign-8.png

运行结果如下:

feign-9.png

可以看到这里是通过 @Autowired 注入刚刚定义的接口的,然后就可以直接使用其来发起 HTTP 请求了,使用是不是很方便、简洁。

Dive into Feign

从上面第一个原生使用的例子可以看到,只是定了接口并没有具体的实现类,但是却可以在测试类中直接调用接口的方法来完成接口的调用,我们知道在 Java 里面接口是无法直接进行使用的,因此可以大胆猜测是 Feign 在背后默默生成了接口的代理实现类,也可以验证一下,只需在刚刚的测试类 debug 一下看看接口实际使用的是什么实现类:

feign-11.png

从 debug 结果可知,框架生成了接口的代理实现类 HardCodedTarget 的对象 $Proxy14 来完成接口请求调用,和刚刚的猜测一致。Feign 主要是封装了 HTTP 请求调用,其整体架构如下:

feign-10.png

测试类代码里面只在 GitHub github = Feign.builder().target(GitHub.class, "https://api.github.com"); 用到了 Feign 框架的功能,所以我们选择从这里来深入源码,点击进入发现是 Feign 抽象类提供的方法,同样我们知道抽象类也是无法进行初始化的,所以肯定是有子类的,如果你刚刚有仔细观察上面的 debug 代码的话,可以发现有一个 ReflectiveFeign 类,这个类就是抽象类 Feign 的子类了。抽象类 feign.Feign 的部分源码如下:

public abstract class Feign {
    
  ...  

  public static Builder builder() {
    return new Builder();
  }

  public abstract <T> T newInstance(Target<T> target);

  public static class Builder {

    ...

    private final List<RequestInterceptor> requestInterceptors = new ArrayList<RequestInterceptor>();
    private Logger.Level logLevel = Logger.Level.NONE;
    private Contract contract = new Contract.Default();
    private Client client = new Client.Default(null, null);
    private Retryer retryer = new Retryer.Default();
    private Logger logger = new NoOpLogger();
    private Encoder encoder = new Encoder.Default();
    private Decoder decoder = new Decoder.Default();
    private QueryMapEncoder queryMapEncoder = new FieldQueryMapEncoder();
    private ErrorDecoder errorDecoder = new ErrorDecoder.Default();
    private Options options = new Options();
    private InvocationHandlerFactory invocationHandlerFactory =
        new InvocationHandlerFactory.Default();
    private boolean decode404;
    private boolean closeAfterDecode = true;
    private ExceptionPropagationPolicy propagationPolicy = NONE;
    private boolean forceDecoding = false;
    private List<Capability> capabilities = new ArrayList<>();

    // 设置输入打印日志级别
    public Builder logLevel(Logger.Level logLevel) {
      this.logLevel = logLevel;
      return this;
    }

    // 设置接口方法注解处理器(契约) 
    public Builder contract(Contract contract) {
      this.contract = contract;
      return this;
    }

    // 设置使用的 Client(默认使用 JDK 的 HttpURLConnection)
    public Builder client(Client client) {
      this.client = client;
      return this;
    }

    // 设置重试器
    public Builder retryer(Retryer retryer) {
      this.retryer = retryer;
      return this;
    }

    // 设置请求编码器 
    public Builder encoder(Encoder encoder) {
      this.encoder = encoder;
      return this;
    }

    // 设置响应解码器
    public Builder decoder(Decoder decoder) {
      this.decoder = decoder;
      return this;
    }

    // 设置 404 返回结果解码器
    public Builder decode404() {
      this.decode404 = true;
      return this;
    }

    // 设置错误解码器
    public Builder errorDecoder(ErrorDecoder errorDecoder) {
      this.errorDecoder = errorDecoder;
      return this;
    }

    // 设置请求拦截器
    public Builder requestInterceptors(Iterable<RequestInterceptor> requestInterceptors) {
      this.requestInterceptors.clear();
      for (RequestInterceptor requestInterceptor : requestInterceptors) {
        this.requestInterceptors.add(requestInterceptor);
      }
      return this;
    }

    public <T> T target(Class<T> apiType, String url) {
      return target(new HardCodedTarget<T>(apiType, url));
    }

    public <T> T target(Target<T> target) {
      return build().newInstance(target);
    }

  }

  ...

}

可以看到在方法 public <T> T target(Class<T> apiType, String url) 中直接创建了 HardCodedTarget 对象出来,这个对象也是上面 debug 看到的对象。再继续深入,就来到了 feign.Feign 的 newInstance(Target<T> target) 的方法了,是个抽象方法,其实现在子类 ReflectiveFeign 中,这个方法就是接口代理实现生成的地方,下面通过源码来看看实现逻辑是怎样的:

public class ReflectiveFeign extends Feign {

  ...  

  private final ParseHandlersByName targetToHandlersByName;
  private final InvocationHandlerFactory factory;
  private final QueryMapEncoder queryMapEncoder;

  ReflectiveFeign(ParseHandlersByName targetToHandlersByName, InvocationHandlerFactory factory,
      QueryMapEncoder queryMapEncoder) {
    this.targetToHandlersByName = targetToHandlersByName;
    this.factory = factory;
    this.queryMapEncoder = queryMapEncoder;
  }

  @SuppressWarnings("unchecked")
  @Override
  public <T> T newInstance(Target<T> target) {
    // <类名#方法签名, MethodHandler>,key 是通过 feign.Feign.configKey(Class targetType, Method method) 生成的
    Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
    // 将 Map<String, MethodHandler> 转换为  Map<Method, MethodHandler> 方便调用
    Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
    // 默认方法处理器
    List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();

    for (Method method : target.type().getMethods()) {
      // 跳过 Object 类定于的方法  
      if (method.getDeclaringClass() == Object.class) {
        continue;
      } else if (Util.isDefault(method)) {
        // 默认方法(接口声明的默认方法)使用默认的方法处理器  
        DefaultMethodHandler handler = new DefaultMethodHandler(method);
        defaultMethodHandlers.add(handler);
        methodToHandler.put(method, handler);
      } else {
        // 接口正常声明的方法(e.g. GitHub.listContributors(String, String))  
        methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
      }
    }

    // 生成 Feign 封装的 InvocationHandler
    InvocationHandler handler = factory.create(target, methodToHandler);
    // 基于 JDK 动态代理生成接口的代理类(e.g. Github 接口)
    T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
        new Class<?>[] {target.type()}, handler);

    for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
      defaultMethodHandler.bindTo(proxy);
    }
    return proxy;
  }

...

}

总体流程就是在方法 <T> T newInstance(Target<T> target) 生成一个含有 FeignInvocationHandler 的代理对象,FeignInvocationHandler 对象会持有 Map<Method, MethodHandler> map,代理对象调用的时候进入 FeignInvocationHandler#invoke 方法,根据调用的方法来获取对应 MethodHandler,然后再 MethodHandler 完成对方法的处理(处理 HTTP 请求等)。

下面再深入 MethodHandler,看看是如何完成对方法 HTTP 请求处理的,MethodHandler 是一个接口定义在 feign.InvocationHandlerFactory 接口中(P.S. 基础知识点,接口是可以在内部定义内部接口的哦),有两个实现类分别为 DefaultMethodHandler 和 SynchronousMethodHandler,第一个 DefaultMethodHandler 用来处理接口的默认方法,第二个是用来处理正常的接口方法的,一般情况下都是由该类来处理的。


final class SynchronousMethodHandler implements MethodHandler {

  ...

  @Override
  public Object invoke(Object[] argv) throws Throwable {
    // 获取 RequestTemplate 将请求参数封装成请求模板  
    RequestTemplate template = buildTemplateFromArgs.create(argv);
    Options options = findOptions(argv);
    // 请求重试器
    Retryer retryer = this.retryer.clone();
    while (true) {
      try {
        // 执行请求并解码后返回  
        return executeAndDecode(template, options);
      } catch (RetryableException e) {
        try {
          // 发生重试异常则进行重试处理  
          retryer.continueOrPropagate(e);
        } catch (RetryableException th) {
          Throwable cause = th.getCause();
          if (propagationPolicy == UNWRAP && cause != null) {
            throw cause;
          } else {
            throw th;
          }
        }
        if (logLevel != Logger.Level.NONE) {
          logger.logRetry(metadata.configKey(), logLevel);
        }
        continue;
      }
    }
  }

  Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
    // 从请求模板 RequestTemplate 构造请求参数对象 Request  
    Request request = targetRequest(template);

    if (logLevel != Logger.Level.NONE) {
      logger.logRequest(metadata.configKey(), logLevel, request);
    }

    Response response;
    long start = System.nanoTime();
    try {
      // 通过 client(Apache HttpComponnets、HttpURLConnection OkHttp 等)执行 HTTP 请求调用,默认是 HttpURLConnection 
      response = client.execute(request, options);
      // ensure the request is set. TODO: remove in Feign 12
      response = response.toBuilder()
          .request(request)
          .requestTemplate(template)
          .build();
    } catch (IOException e) {
      if (logLevel != Logger.Level.NONE) {
        logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
      }
      throw errorExecuting(request, e);
    }
    long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);

    if (decoder != null)
      // 对返回结果进行解码操作
      return decoder.decode(response, metadata.returnType());

    CompletableFuture<Object> resultFuture = new CompletableFuture<>();
    asyncResponseHandler.handleResponse(resultFuture, metadata.configKey(), response,
        metadata.returnType(),
        elapsedTime);

    try {
      if (!resultFuture.isDone())
        throw new IllegalStateException("Response handling not done");

      return resultFuture.join();
    } catch (CompletionException e) {
      Throwable cause = e.getCause();
      if (cause != null)
        throw cause;
      throw e;
    }
  }

...

}

至此,Feign 的核心实现流程介绍完毕,从代码上看 feign.SynchronousMethodHandler 的操作相对比较简单,主要是通过 client 完成请求,对响应进行解码以及异常处理操作,整体流程如下:

feign-12.png

Summary

Feign 通过给我们定义的目标接口(比如例子中的 GitHub)生成一个 HardCodedTarget 类型的代理对象,由 JDK 动态代理实现,生成代理的时候会根据注解来生成一个对应的 Map<Method, MethodHandler>,这个 Map 被 InvocationHandler 持有,接口方法调用的时候,进入 InvocationHandler 的 invoke 方法(为什么会进入这里?JDK 动态代理的基础知识)。

然后根据调用的方法从 Map<Method, MethodHandler> 获取对应的 MethodHandler,然后通过 MethodHandler 根据指定的 client 来完成对应处理, MethodHandler 中的实现类 DefaultMethodHandler 处理默认方法(接口的默认方法)的请求处理的,SynchronousMethodHandler 实现类是完成其它方法的 HTTP 请求的实现,这就是 Feign 的主要核心流程,源码已上传 Github。以上是 Feign 框架实现的核心流程介绍,Spring Cloud 是如何整合 Feign 的呢?请看下篇博文,敬请期待。


感谢各位看官的点赞、收藏和评论,我们下周见!文章持续更新,微信搜一搜「 mghio 」关注这个 “Java 搬运工 & 终身学习者”,一起牛逼~。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容