Spring Boot @Retryable配置noRetryFor后仍重试问题求助
环境信息
- Spring Boot版本:3.3.2
- Java版本:21
问题代码
MyClient类
import io.cloudevents.CloudEvent; import java.io.IOException; import java.util.List; import lombok.extern.slf4j.Slf4j; import org.apache.hc.core5.http.NoHttpResponseException; import org.springframework.http.HttpEntity; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpResponse; import org.springframework.retry.annotation.Backoff; import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Component; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; @Component @Slf4j public class MyClient { private final RestTemplate restTemplate; public MyClient(RestTemplate restTemplate) { this.restTemplate = restTemplate; restTemplate.setInterceptors(createInterceptors()); } @Retryable( retryFor = {NoHttpResponseException.class, RestClientException.class}, noRetryFor = {MyRunTimeException.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 1), listeners = {"MyRetryListener"}) public CloudEvent getDecision(CloudEvent cloudEvent, String endpoint) { HttpEntity<CloudEvent> requestEntity = new HttpEntity<>(cloudEvent); ResponseEntity<CloudEvent> responseEntity = restTemplate.exchange(endpoint, HttpMethod.POST, requestEntity, CloudEvent.class); return responseEntity.getBody(); } private List<ClientHttpRequestInterceptor> createInterceptors() { return List.of( (request, body, execution) -> { try { ClientHttpResponse response = execution.execute(request, body); HttpStatusCode statusCode = response.getStatusCode(); if (!statusCode.is2xxSuccessful()) { throw new MyRunTimeException( "Failed to create decision: response status %s".formatted(statusCode)); } else { return response; } } catch (IOException ex) { throw new MyRunTimeException("Unexpected error: %s".formatted(ex.getMessage()), ex); } }); } }
MyRetryListener类
import lombok.extern.slf4j.Slf4j; import org.springframework.retry.RetryCallback; import org.springframework.retry.RetryContext; import org.springframework.retry.RetryListener; import org.springframework.stereotype.Component; @Component @Slf4j public class MyRetryListener implements RetryListener { public <T, E extends Throwable> void onError( RetryContext context, RetryCallback<T, E> callback, Throwable throwable) { log.error( "My retry attempt {} failed due to: {}", context.getRetryCount(), throwable.getMessage()); } }
问题描述
已将MyRunTimeException添加到@Retryable的noRetryFor列表,但模拟500响应时,拦截器抛出该异常后,仍触发了1次重试,日志显示:
MyRetryListener : My retry attempt 1 failed due to: Failed to create decision: response status 500 INTERNAL_SERVER_ERROR
原因分析
RestTemplate在执行流程中,会将拦截器抛出的自定义异常包装为RestClientException的子类(如ResourceAccessException)。而@Retryable的retryFor中包含了RestClientException,此时实际触发重试的是包装后的异常,noRetryFor中的MyRunTimeException因未被直接抛出,导致规则失效。
解决方案
方案1:缩小重试异常范围
只保留真正需要重试的网络异常(如NoHttpResponseException),移除范围过广的RestClientException,避免误触发重试:
@Retryable( retryFor = {NoHttpResponseException.class}, noRetryFor = {MyRunTimeException.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 1), listeners = {"MyRetryListener"})
方案2:让自定义异常继承RestClientException
修改MyRunTimeException使其继承RestClientException,这样抛出的异常会直接匹配noRetryFor规则(Spring Retry会优先检查noRetryFor,即使异常属于retryFor中的类型):
public class MyRunTimeException extends RestClientException { public MyRunTimeException(String message) { super(message); } public MyRunTimeException(String message, Throwable cause) { super(message, cause); } }
方案3:使用SpEL表达式精准控制
通过exceptionExpression判断异常的根源是否为MyRunTimeException,实现细粒度的重试控制:
@Retryable( retryFor = {NoHttpResponseException.class, RestClientException.class}, exceptionExpression = "#{!(throwable.cause instanceof com.yourpackage.MyRunTimeException)}", maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 1), listeners = {"MyRetryListener"})
验证
修改后重新模拟500响应,MyRunTimeException触发时不会再执行重试,日志中不会出现重试次数的相关记录。
内容的提问来源于stack exchange,提问作者Haohong

