Spring Cloud Feign 聲明式服務調用


一、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-eurekaspring-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")開啟的,FeignRibbon進行了封裝,所以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 微服務實戰》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM