關於註解FeignClient的使用規范

註解FeignClient使用規范

首先是對FeignClient裡的常用屬性

  • contextId:當有多個服務調用方法不想寫在一個接口裡,就要使用到
  • name:指定FeignClient的名稱,如果項目使用瞭Ribbon,name屬性會作為微服務的名稱,用於服務發現
  • url:url一般用於調試,可以手動指定@FeignClient調用的地址
  • fallback:定義容錯的處理類,當調用遠程接口失敗或超時時,會調用對應接口的容錯邏輯,fallback指定的類必須實現@FeignClient標記的接口
  • fallbackFactory:工廠類,用於生成fallback類示例,通過這個屬性我們可以實現每個接口通用的容錯邏輯,減少重復的代碼 
  • path:定義當前FeignClient的統一前綴

使用FeignClient的原因

無非讓一個微服務的接口被另一個服務訪問,微服務的一個特點就是業務隔離,那就要盡量的做到數據庫隔離,雖 然不是真的隔離,但是要盡量在代碼上做隔離。

舉例,系統有一個file-server,是系統的公共服務,附件的下載,上傳,獲取內容等接口實現。對應操作的表是系統附件表。

現在客戶的信息裡有附,正常是在客戶與附件之間做連表查詢,將附件信息拿到,但是在微服務裡這樣子做是不合規的。現在如果進行如下的引用

在controller類裡定義的接口方法

//在controller類裡定義的接口方法   
@RequestMapping(value = "/add" ,method = RequestMethod.GET)
public Integer add(@RequestParam Integer a, @RequestParam Integer b) {
 ServiceInstance instance = client.getLocalServiceInstance();
 Integer r = a + b;
 logger.info("/add, host:" + instance.getHost() + ", service_id:" + instance.getServiceId() + ", result:" + r);
 return r;
}

使用@FeignClient

@FeignClient("compute-service")
public interface ComputeClient {
    @RequestMapping(method = RequestMethod.GET, value = "/add")
    Integer add(@RequestParam(value = "a") Integer a, @RequestParam(value = "b") Integer b);
}

然後在另一個微服務裡的controller層調用

@Autowired
ComputeClient computeClient;
@RequestMapping(value = "/add", method = RequestMethod.GET)
public Integer add() {
    return computeClient.add(10, 20);
}

隻能說使用成功瞭,但是不規范,這是網上隨處可見的錯誤規范,一開始我也是錯誤的在controll層引用,但是被技術經理叼瞭,反正就是不合規。

理由很簡單,給前端的接口不要重復。如果你這邊controller調用原來的服務,不管是哪個微服務的,前端是可以直接訪問的,那這樣子的意義何在。接口給你調用,是讓你用來獲取有用的信息來處理,然後返回給前端。

所以應該在server層調用。

規范的使用@FignClient註解

以公共服務中的文件服務和客戶業務模塊進行服務間的通信為例

在文件服務的api模塊裡寫如下

