1. 程式人生 > 實用技巧 >微服務實戰SpringCloud之Feign簡介及使用

微服務實戰SpringCloud之Feign簡介及使用

Feign的目標

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

引入Feign

專案中使用了gradle作為依賴管理,maven類似。

dependencies {
    //feign
    implementation('org.springframework.cloud:spring-cloud-starter-openfeign:2.0.2.RELEASE')
    //web
    implementation('org.springframework.boot:spring-boot-starter-web')
    //eureka client
    implementation('org.springframework.cloud:spring-cloud-starter-netflix-eureka-client:2.1.0.M1')
    //test
    testImplementation('org.springframework.boot:spring-boot-starter-test')
}

因為feign底層是使用了ribbon作為負載均衡的客戶端,而ribbon的負載均衡也是依賴於eureka 獲得各個服務的地址,所以要引入eureka-client。

SpringbootApplication啟動類加上@FeignClient註解,以及@EnableDiscoveryClient。

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class ProductApplication {

    public static void main(String[] args) {
        SpringApplication.run(ProductApplication.class, args);
    }
}

yaml配置:

server:
  port: 8082

#配置eureka
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka
  instance:
    status-page-url-path: /info
    health-check-url-path: /health

#服務名稱
spring:
  application:
    name: product
  profiles:
    active: ${boot.profile:dev}
#feign的配置,連線超時及讀取超時配置
feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: basic

Feign的使用

@FeignClient(value = "CART")
public interface CartFeignClient {

    @PostMapping("/cart/{productId}")
    Long addCart(@PathVariable("productId")Long productId);
}

上面是最簡單的feign client的使用,宣告完為feign client後,其他spring管理的類,如service就可以直接注入使用了,例如:

//這裡直接注入feign client
@Autowired
private CartFeignClient cartFeignClient;

@PostMapping("/toCart/{productId}")
public ResponseEntity addCart(@PathVariable("productId") Long productId){
    Long result = cartFeignClient.addCart(productId);
    return ResponseEntity.ok(result);
}

可以看到,使用feign之後,我們呼叫eureka 註冊的其他服務,在程式碼中就像各個service之間相互呼叫那麼簡單。

FeignClient註解的一些屬性

屬性名預設值作用備註
value 空字串 呼叫服務名稱,和name屬性相同
serviceId 空字串 服務id,作用和name屬性相同 已過期
name 空字串 呼叫服務名稱,和value屬性相同
url 空字串 全路徑地址或hostname,http或https可選
decode404 false 配置響應狀態碼為404時是否應該丟擲FeignExceptions
configuration {} 自定義當前feign client的一些配置 參考FeignClientsConfiguration
fallback void.class 熔斷機制,呼叫失敗時,走的一些回退方法,可以用來丟擲異常或給出預設返回資料。 底層依賴hystrix,啟動類要加上@EnableHystrix
path 空字串 自動給所有方法的requestMapping前加上字首,類似與controller類上的requestMapping
primary true

此外,還有qualifier及fallbackFactory,這裡就不再贅述。

Feign自定義處理返回的異常

這裡貼上GitHub上openFeign的wiki給出的自定義errorDecoder例子。

public class StashErrorDecoder implements ErrorDecoder {

    @Override
    public Exception decode(String methodKey, Response response) {
        if (response.status() >= 400 && response.status() <= 499) {
            //這裡是給出的自定義異常
            return new StashClientException(
                    response.status(),
                    response.reason()
            );
        }
        if (response.status() >= 500 && response.status() <= 599) {
            //這裡是給出的自定義異常
            return new StashServerException(
                    response.status(),
                    response.reason()
            );
        }
        //這裡是其他狀態碼處理方法
        return errorStatus(methodKey, response);
    }
}

自定義好異常處理類後,要在@Configuration修飾的配置類中宣告此類。

Feign使用OKhttp傳送request

Feign底層預設是使用jdk中的HttpURLConnection傳送HTTP請求,feign也提供了OKhttp來發送請求,具體配置如下:

feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: basic
  okhttp:
    enabled: true
  hystrix:
    enabled: true

Feign原理簡述

  • 啟動時,程式會進行包掃描,掃描所有包下所有@FeignClient註解的類,並將這些類注入到spring的IOC容器中。當定義的Feign中的介面被呼叫時,通過JDK的動態代理來生成RequestTemplate。
  • RequestTemplate中包含請求的所有資訊,如請求引數,請求URL等。
  • RequestTemplate聲場Request,然後將Request交給client處理,這個client預設是JDK的HTTPUrlConnection,也可以是OKhttp、Apache的HTTPClient等。
  • 最後client封裝成LoadBaLanceClient,結合ribbon負載均衡地發起呼叫。

詳細原理請參考原始碼解析。

Feign、hystrix與retry的關係請參考https://xli1224.github.io/2017/09/22/configure-feign/

Feign開啟GZIP壓縮

Spring Cloud Feign支援對請求和響應進行GZIP壓縮,以提高通訊效率。

application.yml配置資訊如下:

feign:
  compression:
    request: #請求
      enabled: true #開啟
      mime-types: text/xml,application/xml,application/json #開啟支援壓縮的MIME TYPE
      min-request-size: 2048 #配置壓縮資料大小的下限
    response: #響應
      enabled: true #開啟響應GZIP壓縮

注意:

由於開啟GZIP壓縮之後,Feign之間的呼叫資料通過二進位制協議進行傳輸,返回值需要修改為ResponseEntity<byte[]>才可以正常顯示,否則會導致服務之間的呼叫亂碼。

示例如下:

@PostMapping("/order/{productId}")
ResponseEntity<byte[]> addCart(@PathVariable("productId") Long productId);

作用在所有Feign Client上的配置方式

方式一:通過java bean 的方式指定。

@EnableFeignClients註解上有個defaultConfiguration屬性,可以指定預設Feign Client的一些配置。

@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class)
@EnableDiscoveryClient
@SpringBootApplication
@EnableCircuitBreaker
public class ProductApplication {

    public static void main(String[] args) {
        SpringApplication.run(ProductApplication.class, args);
    }
}

DefaultFeignConfiguration內容:

@Configuration
public class DefaultFeignConfiguration {

    @Bean
    public Retryer feignRetryer() {
        return new Retryer.Default(1000,3000,3);
    }
}

方式二:通過配置檔案方式指定。

feign:
  client:
    config:
      default:
        connectTimeout: 5000 #連線超時
        readTimeout: 5000 #讀取超時
        loggerLevel: basic #日誌等級

Feign Client開啟日誌

日誌配置和上述配置相同,也有兩種方式。

方式一:通過java bean的方式指定

@Configuration
public class DefaultFeignConfiguration {
    @Bean
    public Logger.Level feignLoggerLevel(){
        return Logger.Level.BASIC;
    }
}

方式二:通過配置檔案指定

logging:
  level:
    com.xt.open.jmall.product.remote.feignclients.CartFeignClient: debug

Feign 的GET的多引數傳遞

目前,feign不支援GET請求直接傳遞POJO物件的,目前解決方法如下:

  1. 把POJO拆散城一個一個單獨的屬性放在方法引數中
  2. 把方法引數程式設計Map傳遞
  3. 使用GET傳遞@RequestBody,但此方式違反restful風格

介紹一個最佳實踐,通過feign的攔截器來實現。

@Component
@Slf4j
public class FeignCustomRequestInteceptor implements RequestInterceptor {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void apply(RequestTemplate template) {
        if (HttpMethod.GET.toString() == template.method() && template.body() != null) {
            //feign 不支援GET方法傳輸POJO 轉換成json,再換成query
            try {
                Map<String, Collection<String>> map = objectMapper.readValue(template.bodyTemplate(), new TypeReference<Map<String, Collection<String>>>() {

                });
                template.body(null);
                template.queries(map);
            } catch (IOException e) {
                log.error("cause exception", e);
            }
        }
    }