前言
上一篇文章, 讲述了如何通过 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.YAML 配置文件如下:
- 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.YAML 文件, 指定 应用名称 为 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 与微服务构建》
来源: https://juejin.im/post/5c4ead926fb9a049dd80ae4f