关于后端:聊聊-Feign-的实现原理

48次阅读

共计 9138 个字符,预计需要花费 23 分钟才能阅读完成。

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

正文完
 0