一、什么是 OpenFeign
OpenFeign 的全称为 Spring Cloud OpenFeign,是Spring Cloud 团队开发的一款基于Feign 的框架,声明式 Web 服务客户端
Feign 是 Netflix 开源的一个声明式的 Web 服务客户端,它简化了基于HTTP 的服务调用,使得服务间通讯变得更加简单灵活。Feign 通过定义接口、注解和动态代理等方式,将服务调用的过程封装起来,开发者只需要定义服务接口,而无需关心底层的 HTTP 请求和序列化等细节。
OpenFeign vs Feign 的功能升级
- 更好的集成 Spring Cloud 组件:OpenFeign 与 Spring Cloud 其他组件(如服务发现,负载均衡等)紧密集成,可以无缝地与其他Spring Cloud 组件一起使用。
- 支持 @FeignClient 注解:OpenFeign 引入了 @FeignClient 注解作为 Feign 客户端的标识,可以方便地定义和使用远程服务地声明式接口。
- 错误处理改进:OpenFeign 对异常的处理做了增强,提供了更好的错误信息和错误异常处理机制,使得开发者可以更方便地进行错误处理。例如:OpenFeign 提供返回错误响应或请求失败时,OpenFeign 会调用回退策略中的逻辑,提供一个默认的处理结果。
- 更丰富的配置项:OpenFeign 提供了丰富的配置选项,可以对 Feign 客户端的行为进行灵活的配置,例如超时设置、重试策略等。
二、OpenFeign 基础使用
OpenFeign 通常要配合注册中心一起使用,并且新版本 OpenFeign 也必须和负载均衡器一起使用,所以它的基础使用步骤如下:
- 添加依赖(Nacos 注册中心、OpenFeign、Spring Cloud LoadBalancer)
- 配置 Nacos 服务端信息
- 在项目中开启 OpenFeign
- 编写 OpenFeign 调用代码
- 编写代码通过 OpenFeign 调用生产者
具体参考 Nacos-注册中心-CSDN博客
三、超时重传机制
在微服务框架中,服务之间是通过网络进行通信的,而网络是非常复杂和不稳定的,所以在调用服务时可能会失败或超时,那么在这种情况下,我们就需要个 OpenFeign 配置超时重试机制了。
什么时超时重试?
超时重试就是一种在网络通信中常用的策略,用于处理请求在一定时间内未能得到响应或得到超时响应的情况。当发起请求后,如果在规定的时间内没有得到预期的响应,就会触发超时重试机制,重新发送请求。
超时重试的目的主要是提高请求的可靠性和稳定性,以应对网络不稳定、服务不可用、响应延迟等不确定因素。
3.1 配置超时重试
spring:
cloud:
openfeign:
client:
config:
default: #全局配置
connect-timeout: 1000 #连接超时时间
read-timeout: 1000 #读取超时时间
3.2 覆盖 Retryer
@Configuration
public class MyRetryer{
@Bean
public Retryer retryer(){
return new Retryer.Default(
2000, //重试间隔时间
2000, //最大重试时间
3 //最大重试次数);
}
}
四、自定义超时重传机制
自定义重试类
public class MyRetryer implements Retryer {
private final int maxAttempts;
private final long period;
int attempt;
public MyRetryer(){
this.maxAttempts=3;
this.period=1;
this.attempt=0;
}
public MyRetryer(int maxAttempts,long period){
this.maxAttempts=maxAttempts;
this.period=period;
this.attempt=0;
}
@Override
public void continueOrPropagate(RetryableException e) {
if(attempt++>=maxAttempts){
throw e;
}
long interval=period;
try {
//重试间隔时间
Thread.sleep((interval*(attempt)));
} catch (InterruptedException ex) {
throw new RuntimeException(ex);
}
}
@Override
public Retryer clone() {
return new MyRetryer(maxAttempts,period);
}
}
设置配置文件
spring:
application:
name: nacos-consumer-demo
cloud:
nacos:
discovery:
username: nacos
password: nacos
server-addr: localhost:8848
register-enabled: true
openfeign:
client:
config:
default: #全局配置
connect-timeout: 1000 #连接超时时间
read-timeout: 1000 #读取超时时间
retryer: com.example.consumer.config.MyRetryer #自定义重试类
server:
port: 58080
五、超时重试底层实现
5.1 超时重试底层原理
OpenFeign 超时的底层实现是通过配置底层的HTTP 客户端来实现的。OpenFeign 允许你在请求连接和读取数据阶段设置超时时间,具体的超时时间可以通过设置HTTP 客户端的连接超时和读取超时来实现,可以在配置文件中设置超时参数。OpenFeign 底层的HTTP 客户端,可以使用 Apache HttpClient 或 OkHttpClient 来实现,默认使用的是 ApacheClient 实现的。
5.2 重试代码底层实现
通过观察 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 终止 weile 循环】
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);
}
}
}
}
retryer 中 continueOrPropagate 的实现源码如下:
public interface Retryer extends Cloneable {
Retryer NEVER_RETRY = new Retryer() {
public void continueOrPropagate(RetryableException e) {
throw e;
}
public Retryer clone() {
return this;
}
};
void continueOrPropagate(RetryableException var1);
Retryer clone();
public static class Default implements Retryer {
private final int maxAttempts;
private final long period;
private final long maxPeriod;
int attempt;
long sleptForMillis;
public Default() {
this(100L, TimeUnit.SECONDS.toMillis(1L), 5);
}
public Default(long period, long maxPeriod, int maxAttempts) {
this.period = period;
this.maxPeriod = maxPeriod;
this.maxAttempts = maxAttempts;
this.attempt = 1;
}
protected long currentTimeMillis() {
return System.currentTimeMillis();
}
public void continueOrPropagate(RetryableException e) {
if (this.attempt++ >= this.maxAttempts) {
throw e;
} else {
long interval;
if (e.retryAfter() != null) {
interval = e.retryAfter().getTime() - this.currentTimeMillis();
if (interval > this.maxPeriod) {
interval = this.maxPeriod;
}
if (interval < 0L) {
return;
}
} else {
interval = this.nextMaxInterval();
}
try {
Thread.sleep(interval);
} catch (InterruptedException var5) {
Thread.currentThread().interrupt();
throw e;
}
this.sleptForMillis += interval;
}
}
long nextMaxInterval() {
long interval = (long)((double)this.period * Math.pow(1.5, (double)(this.attempt - 1)));
return interval > this.maxPeriod ? this.maxPeriod : interval;
}
public Retryer clone() {
return new Default(this.period, this.maxPeriod, this.maxAttempts);
}
}
}