聲明式HTTP客戶端 - Spring Cloud OpenFeign

Feign

什麼是Feign:html

Feign的組成:
聲明式HTTP客戶端 - Spring Cloud OpenFeignjava

  • Feign.Builder:全部的FeignClient都是由Feign.Builder構建
  • Client:feign.Client.Default內部實際用的是HttpURLConnection,而LoadBalanceFeignClient默認狀況下傳入的就是feign.Client.Default,只是加了個負載均衡的功能。相比於feign.Client.Default,LoadBalanceFeignClient支持傳入指定的Client
  • Contract:原生的Feign是不支持@GetMapping、@PostMapping...等SpringMVC註解的,Spring Cloud對其擴展後才支持

指定Feign的日誌級別

Feign默認是不打印任何日誌的,但在實際項目中接口調用出現問題須要調試代碼或須要查看某個接口調用所執行的耗時,那麼第一時間就會想到查看Feign的日誌,此時要如何去開啓Feign的日誌呢?主要有兩種方式,經過代碼配置或經過配置文件配置。另外,配置生效範圍還分爲局部配置和全局配置,咱們先來介紹細粒度的局部配置。node

須要注意的是,Feign的日誌級別與Spring Boot不同,因此不能直接配置Spring Boot的日誌級別去開啓。Feign的日誌級別以下表:
聲明式HTTP客戶端 - Spring Cloud OpenFeigngit

一、局部配置 - 代碼配置;經過代碼配置有兩個主要的步驟,先在代碼定義相應的配置類,而後再到配置文件中配置FeignClient接口的日誌級別。首先,定義Feign日誌級別的配置類。代碼以下:github

package com.zj.node.contentcenter.configuration;

import feign.Logger;
import org.springframework.context.annotation.Bean;

/**
 * @author 01
 * @date 2019-07-29
 **/
public class UserCenterFeignConfig {

    @Bean
    public Logger.Level level(){
        // 設置Feign的日誌級別爲FULL
        return Logger.Level.FULL;
    }
}

注:該類不要加上@Configuration註解,不然將會由於父子上下文掃描重疊而成爲全局配置web

因爲不是作的全局配置,因此除此以外還須要在FeignClient接口中指定該配置類:spring

package com.zj.node.contentcenter.feignclient;

import com.zj.node.contentcenter.configuration.UserCenterFeignConfig;
import com.zj.node.contentcenter.domain.dto.user.UserDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(value = "user-center", configuration = UserCenterFeignConfig.class)
public interface UserCenterFeignClient {

    @GetMapping("/users/{id}")
    UserDTO findById(@PathVariable Integer id);
}

而後在配置文件中添加以下配置:編程

# 設置日誌級別
logging:
  level:
    # 這裏須要配置爲debug,不然feign的日誌級別配置不會生效
    com.zj.node.contentcenter.feignclient.UserCenterFeignClient: debug

配置完成後,啓動項目執行相應的調用代碼,控制檯輸出的日誌以下:
聲明式HTTP客戶端 - Spring Cloud OpenFeignapi


二、局部配置 - 配置文件配置;這種配置方式就比較簡單,也是比較經常使用的方式,只需在配置文件中添加以下配置便可:瀏覽器

# 定義feign相關配置
feign:
  client:
    config:
      # 微服務名稱
      user-center:
        # 設置feign日誌級別
        loggerLevel: full

# 設置日誌級別
logging:
  level:
    # 這裏須要配置爲debug,不然feign的日誌級別配置不會生效
    com.zj.node.contentcenter.feignclient.UserCenterFeignClient: debug

一、全局配置 - 代碼配置;一樣定義一個配置類:

public class GlobalFeignLoggerConfig {

    @Bean
    public Logger.Level level(){
        // 設置Feign的日誌級別爲FULL
        return Logger.Level.FULL;
    }
}

而後配置啓動類上的@EnableFeignClients註解的defaultConfiguration屬性,以下:

@EnableFeignClients(
        basePackages = "com.zj.node.contentcenter.feignclient",
        defaultConfiguration = GlobalFeignLoggerConfig.class
)

接着將配置文件中的日誌配置從特定的類修改成包名,以下:

# 設置日誌級別
logging:
  level:
    # 這裏須要配置爲debug,不然feign的日誌級別配置不會生效
    com.zj.node.contentcenter.feignclient: debug

二、全局配置 - 配置文件配置;

# 定義feign相關配置
feign:
  client:
    config:
      # default表示爲全局配置
      default:
        # 設置feign日誌級別
        loggerLevel: full

# 設置日誌級別
logging:
  level:
    # 這裏須要配置爲debug,不然feign的日誌級別配置不會生效
    com.zj.node.contentcenter.feignclient: debug

Feign支持的配置項

因爲使用代碼方式配置和使用配置文件配置所支持的配置項不一樣,因此分爲兩類。

一、代碼方式所支持的配置項:

配置項 做用
Feign.Builder Feign的入口
Client Feign底層用什麼http客戶端去請求
Contract 契約,註解支持
Encoder 編碼器,用於將對象轉換成Http請求消息體
Decoder ×××,將響應消息體轉換成對象
Logger 日誌管理器
Logger.Level 指定日誌級別
Retryer 指定重試策略
ErrorDecoder 指定異常×××
Request.Options 超時時間
Collection<RequestInterceptor> 請求攔截器
SetterFactory 用於設置Hystrix的配置屬性,Feign整合Hystrix纔會用

二、配置文件所支持的配置項:
聲明式HTTP客戶端 - Spring Cloud OpenFeign

代碼配置 vs 配置文件配置:
聲明式HTTP客戶端 - Spring Cloud OpenFeign

  • 關於優先級:細粒度配置文件配置 > 細粒度代碼配置 > 全局配置文件配置 > 全局代碼配置

配置最佳實踐總結:

  • 儘可能使用配置文件配置,配置文件知足不了需求的狀況下再考慮使用代碼配置
  • 在同一個微服務內儘可能保持單一性,例如統一使用配置文件配置,儘可能不要兩種方式混用,以避免增長定位問題的複雜度

Feign的繼承

所謂Feign的繼承實際是爲了服務之間可以複用代碼,例如如今用戶中心服務有一個按id查詢用戶信息的接口以下:

@Slf4j
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @GetMapping("/{id}")
    public User findById(@PathVariable Integer id) {
        log.info("get request. id is {}", id);
        return userService.findById(id);
    }
}

若我想在內容中心服務經過Feign調用該接口,就須要新建一個interface,並編寫以下代碼:

@FeignClient(name = "user-center")
public interface UserCenterFeignClient {

    @GetMapping("/users/{id}")
    UserDTO findById(@PathVariable Integer id);
}

能夠看到,方法的定義其實是同樣的,因此這時候就能夠利用Feign的繼承特性複用這種代碼。首先須要建立一個單獨的項目或maven模塊,由於這樣才能經過添加maven依賴的方式引入到不一樣的項目中。這裏暫且稱爲api模塊吧,在api模塊中定義一個這樣的接口,代碼以下:

@RequestMapping("/users")
public interface UserApi {

    @GetMapping("/{id}")
    User findById(@PathVariable Integer id);
}

而後在用戶中心服務中添加api模塊的依賴,接着實現UserApi接口,改寫以前的UserController以下:

@Slf4j
@RestController
@RequiredArgsConstructor
public class UserController implements UserApi {

    private final UserService userService;

    @Override
    public User findById(@PathVariable Integer id) {
        log.info("get request. id is {}", id);
        return userService.findById(id);
    }
}

在內容中心服務中也添加api模塊的依賴,改寫以前的UserCenterFeignClient代碼,讓其繼承UserApi,代碼以下:

@FeignClient(name = "user-center")
public interface UserCenterFeignClient extends UserApi {
}

能夠看到,繼承了UserApi後,此時不須要再定義與目標接口相同的方法了,複用了上級接口的代碼,這就是所謂Feign的繼承。

其實關於這種使用方式存在許多爭議,咱們來看看官方怎麼說:

It is generally not advisable to share an interface between a server and a client. It introduces tight coupling, and also actually doesn’t work with Spring MVC in its current form (method parameter mapping is not inherited).

大體翻譯以下:

一般不建議在服務提供者(server)和服務消費者(client)之間共享接口,由於這種方式引入了緊耦合,而且實際上在當前形式下也不適用於Spring MVC(方法參數映射不會被繼承)

  • 關於方法參數映射不會被繼承:在上面的代碼示例中能夠看到,實現UserApi的UserController方法參數上,依舊須要寫MVC相關的註解,由於這些註解是不會被繼承的。簡單來講就是這類註解得寫在實現類的方法參數上纔會生效,而對於團隊中對此不甚熟悉的開發人員來講也會形成必定的」迷惑「

官網文檔地址以下:

https://cloud.spring.io/spring-cloud-static/Greenwich.SR2/single/spring-cloud.html#spring-cloud-feign-inheritance

關於繼承特性的爭議:

  • 官方觀點:不建議使用
    • 理由上面已說明
  • 業界觀點:不少公司使用
    • 理由1:代碼可複用;面向契約
    • 理由2:在業務需求變動比較頻繁的狀況,無需修改太多的代碼

如何抉擇:

根據項目狀況權衡利弊便可,若須要這種特性帶來的好處又能夠承受緊耦合帶來的負面影響,那麼就選擇使用該特性,不然就不要使用


Feign發送多參數GET請求的坑

使用過Spring MVC的都知道,當一個GET接口有多個請求參數時可使用對象來接收。例如用戶服務中,有這樣一個接口以下:

@RestController
@RequestMapping("/users")
public class UserController {

    @GetMapping("/query")
    public User query(User user) {
        return user;
    }
}

使用postman發送以下請求是能夠正常接收並響應的:
聲明式HTTP客戶端 - Spring Cloud OpenFeign

因此在另外一個服務中使用Feign調用這種類型的接口時,咱們很天然而然的就會寫成以下形式:

@FeignClient(name = "user-center")
public interface UserCenterFeignClient {

    @GetMapping("/users/query")
    UserDTO query(UserDTO userDTO);
}

