第六章:聲明式服務調用:Spring Cloud Feign

  Spring Cloud Feign 是基於 Netflix Feign 實現的,整合了 Spring Cloud Ribbon 和 Spring Cloud Hystrix,除了提供這二者的強大功能以外,還提供了一種聲明式的 Web 服務客戶端定義方式。java

快速入門

  • 首先,建立一個 Spring Boot 基礎工程,取名爲 feign-consumer,並在 pom.xml 中引入 spring-cloud-starter-eureka 和 spring-cloud-starter-feign 依賴。具體內容以下:
<?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.example</groupId>
    <artifactId>feign-consumer</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>feign-consumer</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring-cloud.version>Dalston.SR2</spring-cloud.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>${spring-cloud.version}</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>
  • 在主類上經過 @EnableFeignClients 註解開啓 Spring Cloud Feign 的支持功能。
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.feign.EnableFeignClients;

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class FeignConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(FeignConsumerApplication.class, args);
    }
}
  • 定義 HelloService 接口,經過 @FeignClient 註解指定服務名來綁定服務,而後再使用 Spring MVC 的註解來綁定具體該服務提供的 REST 接口。
@FeignClient(value = "hello-service")
public interface HelloService {

    @RequestMapping(value = "/index")
    String hello();
}

  注意:此處服務名不區分大小寫,hello-service 和 HELLO-SERVICE 均可以使用。web

  • 接着,建立一個 ConsumerController 來實現對 Feign 客戶端的調用。使用 @Autowired 直接注入上面定義的 HelloService 實例,並在 helloConsumer 函數中調用這個綁定了 hello-service 服務接口的客戶端來向該服務發起 /index 接口的調用。
package com.example.demo.controller;

import com.example.demo.service.HelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author lxx
 * @version V1.0.0
 * @date 2017-8-16
 */

@RestController
public class ConsumerController {

    @Autowired
    HelloService helloService;

    @RequestMapping(value = "feign-consumer", method = RequestMethod.GET)
    public String helloConsumer(){
        return helloService.hello();
    }
}
  • 最後,同Ribbon實現的服務消費者同樣,須要在 application.properties 中指定註冊中心,並定義自身的服務名爲 feign-consumer,爲了方便本地調試與以前的 Ribbon 消費者的區分,端口使用 9991.
spring.application.name=feign-consumer

server.port=9991

eureka.client.service-url.defaultZone=http://localhost:8082/eureka/

測試驗證

  和Ribbon實現時同樣,啓動 eureka-server 和 兩個hello-service,而後啓動 feign-consumer,發送請求到 http://localhost:9991/feign-consumer,正確返回。與 Ribbon 不一樣的是,經過 Feign 咱們只需定義服務綁定接口,以聲明式的方法,優雅而簡單地實現了服務調用。spring

參數綁定

  上面介紹了一個不帶參數的 REST 服務綁定。然而實際系統中的各類業務接口要複雜的多,咱們會在 HTTP 的各個位置傳入不一樣類型的參數,而且在返回請求響應的時候也多是一個複雜的對象結構。apache

  在開始介紹 Spring Cloud Feign 的參數綁定以前,先擴展一下服務提供方 hello-service 。增長下面這些接口定義,其中包含帶有 Request 參數的請求、帶有 Header 信息的請求、帶有 RequestBody 的請求以及請求響應體中是一個對象的請求。api

package com.example.demo.web;

import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.*;

import java.util.Random;

/**
 * @author lxx
 * @version V1.0.0
 * @date 2017-8-9
 */

@RestController
public class HelloController {

    private final Logger logger = Logger.getLogger(getClass());

    @Autowired
    private DiscoveryClient client;

    @RequestMapping(value = "/index")
    public String index(){
        ServiceInstance instance = client.getLocalServiceInstance();
        // 讓處理線程等待幾秒鐘
        int sleepTime = new Random().nextInt(3000);
        logger.info("sleepTime:"+sleepTime);

        try {
            Thread.sleep(sleepTime);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        logger.info("/hello:host:"+instance.getHost()+" port:"+instance.getPort()
                +" service_id:"+instance.getServiceId());
        return "hello world!";
    }


    @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 String name, @RequestHeader Integer age){
        return new User(name, age);
    }

    @RequestMapping(value = "/hello3", method = RequestMethod.POST)
    public String hello3(@RequestBody User user){
        return "HELLO," + user.getName()+","+user.getAge();
    }
}

  User 對象的定義以下,須要注意,這裏必需要有User 的默認構造函數。否則,Spring Cloud Feign 根據 JSON 字符串轉換 User 對象會拋出異常。app

package com.example.demo.web;

/**
 * @author lxx
 * @version V1.0.0
 * @date 2017-8-16
 */

public class User {
    private String name;

    private Integer age;

    public User() {
    }

