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 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 完成請求,對響應進行解碼以及異常處理操作,整體流程如下:

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 的呢?請看下篇博文,敬請期待。