實際上這種使用Feign發送多參數GET請求的方式是會有坑的,由於將多參數包裝成對象時,Feign在底層會將其轉換爲POST請求,並把對象序列化塞到http body中,因此就會因爲不支持該請求方法而報405錯誤。

關於這個坑咱們作個實驗來驗證一下,在內容中心服務中,定義一個接口以下:

@RestController
@RequestMapping("/shares")
@RequiredArgsConstructor
public class ShareController {

    private final UserCenterFeignClient userCenterFeignClient;

    @GetMapping("/queryUser")
    public UserDTO queryUser(UserDTO userDTO){
        return userCenterFeignClient.query(userDTO);
    }
}

而後經過postman進行請求,能夠看到直接報405錯誤了:
聲明式HTTP客戶端 - Spring Cloud OpenFeign

此時用戶服務的控制檯中,輸出了以下日誌信息:

Resolved [org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'POST' not supported]

那麼咱們要如何去解決這個坑呢?最顯而易見的方式就是不將參數包裝成對象,而是拆解開來使用@RequestParam一個個寫上去。然而這種方式有個很明顯的弊端,若是有不少參數的時候,一個個寫就比較累,並且代碼也很差看。在這種「走投無路」的狀況下,就會想着要不就不用GET了,換成POST吧。雖然這種方法也可行,可是卻違背了RESTful的規範。

那有沒有一個完美的解決方案呢?答案是有的,那就是使用@SpringQueryMap註解,該註解至關於feign.QueryMap,目的是將對象轉換爲GET參數。那麼咱們就來試試看吧,修改UserCenterFeignClient代碼以下:

@FeignClient(name = "user-center")
public interface UserCenterFeignClient {

    @GetMapping("/users/query")
    UserDTO query(@SpringQueryMap UserDTO userDTO);
}

注:該註解在spring-cloud-starter-openfeign: 2.1.0及以後的版本纔開始支持的,以前的版本只能使用其餘方式解決該問題。之因此會有這個坑,也是由於原生Feign的體系讓Spring Cloud沒法封裝得與Spring MVC徹底一致的編程體驗

修改完代碼後重啓項目,再次使用postman請求就沒有報錯了:
聲明式HTTP客戶端 - Spring Cloud OpenFeign


Feign脫離Ribbon使用

咱們都知道Feign內部整合了Ribbon,因此纔能有負載均衡功能及從服務發現組件獲取服務實例的調用地址功能。那麼若是須要調用一個沒有註冊到服務發現組件上的服務或地址,即脫離Ribbon去使用Feign的話,要如何作呢?很是簡單,只須要配置一下@FeignClient註解的url屬性便可。以下示例:

// name是必須配置的,不然項目都沒法啓動,url屬性一般是配置basic地址
@FeignClient(name = "baidu", url = "https://www.baidu.com")
public interface TestFeignClient {

    @GetMapping
    String index();
}

而後定義一個接口測試一下:

@RestController
@RequiredArgsConstructor
public class TestController {

    private final TestFeignClient feignClient;

    @GetMapping("/baidu")
    public String baiduIndex() {
        return feignClient.index();
    }
}

啓動項目,瀏覽器訪問以下:
聲明式HTTP客戶端 - Spring Cloud OpenFeign


Feign性能優化

RestTemplate VS Feign:
聲明式HTTP客戶端 - Spring Cloud OpenFeign

從上圖中能夠看到,Feign只在性能和靈活性上輸給了RestTemplate,至於靈活性官方也說了不管如何優化也不可能像RestTemplate同樣,而性能則是能夠進一步提升的。

默認狀況下Feign的性能在RestTemplate的50%左右,雖然項目的瓶頸通常不會出如今Feign上,但若是能讓Feign的性能更好一些,也只是有利無害,因此本小節簡單談談Feign的性能優化。

默認狀況下Feign底層是使用HttpURLConnection發送請求的,衆所周知HttpURLConnection是沒有使用鏈接池的,因此能夠針對這點進行優化。例如,將底層的http請求客戶端爲更換爲Apache的HttpClient或者OkHttp等使用了鏈接池的http客戶端,據測試使用了鏈接池後能夠提高15%左右的性能。

另外一個優化的點就是設置合理的日誌級別,以前已經介紹過日誌級別的配置方式了,因此這裏僅演示如何爲Feign更換其餘的http請求客戶端及配置鏈接池。

這裏先以HttpClient爲例,第一步加依賴:

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
</dependency>

第二步,添加配置:

feign:
  httpclient:
    # 讓feign啓用httpclient做爲發送http請求的客戶端
    enabled: true
    # 最大鏈接數
    max-connections: 200
    # 單個路徑的最大鏈接數
    max-connections-per-route: 50

使用okhttp也是同樣的,第一步加依賴:

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
</dependency>

第二步,添加配置:

feign:
  okhttp:
    # 讓feign啓用okhttp做爲發送http請求的客戶端
    enabled: true
    # 最大鏈接數
    max-connections: 200
    # 單個路徑的最大鏈接數
    max-connections-per-route: 50
相關文章
相關標籤/搜索