大家好,前面我已經剖析了OpenFeign的動態代理生成原理和Ribbon的運行原理,這篇文章來繼續剖析SpringCloud組件原理,來看一看OpenFeign是如何基于Ribbon來實作負載均衡的,兩組件是如何協同作業的,
一、Feign動態代理呼叫實作rpc流程分析
通過Feign客戶端介面的動態代理生成原理講解,我們可以清楚的知道,Feign客戶端介面的動態代理生成是基于JDK的動態代理來實作的,那么在所有的方法呼叫的時候最終都會走InvocationHandler介面的實作,默認就是ReflectiveFeign.FeignInvocationHandler,那我們接下來就來看看,FeignInvocationHandler是如何實作rpc呼叫的,
FeignInvocationHandler對于invoke方法的實作,
private final Map<Method, MethodHandler> dispatch;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("equals".equals(method.getName())) {
try {
Object otherHandler =
args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
return equals(otherHandler);
} catch (IllegalArgumentException e) {
return false;
}
} else if ("hashCode".equals(method.getName())) {
return hashCode();
} else if ("toString".equals(method.getName())) {
return toString();
}
return dispatch.get(method).invoke(args);
}
前幾個if判斷很簡單,就是判斷是不是呼叫的方法是不是equals,hashCode,toString,因為這些方法的調是不需要走rpc呼叫的,
接下就是從dispatch獲取要呼叫的方法對應的MethodHandler,然后呼叫MethodHandler的invoke方法,那MethodHandler是什么時候生成的呢?MethodHandler是在構建動態代理的時候生成的,不清楚的同學可以翻一下OpenFeign那篇文章最后關于生成動態代理的那部分原始碼,那MethodHandler作用是什么呢?你可以理解為最終rpc的呼叫都是基于這個MethodHandler來實作的,每個方法都有對應MethodHandler來實作rpc呼叫,接下來我們就來看一下MethodHandler的invoke方法的實作,
MethodHandler是個介面,有兩個實作類,一個是DefaultMethodHandler,這個是處理介面中的默認方法的,另一個是SynchronousMethodHandler,這個是實作rpc呼叫的方法,接下來我們就看看SynchronousMethodHandler關于invoke方法的實作,
@Override
public Object invoke(Object[] argv) throws Throwable {
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;
}
}
}
第一行通過方法的引數構建了一個RequestTemplate,RequestTemplate可以看成是組裝http請求所需各種引數的封裝,比如什么情頭,body之類的都放在這里面,
第二行 Options options = findOptions(argv); 這個很有意思,Options主要是封裝了發送請求是連接超時時間和讀超時時間的配置,findOptions(argv)也就是先從引數里面找有沒有Options,沒有就回傳構造SynchronousMethodHandler的入參時的Options,也就是說,連接超時時間和讀超時時間可以從方法入參來傳入,不過一般沒有人這么玩,
第三行就是搞一個重試的組件,是可以實作重試的,一般不設定,
然后執行到executeAndDecode(template, options),進入這個方法
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
Request request = targetRequest(template);
if (logLevel != Logger.Level.NONE) {
logger.logRequest(metadata.configKey(), logLevel, request);
}
Response response;
long start = System.nanoTime();
try {
response = client.execute(request, options);
} 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);
boolean shouldClose = true;
try {
if (logLevel != Logger.Level.NONE) {
response =
logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime);
}
if (Response.class == metadata.returnType()) {
if (response.body() == null) {
return response;
}
if (response.body().length() == null ||
response.body().length() > MAX_RESPONSE_BUFFER_SIZE) {
shouldClose = false;
return response;
}
// Ensure the response body is disconnected
byte[] bodyData = https://www.cnblogs.com/zzyang/p/Util.toByteArray(response.body().asInputStream());
return response.toBuilder().body(bodyData).build();
}
if (response.status() >= 200 && response.status() < 300) {
if (void.class == metadata.returnType()) {
return null;
} else {
Object result = decode(response);
shouldClose = closeAfterDecode;
return result;
}
} else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) {
Object result = decode(response);
shouldClose = closeAfterDecode;
return result;
} else {
throw errorDecoder.decode(metadata.configKey(), response);
}
} catch (IOException e) {
if (logLevel != Logger.Level.NONE) {
logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime);
}
throw errorReading(request, response, e);
} finally {
if (shouldClose) {
ensureClosed(response.body());
}
}
}
首先呼叫了targetRequest方法,貼出原始碼
Request targetRequest(RequestTemplate template) {
for (RequestInterceptor interceptor : requestInterceptors) {
interceptor.apply(template);
}
return target.apply(template);
}
這個方法會遍歷所有的攔截器RequestInterceptor,這是feign的一個擴展點,也就說再發送請求前,你仍然還有機會對請求的內容進行調整,比如說加個請求頭,這也是很常見的一種方式,在微服務之間鑒權的時候使用,RequestInterceptor是在構建Feign.Builder的時候傳進來的,Feign.Builder的組件都是通過ioc容器獲取的,組件又是通過配置類來的,所以你需要的話就可以在配置類中宣告RequestInterceptor物件,配置類有不同的優先級,按照自己的需求,可以在其中一個優先級使用,不過一般這種通用的東西,不是某個微服務特有的功能,一般選擇在springboot啟動中的容器中配置,
執行完targetRequest,回到executeAndDecode之后,會構建出一個Request,Request很好理解,就是一個請求,里面封裝了http請求的東西,接下來就會呼叫Client的execute方法來執行請求,拿到回應,接下來就是基于處理這個回應,將回應資料封裝成需要回傳的引數,之后回傳給呼叫方,
到這里,我們已經分析出介面的動態代理是如何運行的,其實就是通過每個方法對應的MethodHandler來實作的,MethodHandler主要就是拼接各種引數,組裝成一個請求,隨后交由Client介面的實作去發送請求,
二、LoadBalancerFeignClient
通過上面分析整個動態代理呼叫程序可以看出,Client是發送http請求的關鍵類,那么Client是什么玩意?還記得我在關于OpenFeign動態代理生成的那篇文章中留下的一個疑問么,當Feign客戶端在構建動態代理的時候,填充很多組件到Feign.Builder中,其中有個組件就是Client的實作,我們并沒有在FeignClientsConfiguration配置類中找到關于Client的物件的宣告,不過當時我就提到了,這個組件的實作是要依賴負載均衡的,也就是這個組件是Feign用來整合Ribbon的入口,
接下來,我們就著重看一下Client的實作,看看Feign是如何通過ribbon實作負載均衡的,
我們先來看一下Feign跟ribbon整合的配置類,
@Import({ HttpClientFeignLoadBalancedConfiguration.class,
OkHttpFeignLoadBalancedConfiguration.class,
DefaultFeignLoadBalancedConfiguration.class })
public class FeignRibbonClientAutoConfiguration {
@Bean
@Primary
@ConditionalOnMissingBean
@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
public CachingSpringLoadBalancerFactory cachingLBClientFactory(
SpringClientFactory factory) {
return new CachingSpringLoadBalancerFactory(factory);
}
@Bean
@Primary
@ConditionalOnMissingBean
@ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate")
public CachingSpringLoadBalancerFactory retryabeCachingLBClientFactory(
SpringClientFactory factory, LoadBalancedRetryFactory retryFactory) {
return new CachingSpringLoadBalancerFactory(factory, retryFactory);
}
@Bean
@ConditionalOnMissingBean
public Request.Options feignRequestOptions() {
return LoadBalancerFeignClient.DEFAULT_OPTIONS;
}
}
我們來分析一下,首先通過@Impot注解匯入了三個配置類,
-
HttpClientFeignLoadBalancedConfiguration:基于HttpClient實作http呼叫的,
-
OkHttpFeignLoadBalancedConfiguration:基于OkHttp實作http呼叫的,
-
DefaultFeignLoadBalancedConfiguration:默認的,也就是Feign原生的發送http的實作,
這里我們看一下DefaultFeignLoadBalancedConfiguration配置類,因為默認就是這,HttpClientFeignLoadBalancedConfiguration和OkHttpFeignLoadBalancedConfiguration都需要有引入HttpClient和OkHttp依賴才會有用
@Configuration(proxyBeanMethods = false)
class DefaultFeignLoadBalancedConfiguration {
@Bean
@ConditionalOnMissingBean
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
SpringClientFactory clientFactory) {
return new LoadBalancerFeignClient(new Client.Default(null, null), cachingFactory,
clientFactory);
}
}
這個配置類很簡單,宣告了LoadBalancerFeignClient到spring容器,傳入了三個引數,一個Client的實作,一個CachingSpringLoadBalancerFactory和一個SpringClientFactory,LoadBalancerFeignClient這個類實作了Client介面,也就數說我們在構建Feign.Builder填充的就是這個物件,也就是上面說feign的執行流程最后用來執行請求的Client的實作,
接下來我說一下入參的三個引數是什么意思,
-
Client.Default:就是Feign自己實作的Client,里面封裝了真正發送http發送請求的功能,LoadBalancerFeignClient雖然也實作了Client介面,但是這個實作其實是為了整合Ribbon用的,并沒有發送http的功能,所以需要有個可以發送http功能的實作,
-
CachingSpringLoadBalancerFactory:后面會說這個類的作用
-
SpringClientFactory:這個跟Feign里面的FeignContext的作用差不多,用來實作配置隔離的,當然,這個也在關于Ribbon的那篇文章有剖析過,
其實大家可以自行去看OkHttpFeignLoadBalancedConfiguration和HttpClientFeignLoadBalancedConfiguration,其實他們配置跟DefaultFeignLoadBalancedConfiguration是一樣的,宣告的物件都是LoadBalancerFeignClient,只不過將Client.Default換成了基于HttpClient和OkHttp的實作,也就是發送http請求使用的工具不一樣,
FeignRibbonClientAutoConfiguration除了匯入配置類還宣告了CachingSpringLoadBalancerFactory,只不過一種是帶基于spring實作的重試功能的,一種是不帶的,主要看有沒有引入spring重試功能的包,所以上面構建LoadBalancerFeignClient注入的CachingSpringLoadBalancerFactory就是在這宣告的,
這里就說完了Feign整合ribbon的配置類FeignRibbonClientAutoConfiguration,我們也找到了構造Feign.Builder的實作LoadBalancerFeignClient,接下來就來剖析LoadBalancerFeignClient的實作,
public class LoadBalancerFeignClient implements Client {
static final Request.Options DEFAULT_OPTIONS = new Request.Options();
private final Client delegate;
private CachingSpringLoadBalancerFactory lbClientFactory;
private SpringClientFactory clientFactory;
public LoadBalancerFeignClient(Client delegate,
CachingSpringLoadBalancerFactory lbClientFactory,
SpringClientFactory clientFactory) {
this.delegate = delegate;
this.lbClientFactory = lbClientFactory;
this.clientFactory = clientFactory;
}
static URI cleanUrl(String originalUrl, String host) {
String newUrl = originalUrl;
if (originalUrl.startsWith("https://")) {
newUrl = originalUrl.substring(0, 8)
+ originalUrl.substring(8 + host.length());
}
else if (originalUrl.startsWith("http")) {
newUrl = originalUrl.substring(0, 7)
+ originalUrl.substring(7 + host.length());
}
StringBuffer buffer = new StringBuffer(newUrl);
if ((newUrl.startsWith("https://") && newUrl.length() == 8)
|| (newUrl.startsWith("http://") && newUrl.length() == 7)) {
buffer.append("/");
}
return URI.create(buffer.toString());
}
@Override
public Response execute(Request request, Request.Options options) throws IOException {
try {
URI asUri = URI.create(request.url());
String clientName = asUri.getHost();
URI uriWithoutHost = cleanUrl(request.url(), clientName);
FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
this.delegate, request, uriWithoutHost);
IClientConfig requestConfig = getClientConfig(options, clientName);
return lbClient(clientName)
.executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
}
catch (ClientException e) {
IOException io = findIOException(e);
if (io != null) {
throw io;
}
throw new RuntimeException(e);
}
}
IClientConfig getClientConfig(Request.Options options, String clientName) {
IClientConfig requestConfig;
if (options == DEFAULT_OPTIONS) {
requestConfig = this.clientFactory.getClientConfig(clientName);
}
else {
requestConfig = new FeignOptionsClientConfig(options);
}
return requestConfig;
}
protected IOException findIOException(Throwable t) {
if (t == null) {
return null;
}
if (t instanceof IOException) {
return (IOException) t;
}
return findIOException(t.getCause());
}
public Client getDelegate() {
return this.delegate;
}
private FeignLoadBalancer lbClient(String clientName) {
return this.lbClientFactory.create(clientName);
}
static class FeignOptionsClientConfig extends DefaultClientConfigImpl {
FeignOptionsClientConfig(Request.Options options) {
setProperty(CommonClientConfigKey.ConnectTimeout,
options.connectTimeoutMillis());
setProperty(CommonClientConfigKey.ReadTimeout, options.readTimeoutMillis());
}
@Override
public void loadProperties(String clientName) {
}
@Override
public void loadDefaultValues() {
}
}
}
在動態代理呼叫的那里我們得出一個結論,那就是最后會呼叫Client介面的execute方法的實作,所以我們就看一下execute方法的實作,這里就是一堆操作,從請求的URL中拿到了clientName,也就是服務名,
為什么可以拿到服務名?
其實很簡單,OpenFeign構建動態代理的時候,傳入了一個HardCodedTarget,當時說在構建HardCodedTarget的時候傳入了一個url,那個url當時說了其實就是http://服務名,所以到這里,雖然有具體的請求介面的路徑,但是還是類似 http://服務名/api/sayHello這種,所以可以通過路徑拿到你鎖請求的服務名,
拿到服務名之后,再拿到了一個配置類IClientConfig,最后呼叫lbClient,我們看一下lbClient的方法實作,
private FeignLoadBalancer lbClient(String clientName) {
return this.lbClientFactory.create(clientName);
}
就是呼叫CachingSpringLoadBalancerFactory的create方法
public FeignLoadBalancer create(String clientName) {
FeignLoadBalancer client = this.cache.get(clientName);
if (client != null) {
return client;
}
IClientConfig config = this.factory.getClientConfig(clientName);
ILoadBalancer lb = this.factory.getLoadBalancer(clientName);
ServerIntrospector serverIntrospector = this.factory.getInstance(clientName,
ServerIntrospector.class);
client = this.loadBalancedRetryFactory != null
? new RetryableFeignLoadBalancer(lb, config, serverIntrospector,
this.loadBalancedRetryFactory)
: new FeignLoadBalancer(lb, config, serverIntrospector);
this.cache.put(clientName, client);
return client;
}
創建的程序就是從每個服務對應的容器中獲取到IClientConfig和ILoadBalancer,Ribbon那篇文章都講過這些核心類,這里不再贅述,
默認就是創建不帶spring重試功能的FeignLoadBalancer,放入快取,最后回傳這個FeignLoadBalancer,所以第一次來肯定沒有,需要構建,也就是最終一定會回傳FeignLoadBalancer,所以我們通過lbClient方法拿到的是FeignLoadBalancer,從這里可以看出CachingSpringLoadBalancerFactory是構建FeignLoadBalancer的工廠類,只不過先從快取中查找,找不到再創建FeignLoadBalancer,
拿到FeignLoadBalancer之后就會呼叫executeWithLoadBalancer,接收到Response之后直接回傳,
三、FeignLoadBalancer
那么這個FeignLoadBalancer又是啥呢?這里放上FeignLoadBalancer核心原始碼,
public class FeignLoadBalancer extends
AbstractLoadBalancerAwareClient<FeignLoadBalancer.RibbonRequest, FeignLoadBalancer.RibbonResponse> {
private final RibbonProperties ribbon;
protected int connectTimeout;
protected int readTimeout;
protected IClientConfig clientConfig;
protected ServerIntrospector serverIntrospector;
public FeignLoadBalancer(ILoadBalancer lb, IClientConfig clientConfig,
ServerIntrospector serverIntrospector) {
super(lb, clientConfig);
this.setRetryHandler(RetryHandler.DEFAULT);
this.clientConfig = clientConfig;
this.ribbon = RibbonProperties.from(clientConfig);
RibbonProperties ribbon = this.ribbon;
this.connectTimeout = ribbon.getConnectTimeout();
this.readTimeout = ribbon.getReadTimeout();
this.serverIntrospector = serverIntrospector;
}
@Override
public RibbonResponse execute(RibbonRequest request, IClientConfig configOverride)
throws IOException {
Request.Options options;
if (configOverride != null) {
RibbonProperties override = RibbonProperties.from(configOverride);
options = new Request.Options(override.connectTimeout(this.connectTimeout),
override.readTimeout(this.readTimeout));
}
else {
options = new Request.Options(this.connectTimeout, this.readTimeout);
}
Response response = request.client().execute(request.toRequest(), options);
return new RibbonResponse(request.getUri(), response);
}
}
FeignLoadBalancer繼承自AbstractLoadBalancerAwareClient,AbstractLoadBalancerAwareClient又是啥玩意?看過我寫的關于Ribbon核心組件已經運行原理的那篇文章小伙伴肯定知道,AbstractLoadBalancerAwareClient類主要作用是通過ILoadBalancer組件獲取一個Server,然后基于這個Server重構了URI,也就是將你的請求路徑http://服務名/api/sayHello轉換成類似http://192.168.1.101:8088/api/sayHello這種路徑,也就是將原服務名替換成服務所在的某一臺機器ip和埠,替換之后就交由子類實作的exceut方法來發送http請求,
所以我們知道呼叫executeWithLoadBalancer之后,就會重構請求路徑,將服務名替換成某個具體的服務器所在的ip和埠,之后交給子類execute來處理,對于這里來說,也就是FeignLoadBalancer的execute方法,因為FeignLoadBalancer繼承AbstractLoadBalancerAwareClient,
直接定位到execute方法最核心的一行代碼
Response response = request.client().execute(request.toRequest(), options);
request.client()就會拿到構建LoadBalancerFeignClient傳入的那個Client的實作,我提到過,這個Client的實作是具體發送請求的實作,默認的就是Client.Default類(不是默認就有可能是基于HttpClient或者是OkHttp的實作),所以這行代碼就是基于這個Client就成功的發送了Http請求,拿到回應,然后將這個Response 封裝成一個RibbonResponse回傳,最后就回傳給MethodHandler,然后決議回應,封裝成方法的回傳值回傳給呼叫者,
好了,其實到這里就完全知道Feign是如何整合Ribbon的,LoadBalancerFeignClient其實是OpenFeign適配Ribbon的入口,FeignLoadBalancer才是真正實作選擇負載均衡,發送http請求的組件,因為他繼承了AbstractLoadBalancerAwareClient,
為了大家能夠清楚的知道整個動態代理的呼叫程序,我在Ribbon的那張圖的基礎上,加上Feign的呼叫鏈路,

通過這張圖,我們可以清楚地看出OpenFeign、Ribbon以及注冊中心之間的協同關系,
四、總結
到這里,我通過三篇文章,算上Nacos那兩篇,總共五篇文章完整的講述了在微服務架構中,OpenFeign、Ribbon、Nacos(當然其它注冊中心也可以)這三個組件協同作業的核心原始碼和流程,這里我再用簡潔的話來總結一下他們的協同作業原理,OpenFeign在進行rpc呼叫的時候,由于不知道服務具體在哪臺機器上,所以需要Ribbon這個負載均衡組件從服務所在的機器串列中選擇一個,Ribbon中服務所在的機器串列是從注冊中心拉取的,Ribbon提供了一個ServerList介面,注冊中心實作之后,Ribbon就可以獲取到服務所在的機器串列,這就是這三個組件最基本的原理,希望通過這五篇文章,小伙伴們可以對微服務架構的最基本的原理有一定的了解,同時也對OpenFeign、Ribbon、Nacos原始碼有一定的認識,
往期熱門文章推薦
-
Redis分布式鎖實作Redisson 15問
-
Zookeeper分布式鎖實作Curator十一問
-
有關回圈依賴和三級快取的這些問題,你都會么?(面試常問)
-
萬字+28張圖帶你探秘小而美的規則引擎框架LiteFlow
-
7000字+24張圖帶你徹底弄懂執行緒池
- 面渣逆襲:Spring三十五問,四萬字+五十圖詳解!建議收藏!
掃碼或者搜索關注公眾號 三友的java日記 ,及時干貨不錯過,公眾號致力于通過畫圖加上通俗易懂的語言講解技術,讓技術更加容易學習,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/495296.html
標籤:Java
上一篇:這不會又是一個Go的BUG吧?

