詳解Feign的實現原理

一、什麼是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.

二、為什麼用Feign

Feign 的首要目標就是減少 HTTP 調用的復雜性。在微服務調用的場景中,我們調用很多時候都是基於 HTTP 協議的服務,如果服務調用隻使用提供 HTTP 調用服務的 HTTP Client 框架(e.g. Apache HttpComponnets、HttpURLConnection OkHttp 等),我們需要關註哪些問題呢?

相比這些 HTTP 請求框架,Feign 封裝瞭 HTTP 請求調用的流程,而且會強制使用者去養成面向接口編程的習慣(因為 Feign 本身就是要面向接口)。

三、實例

3.1、原生使用方式

以獲取 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)等。

通過定義接口,使用註解的方式描述接口的信息,就可以發起接口調用。最後請求結果如下:

3.2、結合 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 請求瞭,使用是不是很方便、簡潔。

四、探索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 target(Class apiType, String url) 中直接創建瞭 HardCodedTarget 對象出來,這個對象也是上面 debug 看到的對象。再繼續深入,就來到瞭 feign.Feign 的 newInstance(Target 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 newInstance(Target 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 通過給我們定義的目標接口(比如例子中的 GitHub)生成一個 HardCodedTarget 類型的代理對象,由 JDK 動態代理實現,生成代理的時候會根據註解來生成一個對應的 Map<Method, MethodHandler>,這個 Map 被 InvocationHandler 持有,接口方法調用的時候,進入 InvocationHandler 的 invoke 方法(為什麼會進入這裡?JDK 動態代理的基礎知識)。

然後根據調用的方法從 Map<Method, MethodHandler> 獲取對應的 MethodHandler,然後通過 MethodHandler 根據指定的 client 來完成對應處理, MethodHandler 中的實現類 DefaultMethodHandler 處理默認方法(接口的默認方法)的請求處理的,SynchronousMethodHandler 實現類是完成其它方法的 HTTP 請求的實現,這就是 Feign 的主要核心流程。以上是 Feign 框架實現的核心流程介紹。

以上就是詳解Feign的實現原理的詳細內容,更多關於Feign原理的資料請關註WalkonNet其它相關文章!

推薦閱讀: