1. 概述

在本文中,我们将介绍几种在 Spring REST API 中实现请求超时机制的方式,并分析每种方式的优劣。

请求超时主要用于避免用户长时间等待,特别是在调用外部资源时。如果某个请求耗时过长,我们可以采用 Circuit Breaker(熔断器)模式 来降级处理。但本文不深入讨论熔断机制,仅聚焦于如何设置请求超时。

2. 使用 @Transactional 设置超时

一种常见的做法是利用 Spring 的 @Transactional 注解来控制数据库操作的超时时间。它提供了一个 timeout 属性,默认值为 -1,表示永不超时。

示例代码如下:

@GetMapping("/author/transactional")
@Transactional(timeout = 1)
public String getWithTransactionTimeout(@RequestParam String title) {
    bookRepository.wasteTime();
    return bookRepository.findById(title)
      .map(Book::getAuthor)
      .orElse("No book found for this title.");
}

我们模拟了一个耗时操作 wasteTime()

public interface BookRepository extends JpaRepository<Book, String> {

    default int wasteTime() {
        Stopwatch watch = Stopwatch.createStarted();

        // delay for 2 seconds
        while (watch.elapsed(SECONDS) < 2) {
          int i = Integer.MIN_VALUE;
          while (i < Integer.MAX_VALUE) {
              i++;
          }
        }
    }
}

当方法执行超过 1 秒时,会抛出异常,事务回滚。

✅ 优点:

  • 实现简单
  • 适用于数据库事务场景

❌ 缺点:

  • 仅限于数据库操作
  • 需要在每个方法上手动添加注解
  • 超时后仍需等待完整执行时间,无法立即中断

3. 使用 Resilience4j 的 TimeLimiter

Resilience4j 是一个专注于远程调用容错的库,其中的 TimeLimiter 模块非常适合处理请求超时问题。

首先引入依赖:

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-timelimiter</artifactId>
    <version>2.1.0</version>
</dependency>

然后定义一个 500ms 的超时器:

private TimeLimiter ourTimeLimiter = TimeLimiter.of(TimeLimiterConfig.custom()
  .timeoutDuration(Duration.ofMillis(500)).build());

使用方式如下:

@GetMapping("/author/resilience4j")
public Callable<String> getWithResilience4jTimeLimiter(@RequestParam String title) {
    return TimeLimiter.decorateFutureSupplier(ourTimeLimiter, () ->
      CompletableFuture.supplyAsync(() -> {
        bookRepository.wasteTime();
        return bookRepository.findById(title)
          .map(Book::getAuthor)
          .orElse("No book found for this title.");
    }));
}

✅ 优点:

  • 支持毫秒级精度
  • 可以与熔断器配合使用
  • 超时响应更快

❌ 缺点:

  • 仍需手动包装每个接口
  • 返回类型必须是 Callable
  • 错误依然是 500 状态码

4. 使用 Spring MVC 的 request-timeout

Spring 提供了全局配置项 spring.mvc.async.request-timeout,可以设置异步请求的超时时间。

配置示例:

spring.mvc.async.request-timeout=750

接口代码如下:

@GetMapping("/author/mvc-request-timeout")
public Callable<String> getWithMvcRequestTimeout(@RequestParam String title) {
    return () -> {
        bookRepository.wasteTime();
        return bookRepository.findById(title)
          .map(Book::getAuthor)
          .orElse("No book found for this title.");
    };
}

✅ 优点:

  • 全局生效
  • 配置简单
  • 超时返回 503 状态码,语义更清晰

❌ 缺点:

  • 仅对返回 Callable 的接口生效
  • 不支持细粒度控制

5. 在 HTTP 客户端中设置超时

有时候我们只想对某个外部调用设置超时,而不是整个接口。此时可以使用 HTTP 客户端如 WebClientRestClient 进行控制。

5.1. WebClient 超时设置

首先引入 WebFlux 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
    <version>2.4.2</version>
</dependency>

定义 WebClient 并设置响应超时:

@Bean
public WebClient webClient() {
    return WebClient.builder()
      .baseUrl("http://localhost:8080")
      .clientConnector(new ReactorClientHttpConnector(
        HttpClient.create().responseTimeout(Duration.ofMillis(250))
      ))
      .build();
}

使用 WebClient 调用其他接口:

@GetMapping("/author/webclient")
public String getWithWebClient(@RequestParam String title) {
    return webClient.get()
      .uri(uriBuilder -> uriBuilder
        .path("/author/transactional")
        .queryParam("title", title)
        .build())
      .retrieve()
      .bodyToMono(String.class)
      .block();
}

✅ 优点:

  • 可以为不同服务设置不同超时
  • 支持响应式编程模型
  • 提供丰富的错误处理机制

❌ 缺点:

  • 需要引入 WebFlux
  • 语法相对复杂

5.2. RestClient 超时设置

Spring Framework 6 引入了 RestClient,作为 WebClient 的同步替代方案。

引入依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

创建 RestClient Bean:

@Bean
public RestClient restClient() {
    return RestClient.builder()
      .baseUrl("http://localhost:" + serverPort)
      .requestFactory(customRequestFactory())
      .build();
}

ClientHttpRequestFactory customRequestFactory() {
    ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.DEFAULTS
      .withConnectTimeout(Duration.ofMillis(200))
      .withReadTimeout(Duration.ofMillis(200));
    return ClientHttpRequestFactories.get(settings);
}

发起请求:

@GetMapping("/author/restclient")
public String getWithRestClient(@RequestParam String title) {
    return restClient.get()
      .uri(uriBuilder -> uriBuilder
        .path("/author/transactional")
        .queryParam("title", title)
        .build())
      .retrieve()
      .body(String.class);
}

✅ 优点:

  • 同步调用,使用简单
  • API 设计现代化
  • 易于与现有同步代码集成

6. 总结

方案 适用场景 是否全局 是否支持毫秒级 是否需要额外包装
@Transactional 数据库事务
Resilience4j TimeLimiter 熔断/超时控制
spring.mvc.async.request-timeout 全局异步请求超时
WebClient 外部调用超时
RestClient 同步外部调用超时

选择哪种方案取决于你的具体需求:

  • 如果是数据库操作,使用 @Transactional
  • 如果配合熔断机制,推荐 TimeLimiter
  • 全局统一超时控制,使用 spring.mvc.async.request-timeout
  • 细粒度控制外部调用,使用 WebClientRestClient

每种方案都有其适用场景,合理选用可以有效提升系统的健壮性和用户体验。


原始标题:Setting a Request Timeout for a Spring REST API | Baeldung