openFeign奪命連環9問?

碼猿技術專欄發表於2022-01-05

1、前言

前面介紹了Spring Cloud 中的靈魂擺渡者Nacos,和它的前輩們相比不僅僅功能強大,而且部署非常簡單。

今天介紹一款服務呼叫的元件:OpenFeign,同樣是一款超越先輩(RibbonFeign)的狠角色。

文章目錄如下:

2、Feign是什麼?

Feign也是一個狠角色,Feign旨在使得Java Http客戶端變得更容易。

Feign整合了Ribbon、RestTemplate實現了負載均衡的執行Http呼叫,只不過對原有的方式(Ribbon+RestTemplate)進行了封裝,開發者不必手動使用RestTemplate調服務,而是定義一個介面,在這個介面中標註一個註解即可完成服務呼叫,這樣更加符合面向介面程式設計的宗旨,簡化了開發。

但遺憾的是Feign現在停止迭代了,當然現在也是有不少企業在用。

有想要學習Feign的讀者可以上spring Cloud官網學習,陳某這裡也不再詳細介紹了,不是今天的重點。

3、openFeign是什麼?

前面介紹過停止迭代的Feign,簡單點來說:OpenFeign是springcloud在Feign的基礎上支援了SpringMVC的註解,如@RequestMapping等等。OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping註解下的介面,並通過動態代理的方式產生實現類,實現類中做負載均衡並呼叫其他服務。

官網地址:https://docs.spring.io/spring...

4、Feign和openFeign有什麼區別?

FeignopenFiegn
Feign是SpringCloud元件中一個輕量級RESTful的HTTP服務客戶端,Feign內建了Ribbon,用來做客戶端負載均衡,去呼叫服務註冊中心的服務。Feign的使用方式是:使用Feign的註解定義介面,呼叫這個介面,就可以呼叫服務註冊中心的服務OpenFeign 是SpringCloud在Feign的基礎上支援了SpringMVC的註解,如@RequestMapping等。OpenFeign 的@FeignClient可以解析SpringMVC的@RequestMapping註解下的介面,並通過動態代理的方式產生實現類,實現類中做負載均衡並呼叫其他服務。

5、環境準備

本篇文章Spring Cloud版本、JDK環境、專案環境均和上一篇Nacos的環境相同:五十五張圖告訴你微服務的靈魂擺渡者Nacos究竟有多強?

註冊中心就不再使用Eureka了,直接使用Nacos作為註冊和配置中心,有不會的可以檢視Nacos文章。

本篇文章搭建的專案結構如下圖:

註冊中心使用Nacos,建立個微服務,分別為服務提供者Produce,服務消費者Consumer

6、建立服務提供者

既然是微服務之間的相互呼叫,那麼一定會有服務提供者了,建立openFeign-provider9005,註冊進入Nacos中,配置如下:

server:
  port: 9005
spring:
  application:
    ## 指定服務名稱,在nacos中的名字
    name: openFeign-provider
  cloud:
    nacos:
      discovery:
        # nacos的服務地址,nacos-server中IP地址:埠號
        server-addr: 127.0.0.1:8848
management:
  endpoints:
    web:
      exposure:
        ## yml檔案中存在特殊字元,必須用單引號包含,否則啟動報錯
        include: '*'

注意:此處的spring.application.name指定的名稱將會在openFeign介面呼叫中使用。

專案原始碼都會上傳,關於如何註冊進入Nacos,新增什麼依賴原始碼都會有,結合陳某上篇Nacos文章,這都不是難事!

7、建立服務消費者

新建一個模組openFeign-consumer9006作為消費者服務,步驟如下。

1、新增依賴

除了Nacos的註冊中心的依賴,還要新增openFeign的依賴,如下:

<dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

2、新增註解@EnableFeignClients開啟openFeign功能

老套路了,在Spring boot 主啟動類上新增一個註解@EnableFeignClients,開啟openFeign功能,如下:

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class OpenFeignConsumer9006Application
{
    public static void main(String[] args) {
        SpringApplication.run(OpenFeignConsumer9006Application.class, args);
    }
}

3、新建openFeign介面

新建一個openFeign介面,使用@FeignClient註解標註,如下:

@FeignClient(value = "openFeign-provider")
public interface OpenFeignService {
}
注意:該註解@FeignClient中的value屬性指定了服務提供者在nacos註冊中心的服務名

4、新建一個Controller除錯

新建一個controller用來除錯介面,直接呼叫openFeign的介面,如下:

@RestController
@RequestMapping("/openfeign")
public class OpenFeignController {
    
}

好了,至此一個openFeign的微服務就搭建好了,並未實現具體的功能,下面一點點實現。

8、openFeign如何傳參?

開發中介面傳參的方式有很多,但是在openFeign中的傳參是有一定規則的,下面詳細介紹。

1、傳遞JSON資料

這個也是介面開發中常用的傳參規則,在Spring Boot 中通過@RequestBody標識入參。

provider介面中JSON傳參方法如下:

@RestController
@RequestMapping("/openfeign/provider")
public class OpenFeignProviderController {
    @PostMapping("/order2")
    public Order createOrder2(@RequestBody Order order){
        return order;
    }
}

consumer中openFeign介面中傳參程式碼如下:

@FeignClient(value = "openFeign-provider")
public interface OpenFeignService {
    /**
     * 引數預設是@RequestBody標註的,這裡的@RequestBody可以不填
     * 方法名稱任意
     */
    @PostMapping("/openfeign/provider/order2")
    Order createOrder2(@RequestBody Order order);
}

注意:openFeign預設的傳參方式就是JSON傳參(@RequestBody),因此定義介面的時候可以不用@RequestBody註解標註,不過為了規範,一般都填上。

2、POJO表單傳參

這種傳參方式也是比較常用,引數使用POJO物件接收。

provider服務提供者程式碼如下:

@RestController
@RequestMapping("/openfeign/provider")
public class OpenFeignProviderController {
    @PostMapping("/order1")
    public Order createOrder1(Order order){
        return order;
    }
}

consumer消費者openFeign程式碼如下:

@FeignClient(value = "openFeign-provider")
public interface OpenFeignService {
    /**
     * 引數預設是@RequestBody標註的,如果通過POJO表單傳參的,使用@SpringQueryMap標註
     */
    @PostMapping("/openfeign/provider/order1")
    Order createOrder1(@SpringQueryMap Order order);
}

網上很多人疑惑POJO表單方式如何傳參,官方文件明確給出瞭解決方案,如下:

openFeign提供了一個註解@SpringQueryMap完美解決POJO表單傳參。

3、URL中攜帶引數

此種方式針對restful方式中的GET請求,也是比較常用請求方式。

provider服務提供者程式碼如下:

@RestController
@RequestMapping("/openfeign/provider")
public class OpenFeignProviderController {

    @GetMapping("/test/{id}")
    public String test(@PathVariable("id")Integer id){
        return "accept one msg id="+id;
}

consumer消費者openFeign介面如下:

@FeignClient(value = "openFeign-provider")
public interface OpenFeignService {

