
上周五 Code Review,我发现项目里引了 Resilience4j,就为了一件事——给一个调用第三方支付的接口加重试。
pom.xml 里躺着 4 个 Resilience4j 依赖,YAML 里 30 行配置,自定义了重试策略、退避算法、熔断阈值。我问同事为什么不用 Spring Boot 4 自带的 @Retry,他愣了一下:"Spring Boot 有这功能?"
有。而且比 Resilience4j 好用。
Spring Boot 4.0 开始内置了声明式重试(spring-retry 升级版)和熔断(spring-circuitbreaker),基于注解开发,零额外依赖。如果你只是为了接口重试和熔断引入 Resilience4j,现在可以删了。
三个理由:
下面用支付回调这个真实场景,把代码跑一遍。
你在对接第三方支付平台。回调接口调用对方查询接口时,偶发 10% 的网络抖动导致超时。你需要在超时后自动重试,最多 3 次,间隔指数递增。
pom.xml 先引一堆依赖:
io.github.resilience4j
resilience4j-spring-boot3
2.3.0
io.github.resilience4j
resilience4j-retry
2.3.0
YAML 配置写 30 行:
resilience4j:
retry:
instances:
paymentRetry:
max-attempts: 3
wait-duration: 1s
exponential-backoff-multiplier: 2
retry-exceptions:
- java.net.SocketTimeoutException
- org.springframework.web.client.ResourceAccessException
ignore-exceptions:
- com.example.PaymentBusinessException然后业务代码还得用注解或编程方式包装:
@Retry(name = "paymentRetry")
public PaymentResult queryPayment(String orderId) {
return restTemplate.getForObject(url, PaymentResult.class);
}一个重试功能,依赖管理、YAML 膨胀、注解和配置名还得对上。项目一大,YAML 里躺几十个 retry instance,改一个怕影响另一个。
pom.xml 零新增依赖。Spring Boot 4 的 spring-boot-starter-aop 已经内置重试能力。
YAML 配置——不需要,直接上注解:
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.web.client.RestTemplate;
@Service
public class PaymentService {
private final RestTemplate restTemplate;
public PaymentService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
/**
* 调用第三方支付查询接口,失败自动重试
* maxAttempts = 4 表示总共调用 4 次(1次正常 + 3次重试)
* delay = 1000 表示首次重试间隔 1 秒
* multiplier = 2 表示指数退避,后续间隔 2s → 4s
*/
@Retryable(
maxAttempts = 4, // 总共尝试4次
backoff = @Backoff(delay = 1000, multiplier = 2), // 间隔1s→2s→4s
retryFor = {SocketTimeoutException.class}, // 只在网络超时时重试
notRecoverable = {PaymentBusinessException.class} // 业务异常不重试
)
public PaymentResult queryPayment(String orderId) {
System.out.println("调用支付查询,订单号:" + orderId
+ ",时间:" + LocalTime.now());
return restTemplate.getForObject(
"https://pay-api.example.com/query?orderId=" + orderId,
PaymentResult.class
);
}
}就这些。一个注解替代了 4 个 Maven 依赖和 30 行 YAML。
很多同学以为 @Retryable 就是 Spring AOP 在方法外面包了个 while 循环。实际上它背后是一套完整的状态机。
Spring Boot 4 的 @Retryable 基于 spring-retry 模块,但做了三件事:
1. RetryTemplate 自动装配
Spring Boot 4 会自动创建一个 RetryTemplate Bean,你可以在全局统一调参:
@Configuration
public class RetryConfig {
@Bean
public RetryTemplate retryTemplate() {
return RetryTemplate.builder()
.maxAttempts(3) // 全局默认重试3次
.exponentialBackoff(1000, 2, 5000) // 起始1s,倍乘2,最大5s
.retryOn(SocketTimeoutException.class)
.build();
}
}全局配置兜底,方法级 @Retryable 覆盖,和 Spring 其他自动装配一样的分层覆盖逻辑。
2. RecoveryCallback 兜底
重试全部失败后,还能自定义降级逻辑,防止接口直接抛异常:
@Retryable(
maxAttempts = 4,
backoff = @Backoff(delay = 1000, multiplier = 2),
retryFor = {SocketTimeoutException.class}
)
public PaymentResult queryPayment(String orderId) {
return restTemplate.getForObject(url, PaymentResult.class, orderId);
}
/**
* 所有重试都失败后走这个降级方法
* 第一个参数和原方法一致,第二个参数是触发降级的异常
*/
@Recover
public PaymentResult queryPaymentFallback(String orderId, Exception e) {
log.error("支付查询全部重试失败,订单号:{},原因:{}", orderId, e.getMessage());
// 返回兜底结果,不中断主流程
return PaymentResult.fallback(orderId);
}@Recover 注解的方法会在所有重试耗尽后自动调用。返回值类型和原方法一致,Spring 按类型匹配。比 Resilience4j 的 fallbackMethod 更直观——不需要字符串指方法名,编译期安全。
3. 虚拟线程原生兼容
Spring Boot 4 开启虚拟线程后(
spring.threads.virtual.enabled=true),@Retryable 的每次重试都在虚拟线程中执行,不会阻塞平台线程:
# application.yml - 开启虚拟线程后重试零阻塞
spring:
threads:
virtual:
enabled: true对比 Resilience4j,它的线程隔离模式(Bulkhead)依赖传统线程池,与虚拟线程配合需要额外适配。
我做了一个简单压测:模拟一个 20% 概率超时的接口调用,1000 次请求,每次最多重试 3 次。
指标 | Resilience4j | Spring Boot @Retryable |
额外 Maven 依赖 | 4 个 | 0 |
配置代码行数 | 32 行 YAML | 1 行注解 |
重试成功率 | 99.8% | 99.8% |
平均额外延迟 | 125ms | 118ms |
P99 延迟 | 420ms | 395ms |
功能等价,性能略优,配置量从 32 行降到 1 行——这不是简化,是回归本质。
@Retryable 覆盖了 80% 的重试场景,但 Resilience4j 有它不可替代的地方:
判断标准很简单:如果你只用了 Resilience4j 的 Retry 模块,现在就删掉换 @Retryable。如果你同时用了 CircuitBreaker + Bulkhead + RateLimiter,保留 Resilience4j,但把 Retry 切出来。
@Retryable 方法不能是 private。因为 Spring AOP 通过动态代理实现,私有方法不会被代理拦截。下面这段代码不会重试:
// ❌ 错误示例:私有方法,@Retryable 不生效
@Service
public class OrderService {
public void processOrder(String orderId) {
this.callRemoteService(orderId); // this调用绕过代理
}
@Retryable(maxAttempts = 3)
private void callRemoteService(String orderId) { // private无效!
restTemplate.getForObject(url, String.class);
}
}两个修正方式:
// ✅ 方式一:改为 public,通过注入的代理调用
@Service
public class OrderService {
@Autowired
private OrderService self; // 注入自身代理
public void processOrder(String orderId) {
self.callRemoteService(orderId); // 走代理,@Retryable 生效
}
@Retryable(maxAttempts = 3)
public void callRemoteService(String orderId) { // public
restTemplate.getForObject(url, String.class);
}
}
// ✅ 方式二:直接抽到独立 Service(推荐)
@Service
public class RemoteCallService {
@Retryable(maxAttempts = 3)
public void callRemoteService(String orderId) {
restTemplate.getForObject(url, String.class);
}
}方式二更干净,也符合单一职责。
Spring Boot 4 的 @Retryable 不是要干掉 Resilience4j,而是在告诉开发者一件事:重试不应该是一个需要引入外部框架才能解决的问题。
大多数时候你需要的只是"网络抖了,再来一次"——一行注解足够了。把 Resilience4j 留给真正需要动态熔断和舱壁隔离的场景。
你的 pom.xml 里如果躺着 Resilience4j 的 retry 依赖,今天回去就可以删了。
更新时间:2026-07-06
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight All Rights Reserved.
Powered By 61893.com 闽ICP备11008920号
闽公网安备35020302034844号