You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 20:57:10