Spring Retry @Retryable异常处理执行逻辑及重试场景区分问题
问题解答
关于catch块执行时机与recovery配置的问题
你当前的写法存在两个会导致重试完全失效的核心问题:
@Retryable基于Spring AOP代理实现,私有方法无法被AOP拦截,你定义的retrieveItemById是private方法,注解本身不会生效。- AOP重试的触发逻辑是:拦截到方法向外抛出的、匹配
value配置且不在exclude列表里的异常时,才会触发重试。你在方法内部用catch块捕获了所有RestClientException,并且直接返回ResponseEntity没有向外抛异常,代理层完全感知不到异常发生,重试逻辑根本不会执行。
执行顺序上,方法内部的catch块属于方法本身的执行流,优先级远高于外层AOP增强逻辑:只要异常在方法内部被捕获且未重新抛出,重试永远不会触发。
关于recovery块的配置:@Recover注解标记的恢复方法,只有当所有重试次数全部耗尽,方法仍然抛出匹配规则的可重试异常时才会执行。你当前把异常全部吞在方法内部的写法,就算配置了recovery块也无法被触发。是否需要配置recovery取决于业务需求:如果重试耗尽后需要做降级返回、记录特殊告警、返回兜底数据,就配置;如果重试耗尽后直接把异常抛给上层调用方处理,也可以不配置。
区分可重试/不可重试异常的可行方案
你当前直接用RestClientException作为重试触发条件的写法粒度太粗,RestClientException是Spring Rest客户端所有异常的父类,既包含不需要重试的客户端错误,也包含需要重试的临时故障,完全可以通过异常类型+状态码做精准区分:
- 不需要重试的异常:所有4xx开头的客户端错误,对应
HttpClientErrorException类,比如400参数错误、401鉴权失败、403权限不足、404资源不存在,这类异常是请求本身的问题,重试多少次都不会成功,应该直接返回对应错误,不触发重试。 - 需要重试的异常:一类是5xx开头的服务端临时错误,对应
HttpServerErrorException类,比如502网关错误、503服务不可用、504网关超时;另一类是底层网络故障,对应ResourceAccessException类,比如连接超时、读超时、网络闪断、连接重置,这类故障通常是临时的,重试大概率能成功。
具体落地可以参考两种写法:
- 精准配置@Retryable的异常规则,不要用顶层
RestClientException作为触发条件,直接指定需要重试的异常子类,排除不需要重试的异常,示例配置:@Retryable( value = {HttpServerErrorException.class, ResourceAccessException.class}, exclude = HttpClientErrorException.class, maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2) // 指数退避间隔,避免瞬时流量打崩下游 ) - 如果需要更细粒度的判断(比如同样是5xx错误,501 Not Implemented属于服务端永久不支持该方法,不需要重试),可以在方法内捕获异常后做判断,将需要重试的异常包装为自定义的
RetryableApiException抛出,不需要重试的异常直接处理或者包装为业务异常抛出,让@Retryable只匹配自定义的可重试异常即可。
修正后的参考实现
注意要把方法改为public,且放在单独的Spring Bean中,避免同类内部调用导致AOP失效:
@Service @Slf4j public class ItemRemoteService { private final RestTemplate restTemplate; private final HttpConnectionProperties httpConnectionProperties; // 构造函数注入省略 @Retryable( value = {HttpServerErrorException.class, ResourceAccessException.class}, exclude = HttpClientErrorException.class, maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2) ) public ResponseEntity<Item> retrieveItemById(String id) { HttpHeaders headers = new HttpHeaders(); try { return restTemplate.exchange( httpConnectionProperties.getBaseUrl() + "/items/{id}", HttpMethod.GET, new HttpEntity<>(headers), Item.class, id ); } catch (HttpClientErrorException.NotFound e) { log.warn("查询商品不存在,id:{}", id); return new ResponseEntity<>(HttpStatus.NOT_FOUND); } // 可重试异常直接向外抛,交给Spring Retry触发重试 } // 重试耗尽后的兜底逻辑 @Recover public ResponseEntity<Item> recoverForItemQuery(RuntimeException e, String id) { log.error("重试3次仍无法查询商品,id:{}, 触发降级", id, e); return new ResponseEntity<>(HttpStatus.SERVICE_UNAVAILABLE); } }
内容的提问来源于stack exchange,提问作者linuxNoob
相关产品推荐
相关产品推荐

