OpenFeign实战
OpenFeign 是Netflix
开发的声明式、模板化的http
请求客户端,作用和RestTemplate
差不多,只不过OpenFeign
可以更加便捷、优雅地调用http api
。
OpenFeign
可以将提供者提供的http
接口伪装为Java接口进行消费,消费者只需使用 接口 + 注解 的方式便可直接调用提供者提供的http
接口,而无需再使用RestTemplate
。
OpenFeign
与Feign
Spring Cloud
Dalston 版及之前的版本使用的是 Feign
,而该项目现已更新为了 OpenFeign
,新版本中的依赖也发生了变化。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
Feign
本身不支持Spring MVC
的注解,它有一套自己的注解;OpenFeign
是Spring Cloud
在Feign
的基础上支持了Spring MVC
的注解,如@RequesMapping
等等。 OpenFeign
的@FeignClient
可以解析SpringMVC
的@RequestMapping
注解下的接口, 并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。
OpenFeign
与Ribbon
Ribbon
是Netflix
的一个开源的负载均衡项目,是一个客户端负载均衡器,运行在消费者端。OpenFeign
也是运行在消费者端的,并且使用Ribbon
进行负载均衡,所以OpenFeign
直接内置了Ribbon
,即在导入 OpenFeign
依赖后,无需再导入Ribbon
依赖了。
一、OpenFeign
项目搭建
1. 提供者应用
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 https://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>2.3.10.RELEASE</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>user-provider</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>8</java.version>
<spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>
<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>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.properties
server.port=8081
#
eureka.client.service-url.defaultZone=http://admin:123@ek1.com:7901/eureka/,http://admin:123@ek2.com:7902/eureka/
#
# 客户端在注册中心中的名称
eureka.instance.instance-id=user-provider-8081
#
# 设置当前 client 每5秒向 server 发送一次心跳,默认 30s
eureka.instance.lease-renewal-interval-in-seconds=5
#
# 表示eureka server至上一次收到client的心跳之后,等待下一次心跳的超时时间,在这个时间内若没收到下一次心跳,则将移除该instance,默认为90秒
eureka.instance.lease-expiration-duration-in-seconds=90
#
# 当前服务名称
spring.application.name=user-provider
#
# 表示将自己的ip注册到Eureka Server上。不配置,表示将操作系统的 hostname 注册到server
eureka.instance.prefer-ip-address=true
#
# eureka 服务名,默认值 unknown;如果没有配置,则取 spring.application.name
#eureka.instance.appname=user-provider
#
# 实例的虚拟主机名称,默认值 unknown;如果没有配置,则取 spring.application.name
#eureka.instance.virtual-host-name=user-provider
#
# 对外开放所有监控端点
management.endpoints.web.exposure.include=*
#
# 是否将自己注册到其他Eureka Server,默认为true
eureka.client.register-with-eureka=true
#
# 是否从eureka server获取注册信息, 需要
eureka.client.fetch-registry=true
- 实体
bean
public class User implements Serializable {
private String idCard;
private String username;
public User() {
}
public User(String idCard, String username) {
this.idCard = idCard;
this.username = username;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getIdCard() {
return idCard;
}
public void setIdCard(String idCard) {
this.idCard = idCard;
}
@Override
public String toString() {
return "User{" +
"idCard='" + idCard + ''' +
", username='" + username + ''' +
'}';
}
}
controller
接口
@RestController
@RequestMapping("/user")
public class UserController {
private final static Logger log = LoggerFactory.getLogger(UserController.class);
@PostMapping("/save")
public Boolean saveUser(@RequestBody User user) {
log.info("save user success : {}", JSON.toJSONString(user));
return Boolean.TRUE;
}
@DeleteMapping("/del/{id}")
public Boolean deleteUser(@PathVariable("id") Long id) {
log.info("delete user success, user id : {}", id);
return Boolean.TRUE;
}
@GetMapping("/list")
public List<User> getUserList() {
User user = new User("110", "大宝");
log.info("getUserList result : " + user);
return Lists.newArrayList(user);
}
@GetMapping("/get")
public User getUserById(@RequestParam(value = "id", required = true) Long id) {
User user = new User("111", "大宝");
log.info("getUserById result : {} , id : {}", user, id);
return user;
}
}
- 启动类
@SpringBootApplication
@EnableDiscoveryClient
public class UserServiceProviderApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceProviderApplication.class, args);
}
}
2. 消费者应用
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 https://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>2.3.10.RELEASE</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>open-feign-consumer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>open-feign-consumer</name>
<description>open-feign-consumer</description>
<properties>
<java.version>8</java.version>
<spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</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.properties
server.port=7072
# 注册中心地址
eureka.client.service-url.defaultZone=http://admin:123@ek1.com:7901/eureka/,http://admin:123@ek2.com:7902/eureka/
#
# 客户端在注册中心中的名称
eureka.instance.instance-id=open-feign-consumer-7072
#
# 当前服务对外暴露的名称
spring.application.name=open-feign-consumer
#
# 指定 feign 从请求到获取提供者响应的超时时间
feign.client.config.default.read-timeout=5000
#
# 指定 feign 连接提供者的超时时间
feign.client.config.default.connect-timeout=5000
#
# 设置当前 client 每5秒向 server 发送一次心跳,默认 30s
eureka.instance.lease-renewal-interval-in-seconds=5
#
# 表示eureka server至上一次收到client的心跳之后,等待下一次心跳的超时时间,在这个时间内若没收到下一次心跳,则将移除该instance,默认为90秒
eureka.instance.lease-expiration-duration-in-seconds=90
#
# 表示将自己的ip注册到Eureka Server上。不配置,表示将操作系统的 hostname 注册到 server
eureka.instance.prefer-ip-address=true
#
# 是否将自己注册到其他Eureka Server
eureka.client.register-with-eureka=true
#
# 是否从eureka server获取注册信息
eureka.client.fetch-registry=true
#
# 表示eureka client间隔多久去拉取服务注册信息,默认为30秒,如果要迅速获取服务注册状态,可以缩小该值,比如5秒
eureka.client.registry-fetch-interval-seconds=5
- 创建实体
public class User implements Serializable {
private String idCard;
private String username;
public User() {
}
public User(String idCard, String username) {
this.idCard = idCard;
this.username = username;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getIdCard() {
return idCard;
}
public void setIdCard(String idCard) {
this.idCard = idCard;
}
@Override
public String toString() {
return "User{" +
"idCard='" + idCard + ''' +
", username='" + username + ''' +
'}';
}
}
public class ResultBody<T> implements Serializable {
private boolean status;
// 响应码
private String code;
// 响应描述信息
private String message;
// 响应数据
private T data;
public ResultBody() {
}
private ResultBody(T data) {
this.data = data;
}
private ResultBody(String code, String msg) {
this.code = code;
this.message = msg;
}
public static <T> ResultBody<T> success() {
ResultBody<T> result = new ResultBody<>();
result.setCode("1");
result.setStatus(Boolean.TRUE);
result.setMessage("成功");
return result;
}
public static <T> ResultBody<T> success(T data) {
ResultBody<T> result = new ResultBody<>();
result.setCode("1");
result.setStatus(Boolean.TRUE);
result.setMessage("成功");
result.setData(data);
return result;
}
public static <T> ResultBody<T> error(String code, String message) {
ResultBody<T> result = new ResultBody<>(code, message);
result.setStatus(Boolean.FALSE);
return result;
}
public boolean isStatus() {
return status;
}
public void setStatus(boolean status) {
this.status = status;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
@FeignClient
定义接口
/**
* 定义http请求接口
*/
@FeignClient(value = "user-provider/user")
public interface OpenFeignUserService {
@PostMapping("/save")
Boolean saveUser(@RequestBody User user);
@DeleteMapping("/del/{id}")
public Boolean deleteUser(@PathVariable("id") Long id);
@GetMapping("/list")
List<User> getUserList();
@GetMapping("/get")
User getUserById(@RequestParam(value = "id", required = true) Long id);
}
注意: 如果这里你要使用@RequestMapping
注解的时候,你必须说明请求方式,例如:@RequestMapping(value = "/save", method = RequestMethod.POST)
controller
定义
@RestController
@RequestMapping("/open-feign")
public class OpenFeignController {
@Resource
private OpenFeignUserService userService;
@PostMapping("/save")
public ResultBody saveUser(@RequestBody User user) {
Boolean result = userService.saveUser(user);
return ResultBody.success(result);
}
@DeleteMapping("/del/{id}")
public ResultBody deleteUser(@PathVariable("id") Long id) {
Boolean result = userService.deleteUser(id);
return ResultBody.success(result);
}
@GetMapping("/list")
public ResultBody getUserList() {
List<User> userList = userService.getUserList();
return ResultBody.success(userList);
}
@GetMapping("/get")
public ResultBody getUserById(@RequestParam(value = "id", required = true) Long id) {
User user = userService.getUserById(id);
return ResultBody.success(user);
}
}
- 启动类定义,
@EnableFeignClients
启用OpenFeign
@SpringBootApplication
@EnableDiscoveryClient
// 开启 Feign 客户端,指定service接口所在的包
@EnableFeignClients(basePackages = "com.example.openfeign.consumer.service")
public class OpenFeignConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(OpenFeignConsumerApplication.class, args);
}
}
IDEA
的HttpClient
测试文件TestUser.http
###
POST http://127.0.0.1:7072/open-feign/save
Content-Type: application/json
{
"idCard":"123",
"username":"Kate"
}
###
DELETE http://127.0.0.1:7072/open-feign/del/110
###
GET http://127.0.0.1:7072/open-feign/list
###
GET http://127.0.0.1:7072/open-feign/get?id=111
二、 超时和重试
OpenFeign
默认支持Ribbon
,Ribbon
的重试机制和OpenFeign
的重试机制有冲突,所以源码中默认关闭了OpenFeign
的重试机制,使用Ribbon
重试机制。
1. 超时
- 服务提供者接口
private AtomicLong atomicLong = new AtomicLong();
@GetMapping("/retry")
public User retryUser() {
try {
log.info("超时模拟 ...");
Thread.sleep(6000);
} catch (Exception e) {
log.info("执行异常");
}
long i = atomicLong.getAndIncrement();
log.info("retryUser 接口第 {} 次调用", i);
User user = new User("111", "大宝");
return user;
}
- 客户端设置业务超时时间
# 业务超时时间
ribbon.ReadTimeout=2000
@FeignClient(value = "user-provider/user")
public interface OpenFeignUserService {
@GetMapping("/retry")
User retryUser();
}
@RestController
@RequestMapping("/open-feign")
public class OpenFeignController {
@GetMapping("/retry")
public ResultBody retryUser() {
User user = userService.retryUser();
return ResultBody.success(user);
}
}
1. 重试
# 同一台实例最大重试次数,不包括首次调用
ribbon.MaxAutoRetries=3
#
# 重试负载均衡其他实例最大此时,不包括首次调用
ribbon.MaxAutoRetriesNextServer=3
#
# 是否所有操作都重试
ribbon.okToRetryOnAllOperations=false
三、日志配置
在properties
文件中配置日志级别,方便本地调试,参考官方文档:docs.spring.io/spring-clou…
# none:不记录任何日志,默认值;
# basic:仅记录请求方法,url,响应状态码,执行时间;
# headers:在basic基础上,记录header信息;
# full:记录请求和响应的header,body,元数据
#
feign.client.config.default.logger-level=full
#
# logger-level只对 debug 级别日志做出响应
logging.level.com.example.openfeign.consumer=debug
如果不想通过feign.client.config.default.logger-level
的方式配置,也可通过Java
代码的方式来配置
@Configuration
public class FeiginConfig {
@Bean
Logger.Level logLevel(){
return Logger.Level.FULL;
}
}
转载自:https://juejin.cn/post/7253466047889965115