Spring Cloud Feign 宣告式服務呼叫

c旋兒發表於2019-05-28

一、Feign是什麼?

​ 通過對前面Spring Cloud RibbonSpring 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/ 主頁,發現主頁上註冊了四個服務

Spring Cloud Feign 宣告式服務呼叫

訪問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/ 主頁能夠發現如下幾個服務

Spring Cloud Feign 宣告式服務呼叫

訪問 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 微服務實戰》

相關文章