OpenFeign 如何做到 "隔空取物" ?

帶你聊技術發表於2023-05-19

來源:碼猿技術專欄

大家好,我是不才陳某~

OpenFeign 元件的前身是 Netflix Feign 專案,它最早是作為 Netflix OSS 專案的一部分,由 Netflix 公司開發。後來 Feign 專案被貢獻給了開源組織,於是才有了我們今天使用的 Spring Cloud OpenFeign 元件。

OpenFeign 提供了一種宣告式的遠端呼叫介面,它可以大幅簡化遠端呼叫的程式設計體驗。在瞭解 OpenFeign 的原理之前,先來體驗一下 OpenFeign 的最終療效。我用了一個Hello World 的小案例,帶你看一下由 OpenFeign 發起的遠端服務呼叫的程式碼風格是什麼樣的。

String response = helloWorldService.hello("Vincent Y.");

你可能會問,這不就是本地方法呼叫嗎?沒錯!使用 OpenFeign 元件來實現遠端呼叫非常簡單,就像我們使用本地方法一樣,只要一行程式碼就能實現 WebClient 元件好幾行程式碼乾的事情。而且這段程式碼不包含任何業務無關的資訊,完美實現了呼叫邏輯和業務邏輯之間的職責分離。

那麼,OpenFeign 元件在底層是如何實現遠端呼叫的呢?接下來我就帶你瞭解OpenFeign 元件背後的工作流程。

OpenFeign 使用了一種“動態代理”技術來封裝遠端服務呼叫的過程,我們在上面的例子中看到的 helloWorldService 其實是一個特殊的介面,它是由 OpenFeign 元件中的FeignClient 註解所宣告的介面,介面中的程式碼如下所示。

