OpenFeign的全称为Spring Cloud OpenFeign,是Spring Cloud 开发的一款基于Feign的框架,声明式Web服务客户端。
Feign 是Netflix开源的一个声明式的Web服务客户端,它简化了基于HTTP的服务调用,使得服务间的通信变得更加简单和灵活。Feign通过定义接口、注解和动态代理等方式,将服务调用的过程封装起来,开发者只需定义服务接口,而无需关心底层的HTTP请求和序列化等细节。
OpenFeign功能升级
OpenFeign在Feign的基础上提供了以下增强和扩展功能:
OpenFeign通常要配合注册中心一起使用,并且新版本OpenFeign也必须和负载均衡器一起使用,使用步骤如下:
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-loadbalancerartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
spring:
application:
name: nacos-consumer-demo
cloud:
nacos:
discovery:
server-addr: localhost:8848
username: nacos
password: nacos
register-enabled: false # 消费者(不需要注册到nacos中)
在启动类文件中添加@EnableFeignClients
注解即可
@Service
@FeignClient("nacos-discovery") // 表示调用 nacos 中的 nacos-discovery 服务
public interface UserService {
@RequestMapping("/user/getnamebyid") // 调用生产者的"/user/getnamebyid"接口
public String getNameById(@RequestParam("id") int id);
}
@RestController
public class BusinessController {
@Autowired
private UserService userService;
@RequestMapping("/getnamebyid")
public String getNameById(Integer id){
System.out.println("------- do provider getNameById method" +
LocalDateTime.now());
return userService.getNameById(id);
}
}
在微服务架构中,服务之间是通过网络进行通信的,而网络是复杂和不稳定的,所以在调用服务时可能会失败或超时,那么在这种情况下,就需要给OpenFeign配置超时重试机制。
什么是超时重试?
超时重试是一种在网络通信中常用发的策略,用于处理请求在一定时间内未能得到响应的情况。当发起请求后,如果规定时间内没有得到预期的响应,就会触发超时重试机制,重新发送请求。
超时重试的主要目的是提高请求的可靠性和稳定性,以应对网络的不稳定、服务不可用、响应延迟等不确定因素。
OpenFeign默认是不自动开启超时重试
开启有以下步骤:
spring:
cloud:
openfeign:
client:
config:
default:
connect-timeout: 1000 #连接超时时间
read-timeout: 1000 #读取超时时间
@Configuration
public class RetryerConfig {
@Bean
public Retryer retryer(){
return new Retryer.Default(1000,//重试间隔时间
1000,//最大重试间隔时间
3);//最大重试次数
}
}
最大重试次数为3次,最大重试间隔时间是1秒,重试间隔时间是1秒
这时我们启动一个实例,并设置保护阈值为0,启动消费者。
访问服务
此时服务无法访问,并触发了超时重试机制,这时打开生产者者的控制台:
在控制台我们可以看到总共打印了3次日志,因为我们设置的最大重试次数是3
为什么不是4次呢?
为什么不是4次?
因为Retryer的Default方法的源码中重试次数变量attempt是从1开始的,然后核心方法continueOrPropagate中的if判断是当this.attempt++ >= this.maxAttempts 时,才抛出异常。
自定义超时重试机制实现为以下两步:
常见的超时重试策略有以下三种:
public class CustomRetryer implements Retryer {
private final int maxAttempts; //最大尝试次数
private final long backoff; //重试间隔时间
int attempt; //当前重试次数
public CustomRetryer() {
this.maxAttempts=3;
this.backoff =1000;
this.attempt=0;
}
public CustomRetryer(int maxAttempts, long backoff) {
this.maxAttempts = maxAttempts;
this.backoff = backoff;
this.attempt=0;
}
@Override
public void continueOrPropagate(RetryableException e) {
if (attempt++>=maxAttempts){
throw e;
}
long interval = this.backoff;//重试间隔时间
System.out.println(LocalDateTime.now()+" | 执行一次重试:"+interval);
try {
//重试间隔实际
Thread.sleep(interval*attempt);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
@Override
public Retryer clone() {
return new CustomRetryer(maxAttempts,backoff);
}
}
spring:
cloud:
openfeign:
client:
config:
default:
connect-timeout: 1000 #连接超时时间
read-timeout: 1000 #读取超时时间
retryer: com.example.consumer.config.CustomRetryer #自定义失败重试类
启动生产者和消费者服务,并尝试调用服务,并查看控制台
这里我们设定的重试次数是3,但为什么会打印4次呢?
是因为在自定义重试类中的attempt变量是从0开始的。
观察日志文档的时间间隔:从2s->3s->4s,最初attempt为1,1*1+read-timeout的1s所以是2s,然后1*1+read-timeout,以此类推……
首先我们先了解以下OpenFeign的底层实现逻辑
@EnableFeignClients
注解@FeignClient
的接口,并为它们创建代理实例。OpenFeign超时的底层实现是通过配置底层的HTTP客户端来实现的。OpenFeign允许在请求连接和读取数据阶段设置超时时间,具体超时配置可以通过HTTP客户端的连接超时(connectTimeout)和读取超时(readTimeout)来实现,可以在配置文件中设置超时参数。
OpenFeign重试的底层可通过观察源码来了解,它的源码在SynchronousMethodHandler的invoke方法下,如下所示:
public Object invoke(Object[] argv) throws Throwable {
RequestTemplate template = this.buildTemplateFromArgs.create(argv);
Request.Options options = this.findOptions(argv);
Retryer retryer = this.retryer.clone();
// 死循环,如果成功或者重试结束就返回(通过throw终止while循环)
while(true) {
try {
//通过HTTP Client发起通信
return this.executeAndDecode(template, options);
} catch (RetryableException var9) {
RetryableException e = var9;
//判断是否重试
try {
retryer.continueOrPropagate(e);
} catch (RetryableException var8) {
Throwable cause = var8.getCause();
if (this.propagationPolicy == ExceptionPropagationPolicy.UNWRAP && cause != null) {
throw cause;
}
throw var8;
}
if (this.logLevel != Level.NONE) {
this.logger.logRetry(this.metadata.configKey(), this.logLevel);
}
}
}
}
因此OpenFeign的重试功能是通过其内置的Retryer组件和底层的HTTP客户端实现的。
Retryer组件提供了重试策略的逻辑实现,而远程接口则通过HTTP客户端来完成调用。