Spring Cloud實戰系列(三) - 宣告式客戶端呼叫Feign

零壹技術棧發表於2019-01-28

相關

  1. Spring Cloud實戰系列(一) - 服務註冊與發現Eureka

  2. Spring Cloud實戰系列(二) - 客戶端呼叫Rest + Ribbon

  3. Spring Cloud實戰系列(三) - 宣告式客戶端Feign

  4. Spring Cloud實戰系列(四) - 熔斷器Hystrix

  5. Spring Cloud實戰系列(五) - 服務閘道器Zuul

  6. Spring Cloud實戰系列(六) - 分散式配置中心Spring Cloud Config

  7. Spring Cloud實戰系列(七) - 服務鏈路追蹤Spring Cloud Sleuth

  8. Spring Cloud實戰系列(八) - 微服務監控Spring Boot Admin

  9. Spring Cloud實戰系列(九) - 服務認證授權Spring Cloud OAuth 2.0

  10. Spring Cloud實戰系列(十) - 單點登入JWT與Spring Security OAuth

前言

上一篇文章,講述瞭如何通過 RestTemplate 配合 Ribbon 去消費服務。Feign 是一個 宣告式HTTP 偽客戶端,提供 面向介面HTTP 客戶端呼叫 程式設計。本文進一步講如何通過 Feign 去消費服務。

  • Feign 只需要建立一個 介面 並提供 註解 即可呼叫。

  • Feign 具有 可插拔 的註解特性,可使用 Feign 註解JAX-RS 註解

  • Feign 支援 可插拔編碼器解碼器

  • Feign 預設整合了 Ribbon,可以和 Eureka 結合使用,預設實現了 負載均衡 的效果。

正文

1. 建立服務契約模組

建立一個 service-contract 的專案 Module,建立完成後的 pom.xml 如下:

<?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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>io.ostenant.github.springcloud</groupId>
    <artifactId>service-contract</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>service-contract</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

        <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>io.ostenant.github.springcloud</groupId>
            <artifactId>service-contract</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>
複製程式碼

service-contract 中定義 業務介面 和相關的 DTO 物件如下:

User.java

public class User implements Serializable {
    private String name;
    private int age;

    public User() {
    }

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

    public void getName() {
        return this.name;
    }

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

    public int getAge() {
        return this.age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
複製程式碼

UserContract.java

UserContract 定義了 User 的所有行為,是一個使用 @FeignClient 註解標記的 宣告式服務介面。其中,@FeignClientvalue 指定的是 服務提供者服務名稱

@FeignClient("service-provider")
public interface UserContract {
    @PostMapping("/user")
    void add(@RequestBody User user);

    @GetMapping("/user/{name}")  
    User findByName(@PathVariable String name);

    @GetMapping("/users")
    List<User> findAll();
}
複製程式碼

對於 服務提供者 而言,需要實現 UserContract 介面的方法;對於 服務消費者 而言,可以直接注入 UserContract 作為 客戶端樁 使用。

2. 建立服務提供者

建立一個 service-provider 的專案 Module,建立完成後引入 服務契約模組 的依賴,pom.xml 如下:

<?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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>io.ostenant.github.springcloud</groupId>
    <artifactId>service-provider</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>service-provider</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Dalston.SR1</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>io.ostenant.github.springcloud</groupId>
            <artifactId>service-contract</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </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>
複製程式碼

通過 註解 @EnableEurekaClient 表明自己是一個 Eureka Client

@SpringBootApplication
@EnableEurekaClient
@RestController
public class ServiceProviderApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServiceProviderApplication.class, args);
    }
}
複製程式碼

建立一個類 UserService,實現 UserContract 介面的具體業務,對外提供 User 相關的 HTTP 的服務。

@RestController
public class UserService implements UserContract {
    private static final Set<User> userSet = new HashSet<>();

    static {
        userSet.add(new User("Alex", 28));
        userSet.add(new User("Lambert", 32));
        userSet.add(new User("Diouf", 30));
    }

    @Override
    public void add(@RequestBody User user) {
        userSet.add(user);
    }

    @Override
    public User findByName(@PathVariable String name) {
        return userSet.stream().filter(user -> {
            return user.getName().equals(name);
        }).findFirst();
    }

