feign的重试默认是不开启的,一般再业务中对于重试都非常谨慎,特别是写操作,一定都要保证目标接口实现了幂等才能发起重试,不然就是灾难。但是在一些场景下重试可以简化实现逻辑,比如接口的jwt token过期了,那么在请求响应token过期错误的时候将token刷新后发起重试可以避免请求失败,并且也减少了token有效期的维护逻辑。
因为不是全局开启重试,单独写一个Config类然后作为参数传递给feign client是最好的方式。
流程上大概是从请求结果得知token过期并重置token,然后交给重试器去判断是否需要重置,随后在重试的请求拦截器内刷新token并设置新的token。这里需要自定义feign的RequestInterceptor 、Retryer、ErrorDecoder 一起组合完成判断、重试和token的刷新
public class UserApiClientConfig {
@Bean
public RequestInterceptor requestInterceptor() {
return requestTemplate -> requestTemplate.header("Authorization", TokenProvider.getToken());
}
@Bean
public Retryer retryer() {
return new Retryer() {
int tryTimes;
@Override
public void continueOrPropagate(RetryableException e) {
// try only once
if (this.tryTimes++ >= 1) {
throw e;
}
if (e instanceof MyRetryException && e.getClass().equals(MyRetryException.class)) {
return;
}
throw e;
}
@Override
public Retryer clone() {
return this;
}
};
}
@Bean
public ErrorDecoder errorDecoder() {
return (methodKey, response) -> {
FeignException exception = FeignException.errorStatus(methodKey, response);
if (response.status() == HttpStatus.PAYMENT_REQUIRED.value()) {
// 这里也可以对response.body再做一次判断
// String bodyString = exception.contentUTF8();
// 重试之前将token重置,然后在重试请求的interceptor里面通过getToken触发重新获取
TokenProvider.resetToken();
return new MyRetryException(response.status(), exception.getMessage(),
response.request().httpMethod(), exception, null, response.request());
}
return exception;
};
}
static class MyRetryException extends RetryableException {
public MyRetryException(int status, String message, Request.HttpMethod httpMethod, Throwable cause, Date retryAfter, Request request) {
super(status, message, httpMethod, cause, retryAfter, request);
}
}
}
feign通过调用配置的Retryer.continueOrPropagate判断是否可以重试,如果抛exception那么重试就终止。需要重试的时候直接return,详细可以参考Retryer接口的Default实现,默认情况下开启的话feign会重试5次。我们只需要刷新token,因此重试一次即可,加计数器并在clone方法里传递到下一次重试中用于判断重试次数
为了方便判断我定义了一个MyRetryException并在errorDecoder里面判断http状态码为token过期状态码(我们使用的是402),更为严谨的做法是还要判断响应内容的业务状态码。在读取http response body的时候需要注意,在FeignException.errorStatus方法内已经将body读取并关闭了stream,这时可以通过FeignException的contentUTF8方法读取
如果要看重试的情况,可以开启debug日志,在重试前,feign会打印日志如下
---> RETRYING