Feign的目標(biāo)
feign是聲明式的web service客戶端,它讓微服務(wù)之間的調(diào)用變得更簡單了,類似controller調(diào)用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 獲得各個服務(wù)的地址,所以要引入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
#服務(wù)名稱
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之后,我們調(diào)用eureka 注冊的其他服務(wù),在代碼中就像各個service之間相互調(diào)用那么簡單。
FeignClient注解的一些屬性
屬性名 | 默認值 | 作用 | 備注 |
---|---|---|---|
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 | 熔斷機制,調(diào)用失敗時,走的一些回退方法,可以用來拋出異常或給出默認返回數(shù)據(jù)。 | 底層依賴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()
);
}
//這里是其他狀態(tài)碼處理方法
return errorStatus(methodKey, response);
}
}
自定義好異常處理類后,要在@Configuration修飾的配置類中聲明此類。
Feign使用OKhttp發(fā)送request
Feign底層默認是使用jdk中的HttpURLConnection發(fā)送HTTP請求,feign也提供了OKhttp來發(fā)送請求,具體配置如下:
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: basic
okhttp:
enabled: true
hystrix:
enabled: true
Feign原理簡述
- 啟動時,程序會進行包掃描,掃描所有包下所有@FeignClient注解的類,并將這些類注入到spring的IOC容器中。當(dāng)定義的Feign中的接口被調(diào)用時,通過JDK的動態(tài)代理來生成RequestTemplate。
- RequestTemplate中包含請求的所有信息,如請求參數(shù),請求URL等。
- RequestTemplate聲場Request,然后將Request交給client處理,這個client默認是JDK的HTTPUrlConnection,也可以是OKhttp、Apache的HTTPClient等。
- 最后client封裝成LoadBaLanceClient,結(jié)合ribbon負載均衡地發(fā)起調(diào)用。
詳細原理請參考源碼解析。
Feign、hystrix與retry的關(guān)系請參考https://xli1224.github.io/2017/09/22/configure-feign/
Feign開啟GZIP壓縮
Spring Cloud Feign支持對請求和響應(yīng)進行GZIP壓縮,以提高通信效率。
application.yml配置信息如下:
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ù)通過二進制協(xié)議進行傳輸,返回值需要修改為ResponseEntity<byte[]>才可以正常顯示,否則會導(dǎo)致服務(wù)之間的調(diào)用亂碼。
示例如下:
@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內(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 #日志等級
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的多參數(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);
}
}
}