    @GetMapping("/openfeign/provider/test/{id}")
    String get(@PathVariable("id")Integer id);
}

使用註解@PathVariable接收url中的佔位符,這種方式很好理解。

4、普通表單引數

此種方式傳參不建議使用,但是也有很多開發在用。

provider服務提供者程式碼如下:

@RestController
@RequestMapping("/openfeign/provider")
public class OpenFeignProviderController {
    @PostMapping("/test2")
    public String test2(String id,String name){
        return MessageFormat.format("accept on msg id={0},name={1}",id,name);
    }
}

consumer消費者openFeign介面傳參如下:

@FeignClient(value = "openFeign-provider")
public interface OpenFeignService {
    /**
     * 必須要@RequestParam註解標註,且value屬性必須填上引數名
     * 方法引數名可以任意,但是@RequestParam註解中的value屬性必須和provider中的引數名相同
     */
    @PostMapping("/openfeign/provider/test2")
    String test(@RequestParam("id") String arg1,@RequestParam("name") String arg2);
}

5、總結

傳參的方式有很多,比如檔案傳參.....陳某這裡只是列舉了四種常見得傳參方式。

9、超時如何處理?

想要理解超時處理,先看一個例子:我將provider服務介面睡眠3秒鐘,介面如下:

@PostMapping("/test2")
public String test2(String id,String name) throws InterruptedException {
        Thread.sleep(3000);
        return MessageFormat.format("accept on msg id={0},name={1}",id,name);
}

此時,我們呼叫consumer的openFeign介面返回結果如下圖:

很明顯的看出程式異常了,返回了介面呼叫超時。what?why?...........

openFeign其實是有預設的超時時間的,預設分別是連線超時時間10秒、讀超時時間60秒,原始碼在feign.Request.Options#Options()這個方法中,如下圖:

那麼問題來了:為什麼我只設定了睡眠3秒就報超時呢?

其實openFeign整合了Ribbon,Ribbon的預設超時連線時間、讀超時時間都是是1秒,原始碼在org.springframework.cloud.openfeign.ribbon.FeignLoadBalancer#execute()方法中,如下圖:

原始碼大致意思:如果openFeign沒有設定對應得超時時間,那麼將會採用Ribbon的預設超時時間。

理解了超時設定的原理,由之產生兩種方案也是很明瞭了,如下:

  • 設定openFeign的超時時間
  • 設定Ribbon的超時時間

1、設定Ribbon的超時時間(不推薦)

設定很簡單,在配置檔案中新增如下設定:

ribbon:
  # 值的是建立連結所用的時間,適用於網路狀況正常的情況下, 兩端連結所用的時間
  ReadTimeout: 5000
  # 指的是建立連結後從伺服器讀取可用資源所用的時間
  ConectTimeout: 5000

2、設定openFeign的超時時間(推薦)

openFeign設定超時時間非常簡單,只需要在配置檔案中配置,如下:

feign:
  client:
    config:
      ## default 設定的全域性超時時間,指定服務名稱可以設定單個服務的超時時間
      default:
        connectTimeout: 5000
        readTimeout: 5000
default設定的是全域性超時時間,對所有的openFeign介面服務都生效

但是正常的業務邏輯中可能涉及到多個openFeign介面的呼叫,如下圖:

上圖中的虛擬碼如下:

public T invoke(){
    //1. 呼叫serviceA
    serviceA();
    
    //2. 呼叫serviceA
    serviceB();
    
    //3. 呼叫serviceA
    serviceC();
}

那麼上面配置的全域性超時時間能不能通過呢?很顯然是serviceAserviceB能夠成功呼叫,但是serviceC並不能成功執行,肯定報超時。

此時我們可以給serviceC這個服務單獨配置一個超時時間,配置如下:

feign:
  client:
    config:
      ## default 設定的全域性超時時間,指定服務名稱可以設定單個服務的超時時間
      default:
        connectTimeout: 5000
        readTimeout: 5000
      ## 為serviceC這個服務單獨配置超時時間
      serviceC:
        connectTimeout: 30000
        readTimeout: 30000
注意:單個配置的超時時間將會覆蓋全域性配置。

10、如何開啟日誌增強?

openFeign雖然提供了日誌增強功能,但是預設是不顯示任何日誌的,不過開發者在除錯階段可以自己配置日誌的級別。

openFeign的日誌級別如下:

  • NONE:預設的,不顯示任何日誌;
  • BASIC:僅記錄請求方法、URL、響應狀態碼及執行時間;
  • HEADERS:除了BASIC中定義的資訊之外,還有請求和響應的頭資訊;
  • FULL:除了HEADERS中定義的資訊之外,還有請求和響應的正文及後設資料。

配置起來也很簡單,步驟如下:

1、配置類中配置日誌級別

需要自定義一個配置類,在其中設定日誌級別,如下:

注意:這裡的logger是feign包裡的。

2、yaml檔案中設定介面日誌級別

只需要在配置檔案中調整指定包或者openFeign的介面日誌級別,如下:

logging:
  level:
    cn.myjszl.service: debug

這裡的cn.myjszl.service是openFeign介面所在的包名,當然你也可以配置一個特定的openFeign介面。

3、演示效果

上述步驟將日誌設定成了FULL,此時發出請求,日誌效果如下圖:

日誌中詳細的列印出了請求頭、請求體的內容。

11、如何替換預設的httpclient?

Feign在預設情況下使用的是JDK原生的URLConnection傳送HTTP請求,沒有連線池,但是對每個地址會保持一個長連線,即利用HTTP的persistence connection。

在生產環境中,通常不使用預設的http client,通常有如下兩種選擇:

  • 使用ApacheHttpClient
  • 使用OkHttp

至於哪個更好,其實各有千秋,我比較傾向於ApacheHttpClient,畢竟老牌子了,穩定性不在話下。

那麼如何替換掉呢?其實很簡單,下面演示使用ApacheHttpClient替換。

1、新增ApacheHttpClient依賴

在openFeign介面服務的pom檔案新增如下依賴:

<!--     使用Apache HttpClient替換Feign原生httpclient-->
    <dependency>
      <groupId>org.apache.httpcomponents</groupId>
      <artifactId>httpclient</artifactId>
    </dependency>
    
    <dependency>
      <groupId>io.github.openfeign</groupId>
      <artifactId>feign-httpclient</artifactId>
    </dependency>

為什麼要新增上面的依賴呢?從原始碼中不難看出,請看org.springframework.cloud.openfeign.FeignAutoConfiguration.HttpClientFeignConfiguration這個類,程式碼如下:

上述紅色框中的生成條件,其中的@ConditionalOnClass(ApacheHttpClient.class),必須要有ApacheHttpClient這個類才會生效,並且feign.httpclient.enabled這個配置要設定為true

2、配置檔案中開啟

在配置檔案中要配置開啟,程式碼如下:

feign:
  client:
    httpclient:
      # 開啟 Http Client
      enabled: true

3、如何驗證已經替換成功?

其實很簡單,在feign.SynchronousMethodHandler#executeAndDecode()這個方法中可以清楚的看出呼叫哪個client,如下圖:

上圖中可以看到最終呼叫的是ApacheHttpClient

4、總結

上述步驟僅僅演示一種替換方案,剩下的一種不再演示了,原理相同。

12、如何通訊優化?

在講如何優化之前先來看一下GZIP 壓縮演算法,概念如下:

gzip是一種資料格式,採用用deflate演算法壓縮資料;gzip是一種流行的資料壓縮演算法,應用十分廣泛,尤其是在Linux平臺。

當GZIP壓縮到一個純文字資料時,效果是非常明顯的,大約可以減少70%以上的資料大小。

網路資料經過壓縮後實際上降低了網路傳輸的位元組數,最明顯的好處就是可以加快網頁載入的速度。網頁載入速度加快的好處不言而喻,除了節省流量,改善使用者的瀏覽體驗外,另一個潛在的好處是GZIP與搜尋引擎的抓取工具有著更好的關係。例如 Google就可以通過直接讀取GZIP檔案來比普通手工抓取更快地檢索網頁。

GZIP壓縮傳輸的原理如下圖:

按照上圖拆解出的步驟如下:

  • 客戶端向伺服器請求頭中帶有:Accept-Encoding:gzip,deflate 欄位,向伺服器表示,客戶端支援的壓縮格式(gzip或者deflate),如果不傳送該訊息頭,伺服器是不會壓縮的。
  • 服務端在收到請求之後,如果發現請求頭中含有Accept-Encoding欄位,並且支援該型別的壓縮,就對響應報文壓縮之後返回給客戶端,並且攜帶Content-Encoding:gzip訊息頭,表示響應報文是根據該格式壓縮過的。
  • 客戶端接收到響應之後,先判斷是否有Content-Encoding訊息頭,如果有,按該格式解壓報文。否則按正常報文處理。

openFeign支援請求/響應開啟GZIP壓縮,整體的流程如下圖:

上圖中涉及到GZIP傳輸的只有兩塊,分別是Application client -> Application ServiceApplication Service->Application client

注意:openFeign支援的GZIP僅僅是在openFeign介面的請求和響應,即是openFeign消費者呼叫服務提供者的介面。

openFeign開啟GZIP步驟也是很簡單,只需要在配置檔案中開啟如下配置:

feign:
  ## 開啟壓縮
  compression:
    request:
      enabled: true
      ## 開啟壓縮的閾值,單位位元組,預設2048,即是2k,這裡為了演示效果設定成10位元組
      min-request-size: 10
      mime-types: text/xml,application/xml,application/json
    response:
      enabled: true

上述配置完成之後,發出請求,可以清楚看到請求頭中已經攜帶了GZIP壓縮,如下圖:

13、如何熔斷降級?

常見的熔斷降級框架有HystrixSentinel,openFeign預設支援的就是Hystrix,這個在官方文件上就有體現,畢竟是一奶同胞嘛,哈哈...........

但是阿里的Sentinel無論是功能特性、簡單易上手等各方面都完全秒殺Hystrix,因此此章節就使用openFeign+Sentinel進行整合實現服務降級。

說明:此處並不著重介紹Sentinel,陳某打算放在下一篇文章詳細介紹Sentinel的強大之處。

1、新增Sentinel依賴

openFeign-consumer9006消費者的pom檔案新增sentinel依賴(由於使用了聚合模組,不指定版本號),如下:

<dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

2、配置檔案中開啟sentinel熔斷降級

要想openFeign使用sentinel的降級功能,還需要在配置檔案中開啟,新增如下配置:

feign:
  sentinel:
    enabled: true

3、新增降級回撥類

這個類一定要和openFeign介面實現同一個類,如下圖:

OpenFeignFallbackService這個是降級回撥的類,一旦OpenFeignService中對應得介面出現了異常則會呼叫這個類中對應得方法進行降級處理。

4、新增fallback屬性

@FeignClient中新增fallback屬性,屬性值是降級回撥的類,如下:

@FeignClient(value = "openFeign-provider",fallback = OpenFeignFallbackService.class)
public interface OpenFeignService {}

5、演示

經過如上4個步驟,openFeign的熔斷降級已經設定完成了,此時演示下效果。

通過postman呼叫http://localhost:9006/openfeign/order3這個介面,正常邏輯返回如下圖:

現在手動造個異常,在服務提供的介面中丟擲異常,如下圖:

此時重新呼叫http://localhost:9006/openfeign/order3,返回如下圖:

哦豁,可以很清楚的看到服務已經成功降級呼叫,哦了,功能完成。

注意:實際開發中返回結果應該根據架構統一定製,陳某這裡只是為了演示方便,不要借鑑,哈哈。。。

14、總結

本篇文章主要面對初學者,深入的原始碼以及熔斷降級放在後面詳細介紹,文中若有表述不清,錯誤的地方歡迎指正!

這是陳某Spring Cloud 進階專欄的第二篇文章,覺得文章不錯的,歡迎點贊、收藏、轉發。

以上原始碼已經上傳GitHub,需要的公號【碼猿技術專欄】回覆關鍵詞9528獲取。

相關文章