这里写自定义目录标题
需求
实习期间,接到了一个mentor给的技术上的需求:FeignClient调用的时候容易失败,比如被调用方正在部署服务,节点下线的瞬间,还可能会请求到这个下线节点,调用就会失败;添加重试机制,不然总是报错,发邮件。
- AC-1、能够灵活配置哪些接口能够重试,哪些接口不能重试
- AC-2、可以控制重试次数
- AC-3、默认针对Http code 500报错的接口进行重试,400的不需要
解决方案
方案1:自定义超时重试类(实现 Retryer 接口,并重写 continueOrPropagate 方法)
具体实现见OpenFeign 的超时重试机制以及底层实现原理
这种方式实现起来较为麻烦,如果我们在两个feign接口要使用不同的重试参数,是不是要写两个重试类?且无法根据返回状态码来判断是否需要重试。
方案2:使用Spring的@Retryable注解
@Retryable(value = Exception.class, maxAttempts = 4, backoff = @Backoff(delay = 2000L, multiplier = 1.5))
用这种注解的方式,更为灵活,但还是只会根据抛出的异常判断重试,我们发现feign在报错时会抛出FeignException,猜测feign可能是将500等返回状态码的异常进行了封装,所以我们只需要在注解上加入状态码对应的异常不就可以达到预期的效果了吗?下面来看看feign的源码是怎么做的:
Feign部分源码分析
ReflectiveFeign --> invoke
首先,debug断点跟进,进入的第一个方法是ReflectiveFeign中的invode方法,这里也可以看出feign底层使用了动态代理:
这里可以看到就是一些基本的判断逻辑,如果不是equals这种特殊方法,就会进入下面的那个invoke方法里;
SynchronousMethodHandler --> invoke
可以看到,第一行代码,创建了一个RequestTemplate,而且里面的值都是空的,从这里也可以看出,feign底层是创建了一个新的http请求,所以这里要注意了:如果你远程调用的接口需要从请求头里校验一些认证的信息,例如session,那么默认的feign远程调用是不行的,因为它会新创建一个请求,并不会携带这些信息。但是feign提供了这种情况下的解决方案,我们继续往下看。
SynchronousMethodHandler --> executeAndDecode
在上面invoke方法中,最终都会进入到executeAndDecode这个方法:
首先会进入targetRequest方法:
看到这,我相信已经有了前面的那个疑问的答案:feign允许我们指定一些拦截器,在这些拦截器中,我们可以设置一些请求头相关的信息。举个例子:
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor() {
RequestInterceptor requestInterceptor = new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
//1、使用RequestContextHolder拿到刚进来的请求数据
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes != null) {
//老请求
HttpServletRequest request = requestAttributes.getRequest();
if (request != null) {
//2、同步请求头的数据(主要是cookie)
//把老请求的cookie值放到新请求上来,进行一个同步
String cookie = request.getHeader("Cookie");
template.header("Cookie", cookie);
}
}
}
};
return requestInterceptor;
}
再回到executeAndDecode这个方法,在执行完拦截器过后就是真正远程http调用的逻辑了,我们这里重点关注对于请求响应的处理。
IOException处理——自动retry
SynchronousMethodHandler.java --> executeAndDecode
首先就是IO异常,可以看到这里如果出现了IOException,包括连接异常,SocketException等等,会进入errorExecuting方法:
抛出了RetrableException,细心的小伙伴可能注意到了,之前在SynchronousMethodHandler的invoke方法中调用executeAndDecode方法时,对RetrableException进行了catch处理:
现在就很清晰了,对于IOException,feign会自动触发重试机制(retryer.continueOrPropagate(e)),使用默认的实现,默认重试3次,当然也可以自定义,来设置重试次数以及重试的时间间隔。
feign对于不同状态码的异常类封装
我们再次回到executeAndDecode方法,如果没有发生IOException,就会进入handleResponse方法:
SynchronousMethodHandler.java --> executeAndDecode
关键在于InvocationContext这个类的proceed方法:
可以看到feign会先根据返回状态码做判断,我们一般的客户端异常和服务器异常都会进入decodeError方法中:
decodeError会调用实现了ErrorDecoder接口的类的decode方法,所以我们也可以实现ErrorDecoder接口进行自定义异常处理逻辑。
我们来看默认的实现:
在默认的实现中,会调用errorStatus方法,最后会到这里:
到这里,谜底揭开了,feign底层会根据http返回状态码的不同,抛出不同的异常。那么我们就可以在@Retryable注解中加入对应的异常类来实现根据远程调用的返回状态码来判断是否需要重试这一功能。
Feign源码简要总结
- 底层会创建新的http请求,会导致远程调用请求头丢失问题。(SynchronousMethodHandler类的invoke方法)
- 在真正发起远程调用的http请求之前,会执行拦截器,默认为空,用户可以添加,实现RequestInterceptor接口,注入bean。
- 对于远程调用过程中发生的IO异常,feign会抛出RetryableException,并捕获,在catch块中使用默认的重试器自动重试,默认重试3次。
- 对于其他类型的异常,feign会根据http相应状态码抛出对应的异常,它们都继承自FeignException。用户也可以实现ErrorDecoder接口,重写decode方法,自定义异常处理逻辑(除了IOException)。
demo
对远程调用请求返回的http状态码为500的接口,重试3次,
@Retryable(retryFor = {FeignException.FeignServerException.class}, maxAttempts = 3, backoff = @Backoff(delay = 2000L, multiplier = 1.5))
FeignServerException包含了5打头的所有异常。
依然存在的问题
在上面的分析中,我们是利用返回的异常类型来间接判断远程调用的状态码,但有时系统设计时,会手动在底层将所有接口的http返回值都设置为200,在返回类中再定义一个字段,用于存储报错时返回的对象,例如:
如果远程调用的接口返回类型是这样的话,上面的方法就行不通了。
我们注意到,在执行InvocationContext这个类的proceed方法时,如果没有报错,则执行最后一行的decode方法:
它会执行实现了Decoder接口的类的decode方法:
那么我们是不是可以写一个类实现Decoder接口,重写decode方法,因为decode方法接受了Response参数,我们就可以根据这个参数,进行相应的处理?
我认为是可以的,如下:
public static class RetryableDecoder implements Decoder {
private final SpringDecoder decoder;
public RetryableDecoder(SpringDecoder decoder) {
this.decoder = decoder;
}
@Override
public Object decode(Response response, Type type) throws IOException, FeignException {
Integer code = null;
try {
if (response.body() == null)
return null;
Response.Body body = response.body();
ResultData result = JsonUtil.parseObject(Util.toString(body.asReader(Util.UTF_8)), ResultData.class);
code = result.getError().getCode();
} catch (Exception exception) {
log.info("FeignClient decoding error...", exception);
throw RetryFeignException.errorStatus(response);
}
if (code != null && code == HttpStatus.INTERNAL_SERVER_ERROR.value()) {
// The condition here needs to be changed if you want to retry for other response codes
throw RetryFeignException.errorStatus(response);
}
return this.decoder.decode(response, type);
}
}