SpringCloud 源碼系列(15)— 服務調用Feign 之 結合Ribbon進行負載均衡請求

專欄系列文章:SpringCloud系列專欄java

系列文章:web

SpringCloud 源碼系列(1)— 註冊中心Eureka 之 啓動初始化apache

SpringCloud 源碼系列(2)— 註冊中心Eureka 之 服務註冊、續約markdown

SpringCloud 源碼系列(3)— 註冊中心Eureka 之 抓取註冊表網絡

SpringCloud 源碼系列(4)— 註冊中心Eureka 之 服務下線、故障、自我保護機制app

SpringCloud 源碼系列(5)— 註冊中心Eureka 之 EurekaServer集羣負載均衡

SpringCloud 源碼系列(6)— 註冊中心Eureka 之 總結篇ide

SpringCloud 源碼系列(7)— 負載均衡Ribbon 之 RestTemplate源碼分析

SpringCloud 源碼系列(8)— 負載均衡Ribbon 之 核心原理post

SpringCloud 源碼系列(9)— 負載均衡Ribbon 之 核心組件與配置

SpringCloud 源碼系列(10)— 負載均衡Ribbon 之 HTTP客戶端組件

SpringCloud 源碼系列(11)— 負載均衡Ribbon 之 重試與總結篇

SpringCloud 源碼系列(12)— 服務調用Feign 之 基礎使用篇

SpringCloud 源碼系列(13)— 服務調用Feign 之 掃描@FeignClient註解接口

SpringCloud 源碼系列(14)— 服務調用Feign 之 構建@FeignClient接口動態代理

前一篇文章已經分析出,最終在 Feign.Builderbuild() 方法構建了 ReflectiveFeign,而後利用 ReflectiveFeign 的 newInstance 方法建立了動態代理。這個動態代理的代理對象是 ReflectiveFeign.FeignInvocationHandler。最終來講確定就會利用 Client 進行負載均衡的請求。這節就來看看 Feign 若是利用動態代理髮起HTTP請求的。

FeignClient 動態代理請求

使用 FeignClient 接口時,注入的實際上是動態代理對象,調用接口方法時就會進入執行器 ReflectiveFeign.FeignInvocationHandler,從 FeignInvocationHandler 的 invoke 方法能夠看出,就是根據 method 獲取要執行的方法處理器 MethodHandler,而後執行方法。MethodHandler 的實際類型就是 SynchronousMethodHandler

static class FeignInvocationHandler implements InvocationHandler {
    private final Target target;
    private final Map<Method, MethodHandler> dispatch;

    FeignInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) {
      this.target = checkNotNull(target, "target");
      this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //...
        // 根據 method 獲取 MethodHandler,而後執行方法
        return dispatch.get(method).invoke(args);
    }
}
複製代碼

接着看 SynchronousMethodHandler 的 invoke 方法,核心邏輯就兩步:

  • 先根據請求參數構建請求模板 RequestTemplate,就是處理 URI 模板、參數,好比替換掉 uri 中的佔位符、拼接參數等。
  • 而後調用了 executeAndDecode 執行請求,並將相應結果解碼返回。
public Object invoke(Object[] argv) throws Throwable {
    // 構建請求模板,例若有 url 參數,請求參數之類的
    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;
          }
        }
        continue;
      }
    }
}
複製代碼

能夠看到,通過處理後,URI 上的佔位符就被參數替換了,而且拼接了請求參數。

執行請求和解碼

