使用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 HttpComponnets、OkHttp3 、Netty等等,這些框架在基于自身的專注點提供了自身特性。而從角色劃分上來看,他們的職能是一致的提供Http調(diào)用服務(wù)。具體流程如下:

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

四、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,所以整體性能并不高。

五、使用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)用。