一、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 微服務實戰》