FeignClient(contextId = "fileService", value = ServicellaneConstants.FTLE_SERNVTCE,fallbackFactory=RenoteFileFallbackFactory.class)
public interface RemoteFileservice {
/**
*上傳文件*
* aparam file 文件信息*areturn結果
*/
@PostMapping(value = "/upload",consumes = MediaType.WULTIPART_FORN_DATA_VALUE)
public RcLong uplocad(CRequestParan("module ) String nodole ,ORequestParan("sunceId ) String souneId, OlequestFart(value = "file ") liltigartFile file);

此時在客戶業務板塊,即客戶需要和附件進行相關信息的1對多查詢,不能連表查詢,那就調用上面的RemoteFileservice ,一開始我在客戶模塊裡寫瞭一個FileController,然後寫接口,接口裡調用,直接NO。

應該在業務層裡調用就好瞭,用註解@Autowired,將你需要處理的數據獲取到並處理好。

接下來看看fallbackFactory,主要是為降級處理就是出錯瞭應該返回什麼。

代碼僅供參考,可能還會有其它 的寫法

//註意FallbackFactory<你註解對應的接口類名>
@component
public class RemoteFileFallbackFactory implements FallbackFactory<RemoteFileService>
private static final Logger log = LoggerFactory.getLogger(RemoteFileFallbackFactory.class);
0verride
public RemoteFileservice create(Throwable throwable){
//打印出錯日志
log.error("文件朋務調用失敗:{}", throwable.getHessage());return new RemoteFileserviceo
{
Override
public R<Long> upload(String module,String sourceId,MultipartFile file){
return R.fail("上傳失敗:" + throwable.getMessage(o);
}
0verride
public byte[ ] getFileBytes(Long fileId) { return new byte[0];}
@0verride
public R getURL(Long[] ids) { return R.fail("獲取文件URL失敗:" + throwable.getHessage());}
@0verride
public R<List<SysFileApivo>> getURL(List<Long> ids) {
return R.fail("獲取文件URL失敗:" + throwable.getMessage());
}
};

想要怎麼用,就動手實踐吧

@FeignClient註解使用的常見問題

Feign 是一個聲明式的 Web 服務,通過定義一個添加相應註解的接口,即可完成一個 Web 服務的接口。SpringCloud 對 Feign 進行瞭封裝以後,其開始能夠支持 Spring MVC 標準註解,同時在 SpringCloud 架構上結合 Eureka 和 Ribbon,還能夠支持負載均衡。

既然是一個 Web 服務,必然服務端模塊與客戶端模塊都加入 Feign 依賴以及對接的 api 接口,這是 Feign 服務的基本前提。因此雙方引入的 Feign 接口都要保持一致,包括服務地址、入參定義、返回值等。

基本配置

要啟用 @FeignClient 接口,首先需要引入 Feign 依賴:

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

在父 pom.xml 裡還需要引入 SpringCloud 依賴,這樣使用 FeignClient 才有意義。

在啟動類上添加啟動註解 @EnableFeignClients:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
 
@SpringBootApplication
@EnableFeignClients
public class FeignDemoApp {
    public static void main(String[] args) {
        SpringApplication.run(FeignDemoApp.class, args);
    }
}

然後定義 Feign 客戶端提供的服務接口,示例代碼如下:

import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
 
import com.demo.feign.model.request.FeignDemoDetailRequest;
import com.demo.feign.model.response.FeignDemoDetailResponse;
import com.demo.feign.model.response.FeignDemoListResponse;
@FeignClient("spring-boot-feign-demo")
public interface FeignDemoClient {
 
    @RequestMapping(value="/demo/list", method=RequestMethod.GET)
    public FeignDemoListResponse queryListByCode(@PathVariable("code") String code);
     
    @RequestMapping(value="/demo/detail", method=RequestMethod.POST)
    public FeignDemoDetailResponse queryDetail(@RequestBody FeignDemoDetailRequest request);
}

這裡定義的 FeignDemoDetailRequest,FeignDemoDetailResponse 和 FeignDemoListResponse 是定義的實體類,具體就不展示瞭。

常見問題

Feign 的 maven 依賴報紅,或者主啟動類上@EnableFeignClients不識別,一直報紅,或者 feign 接口註入到控制器報紅。

可能的原因及處理方案:

  • maven 包沒有成功導入,或者 maven 包下載不完整,可以從倉庫刪掉依賴包後重新下載導入;
  • 版本沖突,或者無法獲取適當版本,此時可以在 maven 包導入時指定版本號,嘗試其他可用的版本。

項目 Application 啟動時,使用 @Autowired 自動註入的 FeignClient 接口被告知對應的 Bean 無法尋獲

***************************
APPLICATION FAILED TO START
***************************
 
Description:
Field feignDemoClient in com.xxx.xxx.service.impl.DemoServiceImpl required a bean of type 'com.xxx.xxx.feign.FeignDemoClient' that could not be found.
The injection point has the following annotations:
    – @org.springframework.beans.factory.annotation.Autowired(required=true)
 
Action:
Consider defining a bean of type 'com.xxx.xxx.feign.FeignDemoClient' in your configuration.
Disconnected from the target VM, address: '127.0.0.1:51645', transport: 'socket'
Process finished with exit code 1

可能的原因及處理方案:

  • 服務所在模塊沒有在 pom.xml 引入 spring-cloud-starter-openfeign 這個 maven 依賴,補上這個依賴重新構建項目即可。
  • 項目啟動沒有掃描 FeignClient 接口所在的包。項目通過啟動類啟動時默認會掃描同目錄及同目錄下級目錄的類文件。所以,Spring註入第三方包或者其他模塊的包,需要掃描需要註入的包。這種情況,隻需要在啟動類的註解中指定需要掃描的包路徑即可,如:
// 開啟Feign客戶端,指定掃描 FeignClient 接口類所在的包
@EnableFeignClients("com.xxx.xxx.feign")
@SpringBootApplication

正確添加 Feign 依賴,啟動類掃描 api 接口所有包路徑,但註入接口仍然報紅

這種情況現在比較少見,但是在一段時間以前出現比較頻繁,主要是在 SpringBoot 2.0 版本後優化瞭相關邏輯。在 SpringBoot 2.0 之前,如果你的 Feign 接口使用 GetMapping 註解,那麼註入該接口都會報紅,無法註入。相應的,修改成 RequestMapping 或者 PostMapping 就能註入瞭。

處理方案:

1、采用合適的註解形式定義 Feign 接口。目前可行的定義方式基本有以下幾種:

@FeignClient("spring-boot-feign-demo")
public interface FeignDemoClient {
    
    // 使用RequestMapping指定Get方法,入參單個傳入
    @RequestMapping(value="/demo/list", method=RequestMethod.GET)
    public FeignDemoListResponse queryListByCode(@PathVariable("code") String code);
     
    // 使用RequestMapping指定Post方法,傳入實體使用註解@RequestBody
    @RequestMapping(value="/demo/detail", method=RequestMethod.POST)
    public FeignDemoDetailResponse queryDetail(@RequestBody FeignDemoDetailRequest request);
    
    // 使用PostMapping,傳入實體使用註解@RequestBody
    @PostMapping("/demo/seq")
    public FeignDemoSeqResponse querySeq(@RequestBody FeignDemoSeqRequest request);
}

通過 FeignClient 發起 Get 請求報 405 錯誤

通過在服務端斷點捕獲異常可以發現,報 405 錯誤的直接原因,其實是因為定義為 Get 的 Feign 接口,接收到 Post 方法的調用。但是在調用方調用時大多數也是采用 Get 方法,那麼到底是什麼原因呢?

暫且預設 Feign 接口的定義如下:

@FeignClient("spring-boot-feign-demo")
public interface FeignDemoClient {
    // 服務方接收到的請求是Post方法,而不是Get方法
    @GetMapping(value="/demo/list")
    public Response queryList(Request request);

實際上,通過斷點跟蹤接口調用時的調用路徑,就會發現 FeignClient 最後是通過 HttpURLConnection 發起的網絡連接,在發起的過程中,Connection 會判斷自身請求的 body 是否為空。如果 body 不為空,則將 Get 方法轉換成 Post 方法。因為 body 形式的數據隻能方法 RequestBody 內以流的形式進行傳輸,而根據 Http 協議 param 形式的數據可以直接放在 URL 上進行傳輸和獲取。

之所以 FeignClient 在網絡請求時會出現這種轉換,這跟它的初始化規則有關。在項目啟動過程中,@FeignClient 直接的類會被初始化一個動態代理的類,並通過一個 RequestTemplate.Factory 的工廠類生成請求模板,具體規則如下:

  • 如果基本類型參數有 @RequestParam 註解,則會將參數作為 key 放入 RequestTemplate.Factory 中,通過 urlIndex 記錄數組索引。在進行解析時,通過 key 從數組中獲取具體的參數值,拼接在 url 後面。
  • 如果參數沒有任何註解,或者有 @RequestBody 註解,那麼初始化時會使用 bodyIndex 維護參數索引,並通過 bodyType 記錄參數的具體類型。

需要註意的是,@RequestParam 隻能用於對單個基本類型參數的註解,不能用來註解一個實體類。使用實體類作為入參出參時,建議還是使用 Post 方法進行請求。

如果在開發中覺得維護以上的對應關系不方便的話,還有另外一種修改方法可供使用,基本原理就是使用 Apache 的 HTTP Client 替換 Feign 原生的 Http Client,替換後 Get 方法也可以用一個實體類作為請求參數而不用擔心請求被轉換成 Post 方法瞭。具體修改方式如下:

引入 http client 依賴

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId><br>    
    <version>4.5.2</version>
</dependency>
<dependency>
    <groupId>com.netflix.feign</groupId>
    <artifactId>feign-httpclient</artifactId>
    <version>8.18.0</version>
</dependency>

開啟 Feign 對 httpClient 的設置

feign.httpclient.enabled=true

完成配置修改後,內部就可以使用 Apache 的 http client。Feign 接口的定義是相同的,但是由於 Get 方法支持瞭自定義實體類,與 Post 有類似的處理方式,因此參數的傳輸需要我們額外指定其類型,以確保 JSON 序列化與反序列化的正常進行。

這裡給出一個示例接口定義:

@FeignClient("spring-boot-feign-demo")
public interface FeignDemoClient {
    @PostMapping(value="/demo/detail", consumes = MediaType.APPLICATION_JSON_VALUE)
    public Response<List<ResultDTO>> query(@RequestBody FeignQueryRequest request, 
                                           @PathVariable("page") Integer page, 
                                           @PathVariable("page_size") Integer pageSize);
}

JSON 反序列化失敗

Error while extracting response for type [com.xxx.xxx.feign] and content type [application/json].Can not deserialize instance of java.util.ArrayList out of START_OBJECT token

翻譯過來的意思大致就是,Feign 接口的返回值無法通過 application/json 的格式解析出來,也就是調用方返回值的定義就服務方不一致(結構不同)。

處理方案:

確保雙方引入的接口 api 完全一致,可通過將接口所在模塊打包成 jar 包,雙方引用同一個 jar 等方式以保證接口定義一致。

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。

推薦閱讀: