SpringCloud-Feign

張鐵牛發表於2022-03-24

1. Feign簡介

1.1 簡介

Feign是Netflix公司開發的一個宣告式的REST呼叫客戶端; Ribbon負載均衡、 Hystrⅸ服務熔斷是我們Spring Cloud中進行微服務開發非常基礎的元件,在使用的過程中我們也發現它們一般都是同時出現的,而且配置也都非常相似,每次開發都有很多相同的程式碼,因此Spring Cloud基於Netflix Feign整合了Ribbon和Hystrix兩個元件,讓我們的開發工作變得更加簡單, 就像Spring boot是對Spring+ SpringMVC的簡化, Spring Cloud Feign對Ribbon負載均衡、 Hystrⅸ服務熔斷進行簡化,在其基礎上進行了進一步的封裝,不僅在配置上大大簡化了開發工作,同時還提供了一種宣告式的Web服務客戶端定義方式。使用方式類似Dubbo的使用方式。

1.2 Feign和Ribbon的聯絡

Ribbon是一個基於 HTTP 和 TCP 客戶端 的負載均衡的工具。它可以 在客戶端配置

RibbonServerList(服務端列表),使用 HttpClient 或 RestTemplate 模擬http請求,步驟相當繁瑣。

Feign 是在 Ribbon的基礎上進行了一次改進,是一個使用起來更加方便的 HTTP 客戶端。採用介面的 方式, 只需要建立一個介面,然後在上面新增註解即可 ,將需要呼叫的其他服務的方法定義成抽象方 法即可, 不需要自己構建http請求。然後就像是呼叫自身工程的方法呼叫,而感覺不到是呼叫遠端方 法,使得編寫客戶端變得非常容易。

1.3 負載均衡

Feign中本身已經整合了Ribbon依賴和自動配置,因此我們不需要額外引入依賴,也不需要再註冊 RestTemplate 物件。

2. 能幹什麼

feign是宣告式的web service客戶端,它讓微服務之間的呼叫變得更簡單了,類似controller呼叫service。Spring Cloud整合了Ribbon和Eureka,可在使用Feign時提供負載均衡的http客戶端。

3. Quick Start

3.1 建立服務註冊中心

3.1.1 引入依賴座標

<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

3.1.2 配置application.yml

server:
  port: 8080

spring:
  application:
    name: eureka-server

eureka:
  instance:
    hostname: localhost
  client:
    # 是否將自己註冊到Eureka服務中,本身就是所有無需註冊
    registerWithEureka: false
    # 是否從Eureka中獲取註冊資訊
    fetchRegistry: false
    # 客戶端與Eureka服務端進行互動的地址
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

3.1.3 配置啟動類

新增@EnableEurekaServer 標記為EurekaServer

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

3.1.4 啟動專案

瀏覽器輸入localhost:8080訪問註冊中心

3.2 建立服務提供者

3.2.1 引入依賴座標

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

3.2.2 配置application.yml

這裡將port設定為動態傳參,主要是想通過設定vm引數來使啟動兩個埠不同的服務,以便後續通過consumer呼叫的時候實現負載均衡效果

server:
  # 預設為8010
  port: ${port:8010}

spring:
  application:
    name: provider

eureka:
  client:
    # eureka server的路徑
    serviceUrl:
      defaultZone: http://localhost:8080/eureka/

3.2.3 建立測試介面

package com.ldx.provider.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    @Value("${server.port}")
    String port;

    @GetMapping("hi")
    public String hi(){
        // 通過返回port,使掉用端方便檢視呼叫的是那個埠的服務
        return "hi~ my port ===" + port;
    }
    // 測試服務呼叫超時
    @GetMapping("hiWithTimeOut")
    public String hiWithTimeOut() throws InterruptedException {
        Thread.sleep(10000);
        return "hi~ my port ===" + port;
    }
}

3.2.4 配置啟動項

通過idea的複製功能建立兩個啟動模板,且8011模板通過傳port引數實現服務埠的動態替換

3.2.5 啟動服務

檢視註冊中心控制檯

服務已註冊成功

測試服務介面

3.3 建立服務消費者

3.3.1 引入座標依賴

