參考:
https://www.cnblogs.com/crazymakercircle/p/11965726.html
1 SpringCloud 中 Feign 核心原理
如果不了解 SpringCloud 中 Feign 核心原理媚值,不會真正的了解 SpringCloud 的性能優(yōu)化和配置優(yōu)化悄谐,也就不可能做到真正掌握 SpringCloud于样。
本章從Feign 遠程調(diào)用的重要組件開始蔫巩,圖文并茂的介紹 Feigh 遠程調(diào)用的執(zhí)行流程夸浅、Feign 本地 JDK Proxy 實例的創(chuàng)建流程泊交,徹底的為大家解讀 SpringCloud 的核心知識锄弱。使得廣大的工程師不光做到知其然他托,更能知其所以然。
1.1 簡介:Feign遠程調(diào)用的基本流程
Feign遠程調(diào)用甩苛,核心就是通過一系列的封裝和處理蹂楣,將以JAVA注解的方式定義的遠程調(diào)用API接口,最終轉(zhuǎn)換成HTTP的請求形式讯蒲,然后將HTTP的請求的響應(yīng)結(jié)果痊土,解碼成JAVA Bean,放回給調(diào)用者墨林。Feign遠程調(diào)用的基本流程赁酝,大致如下圖所示。
從上圖可以看到旭等,F(xiàn)eign通過處理注解酌呆,將請求模板化,當實際調(diào)用的時候搔耕,傳入?yún)?shù)隙袁,根據(jù)參數(shù)再應(yīng)用到請求上,進而轉(zhuǎn)化成真正的 Request 請求弃榨。通過Feign以及JAVA的動態(tài)代理機制菩收,使得Java 開發(fā)人員,可以不用通過HTTP框架去封裝HTTP請求報文的方式鲸睛,完成遠程服務(wù)的HTTP調(diào)用娜饵。
1.2 Feign 遠程調(diào)用的重要組件
在微服務(wù)啟動時,F(xiàn)eign會進行包掃描官辈,對加@FeignClient注解的接口划咐,按照注解的規(guī)則,創(chuàng)建遠程接口的本地JDK Proxy代理實例钧萍。然后褐缠,將這些本地Proxy代理實例,注入到Spring IOC容器中风瘦。當遠程接口的方法被調(diào)用队魏,由Proxy代理實例去完成真正的遠程訪問,并且返回結(jié)果万搔。
為了清晰的介紹SpringCloud中Feign運行機制和原理胡桨,在這里,首先為大家梳理一下Feign中幾個重要組件瞬雹。
1.2.1 遠程接口的本地JDK Proxy代理實例
遠程接口的本地JDK Proxy代理實例昧谊,有以下特點:
- Proxy代理實例,實現(xiàn)了一個加 @FeignClient 注解的遠程調(diào)用接口酗捌;
- Proxy代理實例呢诬,能在內(nèi)部進行HTTP請求的封裝涌哲,以及發(fā)送HTTP 請求;
- Proxy代理實例尚镰,能處理遠程HTTP請求的響應(yīng)阀圾,并且完成結(jié)果的解碼,然后返回給調(diào)用者狗唉。
下面以一個簡單的遠程服務(wù)的調(diào)用接口 DemoClient 為例初烘,具體介紹一下遠程接口的本地JDK Proxy代理實例的創(chuàng)建過程。
DemoClient 接口分俯,有兩個非常簡單的遠程調(diào)用抽象方法:一個為hello() 抽象方法肾筐,用于完成遠程URL “/api/demo/hello/v1”的HTTP請求;一個為 echo(…) 抽象方法缸剪,用于完成遠程URL “/api/demo/echo/{word}/v1”的HTTP請求局齿。具體如下圖所示。
DemoClient 接口代碼如下:
package com.crazymaker.springcloud.demo.contract.client;
//…省略import
@FeignClient(
value = "seckill-provider", path = "/api/demo/",
fallback = DemoDefaultFallback.class)
public interface DemoClient {
/**
* 測試遠程調(diào)用 * * @return hello */
@GetMapping("/hello/v1")
Result<JSONObject> hello();
/**
* 非常簡單的一個 回顯 接口橄登,主要用于遠程調(diào)用 * * @return echo 回顯消息 */
@RequestMapping(value = "/echo/{word}/v1", method = RequestMethod.GET)
Result<JSONObject> echo(
@PathVariable(value = "word") String word);
}
注意,上面的代碼中讥此,在DemoClient 接口上拢锹,加有@FeignClient 注解。也即是說萄喳,F(xiàn)eign在啟動時卒稳,會為其創(chuàng)建一個本地JDK Proxy代理實例,并注冊到Spring IOC容器他巨。
如何使用呢充坑?可以通過@Resource注解,按照類型匹配(這里的類型為DemoClient接口類型)染突,從Spring IOC容器找到這個代理實例捻爷,并且裝配給需要的成員變量。
DemoClient的 本地JDK Proxy 代理實例的使用的代碼如下:
package com.crazymaker.springcloud.user.info.controller;
//…省略import
@Api(value = "用戶信息份企、基礎(chǔ)學(xué)習(xí)DEMO", tags = {"用戶信息也榄、基礎(chǔ)學(xué)習(xí)DEMO"})
@RestController
@RequestMapping("/api/user")
public class UserController {
@Resource
DemoClient demoClient; //裝配 DemoClient 的本地代理實例
@GetMapping("/say/hello/v1")
@ApiOperation(value = "測試遠程調(diào)用速度")
public Result<JSONObject> hello() {
Result<JSONObject> result = demoClient.hello();
JSONObject data = new JSONObject();
data.put("others", result);
return Result.success(data).setMsg("操作成功");
}
//…
}
DemoClient的本地JDK Proxy代理實例的創(chuàng)建過程,比較復(fù)雜司志,稍后作為重點介紹甜紫。先來看另外兩個重要的邏輯組件。
1.2.2 調(diào)用處理器 InvocationHandler
大家知道骂远,通過 JDK Proxy 生成動態(tài)代理類囚霸,核心步驟就是需要定制一個調(diào)用處理器,具體來說激才,就是實現(xiàn)JDK中位于java.lang.reflect 包中的 InvocationHandler 調(diào)用處理器接口拓型,并且實現(xiàn)該接口的 invoke(…) 抽象方法额嘿。
為了創(chuàng)建Feign的遠程接口的代理實現(xiàn)類,F(xiàn)eign提供了自己的一個默認的調(diào)用處理器吨述,叫做 FeignInvocationHandler 類岩睁,該類處于 feign-core 核心jar包中。當然揣云,調(diào)用處理器可以進行替換捕儒,如果Feign與Hystrix結(jié)合使用,則會替換成 HystrixInvocationHandler 調(diào)用處理器類邓夕,類處于 feign-hystrix 的jar包中刘莹。
1.2.1 默認的調(diào)用處理器 FeignInvocationHandler
默認的調(diào)用處理器 FeignInvocationHandler 是一個相對簡單的類,有一個非常重要Map類型成員 dispatch 映射焚刚,保存著遠程接口方法到MethodHandler方法處理器的映射点弯。
以前面示例中DemoClient 接口為例,其代理實現(xiàn)類的調(diào)用處理器 FeignInvocationHandler 的dispatch 成員的內(nèi)存結(jié)構(gòu)圖如圖3所示矿咕。
為何在圖3中的Map類型成員 dispatch 映射對象中抢肛,有兩個Key-Value鍵值對呢?
原因是:默認的調(diào)用處理器 FeignInvocationHandle碳柱,在處理遠程方法調(diào)用的時候捡絮,會根據(jù)Java反射的方法實例,在dispatch 映射對象中莲镣,找到對應(yīng)的MethodHandler 方法處理器福稳,然后交給MethodHandler 完成實際的HTTP請求和結(jié)果的處理。前面示例中的 DemoClient 遠程調(diào)用接口瑞侮,有兩個遠程調(diào)用方法的圆,所以,其代理實現(xiàn)類的調(diào)用處理器 FeignInvocationHandler 的dispatch 成員半火,有兩個有兩個Key-Value鍵值對越妈。
FeignInvocationHandler的關(guān)鍵源碼,節(jié)選如下:
package feign;
//...省略import
public class ReflectiveFeign extends Feign {
//...
//內(nèi)部類:默認的Feign調(diào)用處理器 FeignInvocationHandler
static class FeignInvocationHandler implements InvocationHandler {
private final Target target;
//方法實例對象和方法處理器的映射
private final Map<Method, MethodHandler> dispatch;
//構(gòu)造函數(shù)
FeignInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) {
this.target = checkNotNull(target, "target");
this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);
}
//默認Feign調(diào)用的處理
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//...
//首先钮糖,根據(jù)方法實例叮称,從方法實例對象和方法處理器的映射中,
//取得 方法處理器藐鹤,然后瓤檐,調(diào)用 方法處理器 的 invoke(...) 方法
return dispatch.get(method).invoke(args);
}
//...
}
源碼很簡單,重點在于invoke(…)方法娱节,雖然核心代碼只有一行挠蛉,但是其功能是復(fù)雜的:
- 根據(jù)Java反射的方法實例,在dispatch 映射對象中肄满,找到對應(yīng)的MethodHandler 方法處理器谴古;
- 調(diào)用MethodHandler方法處理器的 invoke(...) 方法质涛,完成實際的HTTP請求和結(jié)果的處理。
補充說明一下:MethodHandler 方法處理器掰担,和JDK 動態(tài)代理機制中位于 java.lang.reflect 包的 InvocationHandler 調(diào)用處理器接口汇陆,沒有任何的繼承和實現(xiàn)關(guān)系。MethodHandler 僅僅是Feign自定義的带饱,一個非常簡單接口毡代。
1.2.2 方法處理器 MethodHandler
Feign的方法處理器 MethodHandler 是一個獨立的接口,定義在 InvocationHandlerFactory 接口中勺疼,僅僅擁有一個invoke(…)方法教寂,源碼如下:
//定義在InvocationHandlerFactory接口中
public interface InvocationHandlerFactory {
//…
//方法處理器接口,僅僅擁有一個invoke(…)方法
interface MethodHandler {
//完成遠程URL請求
Object invoke(Object[] argv) throws Throwable;
}
//...
}
MethodHandler 的invoke(…)方法执庐,主要職責(zé)是完成實際遠程URL請求酪耕,然后返回解碼后的遠程URL的響應(yīng)結(jié)果。Feign提供了默認的 SynchronousMethodHandler 實現(xiàn)類轨淌,提供了基本的遠程URL的同步請求處理迂烁。有關(guān) SynchronousMethodHandler類以及其與MethodHandler的關(guān)系,大致如圖4所示递鹉。
為了徹底了解方法處理器盟步,來讀一下 SynchronousMethodHandler 方法處理器的源碼,大致如下:
package feign;
//…..省略import
final class SynchronousMethodHandler implements MethodHandler {
//…
// 執(zhí)行Handler 的處理
public Object invoke(Object[] argv) throws Throwable {
RequestTemplate requestTemplate = this.buildTemplateFromArgs.create(argv);
Retryer retryer = this.retryer.clone();
while(true) {
try {
return this.executeAndDecode(requestTemplate);
} catch (RetryableException var5) {
//…省略不相干代碼
}
}
}
//執(zhí)行請求梳虽,然后解碼結(jié)果
Object executeAndDecode(RequestTemplate template) throws Throwable {
Request request = this.targetRequest(template);
long start = System.nanoTime();
Response response;
try {
response = this.client.execute(request, this.options);
response.toBuilder().request(request).build();
}
}
}
SynchronousMethodHandler的invoke(…)方法,調(diào)用了自己的executeAndDecode(…) 請求執(zhí)行和結(jié)果解碼方法灾茁。該方法的工作步驟:
- 首先通 RequestTemplate 請求模板實例窜觉,生成遠程URL請求實例 request;
- 然后用自己的 feign 客戶端client成員北专,excecute(…) 執(zhí)行請求禀挫,并且獲取 response 響應(yīng);
- 對response 響應(yīng)進行結(jié)果解碼拓颓。
1.2.3 Feign 客戶端組件 feign.Client
客戶端組件是Feign中一個非常重要的組件语婴,負責(zé)端到端的執(zhí)行URL請求。其核心的邏輯:發(fā)送request請求到服務(wù)器驶睦,并接收response響應(yīng)后進行解碼砰左。
feign.Client 類,是代表客戶端的頂層接口场航,只有一個抽象方法缠导,源碼如下:
package feign;
/**客戶端接口
* Submits HTTP {@link Request requests}. Implementations are expected to be thread-safe.
*/
public interface Client {
//提交HTTP請求,并且接收response響應(yīng)后進行解碼
Response execute(Request request, Options options) throws IOException;
}
由于不同的feign.Client 實現(xiàn)類溉痢,內(nèi)部完成HTTP請求的組件和技術(shù)不同僻造,故憋他,feign.Client 有多個不同的實現(xiàn)。這里舉出幾個例子:
- Client.Default類:默認的feign.Client 客戶端實現(xiàn)類髓削,內(nèi)部使用HttpURLConnnection 完成URL請求處理竹挡;
- ApacheHttpClient 類:內(nèi)部使用 Apache httpclient 開源組件完成URL請求處理的feign.Client 客戶端實現(xiàn)類;
- OkHttpClient類:內(nèi)部使用 OkHttp3 開源組件完成URL請求處理的feign.Client 客戶端實現(xiàn)類立膛。
- LoadBalancerFeignClient 類:內(nèi)部使用 Ribben 負載均衡技術(shù)完成URL請求處理的feign.Client 客戶端實現(xiàn)類揪罕。
此外,還有一些特殊場景使用的feign.Client客戶端實現(xiàn)類旧巾,也可以定制自己的feign.Client實現(xiàn)類耸序。下面對上面幾個常見的客戶端實現(xiàn)類,進行簡要介紹鲁猩。
一:Client.Default類:
作為默認的Client 接口的實現(xiàn)類坎怪,在Client.Default內(nèi)部使用JDK自帶的HttpURLConnnection類實現(xiàn)URL網(wǎng)絡(luò)請求。
在JKD1.8中廓握,雖然在HttpURLConnnection 底層搅窿,使用了非常簡單的HTTP連接池技術(shù),但是隙券,其HTTP連接的復(fù)用能力男应,實際是非常弱的,性能當然也很低娱仔。具體的原因沐飘,參見后面的“SpringCloud與長連接的深入剖析”專題內(nèi)容。
二:ApacheHttpClient類
ApacheHttpClient 客戶端類的內(nèi)部牲迫,使用 Apache HttpClient開源組件完成URL請求的處理耐朴。
從代碼開發(fā)的角度而言,Apache HttpClient相比傳統(tǒng)JDK自帶的URLConnection盹憎,增加了易用性和靈活性筛峭,它不僅使客戶端發(fā)送Http請求變得容易,而且也方便開發(fā)人員測試接口陪每。既提高了開發(fā)的效率影晓,也方便提高代碼的健壯性。
從性能的角度而言檩禾,Apache HttpClient帶有連接池的功能挂签,具備優(yōu)秀的HTTP連接的復(fù)用能力。關(guān)于帶有連接池Apache HttpClient的性能提升倍數(shù)盼产,具體可以參見后面的對比試驗竹握。
ApacheHttpClient 類處于 feign-httpclient 的專門jar包中,如果使用辆飘,還需要通過Maven依賴或者其他的方式啦辐,倒入配套版本的專門jar包谓传。
三:OkHttpClient類
OkHttpClient 客戶端類的內(nèi)部,使用OkHttp3 開源組件完成URL請求處理芹关。OkHttp3 開源組件由Square公司開發(fā)续挟,用于替代HttpUrlConnection和Apache HttpClient。由于OkHttp3較好的支持 SPDY協(xié)議(SPDY是Google開發(fā)的基于TCP的傳輸層協(xié)議侥衬,用以最小化網(wǎng)絡(luò)延遲诗祸,提升網(wǎng)絡(luò)速度,優(yōu)化用戶的網(wǎng)絡(luò)使用體驗轴总。)直颅,從Android4.4開始,google已經(jīng)開始將Android源碼中的 HttpURLConnection 請求類使用OkHttp進行了替換怀樟。也就是說功偿,對于Android 移動端APP開發(fā)來說,OkHttp3 組件往堡,是基礎(chǔ)的開發(fā)組件之一械荷。
四:LoadBalancerFeignClient 類
LoadBalancerFeignClient 內(nèi)部使用了 Ribben 客戶端負載均衡技術(shù)完成URL請求處理。在原理上虑灰,簡單的使用了delegate包裝代理模式:Ribben負載均衡組件計算出合適的服務(wù)端server之后吨瞎,由內(nèi)部包裝 delegate 代理客戶端完成到服務(wù)端server的HTTP請求;所封裝的 delegate 客戶端代理實例的類型穆咐,可以是 Client.Default 默認客戶端颤诀,也可以是 ApacheHttpClient 客戶端類或OkHttpClient 高性能客戶端類,還可以其他的定制的feign.Client 客戶端實現(xiàn)類型对湃。
LoadBalancerFeignClient 負載均衡客戶端實現(xiàn)類崖叫,具體如下圖所示。
由于Feign遠程調(diào)用接口的JDK Proxy實例的InvokeHandler調(diào)用處理器有多種熟尉,導(dǎo)致Feign遠程調(diào)用的執(zhí)行流程归露,也稍微有所區(qū)別洲脂,但是遠程調(diào)用執(zhí)行流程的主要步驟斤儿,是一致的。這里主要介紹兩類JDK Proxy實例的InvokeHandler調(diào)用處理器相關(guān)的遠程調(diào)用執(zhí)行流程:
- 與 默認的調(diào)用處理器 FeignInvocationHandler 相關(guān)的遠程調(diào)用執(zhí)行流程恐锦;
- 與 Hystrix調(diào)用處理器 HystrixInvocationHandler 相關(guān)的遠程調(diào)用執(zhí)行流程往果。
介紹過程中,還是以前面的DemoClient的JDK Proxy遠程動態(tài)代理實例的執(zhí)行過程為例一铅,演示分析Feigh遠程調(diào)用的執(zhí)行流程陕贮。
1.1.1 與 FeignInvocationHandler 相關(guān)的遠程調(diào)用執(zhí)行流程
FeignInvocationHandler是默認的調(diào)用處理器,如果不對Feign做特殊的配置潘飘,則Feign將使用此調(diào)用處理器肮之。結(jié)合前面的DemoClient的JDK Proxy遠程動態(tài)代理實例的hello()遠程調(diào)用執(zhí)行過程掉缺,在這里,詳細的介紹一下與 FeignInvocationHandler 相關(guān)的遠程調(diào)用執(zhí)行流程戈擒,大致如下圖所示眶明。
整體的遠程調(diào)用執(zhí)行流程,大致分為4步筐高,具體如下:
第1步:通過Spring IOC 容器實例搜囱,裝配代理實例,然后進行遠程調(diào)用柑土。
前文講到蜀肘,F(xiàn)eign在啟動時,會為加上了@FeignClient注解的所有遠程接口(包括 DemoClient 接口)稽屏,創(chuàng)建一個本地JDK Proxy代理實例扮宠,并注冊到Spring IOC容器。在這里诫欠,暫且將這個Proxy代理實例涵卵,叫做 DemoClientProxy,稍后荒叼,會詳細介紹這個Proxy代理實例的具體創(chuàng)建過程轿偎。
然后,在本實例的UserController 調(diào)用代碼中被廓,通過@Resource注解坏晦,按照類型或者名稱進行匹配(這里的類型為DemoClient接口類型),從Spring IOC容器找到這個代理實例嫁乘,并且裝配給@Resource注解所在的成員變量昆婿,本實例的成員變量的名稱為 demoClient。
在需要代進行hello()遠程調(diào)用時蜓斧,直接通過 demoClient 成員變量仓蛆,調(diào)用JDK Proxy動態(tài)代理實例的hello()方法。
第2步:執(zhí)行 InvokeHandler 調(diào)用處理器的invoke(…)方法
前面講到挎春,JDK Proxy動態(tài)代理實例的真正的方法調(diào)用過程看疙,具體是通過 InvokeHandler 調(diào)用處理器完成的。故直奋,這里的DemoClientProxy代理實例能庆,會調(diào)用到默認的FeignInvocationHandler 調(diào)用處理器實例的invoke(…)方法。
通過前面 FeignInvocationHandler 調(diào)用處理器的詳細介紹脚线,大家已經(jīng)知道搁胆,默認的調(diào)用處理器 FeignInvocationHandle,內(nèi)部保持了一個遠程調(diào)用方法實例和方法處理器的一個Key-Value鍵值對Map映射。FeignInvocationHandle 在其invoke(…)方法中渠旁,會根據(jù)Java反射的方法實例攀例,在dispatch 映射對象中,找到對應(yīng)的 MethodHandler 方法處理器顾腊,然后由后者完成實際的HTTP請求和結(jié)果的處理肛度。
所以在第2步中,F(xiàn)eignInvocationHandle 會從自己的 dispatch映射中投慈,找到hello()方法所對應(yīng)的MethodHandler 方法處理器承耿,然后調(diào)用其 invoke(…)方法。
第3步:執(zhí)行 MethodHandler 方法處理器的invoke(…)方法
通過前面關(guān)于 MethodHandler 方法處理器的非常詳細的組件介紹伪煤,大家都知道加袋,feign默認的方法處理器為 SynchronousMethodHandler,其invoke(…)方法主要是通過內(nèi)部成員feign客戶端成員 client抱既,完成遠程 URL 請求執(zhí)行和獲取遠程結(jié)果职烧。
feign.Client 客戶端有多種類型,不同的類型防泵,完成URL請求處理的具體方式不同蚀之。
第4步:通過 feign.Client 客戶端成員,完成遠程 URL 請求執(zhí)行和獲取遠程結(jié)果
如果MethodHandler方法處理器實例中的client客戶端捷泞,是默認的 feign.Client.Default 實現(xiàn)類性足删,則使用JDK自帶的HttpURLConnnection類,完成遠程 URL 請求執(zhí)行和獲取遠程結(jié)果锁右。
如果MethodHandler方法處理器實例中的client客戶端失受,是 ApacheHttpClient 客戶端實現(xiàn)類性,則使用 Apache httpclient 開源組件咏瑟,完成遠程 URL 請求執(zhí)行和獲取遠程結(jié)果拂到。
通過以上四步,應(yīng)該可以清晰的了解到了 SpringCloud中的 feign 遠程調(diào)用執(zhí)行流程和運行機制码泞。
實際上兄旬,為了簡明扼要的介紹清楚默認的調(diào)用流程,上面的流程余寥,實際上省略了一個步驟:第3步领铐,實際可以分為兩小步。為啥呢劈狐? SynchronousMethodHandler 并不是直接完成遠程URL的請求罐孝,而是通過負載均衡機制呐馆,定位到合適的遠程server 服務(wù)器肥缔,然后再完成真正的遠程URL請求。換句話說汹来,SynchronousMethodHandler實例的client成員续膳,其實際不是feign.Client.Default類型改艇,而是 LoadBalancerFeignClient 客戶端負載均衡類型。 因此坟岔,上面的第3步谒兄,如果進一步細分話,大致如下:(1)首先通過 SynchronousMethodHandler 內(nèi)部的client實例社付,實質(zhì)為負責(zé)客戶端負載均衡 LoadBalancerFeignClient 實例承疲,首先查找到遠程的 server 服務(wù)端;(2) 然后再由LoadBalancerFeignClient 實例內(nèi)部包裝的feign.Client.Default 內(nèi)部類實例鸥咖,去請求server端服務(wù)器燕鸽,完成URL請求處理。
最后啼辣,說明下啊研,默認的與 FeignInvocationHandler 相關(guān)的遠程調(diào)用執(zhí)行流程,在運行機制以及調(diào)用性能上鸥拧,滿足不了生產(chǎn)環(huán)境的要求党远,為啥呢? 大致原因有以下兩點:
- 沒有遠程調(diào)用過程中的熔斷監(jiān)測和恢復(fù)機制富弦;
- 也沒有用到高性能的HTTP連接池技術(shù)沟娱。
接下來,將為大家介紹一下用到熔斷監(jiān)測和恢復(fù)機制 Hystrix 技術(shù)的遠程調(diào)用執(zhí)行流程腕柜,該流程中花沉,遠程接口的JDK Proxy動態(tài)代理實例所使用的調(diào)用處理器,叫做 HystrixInvocationHandler 調(diào)用處理器媳握。