    public User(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

  完成對 hello-service 的改造以後,在 feign-consumer 應用中實現這些新增的請求的綁定。dom

  • 首先,在 feign-consumer 中建立與上面同樣的 User 類。
  • 而後,在 HelloService 接口中增長對上述三個新增接口的綁定聲明,修改後,完成的HelloService 接口以下所示:
package com.example.demo.service;

import com.example.demo.entity.User;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.*;

/**
 * @author lxx
 * @version V1.0.0
 * @date 2017-8-16
 */

@FeignClient(value = "hello-service")
public interface HelloService {

    @RequestMapping(value = "/index")
    String hello();

    @RequestMapping(value = "/hello1", method = RequestMethod.GET)
    String hello1(@RequestParam(value = "name") String name);

    @RequestMapping(value = "/hello2", method = RequestMethod.GET)
    User hello2(@RequestParam(value = "name") String name, @RequestHeader(value = "age") Integer age);

    @RequestMapping(value = "/hello3", method = RequestMethod.POST)
    String hello3(@RequestBody User user);
}

  注意:在參數綁定時,@RequestParam、@RequestHeader等能夠指定參數名稱的註解,他們的 value 不能少。在 Spring MVC 中,這些註解會根據參數名來做爲默認值,可是在 Feign 中綁定參數必須經過 value 屬性來指明具體的參數名,否則會拋出異常 IllegalStateException ,value 屬性不能爲空。maven

  • 最後,在 ConsumerController 中新增一個 /feign-consumer2 接口,來對本節新增的聲明接口進行調用,修改後的完整代碼以下所示:
package com.example.demo.controller;

import com.example.demo.entity.User;
import com.example.demo.service.HelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author lxx
 * @version V1.0.0
 * @date 2017-8-16
 */

@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 helloConsumer2(){
        StringBuilder sb = new StringBuilder();
        sb.append(helloService.hello1("didi")).append("\n");
        sb.append(helloService.hello2("didi", 18)).append("\n");
        sb.append(helloService.hello3(new User("didi", 20))).append("\n");
        return sb.toString();
    }
}

  測試驗證

  改造以後,啓動服務註冊中心、兩個hello-service服務以及改造以後的 feign-consumer。經過發送請求到 http://localhost:9991/feign-consumer2 ,觸發 HelloService 對新增接口的調用,獲取以下輸出:ide

繼承特性

  經過上述的實踐能夠發現,當使用 Spring MVC 的註解綁定服務接口時,幾乎能夠從服務提供方的 Controller 中依靠複製操做,構建出相應的服務客戶端綁定接口。既然存在那麼多複製操做,天然須要考慮這部份內容是否能夠獲得進一步的抽象。在 Spring Cloud Feign 中,針對該問題提供了繼承特性來幫助咱們解決這些複製操做,以進一步減小編碼量。下面,詳細看看如何經過 Spring Cloud Feign 的繼承特性來實現 REST 接口定義的複用。函數

  • 爲了可以複用 DTO 與接口定義,咱們先建立一個基礎的 Maven 工程,命名爲 hello-service-api。
  • 因爲在 hello-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.example</groupId>
    <artifactId>hello-service-api</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>hello-service-api</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </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>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>
  • 將上一節中實現的 User 對象複製到 hello-service-api 工程中。
  • 在 hello-service-api 工程中建立 HelloService 接口,內容以下,該接口中的 User 對象爲本項目中添加的 User。
package com.example.demo.service;

import com.example.demo.entity.User;
import org.springframework.web.bind.annotation.*;

/**
 * @author lxx
 * @version V1.0.0
 * @date 2017-8-16
 */

@RequestMapping(value = "/refactor")
public interface HelloService {

    @RequestMapping(value = "/hello4", method = RequestMethod.GET)
    String hello4(@RequestParam(value = "name") String name);

    @RequestMapping(value = "/hello5", method = RequestMethod.GET)
    User hello5(@RequestHeader(value = "name") String name, @RequestHeader(value = "age") Integer age);

    @RequestMapping(value = "/hello6", method = RequestMethod.POST)
    String hello6(@RequestBody User user);
}

  由於後續還會經過以前的hello-service 和 feign-consumer 來重構,爲了不接口混淆,在這裏定義 HelloService 時,除了頭部定義了 /refactor 前綴以外,同時將提供服務的三個接口改名爲 hello四、hello五、hello6.

  以上操做完成後,執行命令 mvn install 將該模塊構建到本地倉庫。

  • 下面對 hello-service 進行重構,在 pom.xml 的 dependency 節點中,新增對 hello-service-api 的依賴。
<dependency>
    <groupId>com.example</groupId>
    <artifactId>hello-service-api</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>
  • 建立 RefactorHelloController 類實現 hello-service-api 中定義的 HelloService 接口,並參考以前的 HelloController 來實現這三個接口,具體內容以下:
package com.example.demo.web;

import entity.*;
import entity.User;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import service.HelloService;

/**
 * @author lxx
 * @version V1.0.0
 * @date 2017-8-16
 */

@RestController
public class RefactorHelloController implements HelloService {

    @Override
    public String hello4(@RequestParam(value = "name") String name) {
        return "HELLO " + name;
    }

    @Override
    public User hello5(@RequestHeader(value = "name") String name, @RequestHeader(value = "age") Integer age) {
        return new User(name, age);
    }

    @Override
    public String hello6(@RequestBody User user) {
        return "HELLO," + user.getName()+","+user.getAge();
    }
}
  • 完成了服務提供者的重構,接下來在服務消費者 feign-consumer 的 pom.xml 文件中,如在服務提供者同樣,新增對 hello-service-api 的依賴。
  • 建立 RefactorHelloService 接口,並繼承 hello-service-api 包中的 HelloService 接口,而後添加 @FeignClient 註解來綁定服務。(不能正常引用,先跳過)
相關文章
相關標籤/搜索