天天看點

SpringCloud 微服務接口調用元件 - OpenFeign 簡介

作者:程式猿不相信眼淚

前言

本篇文章是 SpringCloud 專欄的第一篇,後面會逐漸更新自己工作中使用到的微服務元件,以及所遇到的坑和自己填坑過程中的了解,在自己成長的過程中也希望對其他人有幫助,讓大家能夠少踩坑!

版本介紹

本專欄所有文章使用的版本均為 SpringCloud 2020.0.5、SpringBoot 2.5.0

OpenFeign 簡介

OpenFeign 是一個聲明式的 Rest 接口用戶端,就把它了解成 HttpClient!能夠實作服務接口的遠端調用。也就是說假設現在你的微服務叢集有 A,B 兩個服務,那麼你可以在 A 服務中定義一個接口,通過 OpenFeign 的相關注解,它能夠幫你實作調用 A 的接口方法(interface)會自動調用遠端的 B 服務暴露的 Rest 接口,使得 A 調用遠端服務 B 的接口就像在調用本地方法一樣。示例代碼:

@FeignClient(
    contextId = "AuthClient",
    name = "AUTH",//注冊中心的服務名
    path = "/auth",//項目路徑字首,沒有就不填
    configuration = {FeignClientDecoderConfiguration.class, FeignRequestInterceptor.class}//自定義的配置
)
public interface AuthClient {

  @GetMapping("/getReviewerIds")
  List<Long> getReviewerIds(@RequestParam("adminId") Long adminId);
}
複制代碼           

這樣我們從 Spring 中拿到 Bean 調用 getReviewerIds 方法,即可請求到遠端服務 AUTH 暴露的 Rest 接口。

@Autowired private AuthClient client;
public void demo(){
    List<Long> reviewerIds = client.getReviewerIds(1L);
}
複制代碼           

具體 demo 我們可以參考官網給的示例 Feign Using Eureka。

為什麼要用 OpenFeign

我始終堅持一個理念,那就是我們用技術一定不要為了用而用,要知道用了有什麼好處,解決了什麼問題,又引入了什麼問題(自己學習除外)。

如果你一直投身于一個涉及業務互動的自研産品,那麼大機率會體會到一個項目的演變過程,項目初期單體應用已經完全能夠滿足業務需求,随着業務量不斷擴大,業務功能不斷疊代。我們需要對一個龐大的系統進行業務子產品的拆分、甚至系統拆分。如下圖

SpringCloud 微服務接口調用元件 - OpenFeign 簡介

當拆分出多個系統時就會涉及到系統之間的調用,想一想在 Feign、OpenFeign 之前,我們要調用遠端接口通常是用 HttpClient、RestTemplate,對比三者

HttpClient RestTemplate OpenFeign
負載均衡 需要通過服務提供方的 Nginx 等實作 可以用Ribbon等實作負載均衡 通過服務發現,有官方提供的豐富負載均衡政策
編碼 需要手動建構 url、請求體/請求參數、解析響應等,代碼臃腫。每個服務調用都要寫這段代碼 和 HttpClient差別不大 支援 SpringMVC 注解,聲明接口定義即可,代碼量極少
設計思想 - - 面向接口
熔斷、降級 - - 生态圈有豐富的支援元件

OpenFeign 與 SpringMVC

實際上如果你是從老版本一路更新過來的你就會發現 Feign 是對 RestTemplate + Ribbon 做了一次封裝,而 OpenFeign 又在 Feign 的基礎上進行了封裝,使其支援 SpringMVC 相關注解和相關的消息轉換器 HttpMessageConverters。

OpenFeign 完美結合 SpringMVC 定義 Controller 的注解,@GetMapping、@PathVariable、@RequestBody、@RequestParam 等等 SpringMVC 支援的注解。唯一有差別的是當以 Get 方式傳遞 Pojo 對象時,也使用 SpringMVC 時,要把 url 後面的問号參數映射成對象時,提供了一個新的注解叫做 @SpringQueryMap。

至于負載均衡,在 2020.x 版本的 SpringCloud 中已經移除了 ribbon 使用 SpringCloud-LoadBalance 實作負載均衡。

@FeignClient 配置簡介

雖然内容官網都有,但是就我的經曆對于新手來說官方文檔(尤其是 Spring 的)并不友好,是以還是簡單提一下。

配置方式一

檢視 @FeignClient 源碼,發現 configuration 屬性的注釋上寫着預設值是 FeignClientsConfiguration

SpringCloud 微服務接口調用元件 - OpenFeign 簡介

再檢視這個類發現解碼器、重試、熔斷降級等都幫我們定義好了。其實非常簡單就是用 @ConditionalOnMissingBean 去定義了相關 Bean 。

這樣一來我們就知道了如果想自己定義配置那麼寫一個配置類作為 @FeignClient 的 configuration 屬性值即可,例如上面示例代碼中的 AuthClient 的兩個自定義配置。

值得注意的是我們自己寫的配置類最好不要加 @Configuration 注解,因為我們都知道标注 @Configuration 代表将這個類對象被 Spring 容器管理,這可能會對應用有全局影響。可能有不熟悉 Spring 的人會說,不對呀,你看官方自定義的 FeignClientsConfiguration 都用 @Configuration 标注了,但要注意的是 FeignClientsConfiguration 并沒有被 @Import 或者被 Spring 掃描,是以它不會被 Spring 管理。但是我們寫的配置都是在主類路徑包下面的,預設會被 Spring 掃描到。

這裡有一個最簡單的判斷某個配置類是否交給 Spring 管理了的方法,IDEA 開發環境下,直接在主類用 @Autowired 引入,如果有波浪線提示就說明它現在沒有被 Spring 管理。

SpringCloud 微服務接口調用元件 - OpenFeign 簡介

配置方式二

官方還給我們提供了一種配置方式,在配置檔案中配置,檢視 FeignClientProperties 源碼發現有一個成員變量

private Map<String, FeignClientConfiguration> config = new HashMap<>();
複制代碼           

key 是 FeignClient 的名字,value 是具體配置

feign:
    client:
        config:
            default: # @FeignClient 的名字,default 代表所有 FeignClient 的預設配置
                connectTimeout: 5000
                readTimeout: 5000
                loggerLevel: basic
            feignName: #指定單獨的 FeignClient 進行配置(如果有contextId 這裡填 contextId 值,否則填 name 屬性值)
                connectTimeout: 1000
                readTimeout: 1000
                loggerLevel: FULL
複制代碼           

配置檔案的優先級要高于配置類,如果同時配置了 @FeignClient 的 configuration 和 配置檔案,将會以配置檔案優先。

實作原理推測

首先我們想想它是如何從一個 @FeignClient 标注的 interface 方法就能實作調用到遠端服務接口的?讓我們聯想到 MyBatis 的 @Mapper 。有沒有想過 @Mapper 定義的接口方法為什麼能自動映射到 xml 檔案的 SQL 語句?

我相信大家都會知道是通過代理嘛,肯定是架構給弄一個代理類出來,去實作了定義的接口,實作類裡面其實大概就是使用 HttpClient、RestTemplate 遠端調用的那一套代碼,隻是對于使用者來說是無感覺的,因為都被封裝好了。

SpringCloud 微服務接口調用元件 - OpenFeign 簡介

那麼第二個問題就出現了,@FeignClient 是怎麼知道我們要調用哪一台服務的,我總得知道我要調用的服務所在機器 IP、port 等資訊吧?是以肯定有一個地方是存儲了服務提供方的服務執行個體資訊,例如 執行個體叢集數、每台執行個體的 IP 位址、端口等。那麼很明顯就是注冊中心了,以 eureka 為例

SpringCloud 微服務接口調用元件 - OpenFeign 簡介

@FeignClient 中的 name、value 屬性值就是注冊中心的服務名,表明目前接口綁定的遠端服務。

我們的微服務應用啟動後都會把自己的資訊注冊到注冊中心,當 OpenFeign 進行服務調用時,會根據要調用的服務名去從本地尋找已緩存的注冊中心服務提供者資訊。

@FeignClient 接口放在消費端還是服務端

标題想表達的意思是,例如簡介中的 AuthClient 類以及相關 XxxRequest、XxxResponse 是定義在 A 服務中?還是定義在 B 服務中的某個子產品然後打一個 jar 包出來由 A 服務來引用?兩者我都用過,其實經曆下來我更傾向于前者。假如你用後者的話,讓我們來感受一下開發一個 Feign 接口實作的步驟:

  1. 在 B 服務中寫 Controller 實作
  2. 在 B 服務中定義 FeignClient 接口
  3. 在 B 服務中修改 jar 版本 +1,打一個 jar 包到本地倉庫
  4. 在 A 服務中修改依賴 jar 版本,重新整理 maven/gradle

乍一看不麻煩是吧?但是你要知道我們開發中經常會出現丢參數、缺響應屬性等情況,一旦有任何小問題,都要重新走一遍上述流程。。。。

而對于前者所不好的地方無非是 XxxRequest、XxxResponse 類備援了一份,但其實并沒有什麼問題,因為對于 Feign 來說請求和響應的 BO 類并不需要字段完全一緻,它的解碼器會智能的解析響應并封裝到你的 XxxResponse 接收類中。

當然也有不同的觀點支援并且堅持後者的,對此我隻能說也不是不行

SpringCloud 微服務接口調用元件 - OpenFeign 簡介

其實對于這個問題我在 V2EX 上發現了一個評論,我覺得說的還是蠻有道理的,貼一下

你這麼了解就明白了,這個類 XxxRequest、XxxResponse 等,僅僅是你的 A 服務為了映射請求結果而本地自定義的一個映射資料結構,這個映射資料結構和 B 服務可以說是沒關系的。是以你當然應該放在 A 這裡。
你很糾結無非是你覺得這個東西似乎是可以複用的,是以糾結放 A 還是放 B,以及是不是要抽出來做個公共依賴。我很久以前也很糾結這個東西,但是踩了太多坑以後我的想法就變了,高内聚低耦合本質的意義,就是把和一個服務(元件,應用,包,等等等等)相關的代碼全部包在一起,不要和外界有牽扯,你有牽扯就會引發修改時的依賴地獄。

引入 OpenFeign 帶來的問題

引入一個新的技術元件,第一個要面對的問題就是需要有人力去學習研究,倒不是學習如何使用,因為這玩意随便看看官方文檔就能用起來了,而是要在遇到生産問題時能夠結合源碼準确定位出問題,或者在元件基礎上能夠進行自定義的擴充去滿足公司的業務需求,這通常需要有一定經驗和能力的程式員。

其次引入 OpenFeign 帶來的一個最直接的問題就是分布式事務問題,然而實際上這并不是 OpenFeign 帶來的,而是更新到微服務系統不得不面對的一個問題,目前 Spring 的事務隻能支援應用内復原,當涉及到跨應用互動就不适用了。一個最簡單的例子,使用者下單使用優惠券。

@Transactional
public void submit(){
    //前置業務...
   couponClient.useCoupon(request);//跨服務使用優惠券成功
   //後置業務... throw 了一個 RunTimeException
}
複制代碼           

訂單服務調用優惠券服務,優惠券被成功使用了,但是訂單服務報錯,訂單被復原了,但是優惠券無法復原,還是被使用了。

結語

本篇文章簡單介紹了 OpenFeign ,這玩意用起來真的非常簡單。後面會陸續介紹生産環境中使用 OpenFeign 遇到的問題以及解決方案。

原文連結:https://juejin.cn/post/7122405099904712711

來源:稀土掘金