Spring Cloud实战系列(三) - 声明式客户端Feig
前言
上一篇文章,讲述了如何通过 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
完成 负载均衡 功能。
参考
- 方志朋《深入理解Spring Cloud与微服务构建》
欢迎关注技术公众号: 零壹技术栈
零壹技术栈本帐号将持续分享后端技术干货,包括虚拟机基础,多线程编程,高性能框架,异步、缓存和消息中间件,分布式和微服务,架构学习和进阶等学习资料和文章。