Springcloud基础知识(5)- Spring Cloud OpenFeign | 声明式服务调用
Feign 是 Netflix 公司发布的一种实现负载均衡和服务调用的开源组件。Spring Cloud 将其与 Netflix 中的其他开源服务组件(例如 Eureka、Ribbon 以及 Hystrix 等)一起整合进 Spring Cloud Netflix 模块中,整合后全称为 Spring Cloud Netflix Feign。
Feign 对 Ribbon 进行了集成,利用 Ribbon 维护了一份可用服务清单,并通过 Ribbon 实现了客户端的负载均衡。
Feign 是一种声明式服务调用组件,它在 RestTemplate 的基础上做了进一步的封装。通过 Feign,我们只需要声明一个接口并通过注解进行简单的配置(类似于 Dao 接口上面的 Mapper 注解一样)即可实现对 HTTP 接口的绑定。
Feign 支持多种注解,例如 Feign 自带的注解以及 JAX-RS 注解等,但遗憾的是 Feign 本身并不支持 Spring MVC 注解,这无疑会给广大 Spring 用户带来不便。
2019 年 Netflix 公司宣布 Feign 组件正式进入停更维护状态,于是 Spring 官方便推出了一个名为 OpenFeign 的组件作为 Feign 的替代方案。
1. OpenFeign 简介
OpenFeign 全称 Spring Cloud OpenFeign,它是 Spring 官方推出的一种声明式服务调用与负载均衡组件,它的出现就是为了替代进入停更维护状态的 Feign。
OpenFeign 是 Spring Cloud 对 Feign 的二次封装,它具有 Feign 的所有功能,并在 Feign 的基础上增加了对 Spring MVC 注解的支持,例如 @RequestMapping、@GetMapping 和 @PostMapping 等。
OpenFeign 常用注解如下表。
注解 | 描述 |
@FeignClient | 该注解用于通知 OpenFeign 组件对 @RequestMapping 注解下的接口进行解析,并通过动态代理的方式产生实现类,实现负载均衡和服务调用。 |
@EnableFeignClients | 该注解用于开启 OpenFeign 功能,当 Spring Cloud 应用启动时,OpenFeign 会扫描标有 @FeignClient 注解的接口,生成代理并注册到 Spring 容器中。 |
@RequestMapping | Spring MVC 注解,在 Spring MVC 中使用该注解映射请求,通过它来指定控制器(Controller)可以处理哪些 URL 请求,相当于 Servlet 中 web.xml 的配置。 |
@GetMapping | Spring MVC 注解,用来映射 GET 请求,它是一个组合注解,相当于 @RequestMapping(method = RequestMethod.GET) 。 |
@PostMapping | Spring MVC 注解,用来映射 POST 请求,它是一个组合注解,相当于 @RequestMapping(method = RequestMethod.POST) 。 |
Spring Cloud Finchley 及以上版本一般使用 OpenFeign 作为其服务调用组件。由于 OpenFeign 是在 2019 年 Feign 停更进入维护后推出的,因此大多数 2019 年及以后的新项目使用的都是 OpenFeign,而 2018 年以前的项目一般使用 Feign。
Feign 和 OpenFegin 具有以下相同点:
(1) Feign 和 OpenFeign 都是 Spring Cloud 下的远程调用和负载均衡组件;
(2) Feign 和 OpenFeign 作用一样,都可以实现服务的远程调用和负载均衡;
(3) Feign 和 OpenFeign 都对 Ribbon 进行了集成,都利用 Ribbon 维护了可用服务清单,并通过 Ribbon 实现了客户端的负载均衡;
(4) Feign 和 OpenFeign 都是在服务消费者(客户端)定义服务绑定接口并通过注解的方式进行配置,以实现远程服务的调用;
Feign 和 OpenFeign 具有以下不同:
(1) Feign 和 OpenFeign 的依赖项不同,Feign 的依赖为 spring-cloud-starter-feign,而 OpenFeign 的依赖为 spring-cloud-starter-openfeign;
(2) Feign 和 OpenFeign 支持的注解不同,Feign 支持 Feign 注解和 JAX-RS 注解,但不支持 Spring MVC 注解;(3) OpenFeign 除了支持 Feign 注解和 JAX-RS 注解外,还支持 Spring MVC 注解;
2. OpenFeign 实现服务调用
本文将在 “ Springcloud基础知识(4)- Spring Cloud Ribbon | 负载均衡与服务调用 ” 里 SpringcloudDemo03 项目基础上,添加一个 ConsumerFeign 子模块,来演示 OpenFeign 服务调用。SpringcloudDemo03 的 Spring Boot 版本是 2.3.12.RELEASE。
1) 创建 ConsumerFeign 模块
选择左上的项目列表中的 SpringcloudDemo03,点击鼠标右键,选择 New -> Module 进入 New Module 页面:
Maven -> Project SDK: 1.8 -> Check "Create from archtype" -> select "org.apache.maven.archtypes:maven-archtype-quickstart" -> Next
Name: ConsumerFeign
GroupId: com.example
ArtifactId: ConsumerFeign
-> Finish
注:模块 ConsumerFeign 创建后,Maven 命令会自动修改主项目 SpringcloudDemo03 的 pom.xml,添加如下内容:
<modules>
...
<module>ConsumerFeign</module>
</modules>
2) 修改 ConsumerFeign 的 pom.xml 内容如下
1 <?xml version="1.0" encoding="UTF-8"?> 2 <project xmlns="http://maven.apache.org/POM/4.0.0" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 5 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 6 <parent> 7 <artifactId>SpringcloudDemo03</artifactId> 8 <groupId>com.example</groupId> 9 <version>1.0-SNAPSHOT</version> 10 </parent> 11 <modelVersion>4.0.0</modelVersion> 12 13 <artifactId>ConsumerFeign</artifactId> 14 15 <name>ConsumerFeign</name> 16 <!-- FIXME change it to the project's website --> 17 <url>http://www.example.com</url> 18 19 <properties> 20 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 21 <maven.compiler.source>1.8</maven.compiler.source> 22 <maven.compiler.target>1.8</maven.compiler.target> 23 </properties> 24 25 <dependencies> 26 <dependency> 27 <groupId>junit</groupId> 28 <artifactId>junit</artifactId> 29 <version>4.12</version> 30 <scope>test</scope> 31 </dependency> 32 <dependency> 33 <groupId>org.springframework.boot</groupId> 34 <artifactId>spring-boot-starter-web</artifactId> 35 </dependency> 36 <dependency> 37 <groupId>org.springframework.boot</groupId> 38 <artifactId>spring-boot-starter-test</artifactId> 39 <scope>test</scope> 40 </dependency> 41 <!-- 引入公共子模块 --> 42 <dependency> 43 <groupId>com.example</groupId> 44 <artifactId>CommonAPI</artifactId> 45 <version>${project.version}</version> 46 </dependency> 47 <dependency> 48 <groupId>org.projectlombok</groupId> 49 <artifactId>lombok</artifactId> 50 <version>1.18.8</version> 51 </dependency> 52 <dependency> 53 <groupId>org.springframework.cloud</groupId> 54 <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> 55 </dependency> 56 <dependency> 57 <groupId>org.springframework.cloud</groupId> 58 <artifactId>spring-cloud-starter-openfeign</artifactId> 59 </dependency> 60 </dependencies> 61 62 <build> 63 <plugins> 64 <plugin> 65 <groupId>org.springframework.boot</groupId> 66 <artifactId>spring-boot-maven-plugin</artifactId> 67 <configuration> 68 <mainClass>com.example.App</mainClass> 69 <layout>JAR</layout> 70 </configuration> 71 <executions> 72 <execution> 73 <goals> 74 <goal>repackage</goal> 75 </goals> 76 </execution> 77 </executions> 78 </plugin> 79 </plugins> 80 </build> 81 </project>
注:spring-cloud-starter-openfeign 内置了 Ribbon,不需要另外导入 Ribbon 的依赖。
Spring Boot 2.3.12.RELEASE 自动导入的 OpenFeign 版本是 2.2.9.RELEASE,没有遇到客户端默认 1 秒超时问题。 在 Spring Boot 2.6.6 下自动导入的 OpenFeign 版本是 3.1.3,也没有遇到客户端默认 1 秒超时问题。
3) 创建 src/main/resources/application.yml 文件
1 server: 2 port: 80 3 eureka: 4 client: 5 register-with-eureka: false # 消费者不向服务注册中心注册服务 6 fetch-registry: true # 消费者客户端需要去检索服务 7 service-url: 8 defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
4) 创建 src/main/java/com/example/service/EmployeeFeignService.java 文件
1 package com.example.service; 2 3 import java.util.List; 4 5 import org.springframework.stereotype.Service; 6 import org.springframework.cloud.openfeign.FeignClient; 7 import org.springframework.web.bind.annotation.PathVariable; 8 import org.springframework.web.bind.annotation.RequestMapping; 9 import org.springframework.web.bind.annotation.RequestMethod; 10 11 import com.example.entity.Employee; 12 13 @Service 14 @FeignClient(value = "EMPLOYEE-SERVICE-PROVIDER") 15 public interface EmployeeFeignService { 16 17 @RequestMapping(value = "/employee/get/{id}", method = RequestMethod.GET) 18 public Employee get(@PathVariable("id") int id); 19 20 @RequestMapping(value = "/employee/list", method = RequestMethod.GET) 21 public List<Employee> list(); 22 }
@FeignClient 注解中,value 属性的取值为:服务提供者的服务名,即 ServiceProvider 的配置文件(application.yml)中 spring.application.name 的取值。
接口中定义的方法与服务提供者(ServiceProvider 的各实例)中 EmployeeController 定义的服务方法对应。
5) 创建 src/main/java/com/example/controller/ConsumerController.java 文件
1 package com.example.controller; 2 3 import java.util.List; 4 5 import org.springframework.beans.factory.annotation.Autowired; 6 import org.springframework.web.bind.annotation.PathVariable; 7 import org.springframework.web.bind.annotation.RequestMapping; 8 import org.springframework.web.bind.annotation.RestController; 9 10 import com.example.entity.Employee; 11 import com.example.service.EmployeeFeignService; 12 13 @RestController 14 @RequestMapping(value = "/consumer") 15 public class ConsumerController { 16 @Autowired 17 private EmployeeFeignService employeeFeignService; 18 19 @RequestMapping(value = "/employee/get/{id}") 20 public Employee get(@PathVariable("id") Integer id) { 21 return employeeFeignService.get(id); 22 } 23 24 @RequestMapping(value = "/employee/list") 25 public List<Employee> list() { 26 return employeeFeignService.list(); 27 } 28 29 }
6) 修改 src/main/java/com/example/App.java 文件
1 package com.example; 2 3 import org.springframework.boot.SpringApplication; 4 import org.springframework.boot.autoconfigure.SpringBootApplication; 5 import org.springframework.cloud.openfeign.EnableFeignClients; 6 7 @SpringBootApplication 8 @EnableFeignClients // 开启 OpenFeign 功能 9 public class App { 10 public static void main(String[] args) { 11 SpringApplication.run(App.class, args); 12 } 13 }
7) 运行
下面我们在 “ Springcould基础知识(3)- Spring Cloud Eureka (二) | Eureka Server 集群 ” 里的集群基础上,测试 OpenFeign 实现服务调用。
依次启动 server-7001、server-7002、server-7003,启动的间隔 5 ~ 10 秒,都启动后等待 10 秒左右。
再依次启动 service-8001、service-8002、service-8003,启动的间隔 5 ~ 10 秒,都启动后等待 10 秒左右。
浏览器访问 http://eureka7001.com:7001/,页面上 “Instances currently registered with Eureka” 区域显示:
Application AMIs Availability Zones Status
EMPLOYEE-SERVICE-PROVIDER n/a (3) (3) UP (3) - sevice-provider-8003 , sevice-provider-8002 , sevice-provider-8001
启动 ConsumerFeign 模块:
浏览器访问 http://localhost/consumer/employee/list,显示结果如下:
[{"id":1,"name":"Test Name1","port":8001},{"id":2,"name":"Test Name2","port":8001}]
刷新页面:
[{"id":1,"name":"Test Name1","port":8002},{"id":2,"name":"Test Name2","port":8002}]
再次刷新页面:
[{"id":1,"name":"Test Name1","port":8003},{"id":2,"name":"Test Name2","port":8003}]
注:通过 port 的变化可以看出,OpenFeign 内置了 Ribbon,因此它也实现了客户端的负载均衡,其默认负载均衡策略为轮询策略。
3. OpenFeign 日志增强
OpenFeign 提供了日志打印功能,我们可以通过配置调整日志级别,来了解请求的细节。
Feign 为每一个 FeignClient 都提供了一个 feign.Logger 实例,通过它可以对 OpenFeign 服务绑定接口的调用情况进行监控。
OpenFeign 日志打印功能的开启方式比较简单,下面我们在 ConsumerFeign 模块里进行演示。
1) 修改 ConsumerFeign 的 application.yml,添加配置如下
logging:
level:
# OpenFeign 日志以什么样的级别监控该接口
com.example.service.EmployeeFeignService: debug
以上配置的含义就是,OpenFeign 以 debug 级别监控 com.example.service.EmployeeFeignService 接口。
com.example.service.EmployeeFeignService 是开启 @FeignClient 注解的接口(即服务绑定接口)的完整类名。也可以只配置部分路径,表示监控该路径下的所有服务绑定接口。debug表示监听该接口的日志级别。
2) 创建 src/main/java/com/example/config/OpenFeignConfig.java 文件
1 package com.example.config; 2 3 import feign.Logger; 4 import org.springframework.context.annotation.Bean; 5 import org.springframework.context.annotation.Configuration; 6 7 @Configuration 8 public class OpenFeignConfig { 9 @Bean 10 Logger.Level feginLoggerLevel() { 11 return Logger.Level.FULL; 12 } 13 }
该配置的作用是通过配置的 Logger.Level 对象告诉 OpenFeign 记录哪些日志内容。
Logger.Level 的具体级别如下:
(1) NONE:不记录任何信息;
(2) BASIC:仅记录请求方法、URL 以及响应状态码和执行时间;
(3) HEADERS:除了记录 BASIC 级别的信息外,还会记录请求和响应的头信息;
(4) FULL:记录所有请求与响应的明细,包括头信息、请求体、元数据等等;
3) 重启 ConsumerFeign 模块
浏览器访问 http://localhost/consumer/employee/list,控制台输出如下:
2022-06-25 17:56:17.392 DEBUG 10736 --- [p-nio-80-exec-2] c.example.service.ConsumerFeignService : [ConsumerFeignService#list] ---> END HTTP (0-byte body)
2022-06-25 17:56:17.397 DEBUG 10736 --- [p-nio-80-exec-2] c.example.service.ConsumerFeignService : [ConsumerFeignService#list] <--- HTTP/1.1 200 (4ms)
2022-06-25 17:56:17.397 DEBUG 10736 --- [p-nio-80-exec-2] c.example.service.ConsumerFeignService : [ConsumerFeignService#list] connection: keep-alive
2022-06-25 17:56:17.397 DEBUG 10736 --- [p-nio-80-exec-2] c.example.service.ConsumerFeignService : [ConsumerFeignService#list] content-type: application/json
2022-06-25 17:56:17.397 DEBUG 10736 --- [p-nio-80-exec-2] c.example.service.ConsumerFeignService : [ConsumerFeignService#list] date: Sat, 25 Jun 2022 09:56:17 GMT
2022-06-25 17:56:17.397 DEBUG 10736 --- [p-nio-80-exec-2] c.example.service.ConsumerFeignService : [ConsumerFeignService#list] keep-alive: timeout=60
2022-06-25 17:56:17.397 DEBUG 10736 --- [p-nio-80-exec-2] c.example.service.ConsumerFeignService : [ConsumerFeignService#list] transfer-encoding: chunked
2022-06-25 17:56:17.398 DEBUG 10736 --- [p-nio-80-exec-2] c.example.service.ConsumerFeignService : [ConsumerFeignService#list]
2022-06-25 17:56:17.398 DEBUG 10736 --- [p-nio-80-exec-2] c.example.service.ConsumerFeignService : [ConsumerFeignService#list] [{"id":1,"name":"Test Name1","port":8002},{"id":2,"name":"Test Name2","port":8002}]
2022-06-25 17:56:17.398 DEBUG 10736 --- [p-nio-80-exec-2] c.example.service.ConsumerFeignService : [ConsumerFeignService#list] <--- END HTTP (83-byte body)