Spring Cloud實戰系列(三) - 宣告式客戶端呼叫Feign
上一篇文章,講述瞭如何通過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
註解標記的宣告式服務介面
。其中,@FeignClient
的value
指定的是服務提供者
的服務名稱
。
@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=sp1
和spring.profiles.active=sp2
作為Spring Boot
的啟動命令引數
,在埠號
8770
和8771
啟動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
的原始碼實現
過程如下:
-
首先通過
@EnableFeignClients
註解開啟FeignClient
的功能。只有這個註解 存在,才會在程式啟動時啟動@FeignClient
註解 的包掃描 。 -
服務提供者實現基於
Feign
的契約介面 ,並在契約介面 上面加上@FeignClient
註解。 -
服務消費者啟動後,會進行包掃描 操作,掃描所有的
@FeignClient
的註解 的類,並將這些資訊注入Spring
上下文中。 -
當介面 的方法被呼叫時,通過
JDK
的代理 來生成具體的RequestTemplate
模板物件 。 -
根據
RequestTemplate
再生成HTTP
請求的Request
物件。 -
Request
物件交給Client
去處理,其中Client
內嵌的網路請求框架 可以是HTTPURLConnection
、HttpClient
和OkHttp
。 -
最後
Client
被封裝到LoadBalanceClient
類,這個類結合Ribbon
完成負載均衡 功能。