聊聊 Feign 的实现原理

Posted mghio

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了聊聊 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 等),我们需要关注哪些问题呢?

相比这些 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 调用的复杂性,只要定义接口即可。

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

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

结合 Spring Cloud 使用方式

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

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

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

测试类如下所示:

运行结果如下:

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

Dive into Feign

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

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

测试类代码里面只在 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 完成请求,对响应进行解码以及异常处理操作,整体流程如下:

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 搬运工 & 终身学习者”,一起牛逼~。

以上是关于聊聊 Feign 的实现原理的主要内容,如果未能解决你的问题,请参考以下文章

SpringCloud + Zookeeper + Feign整合及Feign原理

聊聊如何根据环境动态指定feign调用服务名

一起学源码-微服务Feign 源码三:Feign结合Ribbon实现负载均衡的原理分析

Feign远程调用原理

SpringCloud系列之Feign-4.Feign的动态代理

深入微服务-SpringCloud调用组件Feign