這裡之所以將全部的依賴都展示

  1. 因為Netflix 公司2018年已經宣佈停止Hystrix的維護,導致springcloud的最新版本即:2020.0.1已經完全廢棄了Hystrix

    1. Hystrix替代品為

      文件連結為:Resilience4J

    2. 所以如果要使用Hystrix需使用Hoxton.SR10及以下版本 Hoxton.SR10對應其他服務的版本

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.3.8.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.ldx</groupId>
	<artifactId>consumer</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>consumer</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>1.8</java.version>
		<spring-cloud.version>Hoxton.SR10</spring-cloud.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-openfeign</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>
	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>${spring-cloud.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

3.3.2 配置application.yml

server:
  port: 8082

spring:
  application:
    name: consumer

eureka:
  client:
    # eureka server的路徑
    serviceUrl:
      defaultZone: http://localhost:8080/eureka/

feign:
  #開啟hystrix熔斷機制
  hystrix:
    enabled: true
  client:
    config:
      #配置服務名為provider的相關資訊
      provider:
        #列印的日誌級別
        loggerLevel: FULL
        #指的是建立連線所用的時間
        connectTimeout: 2000
        #指的是建立連線後從伺服器讀取到可用資源所用的時間
        readTimeout: 5000
      #default代表所有服務
      default:
        #feign客戶端建立連線超時時間
        connectTimeout: 2000
        #feign客戶端建立連線後讀取資源超時時間
        readTimeout: 3000
#feign.client.config.provider.loggerLevel 對應的日誌級別需配合logging.level
logging:
  level:
    com.ldx.consumer.service.HelloService: debug

# 配置熔斷超時時間
hystrix:
  command:
    default:
      execution:
        timeout:
          enabled: true
        isolation:
          thread:
            timeoutInMilliseconds: 50000

3.3.3 配置啟動類

通過EnableFeignClients開啟服務對feign的支援

package com.ldx.consumer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

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

3.3.4 建立測試介面

建立測試訪問介面

package com.ldx.consumer.controller;

import com.ldx.consumer.service.HelloService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;

@RestController
public class HelloController {
    @Resource
    HelloService helloService;

    @GetMapping("hi")
    public String hi(){
        return helloService.hi();
    }

    @GetMapping("hiWithTimeOut")
    public String hiWithTimeOut(){
        return helloService.hiWithTimeOut();
    }
}

@FeignClient:

  • name:指定需要呼叫的微服務的名稱(不分大小寫),用於建立Ribbon的負載均衡器。 所以Ribbon會把 provider 解析為註冊中心的服務。
  • fallback:指定請求失敗時的回退邏輯
package com.ldx.consumer.service;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

@FeignClient(name = "provider",fallback = HelloFallbackImpl.class)
public interface HelloService {
    @GetMapping("hi")
    String hi();

    @GetMapping("hiWithTimeOut")
    String hiWithTimeOut();
}

請求失敗回退處理

package com.ldx.consumer.service;

import org.springframework.stereotype.Component;

@Component
public class HelloFallbackImpl implements HelloService {
    @Override
    public String hi() {
        return "遠端服務不可用,請稍後重試。。。。。";
    }

    @Override
    public String hiWithTimeOut() {
        return "請求超時。";
    }
}

3.3.5 啟動專案

檢視註冊中心控制檯

服務註冊已成功

測試介面

第一次訪問:

第二次訪問:

測試請求服務超時

列印堆疊資訊如下:

當直接把服務提供者關閉後,請求返回結果也是熔斷返回的資訊,符合預期。

4. 客戶端Hystrix整合

springcloud每個版本的差異比較大,有的低版本可能需要顯性的引用hystrix依賴

當前使用的Hoxton.SR10版本不需要引用,因為已經被feign整合了

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

4.1 熔斷器使用

在網路請求時,可能會出現異常請求,如果還想再異常情況下使系統可用,那麼就需要容錯處理,比如:網路請求超時時給使用者提示“稍後重試”或使用本地快照資料等等。

Spring Cloud Feign就是通過Fallback實現的,有兩種方式:

  1. @FeignClient.fallback = xxxFeignFallback.class指定一個實現Feign介面的實現類。
  2. @FeignClient.fallbackFactory = xxxFeignFactory.class指定一個實現FallbackFactory工廠介面類

注意:feign的註解@FeignClient:fallbackFactory與fallback方法不能同時使用,這個兩個方法其實都類似於Hystrix的功能,當網路不通時返回預設的配置資料。

4.2 配置檔案配置

在application.properties 啟用hystrix

feign:
  #開啟hystrix熔斷機制
  hystrix:
    enabled: true

請務必注意,從Spring Cloud Dalston開始,Feign預設是不開啟Hystrix的。

因此,如使用Dalston以及以上版本請務必額外設定屬性:feign.hystrix.enabled=true,否則 斷路器不會生效。

Spring Cloud Angel/Brixton/Camden中,Feign預設都是開啟Hystrix的。

4.3 fallback 實現

建立HelloFallbackImpl的回撥實現,由spring建立使用@Component(其他的註冊也可以)註解

HystrixTargeter.targetWithFallback方法實現了@FeignClient.fallback處理邏輯,通過原始碼可以知道HelloFallbackImpl回撥類是從Spring容器中獲取的,所以HelloFallbackImpl由spring建立。

4.3.1 HelloService 介面類

package com.ldx.consumer.service;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

@FeignClient(name = "provider",fallback = HelloFallbackImpl.class)
public interface HelloService {
    @GetMapping("hi")
    String hi();

    @GetMapping("hiWithTimeOut")
    String hiWithTimeOut();
}

4.3.2 HelloFallbackImpl回退類

package com.ldx.consumer.service;

import org.springframework.stereotype.Component;

@Component
public class HelloFallbackImpl implements HelloService {
    @Override
    public String hi() {
        return "遠端服務不可用,請稍後重試。。。。。";
    }

    @Override
    public String hiWithTimeOut() {
        return "請求超時。";
    }
}

4.3.3 驗證

關閉服務提供者後測試

4.4 FallbackFactory工廠

上面的實現方式簡單,但是獲取不到HTTP請求錯誤狀態碼和資訊 ,這時就可以使用工廠模式來實現Fallback

同樣工廠實現類也要交由spring管理。

4.4.1 HelloService 介面類

fallbackFactory 屬性 指定了 自定義的回退工廠

package com.ldx.consumer.service;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

@FeignClient(name = "provider",fallbackFactory = HelloFallbackFactory.class)
public interface HelloService {
    @GetMapping("hi")
    String hi();

    @GetMapping("hiWithTimeOut")
    String hiWithTimeOut();
}

4.4.2 HelloFallbackFactory回退類

注意 實現的是feign.hystrix.FallbackFactory

package com.ldx.consumer.service;

import feign.hystrix.FallbackFactory;
import org.springframework.stereotype.Component;

@Component
public class HelloFallbackFactory implements FallbackFactory<HelloService> {
    @Override
    public HelloService create(Throwable throwable) {
        return new HelloService() {
            @Override
            public String hi() {
                return "請求失敗~ error msg:" + throwable.getMessage();
            }

            @Override
            public String hiWithTimeOut() {
                return "請求超時~ error msg:" + throwable.getMessage();
            }
        };
    }
}

4.4.3 驗證

關閉服務提供者後測試

5. 自定義ErrorDecoder

ErrorDecoder介面處理請求錯誤資訊,預設實現ErrorDecoder.Default丟擲FeignException異常

FeignException.status 方法返回HTTP狀態碼,FallbackFactory.create預設情況下可以強制轉換成FeignException異常這樣就可以獲取到HTTP狀態碼了。

5.1 FeignErrorDecoder

自定義FeignErrorDecoder

@Configuration
public class FeginErrorDecoder implements ErrorDecoder {
    @Override
    public Exception decode(String methodKey, Response response) {
        ServiceException serviceException = new ServiceException();
        serviceException.setMethod(methodKey);
        if (response.status() >= 400 && response.status() <= 499) {
            serviceException.setCode(response.status());
            serviceException.setErrorMessage(response.reason());
            serviceException.setMessage("頁面或者引數錯誤");
        }
        if (response.status() >= 500 && response.status() <= 599) {
            serviceException.setCode(response.status());
            serviceException.setErrorMessage(response.reason());
            serviceException.setMessage("伺服器錯誤");
        }
        return serviceException;
    }
}

class ServiceException extends RuntimeException {
    private int code;
    private String message;
    private String method;
    private String errorMessage;

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getErrorMessage() {
        return errorMessage;
    }

    public void setErrorMessage(String errorMessage) {
        this.errorMessage = errorMessage;
    }

    public String getMethod() {
        return method;
    }

    public void setMethod(String method) {
        this.method = method;
    }

    @Override
    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    @Override
    public String getLocalizedMessage() {
        return "錯誤碼:" + code + ",錯誤資訊:" + message + ",方法:" + method + ",具體錯誤資訊:" + errorMessage;
    }
}

消費者提供請求報錯的方法,供呼叫者呼叫

@GetMapping("hiWithError")
public String hiWithError() {
    int a = 1/0;
    return "hi~ my port ===" + port;
}

5.2 流程

在Feign客戶端發生http請求層面(提供者業務程式碼報錯)的錯誤時會呼叫decode方法。在decode方法中實現自定義的錯誤處理,當出現異常時首先會通過FeginErrorDecoder進行異常的封裝,然後會呼叫HelloFallbackFactory進行異常的回撥處理

消費者呼叫結果如下

6. 擴充套件

6.1 Feign使用HttpClient

Feign在預設情況下使用的是JDK原生URLConnection傳送HTTP請求,沒有連線池,但是對每個地址會保持一個長連線,即利用HTTP的persistence connection。我們可以用Apache的HTTP Client替換Feign原始的http client,從而獲取連線池、超時時間等與效能息息相關的控制能力。Spring Cloud從Brixtion.SR5版本開始支援這種替換。

6.1.2 匯入POM依賴

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

6.1.3 配置檔案配置

feign:
  httpclient:
     enabled: true

主要:這個配置可加可不加,在該版本中預設為true,可以不加,在HttpClientFeignLoadBalancedConfiguration原始碼中有的配置預設為true

@ConditionalOnProperty(
    value = {"feign.httpclient.enabled"},
    matchIfMissing = true
)

6.1.4 驗證

首先在工程配置檔案中,將配置項 feign.httpclient.enabled 的值,設定為 false 。然後,在HttpClientFeignLoadBalancedConfiguration 的 feignClient(…)方法內的某行打上斷點,重新啟動專案,注意觀察會發現,整個啟動過程中,斷點沒有被命中。接下來,將配置項 feign.httpclient.enabled 的值設定為 true,再一次啟動專案,斷點被命中。由此,可以驗證 HttpClientFeignLoadBalancedConfiguration 自動配置類被啟動。

6.2 在feign請求之前操作

feign元件提供了請求操作介面RequestInterceptor,實現之後對apply函式進行重寫就能對request進行修改,包括header和body操作。

@Component
public class TokenRequestInterceptor implements RequestInterceptor {
    @Override
    public void apply(feign.RequestTemplate template) {
        String method = template.method();
        String url = template.url();
        System.out.println("呼叫方法:" + method + ",URL地址:" + url);
    }
}

6.3 請求壓縮

Spring Cloud Feign支援對請求和響應進行GZIP壓縮,以減少通訊過程中的效能損耗。我們只需通過下面兩個引數設定,就能開啟請求與響應的壓縮功能:

feign.compression.request.enabled=true
feign.compression.response.enabled=true

同時,我們還能對請求壓縮做一些更細緻的設定,比如下面的配置內容指定了壓縮的請求資料型別,並設定了壓縮的大小下限,只有超過這個大小的請求才會對其進行壓縮。

feign.compression.request.enabled=true
feign.compression.request.mime-types=text/xml,application/xml,application/json
feign.compression.request.min-request-size=2048

上述配置的feign.compression.request.nime-types和feign.compression.requestmin-request-size均為預設值。

6.4 日誌配置

Spring Cloud Feign在構建被@FeignClient註解修飾的服務客戶端時,會為每一個客戶端都建立一個feign的請求細節。可以在application.properties檔案中使用logging.level.的引數配置格式來開啟指定Feign客戶端的DEBUG日誌,其中為Feign客戶端引用全路徑路徑,比如針對本文中我們實現的HelloService可以如下配置開啟:

logging.level.com.springcloud.user.service.HelloService=DEBUG

但是,只是新增了如上配置,還無法實現對DEBUG日誌的輸出。這時由於Feign客戶端預設對Logger.Level物件定義為NONE級別,該界別不會記錄任何Feign呼叫過程中對資訊,所以我們需要調整它對級別,針對全域性對日誌級別,可以在應用主類中直接假如Logger.Level的Bean建立,具體如下:

@Bean
public Logger.Level feignLoggerLevel() {
    return Logger.Level.FULL;
}

在調整日誌級別為FULL之後,我們呼叫介面測試,檢視日誌

2021-03-02 20:12:56.169 DEBUG 4186 --- [trix-provider-2] com.ldx.consumer.service.HelloService    : [HelloService#hi] ---> GET http://provider/hi HTTP/1.1
2021-03-02 20:12:56.169 DEBUG 4186 --- [trix-provider-2] com.ldx.consumer.service.HelloService    : [HelloService#hi] Accept-Encoding: gzip
2021-03-02 20:12:56.169 DEBUG 4186 --- [trix-provider-2] com.ldx.consumer.service.HelloService    : [HelloService#hi] Accept-Encoding: deflate
2021-03-02 20:12:56.169 DEBUG 4186 --- [trix-provider-2] com.ldx.consumer.service.HelloService    : [HelloService#hi] ---> END HTTP (0-byte body)
2021-03-02 20:12:56.175 DEBUG 4186 --- [trix-provider-2] com.ldx.consumer.service.HelloService    : [HelloService#hi] <--- HTTP/1.1 200  (5ms)
2021-03-02 20:12:56.175 DEBUG 4186 --- [trix-provider-2] com.ldx.consumer.service.HelloService    : [HelloService#hi] connection: keep-alive
2021-03-02 20:12:56.175 DEBUG 4186 --- [trix-provider-2] com.ldx.consumer.service.HelloService    : [HelloService#hi] content-length: 19
2021-03-02 20:12:56.175 DEBUG 4186 --- [trix-provider-2] com.ldx.consumer.service.HelloService    : [HelloService#hi] content-type: text/plain;charset=UTF-8
2021-03-02 20:12:56.175 DEBUG 4186 --- [trix-provider-2] com.ldx.consumer.service.HelloService    : [HelloService#hi] date: Tue, 02 Mar 2021 12:12:56 GMT
2021-03-02 20:12:56.175 DEBUG 4186 --- [trix-provider-2] com.ldx.consumer.service.HelloService    : [HelloService#hi] keep-alive: timeout=60
2021-03-02 20:12:56.175 DEBUG 4186 --- [trix-provider-2] com.ldx.consumer.service.HelloService    : [HelloService#hi] 
2021-03-02 20:12:56.175 DEBUG 4186 --- [trix-provider-2] com.ldx.consumer.service.HelloService    : [HelloService#hi] hi~ my port ===8011
2021-03-02 20:12:56.176 DEBUG 4186 --- [trix-provider-2] com.ldx.consumer.service.HelloService    : [HelloService#hi] <--- END HTTP (19-byte body)

6.4.1 fegin日誌級別

對於Feign的Logger級別主要有下面4類,可根據實際需要進行調整使用。

NONE:不記錄任何資訊。

BASIC:僅記錄請求方法、URL以及響應狀態碼和執行時間。

HEADERS:出了記錄BASIC級別的資訊之外,還會記錄請求和響應的頭資訊。

FULL:記錄所有請求與響應的細節,包括頭資訊、請求體、後設資料等。

7. FeignClient註解的一些屬性

屬性名 預設值 作用 備註
value 空字串 呼叫服務名稱,和name屬性相同
serviceId 空字串 服務id,作用和name屬性相同 已過期
name 空字串 呼叫服務名稱,和value屬性相同
contextId 空字串 參考下方介紹
url 空字串 全路徑地址或hostname,http或https可選 url用於配置指定服務的地址,相當於直接請求這個服務,不經過Ribbon的服務選擇。
decode404 FALSE 配置響應狀態碼為404時是否應該丟擲FeignExceptions
configuration {} 自定義當前feign client的一些配置 參考FeignClientsConfiguration
fallback void.class 熔斷機制,呼叫失敗時,走的一些回退方法,可以用來丟擲異常或給出預設返回資料。 底層依賴hystrix,啟動類要加上@EnableHystrix
fallbackFactory void.class 和fallback功能相似,只不過fallbackFactory中有報錯的Throwable資訊
path 空字串 自動給所有方法的requestMapping前加上字首,類似與controller類上的requestMapping
primary TRUE primary對應的是@Primary註解,預設為true,官方這樣設定也是有原因的。當我們的Feign實現了fallback後,也就意味著Feign Client有多個相同的Bean在Spring容器中,當我們在使用@Autowired進行注入的時候,不知道注入哪個,所以我們需要設定一個優先順序高的,@Primary註解就是幹這件事情的。
qualifier 介紹如下

7.1 contextId

比如我們有個user服務,但user服務中有很多個介面,我們不想將所有的呼叫介面都定義在一個類中,比如:

Client 1

@FeignClient(name = "optimization-user")
public interface UserRemoteClient {
	@GetMapping("/user/get")
	public User getUser(@RequestParam("id") int id);
}

Client 2

@FeignClient(name = "optimization-user")
public interface UserRemoteClient2 {
	@GetMapping("/user2/get")
	public User getUser(@RequestParam("id") int id);
}

這種情況下啟動就會報錯了,因為Bean的名稱衝突了,具體錯誤如下:

Description:
The bean 'optimization-user.FeignClientSpecification', defined in null, could not be registered. A bean with that name has already been defined in null and overriding is disabled.
Action:
Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true

解決方案可以增加下面的配置,作用是允許出現beanName一樣的BeanDefinition。

spring.main.allow-bean-definition-overriding=true

另一種解決方案就是為每個Client手動指定不同的contextId,這樣就不會衝突了。

上面給出了Bean名稱衝突後的解決方案,下面來分析下contextId在Feign Client的作用,在註冊Feign Client Configuration的時候需要一個名稱,名稱是通過getClientName方法獲取的:

String name = getClientName(attributes);

registerClientConfiguration(registry, name,
attributes.get("configuration"));
private String getClientName(Map<String, Object> client) {
    if (client == null) {
      return null;
    }
    String value = (String) client.get("contextId");
    if (!StringUtils.hasText(value)) {
      value = (String) client.get("value");
    }
    if (!StringUtils.hasText(value)) {
      value = (String) client.get("name");
    }
    if (!StringUtils.hasText(value)) {
      value = (String) client.get("serviceId");
    }
    if (StringUtils.hasText(value)) {
      return value;
    }


    throw new IllegalStateException("Either 'name' or 'value' must be provided in @"
        + FeignClient.class.getSimpleName());
  }

可以看到如果配置了contextId就會用contextId,如果沒有配置就會去value然後是name最後是serviceId。預設都沒有配置,當出現一個服務有多個Feign Client的時候就會報錯了。

其次的作用是在註冊FeignClient中,contextId會作為Client 別名的一部分,如果配置了qualifier優先用qualifier作為別名。

7.2 qualifier

qualifier對應的是@Qualifier註解,使用場景跟上面的primary關係很淡,一般場景直接@Autowired直接注入就可以了。

如果我們的Feign Client有fallback實現,預設@FeignClient註解的primary=true, 意味著我們使用@Autowired注入是沒有問題的,會優先注入你的Feign Client。

如果你鬼斧神差的把primary設定成false了,直接用@Autowired注入的地方就會報錯,不知道要注入哪個物件。

解決方案很明顯,你可以將primary設定成true即可,如果由於某些特殊原因,你必須得去掉primary=true的設定,這種情況下我們怎麼進行注入,我們可以配置一個qualifier,然後使用@Qualifier註解進行注入,示列如下:

Feign Client定義

@FeignClient(name = "optimization-user", path="user", qualifier="userRemoteClient")
public interface UserRemoteClient {
	
	@GetMapping("/get")
	public User getUser(@RequestParam("id") int id);
}

Feign Client注入

@Autowired
@Qualifier("userRemoteClient")
private UserRemoteClient userRemoteClient;