1. 程式人生 > >Spring Cloud Feign 宣告式服務呼叫

Spring Cloud Feign 宣告式服務呼叫

目錄

    • 一、Feign是什麼?
    • 二、Feign的快速搭建
    • 三、Feign的幾種姿態
      • 引數繫結
      • 繼承特性
    • 四、其他配置
      • Ribbon 配置
      • Hystrix 配置

一、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/ 主頁,發現主頁上註冊了四個服務

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