一、Feign是什么?
通過對前面Spring Cloud Ribbon
和 Spring Cloud Hystrix
,我們已經掌握了開發微服務應用時的兩個重磅武器,學會了如何在微服務框架中進行服務間的調用
和如何使用斷路器
來保護我們的服務,這兩者被作為基礎工具類框架廣泛的應用在各個微服務框架中。既然這兩個組件這么重要,那么有沒有更高層次的封裝來整合這兩個工具以簡化開發呢?Spring Cloud Feign
就是這樣的一個工具,它整合了Spring Cloud Ribbon 和 Spring Cloud Hystrix 來達到簡化開發的目的。
我們在使用Spring Cloud Ribbon
時,通常都會使用RestTemplate
的請求攔截來實現對依賴服務的接口調用,而RestTemplate
已經實現了對Http請求
的封裝,形成了一套模板化的調用方法。在之前Ribbon
的例子中,我們都是一個接口對應一個服務調用的url,那么在實際項目開發過程中,一個url可能會被復用,也就是說,一個接口可能會被多次調用,所以有必要把復用的接口封裝起來公共調用。Spring Cloud Feign
在此基礎上做了進一步封裝,由它來幫助我們定義和實現依賴服務的接口定義。
二、Feign的快速搭建
我們通過一個示例來看一下Feign的調用過程,下面的示例將繼續使用之前的server-provider
服務,這里我們通過Spring Cloud Feign
提供的聲明式服務綁定功能來實現對該服務接口的調用
- 首先,搭建一個SpringBoot項目,取名為
feign-consumer
,並在pom.xml
文件中引入spring-cloud-starter-eureka
和spring-cloud-starter-feignn
依賴,具體內容如下:
<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 http://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>1.3.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.feign.consumer</groupId>
<artifactId>feign-consumer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>feign-consumer</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.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-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</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>Brixton.SR5</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>
- 搭建完成pom.xml之后,我們在
feign-consumer
的啟動類上添加如下注解
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication
public class FeignConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(FeignConsumerApplication.class, args);
}
}
@EnableDiscoveryClient : 這個注解和@EnableEurekaClient 用法相同,表明這是一個Eureka客戶端
@EnableFeignClients : 這個注解表明這個服務是一個Feign服務,能夠使用@FeignClient 實現遠程調用
- 新建一個
HelloService
接口,在接口上加上__@FeignClient__注解,表明這個接口是可以進行遠程訪問的,也表明這個接口可以實現復用的接口,它提供了一些遠程調用的方法,也相當於制定了一些規則。
// 此處填寫的是服務的名稱
@FeignClient(value = "server-provider")
public interface HelloService {
@RequestMapping(value = "hello")
String hello();
}
@FeignClient 后面的value值指向的是提供服務的服務名,這樣就能夠對spring.application.name = server.provider 的服務發起服務調用
- 新建一個Controller,提供外界訪問的入口,調用HelloService,完成一系列的服務請求-服務分發-服務調用
@RestController
public class ConsumerController {
@Autowired
HelloService helloService;
@RequestMapping(value = "/feign-consumer", method = RequestMethod.GET)
public String helloConsumer(){
return helloService.hello();
}
}
- 最后,為
feign-consumer
指定服務的端口號,服務的名稱,並向注冊中心注冊自己
spring.application.name=feign-consumer
server.port=9001
eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/
測試驗證:
像之前一樣,啟動四個服務: eureka-server
, server-provider(8081,8082)
, feign-consumer
,啟動http://localhost:9000/eureka/ 主頁,發現主頁上注冊了四個服務
訪問http://localhost:9001/feign-consumer 端口,發現 "Hello World" 能夠返回
三、Feign的幾種姿態
參數綁定
在上一節的事例中,我們使用Spring Cloud Feign搭建了一個簡單的服務調用的示例,但是實際的業務場景中要比它復雜很多,我們會在HTTP的各個位置傳入不同類型的參數,並且返回的也是一個復雜的對象結構,下面就來看一下不同的參數綁定方法
- 首先擴展一下
server-provider
中HelloController的內容
@RequestMapping(value = "/hello1", method = RequestMethod.GET)
public String hello1(@RequestParam String name){
return "Hello " + name;
}
@RequestMapping(value = "/hello2", method = RequestMethod.GET)
public User hello2(@RequestHeader Integer id,@RequestHeader String name){
return new User(id,name);
}
@RequestMapping(value = "/hello3",method = RequestMethod.POST)
public String hello3(@RequestBody User user){
return "Hello " + user.getId() + ", " + user.getName();
}
- User 對象的定義入下,省略了get和set方法,需要注意的是,這里必須要有User的默認構造函數,否則反序列化的時候,會報Json解析異常
public class User {
private Integer id;
private String name;
public User(){}
public User(Integer id, String name) {
this.id = id;
this.name = name;
}
get and set...
}
- 在
feign-consumer
中的HelloService中聲明對服務提供者的調用
@FeignClient(value = "server-provider")
public interface HelloService {
@RequestMapping(value = "hello")
String hello();
@RequestMapping(value = "/hello1", method = RequestMethod.GET)
String hello1(@RequestParam("name") String name);
@RequestMapping(value = "/hello2", method = RequestMethod.GET)
User hello2(@RequestHeader("id") Integer id,@RequestHeader("name") String name);
@RequestMapping(value = "/hello3", method = RequestMethod.POST)
String hello3(@RequestBody User user);
}
hello1 方法傳遞了一個參數為name的請求參數,它對應遠程調用server-provider服務中的hello1方法
hello2 方法傳遞了一個請求頭尾id 和 name的參數,對應遠程調用server-provider服務中的hello2方法
hello3 方法傳遞了一個請求體為user的參數,對應遠程調用呢server-provider服務中的hello3方法
- 下面在ConsumerController類中定義一個helloConsumer1的方法,分別對hello1,hello2,hello3方法進行服務調用
@RestController
public class ConsumerController {
@Autowired
HelloService helloService;
@RequestMapping(value = "/feign-consumer", method = RequestMethod.GET)
public String helloConsumer(){
return helloService.hello();
}
@RequestMapping(value = "/feign-consumer2", method = RequestMethod.GET)
public String helloConsumer1(String name){
StringBuilder builder = new StringBuilder();
builder.append(helloService.hello()).append("\n");
builder.append(helloService.hello1("lx")).append("\n");
builder.append(helloService.hello2(23,"lx")).append("\n");
builder.append(helloService.hello3(new User(24,"lx"))).append("\n");
return builder.toString();
}
}
上面的helloConsumer1方法,分別調用了HelloServcie接口中的hello、hello1、hello2、hello3方法,傳遞對應的參數,然后對每一個方法進行換行
測試驗證
在完成上述的改造之后,啟動服務注冊中心
、兩個 server-provider
服務以及我們改造過的feign-consumer
。通 過發送GET請求到 htttp://localhost:9001/feign-consumer2, 觸發 HelloService對新增接口的調用。最終,我們會獲得如下輸出,代表接口綁定和調用成功。
繼承特性
通過上述的示例,我們能夠發現能夠從服務提供方的Controller中依靠復制操作,構建出相應的服務客戶端綁定接口。既然存在很多復制操作,我們自然考慮能否把公用的接口抽象出來?事實上也是可以的,Spring Cloud Feign提供了通過繼承來實現Rest接口的復用,下面就來演示一下具體的操作過程
- 首先為了演示Spring Cloud Feign的
繼承
特性,我們新建一個maven 項目,名為feign-service-api,我們需要用到Spring MVC的注解,所以在pom.xml 中引入spring-boot-starter-web依賴,具體內容如下:
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.feign</groupId>
<artifactId>feign-service-api</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.7.RELEASE</version>
<relativePath />
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
- 將User 對象復制到feign-service-api 中,如下
public class User {
private Integer id;
private String name;
// 必須加上
public User(){}
public User(Integer id, String name) {
this.id = id;
this.name = name;
}
get and set...
}
- 創建
HelloService
接口,並在接口中定義如下三個方法:
@RequestMapping(value = "/refactor")
public interface HelloService {
@RequestMapping(value = "/hello4", method = RequestMethod.GET)
String hello(@RequestParam("name")String name);
@RequestMapping(value = "/hello5", method = RequestMethod.GET)
User hello(@RequestHeader("id")Integer id,@RequestHeader("name")String name);
@RequestMapping(value = "/hello6", method = RequestMethod.POST)
String hello(@RequestBody User user);
}
- 定義完成后,使用idea 右側的maven 工具,依次執行mvn clean ,mvn install,把feign-service-api打成
jar
包之后,現在切換項目至 server-provider ,並讓server-provider
依賴這個maven項目]
server-provider
server-provider
的pom.xml 添加feign-service-api
打包后的依賴
<dependency>
<groupId>com.feign</groupId>
<artifactId>feign-service-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
- 創建
RefactorHelloController
實現feign-service-api
中的HelloService 方法
@RestController
public class RefactorHelloController implements HelloService {
// 注解沒有帶過來,這是自己加的
@Override
public String hello(@RequestParam("name") String name) {
return "Hello " + name;
}
@Override
public User hello(@RequestHeader("id") Integer id, @RequestHeader("name") String name) {
return new User(id,name);
}
@Override
public String hello(@RequestBody User user) {
return "Hello " + user.getId() + ", " + user.getName();
}
}
這里有一個問題,當繼承了HelloService 之后,@RestController,@RequestParam,@RequestHeader,@RequestBody 注解都沒有帶過來, 但是書上說是只有 @RestController 注解是帶不過來的,余下三個都是可以的。這里未查明是何原因 …...
feign-consumer
- 在完成了對
server-provoder
的構建之后,下面來構建feign-consumer
服務,像server-provider 一樣,在pom.xml 中添加對feign-service-api
的依賴
<dependency>
<groupId>com.feign</groupId>
<artifactId>feign-service-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
- 創建
RefactorHelloService
接口,繼承feign-service-api
中的 HelloService接口
@FeignClient(value = "server-provider")
public interface RefactorHelloService extends HelloService {}
- 在
ConsumerController
類注入 RefactorHelloService,並測試 feign-service-api 中的方法,遠程調用server-provider 中的/hello4 /hello5 /hello6
方法。
@RequestMapping(value = "/feign-consumer3", method = RequestMethod.GET)
public String helloConsumer3(String name){
StringBuilder builder = new StringBuilder();
builder.append(refactorHelloService.hello("lx")).append("\n");
builder.append(refactorHelloService.hello(new com.feignservice.api.User(24,"lx"))).append("\n");
builder.append(refactorHelloService.hello(23,"lx")).append("\n");
return builder.toString();
}
測試驗證
依次啟動服務服務注冊中心
,server-provider
的兩個實例,feign-consumer
服務,在http://localhost:1111/ 主頁能夠發現如下幾個服務
訪問 http://localhost:9001/feign-consumer3( 使用Postman 訪問),發現能夠顯示出來如下內容
Hello lx
Hello 24, lx
com.feignservice.api.User@5865261
優點和缺點
使用Spring Cloud Feign
的優點很多,可以將接口的定義從Controller 中剝離,同時配合Maven 構建就能輕易的實現接口的復用,實現在構建期的接口綁定,從而有效的減少服務客戶端的綁定配置。但是這種配置使用不當也會帶來副作用就是:你不能忽略頻繁變更接口帶來的影響。所以,如果團隊打算采用這種方式來構建項目的話,最好在開發期間就嚴格遵守面向對象的開閉原則。避免牽一發而動全身,造成不必要的維護量。
四、其他配置
Ribbon 配置
由於Spring Cloud Feign
的客戶端負載均衡是通過Spring Cloud Ribbon
實現的,所以我們可以通過配置Spring Cloud Feign 從而配置 Spring Cloud Ribbon 。
全局配置
全局配置的方法很簡單,我們可以使用如下配置來設置全局參數
ribbon.ConnectTimeout=5000
ribbon.ReadTimeout=5000
指定服務配置
大多數情況下,我們對於服務的調用時間可能會根據實際服務特性來做一些調整,所以僅僅依靠全局的配置是不行的,因為Feign 這個組件是整合了 Ribbon和 Hystrix的,所以通過設置Feign的屬性來達到屬性傳遞的目的。在定義Feign 客戶端的時候,我們使用了@FeignClient()
注解,其實在創建@FeignClient(value = server-provider
)的時候,同時也創建了一個名為server-provider
的ribbon 客戶端,所以我們就可以使用@FeignClient中的nane 和value 值來設置對應的Ribbon 參數。
# 使用feign-clients 中的注解的value值設置如下參數
# HttpClient 的連接超時時間
server-provider.ribbon.ConnectTimeout=500
# HttpClient 的讀取超時時間
server-provider.ribbon.ReadTimeout=2000
# 是否可以為此客戶端重試所有操作
server-provider.ribbon.OkToRetryOnAllOperations=true
# 要重試的下一個服務器的最大數量(不包括第一個服務器)
server-provider.ribbon.MaxAutoRetriesNextServer=2
# 同一個服務器上的最大嘗試次數(不包括第一個)
server-provider.ribbon.MaxAutoRetries=1
重試機制
Spring Cloud Feign 中實現了默認的請求重試機制,我們可以通過修改server-provider
中的示例做一些驗證:
- 在
server-provider
應用中的/hello
接口實現中,增加一些隨機延遲,比如
@RequestMapping(value = "hello", method = RequestMethod.GET)
public String hello() throws Exception{
ServiceInstance instance = discoveryClient.getLocalServiceInstance();
log.info("instance.host = " + instance.getHost() + "instance.service = " + instance.getServiceId()
+ "instance.port = " + instance.getPort());
log.info("Thread sleep ... ");
int sleepTime = new Random().nextInt(3000);
log.info("sleepTime = " + sleepTime);
Thread.sleep(sleepTime);
System.out.println("Thread awake");
return "Hello World";
}
- 在
feign-consumer
應用中增加上文提到的重試配置參數,來解釋一下上面的配置
MaxAutoRetriesNextServer 設置為2 表示的是下一個服務器的最大數量,也就是說如果調用失敗,會更換兩次實例進行重試,MaxAutoRetries設置為1 表示的是每一個實例會進行一次調用,失敗了再換為其他實例。OKToRetryOnAllOperations的意義是無論是請求超時或者socket read timeout都進行重試,
這里需要注意一點,Ribbon超時和Hystrix超時是兩個概念,為了讓上述實現有效,我們需要 讓Hystrix的超時時間大於Ribbon的超時時間, 否則Hystrix命令超時后, 該命令直接熔斷,重試機制就沒有任何意義了。
Hystrix 配置
在Spring Cloud Feign
中,除了引入Spring Cloud Ribbon
外,還引入了服務保護工具Spring Cloud Hystrix
,下面就來介紹一下如何使用Spring Cloud Feign配置Hystrix屬性實現服務降級。
全局配置
對於Hystrix全局配置同Spring Cloud Ribbon 的全局配置一樣,直接使用默認前綴 hystrix.command.default 就可以進行配置,比如設置全局的超時
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=5000
另外,在對Hystrix進行配置之前,我們需要確認feign.hystrix.enable
參數沒有設置為false,否則該參數設置會關閉Feign客戶端的Hystrix支持。
// 關閉Hystrix 功能(全局關閉)
feign.hystrix.enabled=false
// 關閉熔斷功能
hystrix.command.default.execution.timeout.enabled=false
禁用hystrix
如果不想全局地關閉Hystrix支持,而只想針對某個服務客戶端關閉Hystrix支持,需要通過使用@Scope("prototype")
注解為指定的客戶端配置Feign.Builder 實例
- 構建一個關閉Hystrix的配置類
@Configuration
public class DisableHystrixConfiguration {
@Bean
@Scope("prototype")
public Feign.Builder builder(){
return new Feign.Builder();
}
}
- 在
HelloService
的@FeignClient注解中,通過Configuration參數引入上面實現的配置
@FeignClient(value = "server-provider", fallback = DisableHystrixConfiguration.class)
public interface RefactorHelloService extends HelloService {}
服務降級配置
Hystrix 提供的服務降級是服務容錯的重要功能,之前我們開啟Ribbon
的服務降級是通過使用 @HystrixCommand(fallbackMethod = "hystrixCallBack")
開啟的,Feign
對Ribbon
進行了封裝,所以Feign 也提供了一種服務降級策略。下面我們就來看一下Feign 如何使用服務降級策略。我們在feign-consumer
中進行改造
- 服務降級邏輯的實現只需要為Feign客戶端的定義接口編寫一個具體的接口實現類,比如為
server-provider
接口實現一個服務降級類HelloServiceFallback
,其中每個重寫方法的邏輯都可以用來定義相應的服務降級邏輯,具體代碼如下
@Component
public class FeignServiceCallback implements FeignService{
@Override
public String hello() {
return "error";
}
@Override
public String hello(@RequestParam("name") String name) {
return "error";
}
@Override
public User hello(@RequestHeader("id") Integer id, @RequestHeader("name") String name) {
return new User(0,"未知");
}
@Override
public String hello(@RequestBody User user) {
return "error";
}
}
- 在服務綁定接口中,通過
@FeignClient
注解的fallback 屬性來指定對應的服務降級類
@FeignClient(value = "server-provider",fallback = FeignServiceCallback.class)
public interface FeignService {
@RequestMapping(value = "/hello")
String hello();
@RequestMapping(value = "/hello1", method = RequestMethod.GET)
String hello(@RequestParam("name") String name);
@RequestMapping(value = "/hello2", method = RequestMethod.GET)
User hello(@RequestHeader("id") Integer id,@RequestHeader("name") String name);
@RequestMapping(value = "/hello3", method = RequestMethod.POST)
String hello(@RequestBody User user);
}
測試驗證
下面我們來驗證一下服務降級邏輯,啟動注冊中心Eureka-server
,服務消費者feign-consumer
,不啟動server-provider
,發送GET 請求到http://localhost:9001/feign-consumer2,該接口會分別調用FeignService中的四個接口,因為feign-consumer
沒有啟動,會直接觸發服務降級,使用Postman調用接口的返回值如下
error
error
error
com.feign.consumer.pojo.User@5ac0702f
后記: Spring Cloud Feign 聲明式服務調用就先介紹到這里,下一篇介紹Spring Cloud Zuul服務網關
文章參考:
https://www.cnblogs.com/zhangjianbin/p/7228628.html
《Spring Cloud 微服務實戰》