Spring 7.0 三大王炸注解來襲
環(huán)境:SpringBoot3.4.2
1. 簡介
在Spring Boot開發(fā)中,重試機制用于應(yīng)對網(wǎng)絡(luò)波動、服務(wù)不可用等異常,通過自動重試提升系統(tǒng)健壯性;接口并發(fā)控制則防止過多請求導(dǎo)致性能下降。
重試與接口并發(fā)控制功能往往需借助第三方庫或手動實現(xiàn)。例如,重試機制通常通過引入spring-retry包,利用其 @Retryable 注解便捷配置重試次數(shù)、退避策略等,避免在業(yè)務(wù)代碼中硬編碼循環(huán)邏輯;而接口并發(fā)控制則可能依賴Guava RateLimiter實現(xiàn)單機限流,或結(jié)合Redis等分布式鎖工具(如Redisson)保障多實例環(huán)境下的并發(fā)安全。部分場景下,開發(fā)者也會通過AOP自定義注解實現(xiàn)重試或并發(fā)攔截,但需額外編寫切面邏輯,增加了維護成本。
自 Spring 7.x 版本起,框架原生集成了重試與接口并發(fā)控制功能,其中重試機制支持靈活的退避策略與異常匹配配置,而并發(fā)控制目前僅提供單機版限流能力,暫未涵蓋分布式場景下的集群限流支持。同時還提供了 @ImportHttpServices 注解簡化了聲明書Http Client的注冊方式。
2.實戰(zhàn)案例
目前 Spring Framework 7.0 尚未發(fā)布正式版本(尚未進入 RELASE 階段,當(dāng)前仍處于快照版本(SNAPSHOT)開發(fā)階段),正式版本將于2025-11-13發(fā)布。我們需要進行如下配置:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.0-SNAPSHOT</version>
</parent>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>接下來,還需要開啟功能
@SpringBootApplication
@EnableResilientMethods
public class Application {}接下來,我們詳細(xì)介紹這2個實用的功能。
2.1 重試機制@Retryable
@Retryable 是一種通用注解,用于指定單個方法(在方法級別聲明注解)或給定類層次結(jié)構(gòu)中所有代理調(diào)用方法(在類型級別聲明注解)的重試特性。
@Retryable
public void sendMail() {
System.err.printf("%d - 發(fā)送郵件...", DateTimeFormatter.ofPattern("mm:ss").format(LocalDateTime.now())) ;
try {TimeUnit.SECONDS.sleep(1) ;} catch (InterruptedException e) {}
System.err.println(1 / 0) ;
}默認(rèn)情況下,方法調(diào)用將重試拋出的任何異常:初始失敗后最多重試 3 次,兩次重試之間的延遲時間為 1 秒。
如有必要,可對每種方法進行專門調(diào)整,例如,縮小重試的例外情況范圍:
@Retryable(EmailException.class)
public void sendMail() {
// ...
}當(dāng)方法執(zhí)行拋出了EmailException異常,才會進行重試。
或者進行 5 次重試,并采用指數(shù)退避策略,但有一定的抖動:
@Retryable(maxAttempts = 2, delay = 100,
jitter = 10, multiplier = 2, maxDelay = 1000)
public void sendMail() {
// ...
}該示例運行結(jié)果
圖片
重試次數(shù)完了以后才拋出異常。
最后但并非最不重要的一點是,@Retryable 還適用于具有反應(yīng)式返回類型的反應(yīng)式方法,為管道裝飾了 Reactor 的重試功能:
@Retryable(maxAttempts = 5, delay = 100, jitter = 10,
multiplier = 2, maxDelay = 1000)
public Mono<Void> sendMail() {
// ...
return Mono.from(...) ;
}2.2 并發(fā)控制@ConcurrencyLimit
@ConcurrencyLimit 是一種注解,用于指定單個方法(在方法級別聲明注解)或給定類層次結(jié)構(gòu)中所有代理調(diào)用方法(在類型級別聲明注解)的并發(fā)限制。
@ConcurrencyLimit(1)
public void callPhone() {
System.err.printf("%s - %s - 撥打電話...%n", Thread.currentThread().getName(),
DateTimeFormatter.ofPattern("mm:ss:SSS").format(LocalDateTime.now())) ;
try {TimeUnit.SECONDS.sleep(3) ;} catch (InterruptedException e) {}
}這里并發(fā)數(shù)設(shè)置為1,當(dāng)有請求在處理,那么其它的請求則進入等待狀態(tài)。這樣做的目的是防止目標(biāo)資源同時被太多線程訪問,效果類似于線程池或連接池的池大小限制,如果達到限制,就會阻止訪問。
這種限制對虛擬線程特別有用,因為虛擬線程通常沒有線程池限制。對于異步任務(wù),可以在 SimpleAsyncTaskExecutor 上進行限制。對于同步調(diào)用,該注解通過 ConcurrencyThrottleInterceptor 提供了等效的行為。
圖片
2.3 自動注冊HttpClient
通過簡單地聲明接口,Spring 6 可以更輕松地定義 HTTP 客戶端,這與 Feign 的工作原理類似,如下示例:
public interface UserClient {
@GetExchange("/users")
List<User> getUsers();
@GetExchange("/users/{id}")
User getUserById(@PathVariable Long id);
}接下來,我們還需要進行如下的代理配置:
@Configuration
public class HttpClientConfiguration {
@Bean
RestClient.Builder restClient() {
return RestClient.builder();
}
@Bean
UserClient userHttpClient(RestClient.Builder builder) {
RestClient restClient = builder
.baseUrl("http://localhost:8081")
.build();
HttpServiceProxyFactory factory = HttpServiceProxyFactory
.builderFor(RestClientAdapter.create(restClient))
.build();
return factory.createClient(UserClient.class);
}
}如上配置后,我們就可以在其它的Bean中注入UserClient進行遠(yuǎn)程接口調(diào)用了。
從 Spring Framework 7(Spring Boot 4)開始,你可以使用強大的 @ImportHttpServices 注解讓 Spring 自動掃描和注冊你的聲明式接口,從而進一步簡化工作,如下示例:
@Configuration
@ImportHttpServices(
group = "users",
basePackages = "com.pack.client"
)
public class HttpClientConfiguration {
@Bean
RestClient.Builder restClient() {
return RestClient.builder();
}
}通過如上的配置,Spring 會自動檢測并使用組和基礎(chǔ)包注冊它,如下示例:
@RestController
@RequestMapping("/api")
public class ApiController {
private final UserClient userClient ;
public ApiController(UserClient userClient) {
this.userClient = userClient ;
}
@GetMapping("/users")
public List<User> query() {
return this.userClient.getUsers() ;
}
}


































