feign

使用Eureka作為服務(wù)注冊中心,在服務(wù)啟動后,各個微服務(wù)會將自己注冊到Eureka server。那么服務(wù)之間是如何調(diào)用?又是如何進(jìn)行負(fù)載均衡的呢?
目前,在Spring cloud 中服務(wù)之間通過restful方式調(diào)用有兩種方式

  • restTemplate+Ribbon
  • feign

從實踐上看,采用feign的方式更優(yōu)雅(feign內(nèi)部也使用了ribbon做負(fù)載均衡)。

一、什么是 feign?

Feign 的英文表意為“假裝,偽裝,變形”, 是一個http請求調(diào)用的輕量級框架,可以以Java接口注解的方式調(diào)用Http請求,而不用像Java中通過封裝HTTP請求報文的方式直接調(diào)用。Feign通過處理注解,將請求模板化,當(dāng)實際調(diào)用的時候,傳入?yún)?shù),根據(jù)參數(shù)再應(yīng)用到請求上,進(jìn)而轉(zhuǎn)化成真正的請求,這種請求相對而言比較直觀。

  • Feign 支持Ribbon的負(fù)載均衡
  • Feign 集成了Hystrix(服務(wù)熔斷)
  • Feign 采用的是基于接口配置
  • 支持可插拔的HTTP編碼器和解碼器
  • 支持HTTP請求和響應(yīng)的壓縮

二、Feign解決了什么問題?

封裝了Http調(diào)用流程,更適合面向接口化的變成習(xí)慣
在服務(wù)調(diào)用的場景中,我們經(jīng)常調(diào)用基于Http協(xié)議的服務(wù),而我們經(jīng)常使用到的框架可能有HttpURLConnection、Apache HttpComponnetsOkHttp3 、Netty等等,這些框架在基于自身的專注點提供了自身特性。而從角色劃分上來看,他們的職能是一致的提供Http調(diào)用服務(wù)。具體流程如下:

image.png

三、Feign是如何設(shè)計的?

image.png

四、RestTemplate + Ribbon與Feign(自帶Ribbon)的比較

角度 RestTemplate + Ribbon Feign(自帶Ribbon)
可讀性、可維護(hù)性 欠佳(無法從URL直觀了解這個遠(yuǎn)程調(diào)用是干什么的) 極佳(能在接口上寫注釋,方法名稱也是可讀的,能一眼看出這個遠(yuǎn)程調(diào)用是干什么的)
開發(fā)體驗 欠佳(拼湊URL不幸福) 極佳(漂亮的代碼)
風(fēng)格一致性 欠佳(本地API調(diào)用和RestTemplate調(diào)用的代碼風(fēng)格截然不同) 極佳(完全一致,不點開Feign的接口,根本不會察覺這是一個遠(yuǎn)程調(diào)用而非本地API調(diào)用)
性能 較好 中等(性能是RestTemplate的50%左右;如果為Feign配置連接池,性能可提升15%左右)
靈活性 極佳 中等(內(nèi)置功能能滿足大多數(shù)項目的需求)

Feign 整體框架非常小巧,在處理請求轉(zhuǎn)換和消息解析的過程中,基本上沒什么時間消耗。真正影響性能的,是處理Http請求的環(huán)節(jié)。由于默認(rèn)情況下,F(xiàn)eign采用的是JDK的HttpURLConnection,所以整體性能并不高。

幾種Http框架的抽象層次

五、使用Feign

1. 導(dǎo)入依賴

    <dependencies>
        <!--openfein的依賴-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>2.1.3.RELEASE</version>
        </dependency>
    </dependencies>

2. 啟用Feign

啟用類上添加注解@EnableFeignClients客戶端允許開啟使用Feign調(diào)用,掃描@FeignClient標(biāo)注的FeignClient接口

@SpringBootApplication
@EnableFeignClients(basePackages = { "com.xxx.xxx.xxx" })//開啟feign,封裝http的rest請求??梢杂胋asePackages = { "com.xxx.xxx.xxx" }指定包掃描@FeignClient標(biāo)注的FeignClient接口
//@EnableDiscoveryClient和@EnableEurekaClient共同點就是:都是能夠讓注冊中心能夠發(fā)現(xiàn),掃描到改服務(wù)。
//不同點:@EnableEurekaClient只適用于Eureka作為注冊中心,@EnableDiscoveryClient 可以是其他注冊中心。
@EnableEurekaClient
//@EnableDiscoveryClient
public class FeignApplication {
    public static void main(String[] args) {
        SpringApplication.run(FeignApplication.class,args);
    }
}

3. 編寫FeignClient接口

@FeignClient( value = "hello-service-provider" ) 聲明的方式指向了 服務(wù)提供者,而接口方法則實現(xiàn)了對服務(wù)提供者接口的實際調(diào)用

@FeignClient(value = "hello-service-provider")
public interface SchedualServiceHi {

    @RequestMapping(value = "/hi", method = RequestMethod.GET)
    String sayHiFromClientOne(@RequestParam("name") String name);
}

注意:這里服務(wù)名不區(qū)分大小寫,所以使用hello-service-provider和HELLO-SERVICE-PROVIDER都是可以的。另外,在Brixton.SR5版本中,原有的serviceId屬性已經(jīng)被廢棄,若要寫屬性名,可以使用name或value。

FeignClient注解的一些屬性