@FeignClient(value = "hello-world-serv"
public interface HelloWorldService 
    @PostMapping("/sayHello"
    String hello(String guestName)
}

到這裡你一定恍然大悟了,原來遠端服務呼叫的資訊被寫在了 FeignClient 介面中

在上面的程式碼裡,你可以看到,服務的名稱、介面型別、訪問路徑已經透過註解做了宣告。

OpenFeign 透過解析這些註解標籤生成一個“動態代理類”,這個代理類會將介面呼叫轉化為一個遠端服務呼叫的 Request,併傳送給目標服務。

那麼 OpenFeign 的動態代理是如何運作的呢?接下來,我就帶你去深入瞭解這背後的流程。

OpenFeign 的動態代理

在專案初始化階段,OpenFeign 會生成一個代理類,對所有透過該介面發起的遠端呼叫進行動態代理。我畫了一個流程圖,幫你理解 OpenFeign 的動態代理流程:

OpenFeign 如何做到 "隔空取物" ?

上圖中的步驟 1 到步驟 3 是在專案啟動階段載入完成的,只有第 4 步“呼叫遠端服務”是發生在專案的執行階段。

下面我來解釋一下上圖中的幾個關鍵步驟。

首先,在專案啟動階段,OpenFeign 框架會發起一個主動的掃包流程,從指定的目錄下掃描並載入所有被 @FeignClient 註解修飾的介面。

然後,OpenFeign 會針對每一個 FeignClient 介面生成一個動態代理物件,即圖中的FeignProxyService,這個代理物件在繼承關係上屬於 FeignClient 註解所修飾的介面的例項。

接下來,這個動態代理物件會被新增到 Spring 上下文中,並注入到對應的服務裡,也就是圖中的 LocalService 服務。

最後,LocalService 會發起底層方法呼叫。實際上這個方法呼叫會被 OpenFeign 生成的代理物件接管,由代理物件發起一個遠端服務呼叫,並將呼叫的結果返回給LocalService。

我猜你一定很好奇:OpenFeign 是如何透過動態代理技術建立代理物件的?我畫了一張流程圖幫你梳理這個過程,你可以參考一下。

OpenFeign 如何做到 "隔空取物" ?

我把 OpenFeign 元件載入過程的重要階段畫在了上圖中。接下來我帶你梳理一下OpenFeign 動態代理類的建立過程。

  1. 專案載入:在專案的啟動階段,EnableFeignClients 註解扮演了“啟動開關”的角色,它使用 Spring 框架的 Import 註解匯入了 FeignClientsRegistrar 類,開始了OpenFeign 元件的載入過程。
  2. 掃包FeignClientsRegistrar 負責 FeignClient 介面的載入,它會在指定的包路徑下掃描所有的 FeignClients 類,並構造 FeignClientFactoryBean 物件來解析FeignClient 介面。
  3. 解析 FeignClient 註解FeignClientFactoryBean 有兩個重要的功能,一個是解析FeignClient 介面中的請求路徑和降級函式的配置資訊;另一個是觸發動態代理的構造過程。其中,動態代理構造是由更下一層的 ReflectiveFeign 完成的。
  4. 構建動態代理物件ReflectiveFeign 包含了 OpenFeign 動態代理的核心邏輯,它主要負責建立出 FeignClient 介面的動態代理物件。ReflectiveFeign 在這個過程中有兩個重要任務,一個是解析 FeignClient 介面上各個方法級別的註解,將其中的遠端介面URL、介面型別(GET、POST 等)、各個請求引數等封裝成後設資料,併為每一個方法生成一個對應的 MethodHandler 類作為方法級別的代理;另一個重要任務是將這些MethodHandler 方法代理做進一步封裝,透過 Java 標準的動態代理協議,構建一個實現了 InvocationHandler 介面的動態代理物件,並將這個動態代理物件繫結到FeignClient 介面上。這樣一來,所有發生在 FeignClient 介面上的呼叫,最終都會由它背後的動態代理物件來承接。

MethodHandler 的構建過程涉及到了複雜的後設資料解析,OpenFeign 元件將FeignClient 介面上的各種註解封裝成後設資料,並利用這些後設資料把一個方法呼叫“翻譯”成一個遠端呼叫的 Request 請求。

那麼上面說到的“後設資料的解析”是如何完成的呢?

它依賴於 OpenFeign 元件中的Contract 協議解析功能。Contract 是 OpenFeign 元件中定義的頂層抽象介面,它有一系列的具體實現,其中和我們實戰專案有關的是 SpringMvcContract 這個類,從這個類的名字中我們就能看出來,它是專門用來解析 Spring MVC 標籤的。

SpringMvcContract 的繼承結構是 SpringMvcContract->BaseContract->Contract。我這裡拿一段 SpringMvcContract 的程式碼,幫助你深入理解它是如何將註解解析為後設資料的。這段程式碼的主要功能是解析 FeignClient 方法級別上定義的 Spring MVC 註解。


// 解析FeignClient介面方法級別上的RequestMapping註解
protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) {
   // 省略部分程式碼...
   
   // 如果方法上沒有使用RequestMapping註解,則不進行解析
   // 其實GetMapping、PostMapping等註解都屬於RequestMapping註解
   if (!RequestMapping.class.isInstance(methodAnnotation)
         && !methodAnnotation.annotationType().isAnnotationPresent(RequestMapping.class)) 
{
      return;
   }

   // 獲取RequestMapping註解例項
   RequestMapping methodMapping = findMergedAnnotation(method, RequestMapping.class);
   // 解析Http Method定義,即註解中的GET、POST、PUT、DELETE方法型別
   RequestMethod[] methods = methodMapping.method();
   // 如果沒有定義methods屬性則預設當前方法是個GET方法
   if (methods.length == 0) {
      methods = new RequestMethod[] { RequestMethod.GET };
   }
   checkOne(method, methods, "method");
   data.template().method(Request.HttpMethod.valueOf(methods[0].name()));

   // 解析Path屬性,即方法上寫明的請求路徑
   checkAtMostOne(method, methodMapping.value(), "value");
   if (methodMapping.value().length > 0) {
      String pathValue = emptyToNull(methodMapping.value()[0]);
      if (pathValue != null) {
         pathValue = resolve(pathValue);
         // 如果path沒有以斜槓開頭,則補上/
         if (!pathValue.startsWith("/") && !data.template().path().endsWith("/")) {
            pathValue = "/" + pathValue;
         }
         data.template().uri(pathValue, true);
         if (data.template().decodeSlash() != decodeSlash) {
            data.template().decodeSlash(decodeSlash);
         }
      }
   }

   // 解析RequestMapping中定義的produces屬性
   parseProduces(data, method, methodMapping);

   // 解析RequestMapping中定義的consumer屬性
   parseConsumes(data, method, methodMapping);

   // 解析RequestMapping中定義的headers屬性
   parseHeaders(data, method, methodMapping);
   data.indexToExpander(new LinkedHashMap<>());
}

透過上面的方法,我們可以看到,OpenFeign 對 RequestMappings 註解的各個屬性都做了解析。

如果你在專案中使用的是 GetMapping、PostMapping 之類的註解,沒有使用 RequestMapping,那麼 OpenFeign 還能解析嗎?當然可以。以 GetMapping 為例,它對 RequestMapping 註解做了一層封裝。如果你檢視下面關於 GetMapping 註解的程式碼,你會發現這個註解頭上也掛了一個 RequestMapping 註解。因此 OpenFeign 可以正確識別 GetMapping 並完成載入。


@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.GET)
public @interface GetMapping {
// ...省略部分程式碼
}

總結

今天你清楚了 OpenFeign 要解決的問題,我還帶你瞭解了 OpenFeign 的工作流程,這裡面的重點是動態代理機制。OpenFeing 透過 Java 動態代理生成了一個“代理類”,這個代理類將介面呼叫轉化成為了一個遠端服務呼叫。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024922/viewspace-2953344/,如需轉載,請註明出處,否則將追究法律責任。

相關文章