Spring @Retryable调用时出现Cannot locate recovery method异常排查
@Retryable注解恢复方法未触发的排查思路
问题概述
调用标注@Retryable的方法时出现Cannot locate recovery method异常,已创建@Recover恢复方法,但重试耗尽后恢复方法未触发。应用已添加@EnableRetry注解,通过故意返回500状态码抛出MyClientException触发重试,但最终仅抛出异常,恢复逻辑未执行。
业务代码
@Retryable(maxAttempts = 3, backoff = @Backoff(random = true, maxDelay = 4000, multiplier = 2.5), value = {MyClientException.class}) public ResponseEntity<String> getResponseEntity(MyModel myModel, int retryCount, JsonNode rootNode,String uri) throws MyClientException { ResponseEntity<String> response = ResponseEntity.internalServerError().body("ghj"); //故意触发恢复方法 if (!HttpStatus.OK.equals(response.getStatusCode())) { log.info("[METHOD] Error while publishing message: {} uri: {}, result: {}, attempt: {}", serviceStatus, uri, response, retryCount); throw new MyClientException(); } return response; } @Recover @Counted(value = CustomMetrics.TOTAL_MSG_PROCESSED) public ResponseEntity<String> recover(Exception e, MyModel myModel) { try { log.warn("All retries to persist serviceStatus: {} failed. publishing event async as fallback.", myModel); asyncClient.publish(serviceStatus); return ResponseEntity.ok().body("Submitted to persist as async"); } catch (Exception ex) { log.error("Unable to publish event: {} as fallback. Failed with error: {} ", serviceStatus, properties.getDeadBucket(), ExceptionUtils.getStackTrace(ex)); return ResponseEntity.internalServerError().body("Unable to publish"); } }
单元测试代码
@Test public void testRecoverCalledWhenRetryExhausted() throws Exception { RetryTemplate retryTemplate = new RetryTemplate(); RetryPolicy retryPolicy = new SimpleRetryPolicy(3); retryTemplate.setRetryPolicy(retryPolicy); try { retryTemplate.execute(context -> { myClientTest.getResponseEntity(new MyModel(), context.getRetryCount(), mock(JsonNode.class), "testUri"); return null; }); } catch (ExhaustedRetryException e) { // 验证恢复方法是否被调用 verify(MyClientException, times(1)).recover(any(Exception.class), any(myModel.class)); } }
日志信息
10:43:29.586 [main] INFO com.----ient -- [[METHOD] Error while publishing message: {"--(hiding data--"} uri: testUri, result: <500 INTERNAL_SERVER_ERROR Internal Server Error,ghj,[]>, attempt: 0 10:43:29.591 [main] INFO com.-... -- [[METHOD] Error while publishing message: {"--(hiding data--"} uri: testUri, result: <500 INTERNAL_SERVER_ERROR Internal Server Error,ghj,[]>, attempt: 1 10:43:29.591 [main] INFO com.... -- [METHOD] Error while publishing message: {"--(hiding data--"} uri: testUri, result: <500 INTERNAL_SERVER_ERROR Internal Server Error,ghj,[]>, attempt: 2 MyException() at com.wayfair.sct.ocp.status.service.client.services.StatusServiceRestClient.getResponseEntity(StatusServiceRestClient.java:73) at com.......MyClientTest.lambda$testRecoverCalledWhenRetryExhausted$0(MyCliemtClientTest.java:131)
排查步骤
- 参数匹配检查:Spring Retry要求
@Recover方法的参数需满足:第一个参数为触发重试的异常类型,后续参数需与原方法的参数按顺序完全匹配(可省略末尾参数,但不能跳过中间参数)。当前恢复方法仅匹配了MyModel,缺少原方法的retryCount、JsonNode、uri参数,导致无法定位恢复方法。需修改恢复方法参数为:@Recover @Counted(value = CustomMetrics.TOTAL_MSG_PROCESSED) public ResponseEntity<String> recover(MyClientException e, MyModel myModel, int retryCount, JsonNode rootNode, String uri) { // 原有逻辑 } - 异常类型匹配:
@Retryable指定的重试异常是MyClientException,恢复方法的第一个参数建议精确使用MyClientException而非父类Exception,避免匹配歧义。 - 单元测试逻辑修正:手动创建
RetryTemplate会绕过Spring Retry的注解驱动机制,@Retryable和@Recover依赖Spring AOP代理生效。测试时需通过Spring容器获取代理后的bean调用方法,而非直接调用或手动用RetryTemplate包裹。 - AOP代理有效性检查:确保被
@Retryable标注的方法所在类被Spring管理(如标注@Service/@Component),且避免类内部自调用(自调用会跳过AOP代理,导致重试和恢复逻辑不生效)。 - 恢复方法访问权限检查:确认
@Recover方法为public修饰符,且与原方法在同一类中(默认仅识别同类中的恢复方法)。
内容的提问来源于stack exchange,提问作者Geek_To_Learn
相关产品推荐
相关产品推荐