接着看 executeAndDecode,主要有三步:

  • 先調用 targetRequest 方法,主要就是遍歷 RequestInterceptor 對請求模板 RequestTemplate 定製化,而後調用 HardCodedTargettarget 方法將 RequestTemplate 轉換成 Request 請求對象,Request 封裝了請求地址、請求頭、body 等信息。
  • 而後使用客戶端 client 來執行請求,就是 LoadBalancerFeignClient,這裏就進入了負載均衡請求了。
  • 最後用解碼器 decoder 來解析響應結果,將結果轉換成接口的返回類型。
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
    // 處理RequestTemplate,獲得請求對象 Request
    Request request = targetRequest(template);

    Response response;
    try {
      // 調用 client 執行請求,client => LoadBalancerFeignClient
      response = client.execute(request, options);
      // 構建響應 Response
      response = response.toBuilder()
          .request(request)
          .requestTemplate(template)
          .build();
    } catch (IOException e) {
      //...
    }

    if (decoder != null) {
      // 使用解碼器解碼,將返回數據轉換成接口的返回類型
      return decoder.decode(response, metadata.returnType());
    }

    //....
}
// 應用攔截器處理 RequestTemplate,最後使用 target 從 RequestTemplate 中獲得 Request
Request targetRequest(RequestTemplate template) {
    for (RequestInterceptor interceptor : requestInterceptors) {
      interceptor.apply(template);
    }
    // target => HardCodedTarget
    return target.apply(template);
}
複製代碼

HardCodedTarget 是硬編碼寫死的,咱們沒有辦法定製化,看下它的 apply 方法,主要就是處理 RequestTemplate 模板的地址,生成完成的請求地址。最後返回 Request 請求對象。

public Request apply(RequestTemplate input) {
  if (input.url().indexOf("http") != 0) {
    // url() => http://demo-producer
    // input.target 處理請求模板
    input.target(url());
  }
  return input.request();
}
複製代碼

能夠看到通過 HardCodedTarget 的 apply 方法以後,就拼接上了 url 前綴了。

LoadBalancerFeignClient 負載均衡

LoadBalancerFeignClient 是 Feign 實現負載均衡核心的組件,是 Feign 網絡請求組件 Client 的默認實現,LoadBalancerFeignClient 最後是使用 FeignLoadBalancer 來進行負載均衡的請求。

看 LoadBalancerFeignClient 的 execute 方法,從這裏到後面執行負載均衡請求,其實跟分析 Ribbon 源碼中 RestTemplate 的負載均衡請求都是相似的了。

  • 能夠看到也是先將請求封裝到 ClientRequest,實現類是 FeignLoadBalancer.RibbonRequest。注意 RibbonRequest 第一個參數 Client 就是設置的 LoadBalancerFeignClient 的代理對象,啓用 apache httpclient 時,就是 ApacheHttpClient
  • 而後獲取客戶端配置,也就是說 Ribbon 的客戶端配置對 Feign 一樣生效
  • 最後獲取了負載均衡器 FeignLoadBalancer,而後執行負載均衡請求。
public Response execute(Request request, Request.Options options) throws IOException {
    try {
        URI asUri = URI.create(request.url());
        // 客戶端名稱:demo-producer
        String clientName = asUri.getHost();
        URI uriWithoutHost = cleanUrl(request.url(), clientName);
        // 封裝 ClientRequest => FeignLoadBalancer.RibbonRequest
        FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
                this.delegate, request, uriWithoutHost);
        // 客戶端負載均衡配置 ribbon.demo-producer.*
        IClientConfig requestConfig = getClientConfig(options, clientName);
        // lbClient => 負載均衡器 FeignLoadBalancer,執行負載均衡請求
        return lbClient(clientName)
                .executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
    }
    catch (ClientException e) {
        //...
    }
}

private FeignLoadBalancer lbClient(String clientName) {
    return this.lbClientFactory.create(clientName);
}
複製代碼

進入 executeWithLoadBalancer 方法,這就跟 Ribbon 源碼中分析的是同樣的了,最終就驗證了 Feign 基於 Ribbon 來作負載均衡請求。

public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
    // 負載均衡器執行命令
    LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig);

    try {
        return command.submit(
            new ServerOperation<T>() {
                @Override
                public Observable<T> call(Server server) {
                    // 用Server的信息重構URI地址
                    URI finalUri = reconstructURIWithServer(server, request.getUri());
                    S requestForServer = (S) request.replaceUri(finalUri);
                    try {
                        // 實際調用 LoadBalancerFeignClient 的 execute 方法
                        return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
                    }
                    catch (Exception e) {
                        return Observable.error(e);
                    }
                }
            })
            .toBlocking()
            .single();
    } catch (Exception e) {
        //....
    }
}
複製代碼

