詳解Feign的實現(xiàn)原理
一、什么是Feign
Feign 是⼀個 HTTP 請求的輕量級客戶端框架。通過 接口 + 注解的方式發(fā)起 HTTP 請求調(diào)用,面向接口編程,而不是像 Java 中通過封裝 HTTP 請求報文的方式直接調(diào)用。服務(wù)消費方拿到服務(wù)提供方的接⼝,然后像調(diào)⽤本地接⼝⽅法⼀樣去調(diào)⽤,實際發(fā)出的是遠程的請求。讓我們更加便捷和優(yōu)雅的去調(diào)⽤基于 HTTP 的 API,被⼴泛應(yīng)⽤在 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 的首要目標(biāo)就是減少 HTTP 調(diào)用的復(fù)雜性。在微服務(wù)調(diào)用的場景中,我們調(diào)用很多時候都是基于 HTTP 協(xié)議的服務(wù),如果服務(wù)調(diào)用只使用提供 HTTP 調(diào)用服務(wù)的 HTTP Client 框架(e.g. Apache HttpComponnets、HttpURLConnection OkHttp 等),我們需要關(guān)注哪些問題呢?

相比這些 HTTP 請求框架,F(xiàn)eign 封裝了 HTTP 請求調(diào)用的流程,而且會強制使用者去養(yǎng)成面向接口編程的習(xí)慣(因為 Feign 本身就是要面向接口)。
三、實例
3.1、原生使用方式
以獲取 Feign 的 GitHub 開源項目的 Contributors 為例,原生方式使用 Feign 步驟有如下三步(這里以使用 Gradle 進行依賴管理的項目為例):
第一步: 引入相關(guān)依賴:implementation 'io.github.openfeign:feign-core:11.0'
在項目的 build.gradle 文件的依賴聲明處 dependencies 添加該依賴聲明即可。
第二步: 聲明 HTTP 請求接口
使用 Java 的接口和 Feign 的原生注解 @RequestLine 聲明 HTTP 請求接口,從這里就可以看到 Feign 給使用者封裝了 HTTP 的調(diào)用細節(jié),極大的減少了 HTTP 調(diào)用的復(fù)雜性,只要定義接口即可。

第三步: 配置初始化 Feign 客戶端
最后一步配置初始化客戶端,這一步主要是設(shè)置請求地址、編碼(Encoder)、解碼(Decoder)等。

通過定義接口,使用注解的方式描述接口的信息,就可以發(fā)起接口調(diào)用。最后請求結(jié)果如下:

3.2、結(jié)合 Spring Cloud 使用方式
同樣還是以獲取 Feign 的 GitHub 開源項目的 Contributors 為例,結(jié)合 Spring Cloud 的使用方式有如下三步:
第一步: 引入相關(guān) starter 依賴:org.springframework.cloud:spring-cloud-starter-openfeign
在項目的 build.gradle 文件的依賴聲明處 dependencies 添加該依賴聲明即可。
第二步: 在項目的啟動類 XXXApplication 上添加 @EnableFeignClients 注解啟用 Feign 客戶端功能。

第三步: 創(chuàng)建 HTTP 調(diào)用接口,并添加聲明 @FeignClient 注解。
最后一步配置初始化客戶端,這一步主要是設(shè)置請求地址(url)、編碼(Encoder)、解碼(Decoder)等,與原生使用方式不同的是,現(xiàn)在我們是通過 @FeignClient 注解配置的 Feign 客戶端屬性,同時請求的 URL 也是使用的 Spring MVC 提供的注解。

測試類如下所示:

運行結(jié)果如下:

可以看到這里是通過 @Autowired 注入剛剛定義的接口的,然后就可以直接使用其來發(fā)起 HTTP 請求了,使用是不是很方便、簡潔。
四、探索Feign
從上面第一個原生使用的例子可以看到,只是定了接口并沒有具體的實現(xiàn)類,但是卻可以在測試類中直接調(diào)用接口的方法來完成接口的調(diào)用,我們知道在 Java 里面接口是無法直接進行使用的,因此可以大膽猜測是 Feign 在背后默默生成了接口的代理實現(xiàn)類,也可以驗證一下,只需在剛剛的測試類 debug 一下看看接口實際使用的是什么實現(xiàn)類:

從 debug 結(jié)果可知,框架生成了接口的代理實現(xiàn)類 HardCodedTarget 的對象 $Proxy14 來完成接口請求調(diào)用,和剛剛的猜測一致。Feign 主要是封裝了 HTTP 請求調(diào)用,其整體架構(gòu)如下:

測試類代碼里面只在 GitHub github = Feign.builder().target(GitHub.class, "https://api.github.com"); 用到了 Feign 框架的功能,所以我們選擇從這里來深入源碼,點擊進入發(fā)現(xiàn)是 Feign 抽象類提供的方法,同樣我們知道抽象類也是無法進行初始化的,所以肯定是有子類的,如果你剛剛有仔細觀察上面的 debug 代碼的話,可以發(fā)現(xiàn)有一個 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<>();
// 設(shè)置輸入打印日志級別
public Builder logLevel(Logger.Level logLevel) {
this.logLevel = logLevel;
return this;
}
// 設(shè)置接口方法注解處理器(契約)
public Builder contract(Contract contract) {
this.contract = contract;
return this;
}
// 設(shè)置使用的 Client(默認使用 JDK 的 HttpURLConnection)
public Builder client(Client client) {
this.client = client;
return this;
}
// 設(shè)置重試器
public Builder retryer(Retryer retryer) {
this.retryer = retryer;
return this;
}
// 設(shè)置請求編碼器
public Builder encoder(Encoder encoder) {
this.encoder = encoder;
return this;
}
// 設(shè)置響應(yīng)解碼器
public Builder decoder(Decoder decoder) {
this.decoder = decoder;
return this;
}
// 設(shè)置 404 返回結(jié)果解碼器
public Builder decode404() {
this.decode404 = true;
return this;
}
// 設(shè)置錯誤解碼器
public Builder errorDecoder(ErrorDecoder errorDecoder) {
this.errorDecoder = errorDecoder;
return this;
}
// 設(shè)置請求攔截器
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
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> 轉(zhuǎn)換為 Map<Method, MethodHandler> 方便調(diào)用
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 動態(tài)代理生成接口的代理類(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;
}
...
}
總體流程就是在方法
下面再深入 MethodHandler,看看是如何完成對方法 HTTP 請求處理的,MethodHandler 是一個接口定義在 feign.InvocationHandlerFactory 接口中(P.S. 基礎(chǔ)知識點,接口是可以在內(nèi)部定義內(nèi)部接口的哦),有兩個實現(xiàn)類分別為 DefaultMethodHandler 和 SynchronousMethodHandler,第一個 DefaultMethodHandler 用來處理接口的默認方法,第二個是用來處理正常的接口方法的,一般情況下都是由該類來處理的。
final class SynchronousMethodHandler implements MethodHandler {
...
@Override
public Object invoke(Object[] argv) throws Throwable {
// 獲取 RequestTemplate 將請求參數(shù)封裝成請求模板
RequestTemplate template = buildTemplateFromArgs.create(argv);
Options options = findOptions(argv);
// 請求重試器
Retryer retryer = this.retryer.clone();
while (true) {
try {
// 執(zhí)行請求并解碼后返回
return executeAndDecode(template, options);
} catch (RetryableException e) {
try {
// 發(fā)生重試異常則進行重試處理
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 構(gòu)造請求參數(shù)對象 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 等)執(zhí)行 HTTP 請求調(diào)用,默認是 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)
// 對返回結(jié)果進行解碼操作
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;
}
}
...
}
至此,F(xiàn)eign 的核心實現(xiàn)流程介紹完畢,從代碼上看 feign.SynchronousMethodHandler 的操作相對比較簡單,主要是通過 client 完成請求,對響應(yīng)進行解碼以及異常處理操作,整體流程如下:

五、總結(jié)
Feign 通過給我們定義的目標(biāo)接口(比如例子中的 GitHub)生成一個 HardCodedTarget 類型的代理對象,由 JDK 動態(tài)代理實現(xiàn),生成代理的時候會根據(jù)注解來生成一個對應(yīng)的 Map<Method, MethodHandler>,這個 Map 被 InvocationHandler 持有,接口方法調(diào)用的時候,進入 InvocationHandler 的 invoke 方法(為什么會進入這里?JDK 動態(tài)代理的基礎(chǔ)知識)。
然后根據(jù)調(diào)用的方法從 Map<Method, MethodHandler> 獲取對應(yīng)的 MethodHandler,然后通過 MethodHandler 根據(jù)指定的 client 來完成對應(yīng)處理, MethodHandler 中的實現(xiàn)類 DefaultMethodHandler 處理默認方法(接口的默認方法)的請求處理的,SynchronousMethodHandler 實現(xiàn)類是完成其它方法的 HTTP 請求的實現(xiàn),這就是 Feign 的主要核心流程。以上是 Feign 框架實現(xiàn)的核心流程介紹。
以上就是詳解Feign的實現(xiàn)原理的詳細內(nèi)容,更多關(guān)于Feign原理的資料請關(guān)注腳本之家其它相關(guān)文章!
- 輕量級聲明式的Http庫——Feign的獨立使用
- 淺談SpringCloud feign的http請求組件優(yōu)化方案
- SpringCloud Open feign 使用okhttp 優(yōu)化詳解
- 基于Feign使用okhttp的填坑之旅
- 使用okhttp替換Feign默認Client的操作
- Java 如何使用Feign發(fā)送HTTP請求
- spring boot openfeign從此和httpClient說再見詳析
- 使用Spring Cloud Feign作為HTTP客戶端調(diào)用遠程HTTP服務(wù)的方法(推薦)
- spring cloud 之 Feign 使用HTTP請求遠程服務(wù)的實現(xiàn)方法
相關(guān)文章
SpringBoot @CompentScan excludeFilters配置無效的解決方案
這篇文章主要介紹了SpringBoot @CompentScan excludeFilters配置無效的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11
selenium + ChromeDriver安裝及使用方法
這篇文章主要介紹了selenium + ChromeDriver安裝及使用方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-06-06
Java實現(xiàn)經(jīng)典大富翁游戲的示例詳解
大富翁,又名地產(chǎn)大亨。是一種多人策略圖版游戲。參與者分得游戲金錢,憑運氣(擲骰子)及交易策略,買地、建樓以賺取租金。本文將通過Java實現(xiàn)這一經(jīng)典游戲,感興趣的可以跟隨小編一起學(xué)習(xí)一下2022-02-02

