OpenFeign 是Netflix
开发的声明式、模板化的http
请求客户端,作用和RestTemplate
差不多,只不过OpenFeign
可以更加便捷、优雅地调用http api
。
OpenFeign
可以将提供者提供的http
接口伪装为Java接口进行消费,消费者只需使用 接口 + 注解 的方式便可直接调用提供者提供的http
接口,而无需再使用RestTemplate
。
OpenFeign
与 Feign
Spring Cloud
Dalston 版及之前的版本使用的是 Feign
,而该项目现已更新为了 OpenFeign
,新版本中的依赖也发生了变化。
org.springframework.cloud
spring-cloud-starter-openfeign
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
文件
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.3.10.RELEASE
com.example
user-provider
0.0.1-SNAPSHOT
8
Hoxton.SR12
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
org.springframework.boot
spring-boot-starter-actuator
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.boot
spring-boot-starter-web
com.alibaba
fastjson
1.2.83
org.springframework.boot
spring-boot-maven-plugin
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 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
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.3.10.RELEASE
com.example
open-feign-consumer
0.0.1-SNAPSHOT
open-feign-consumer
open-feign-consumer
8
Hoxton.SR12
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.cloud
spring-cloud-starter-openfeign
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
org.springframework.boot
spring-boot-maven-plugin
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 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 ResultBody success() {
ResultBody result = new ResultBody();
result.setCode("1");
result.setStatus(Boolean.TRUE);
result.setMessage("成功");
return result;
}
public static ResultBody success(T data) {
ResultBody result = new ResultBody();
result.setCode("1");
result.setStatus(Boolean.TRUE);
result.setMessage("成功");
result.setData(data);
return result;
}
public static ResultBody error(String code, String message) {
ResultBody 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 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 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;
}
}