屬性名 默認(rèn)值 作用 備注
value 空字符串 調(diào)用服務(wù)名稱,和name屬性相同
serviceId 空字符串 服務(wù)id,作用和name屬性相同 已過期
name 空字符串 調(diào)用服務(wù)名稱,和value屬性相同
url 空字符串 全路徑地址或hostname,http或https可選
decode404 false 配置響應(yīng)狀態(tài)碼為404時是否應(yīng)該拋出FeignExceptions
configuration {} 自定義當(dāng)前feign client的一些配置 參考FeignClientsConfiguration
fallback void.class 熔斷機(jī)制,調(diào)用失敗時,走的一些回退方法,可以用來拋出異?;蚪o出默認(rèn)返回數(shù)據(jù)。 底層依賴hystrix,啟動類要加上@EnableHystrix
path 空字符串 自動給所有方法的requestMapping前加上前綴,類似與controller類上的requestMapping

4. 實現(xiàn)對Feign客戶端的調(diào)用

接著,創(chuàng)建一個RestClientController來實現(xiàn)對Feign客戶端的調(diào)用。使用@Autowired直接注入上面定義的HelloServiceFeign實例,并在postPerson函數(shù)中調(diào)用這個綁定了hello-service-provider服務(wù)接口的客戶端來向該服務(wù)發(fā)起/demo/getHost和/demo/postPerson接口的調(diào)用。

@RestController
public class HiController {

    @Autowired
    SchedualServiceHi schedualServiceHi;

    @GetMapping(value = "/test/hi")
    public String sayHi(@RequestParam String name){
        return schedualServiceHi.sayHiFromClientOne(name);

    }
}

5. 需要在application.yml中指定服務(wù)注冊中心,并定義自身的服務(wù)名為service-feign,端口使用8765。

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
server:
  port: 8765
spring:
  application:
    name: service-feign

六、其它

1. Feign自定義處理返回的異常

實現(xiàn)Feign的 ErrorDecoder 的接口就可以實現(xiàn) http請求層面的錯誤處理。
StashErrorDecoder類實現(xiàn)了ErrorDecoder接口。在Feign客戶端發(fā)生http請求層面的錯誤時會調(diào)用decode方法。在decode方法中實現(xiàn)自定義的錯誤處理。

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()
            );
        }
        //這里是其他狀態(tài)碼處理方法
        return errorStatus(methodKey, response);
    }
}
  • 接下來就需要注冊這個錯誤攔截器了,如果是直接手動構(gòu)建FeignClient的使用方法。那需要在構(gòu)建客戶端時指定errorDecoder
return Feign.builder()
                .errorDecoder(new StashErrorDecoder())
                .target(StashApi.class, url);
  • 如果是使用了spring-cloud-open-feign的使用方式,包含了啟動的自動配置,所以只需要將錯誤處理類注冊為配置類,即添加@Configuration注解即可。
@Configuration
public class StashErrorDecoder implements ErrorDecoder {
    @Override
    public Exception decode(String methodKey, Response response) {
        // 實現(xiàn)代碼
    }
}

2. Feign使用OKhttp發(fā)送request

Feign底層默認(rèn)是使用jdk中的HttpURLConnection發(fā)送HTTP請求,feign也提供了OKhttp來發(fā)送請求,具體配置如下:

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

3. Feign開啟GZIP壓縮

Spring Cloud Feign支持對請求和響應(yīng)進(jìn)行GZIP壓縮,以提高通信效率。

feign:
  compression:
    request: #請求
      enabled: true #開啟
      mime-types: text/xml,application/xml,application/json #開啟支持壓縮的MIME TYPE
      min-request-size: 2048 #配置壓縮數(shù)據(jù)大小的下限
    response: #響應(yīng)
      enabled: true #開啟響應(yīng)GZIP壓縮

注意:
由于開啟GZIP壓縮之后,F(xiàn)eign之間的調(diào)用數(shù)據(jù)通過二進(jìn)制協(xié)議進(jìn)行傳輸,返回值需要修改為ResponseEntity<byte[]>才可以正常顯示,否則會導(dǎo)致服務(wù)之間的調(diào)用亂碼。

示例如下:

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

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

方式一:通過java bean 的方式指定。
@EnableFeignClients注解上有個defaultConfiguration屬性,可以指定默認(rèn)Feign Client的一些配置。

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

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

DefaultFeignConfiguration內(nèi)容:

@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 #日志等級

5. 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

6. Feign 的GET的多參數(shù)傳遞

目前,feign不支持GET請求直接傳遞POJO對象的,目前解決方法如下:

  • 把POJO拆散城一個一個單獨的屬性放在方法參數(shù)中
  • 把方法參數(shù)編程Map傳遞
  • 使用GET傳遞@RequestBody,但此方式違反restful風(fēng)格

介紹一個最佳實踐,通過feign的攔截器來實現(xiàn)。

@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 轉(zhuǎn)換成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);
            }
        }
    }

七、總結(jié)

總到來說,F(xiàn)eign的源碼實現(xiàn)的過程如下:

  • @EnableFeignCleints注解開啟FeignCleint
  • 啟動時,程序會進(jìn)行包掃描,掃描所有包下所有@FeignClient注解的類,并將這些類注入到spring的IOC容器中。當(dāng)定義的Feign中的接口被調(diào)用時,通過JDK的動態(tài)代理來生成RequestTemplate。
  • RequestTemplate中包含請求的所有信息,如請求參數(shù),請求URL等。
  • RequestTemplate聲明Request,然后將Request交給client處理,這個client默認(rèn)是JDK的HTTPUrlConnection,也可以是OKhttp、Apache的HTTPClient等。
  • 最后client封裝成LoadBaLanceClient,結(jié)合ribbon負(fù)載均衡地發(fā)起調(diào)用。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容