重構URI後,實際是調用 FeignLoadBalancer 的 execute 方法來執行最終的HTTP調用的。看下 FeignLoadBalancer 的 execute 方法,最終來講,就是使用代理的HTTP客戶端來執行請求。

默認狀況下,就是 Client.Default,用 HttpURLConnection 執行HTTP請求;啓用了 httpclient 後,就是 ApacheHttpClient;啓用了 okhttp,就是 OkHttpClient。

這裏有一點須要注意的是,FeignClient 雖然能夠配置超時時間,但進入 FeignLoadBalancer 的 execute 方法後,能夠看到會用 Ribbon 的超時時間覆蓋 Feign 配置的超時時間,最終以 Ribbon 的超時時間爲準。

public RibbonResponse execute(RibbonRequest request, IClientConfig configOverride) throws IOException {
    Request.Options options;
    if (configOverride != null) {
        // 用 Ribbon 的超時時間覆蓋了feign配置的超時時間
        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);
    }
    // request.client() HTTP客戶端對象
    Response response = request.client().execute(request.toRequest(), options);
    return new RibbonResponse(request.getUri(), response);
}
複製代碼

一張圖總結 Feign 負載均衡請求

關於Ribbon的源碼分析請看前面 Ribbon 相關的文章,Ribbon 如何從 eureka 註冊中心獲取 Server 就再也不分析了。

下面這張圖總結了 Feign 負載均衡請求的流程:

  • 首先服務啓動的時候會掃描解析 @FeignClient 註解的接口,並生成代理類注入到容器中。咱們注入 @FeignClient 接口時其實就是注入的這個代理類。
  • 調用接口方法時,會被代理對象攔截,進入 ReflectiveFeign.FeignInvocationHandlerinvoke 方法執行請求。
  • FeignInvocationHandler 會根據調用的接口方法獲取已經構建好的方法處理器 SynchronousMethodHandler,而後調用它的 invoke 方法執行請求。
  • 在 SynchronousMethodHandler 的 invoke 方法中,會先根據請求參數構建請求模板 RequestTemplate,這個時候會處理參數中的佔位符、拼接請求參數、處理body中的參數等等。
  • 而後將 RequestTemplate 轉成 Request,在轉換的過程當中:
    • 先是用 RequestInterceptor 處理請求模板,所以咱們能夠自定義攔截器來定製化 RequestTemplate。
    • 以後用 Target(HardCodedTarget)處理請求地址,拼接上服務名前綴。
    • 最後調用 RequestTemplate 的 request 方法獲取到 Request 對象。
  • 獲得 Request 後,就調用 LoadBalancerFeignClient 的 execute 方法來執行請求並獲得請求結果 Response
    • 先構造 ClientRequest,並獲取到負載均衡器 FeignLoadBalancer,而後就執行負載均衡請求。
    • 負載均衡請求最終進入到 AbstractLoadBalancerAwareClient,executeWithLoadBalancer 方法中,會先構建一個 LoadBalancerCommand,而後提交一個 ServerOperation。
    • LoadBalancerCommand 會經過 LoadBalancerContext 根據服務名獲取一個 Server。
    • 在 ServerOperation 中根據 Server 的信息重構URI,將服務名替換爲具體的IP地址,以後就能夠發起真正的HTTP調用了。
    • HTTP調用時,底層使用的組件默認是 HttpURLConnection;啓用了okhttp,就是 okhttp 的 OkHttpClient;啓用了 httpclient,就是 apache 的 HttpClient。
    • 最紅用 HTTP 客戶端組件執行請求,獲得響應結果 Response
  • 獲得 Response 後,就使用解碼器 Decoder 解析響應結果,返回接口方法定義的返回類型。

負載均衡獲取Server的核心組件是 LoadBalancerClient,具體的源碼分析能夠參考 Ribbon 源碼分析的兩篇文章。LoadBalancerClient 負載均衡的原理能夠看下面這張圖。

相關文章
相關標籤/搜索