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

如何在@Retryable中排除读取超时但保留连接超时重试?

区分读取超时与连接超时的重试控制

问题核心在于:SocketTimeoutException(读取超时)是被ResourceAccessException包裹的嵌套异常,而@Retryable的exclude属性仅匹配顶层异常类型,不会递归检查嵌套异常,因此直接排除SocketTimeoutException无法生效。以下是三种可行的解决方法:


方法一:自定义RetryPolicy(最灵活)

通过实现自定义重试策略,在判断是否重试时递归检查异常链,精准区分读取超时和连接超时:

import org.springframework.retry.RetryContext;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.web.client.ResourceAccessException;
import java.net.ConnectTimeoutException;
import java.net.SocketTimeoutException;

public class CustomRestRetryPolicy extends SimpleRetryPolicy {

    @Override
    public boolean canRetry(RetryContext context) {
        Throwable lastException = context.getLastThrowable();
        // 若为读取超时,直接返回不重试
        if (isReadTimeout(lastException)) {
            return false;
        }
        // 其他情况沿用父类的重试规则
        return super.canRetry(context);
    }

    // 递归检查异常链,判断是否为读取超时
    private boolean isReadTimeout(Throwable throwable) {
        if (throwable == null) {
            return false;
        }
        if (throwable instanceof ResourceAccessException) {
            Throwable cause = throwable.getCause();
            return cause instanceof SocketTimeoutException;
        }
        return isReadTimeout(throwable.getCause());
    }
}

接着在配置类中注册这个自定义策略:

@Configuration
@EnableRetry
public class RetryConfig {

    @Bean
    public CustomRestRetryPolicy customRestRetryPolicy() {
        return new CustomRestRetryPolicy();
    }
}

最后在业务方法上指定使用该策略:

@Retryable(policy = "customRestRetryPolicy", maxAttempts = 2,
           exclude = {HttpClientErrorException.BadRequest.class})
public ResponseEntity<String> testMethod() throws Exception {
    return restTemplateRtp.exchange("http://localhost:8080/api/test", HttpMethod.POST, request, String.class);
}

方法二:捕获异常并重新抛出区分类型的自定义异常

在业务方法内部捕获ResourceAccessException,解析嵌套异常后抛出不同的自定义异常,通过@Retryable的exclude属性精准控制:

首先定义两个自定义异常:

// 读取超时异常,用于标记不重试的场景
public class ReadTimeoutException extends RuntimeException {
    public ReadTimeoutException(Throwable cause) {
        super(cause);
    }
}

// 连接超时异常,用于标记需要重试的场景
public class ConnectTimeoutException extends RuntimeException {
    public ConnectTimeoutException(Throwable cause) {
        super(cause);
    }
}

修改业务方法逻辑:

@Retryable(value = {RestClientException.class, ConnectTimeoutException.class},
           exclude = {HttpClientErrorException.BadRequest.class, ReadTimeoutException.class},
           maxAttempts = 2)
public ResponseEntity<String> testMethod() throws Exception {
    try {
        return restTemplateRtp.exchange("http://localhost:8080/api/test", HttpMethod.POST, request, String.class);
    } catch (ResourceAccessException e) {
        Throwable rootCause = e.getCause();
        if (rootCause instanceof SocketTimeoutException) {
            // 读取超时,抛出标记性异常,触发不重试
            throw new ReadTimeoutException(rootCause);
        } else if (rootCause instanceof java.net.ConnectTimeoutException) {
            // 连接超时,抛出标记性异常,触发重试
            throw new ConnectTimeoutException(rootCause);
        }
        // 其他IO异常,原样抛出
        throw e;
    }
}

方法三:使用@Retryable的exceptionExpression(Spring Retry 1.3+)

利用SpEL表达式直接检查异常的嵌套情况,无需额外代码:

@Retryable(value = {RestClientException.class},
           exclude = {HttpClientErrorException.BadRequest.class},
           maxAttempts = 2,
           exceptionExpression = "#{!(root.cause instanceof T(java.net.SocketTimeoutException))}")
public ResponseEntity<String> testMethod() throws Exception {
    return restTemplateRtp.exchange("http://localhost:8080/api/test", HttpMethod.POST, request, String.class);
}

表达式含义:当异常的根因是SocketTimeoutException时,不执行重试;其他符合条件的异常正常触发重试。


内容的提问来源于stack exchange,提问作者Guru

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 05:35:05