    @Override
    public List<User> findAll() {
        return new ArrayList<>(userSet);
    }
}
複製程式碼

配置檔案 中註明的 服務註冊中心 的地址,application.yml 配置檔案如下:

spring:
  active:
    profiles: sp1 # sp2

---

spring:
  profiles: sp1
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
server:
  port: 8770
spring:
  application:
    name: service-provider

---

spring:
  profiles: sp2
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
server:
  port: 8771
spring:
  application:
    name: service-provider
複製程式碼

分別以 spring.profiles.active=sp1spring.profiles.active=sp2 作為 Spring Boot啟動命令引數,在 埠號 87708771 啟動 2服務提供者 例項。

3. 建立服務消費者

新建一個專案 Module,取名為 service-consumer,在它的 pom 檔案中引入 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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>io.ostenant.github.springcloud</groupId>
    <artifactId>service-consumer</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>service-consumer</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Dalston.SR1</spring-cloud.version>
    </properties>

    <dependencies>
        <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-web</artifactId>
        </dependency>
        <dependency>
            <groupId>io.ostenant.github.springcloud</groupId>
            <artifactId>service-contract</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </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>
複製程式碼

在專案的配置檔案 application.yml 檔案,指定 應用名稱service-consumer埠號8772服務註冊地址http://localhost:8761/eureka/ ,程式碼如下:

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
server:
  port: 8772
spring:
  application:
    name: service-feign
複製程式碼

在應用的啟動類 ServiceConsumerApplication 上加上 @EnableFeignClients 註解開啟 Feign 的功能。

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class ServiceConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ServiceConsumerApplication.class, args);
    }
}
複製程式碼

定義一個 UserController 控制器,用於呼叫 服務提供者 提供的服務並 響應 前端。

@RestController
public class UserController {
    @Autowired
    private UserContract userContract;

    @PostMapping("/user")
    public void add(@RequestBody User user) {
        userContract.add(user);
    }

    @GetMapping("/user/{name}")
    public User findByName(@PathVariable String name) {
        return userContract.findByName(name);
    }

    @GetMapping("/users")
    public List<User> findAll() {
        return userContract.findAll();
    }
}
複製程式碼

控制層 UserController 引入 Feign 介面,通過 @FeignClient服務名稱),來指定呼叫的是哪個 服務

啟動 服務消費者 應用,訪問 http://localhost:8772/users 測試 服務消費者 的訪問連通性,響應內容為:

[
    {
      "name": "Alex",
      "age": 28
    },
    {
      "name": "Lambert",
      "age": 32
    },
    {
      "name": "Diouf",
      "age": 30
    }
]
複製程式碼

4. Feign的原始碼實現過程

總的來說,Feign原始碼實現 過程如下:

  1. 首先通過 @EnableFeignClients 註解開啟 FeignClient 的功能。只有這個 註解 存在,才會在程式啟動時 啟動 @FeignClient 註解包掃描

  2. 服務提供者 實現基於 Feign契約介面,並在 契約介面 上面加上 @FeignClient 註解。

  3. 服務消費者 啟動後,會進行 包掃描 操作,掃描所有的 @FeignClient註解 的類,並將這些資訊注入 Spring 上下文中。

  4. 介面 的方法被呼叫時,通過 JDK代理 來生成具體的 RequestTemplate 模板物件

  5. 根據 RequestTemplate 再生成 HTTP 請求的 Request 物件。

  6. Request 物件交給 Client 去處理,其中 Client 內嵌的 網路請求框架 可以是 HTTPURLConnectionHttpClientOkHttp

  7. 最後 Client 被封裝到 LoadBalanceClient 類,這個類結合 Ribbon 完成 負載均衡 功能。

參考

  • 方誌朋《深入理解Spring Cloud與微服務構建》

歡迎關注技術公眾號: 零壹技術棧

零壹技術棧

本帳號將持續分享後端技術乾貨,包括虛擬機器基礎,多執行緒程式設計,高效能框架,非同步、快取和訊息中介軟體,分散式和微服務,架構學習和進階等學習資料和文章。

相關文章