Spring Boot 3.2 RestClient 实战:替代 RestTemplate 调外部接口,超时、连接池与错误处理

Spring Boot 3.2 RestClient 实战:替代 RestTemplate 调外部接口,超时、连接池与错误处理

调外部接口这件事,RestTemplate 用了十年,但它的 API 冗长、异常处理反人类,而 WebClient 又强制你引入 WebFlux 一整套响应式栈,同步场景下水土不服。Spring 6.1 / Spring Boot 3.2 给了第三个选择:RestClient------同步、链式、基于和 WebClient 一样的底层,却不用你写一行响应式代码。这篇手把手把它配到生产可用。

先看 RestTemplate 的痛在哪

一个「带超时、带错误处理」的 GET 请求,RestTemplate 大概长这样:

java 复制代码
// RestTemplate:配置和调用是割裂的
RestTemplate rt = new RestTemplate();
try {
    ResponseEntity<User> resp = rt.getForEntity(
        "https://api.example.com/users/{id}", User.class, 42);
    User user = resp.getBody();
} catch (HttpClientErrorException e) {
    // 4xx 在这里
} catch (HttpServerErrorException e) {
    // 5xx 在这里,还得分开两个 catch
}

超时配置要单独在构造 RestTemplate 时通过 ClientHttpRequestFactory 设,和调用点分离;错误又拆成 HttpClientErrorException / HttpServerErrorException 两个异常。写多了很啰嗦。

RestClient 的基础用法

RestClient 是链式的,读起来像一句话:

java 复制代码
RestClient client = RestClient.create();

User user = client.get()
        .uri("https://api.example.com/users/{id}", 42)
        .retrieve()                 // 触发请求
        .body(User.class);          // 反序列化响应体

retrieve() 默认对 4xx/5xx 抛 RestClientResponseException。如果你想拿到完整响应(状态码、header),用 toEntity:

java 复制代码
ResponseEntity<User> entity = client.get()
        .uri("https://api.example.com/users/{id}", 42)
        .retrieve()
        .toEntity(User.class);

int status = entity.getStatusCode().value();
User body = entity.getBody();

关键:配一个能上生产的 RestClient Bean

RestClient.create() 用的是 JDK 默认的 HttpURLConnection,没有连接池、没有合理超时,高并发下会不断新建连接、慢接口会把线程拖死。生产环境务必换成 Apache HttpClient 5 并显式配连接池和超时。

先加依赖(Spring Boot 已管版本,不用写版本号):

xml 复制代码
<dependency>
    <groupId>org.apache.httpcomponents.client5</groupId>
    <artifactId>httpclient5</artifactId>
</dependency>

然后把它做成一个 Bean 复用------别在每次调用时新建 RestClient,那样连接池根本复用不起来:

java 复制代码
@Configuration
public class RestClientConfig {

    @Bean
    public RestClient externalApiClient() {
        // 连接池:控制总连接数和每个路由(目标主机)的连接数
        PoolingHttpClientConnectionManager cm =
                PoolingHttpClientConnectionManagerBuilder.create()
                        .setMaxConnTotal(100)      // 池子总上限
                        .setMaxConnPerRoute(20)     // 单个目标主机上限
                        .build();

        // 超时:连接超时 + 响应超时,两个都要设
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectionRequestTimeout(Timeout.ofSeconds(2)) // 从池里拿连接的等待上限
                .setResponseTimeout(Timeout.ofSeconds(5))          // 等响应的上限
                .build();

        CloseableHttpClient httpClient = HttpClients.custom()
                .setConnectionManager(cm)
                .setDefaultRequestConfig(requestConfig)
                .build();

        var factory = new HttpComponentsClientHttpRequestFactory(httpClient);

        return RestClient.builder()
                .baseUrl("https://api.example.com")  // 统一前缀,调用处只写路径
                .requestFactory(factory)
                .defaultHeader("User-Agent", "my-service/1.0")
                .build();
    }
}

三个超时各管一段,缺一不可:connectionRequestTimeout 是「从连接池里借连接」的等待上限(池满时保护你不被无限阻塞),connect(可在 factory 上设)是 TCP 握手上限,responseTimeout 是「发出请求后等对方回数据」的上限。只设一个都会在某种慢故障下裸奔。

错误处理:onStatus 精准兜住失败

retrieve() 默认把 4xx/5xx 都抛异常,但异常里的 body 常常被吞掉。用 onStatus 可以针对状态码段自定义处理,把对方返回的错误信息读出来:

java 复制代码
User user = client.get()
        .uri("/users/{id}", id)
        .retrieve()
        .onStatus(HttpStatusCode::is4xxClientError, (req, resp) -> {
            // 读出对方返回的错误 body,别让它白白丢掉
            String errBody = new String(resp.getBody().readAllBytes(),
                                        StandardCharsets.UTF_8);
            if (resp.getStatusCode().value() == 404) {
                throw new UserNotFoundException(id);
            }
            throw new ExternalApiException("客户端错误: " + errBody);
        })
        .onStatus(HttpStatusCode::is5xxServerError, (req, resp) -> {
            // 5xx 通常可重试,抛一个可识别的异常给上层做退避重试
            throw new ExternalApiRetryableException(
                    "对方服务异常: " + resp.getStatusCode());
        })
        .body(User.class);

把「客户端错误」和「可重试的服务端错误」分成不同异常类型,上层就能针对性决定是直接失败还是退避重试,而不是笼统 catch 一个 RestClientException

POST 与传对象

发 JSON 只需 body(对象),Jackson 自动序列化,contentType 也会自动带上:

java 复制代码
CreateOrderResponse resp = client.post()
        .uri("/orders")
        .contentType(MediaType.APPLICATION_JSON)
        .body(new CreateOrderRequest("SKU-123", 2))
        .retrieve()
        .body(CreateOrderResponse.class);

一个容易忽略的坑:baseUrl 拼接与前导斜杠

设了 baseUrl("https://api.example.com/api/v1") 后,调用处的 uri 写法会影响最终地址:

java 复制代码
// baseUrl = https://api.example.com/api/v1
.uri("/users")    // → https://api.example.com/users   ❌ 前导斜杠会覆盖 baseUrl 的路径部分
.uri("users")     // → https://api.example.com/api/v1/users  ✅

/users 这种带前导斜杠的绝对路径会把 baseUrl 里的 /api/v1 覆盖掉,这是 URI 解析规范的行为,不是 bug。用了带路径的 baseUrl 时,拼接段不要加前导斜杠。

小结

  • RestClient(Spring Boot 3.2+)是同步 HTTP 调用的新首选:链式 API、不依赖 WebFlux,底层与 WebClient 同源。
  • 生产环境必须 换 Apache HttpClient 5 并配连接池,把 RestClient 做成 Bean 复用 ;裸 create() 无池无超时,高并发会崩。
  • 三个超时各管一段------借连接、TCP 握手、等响应,一个都不能少。
  • onStatus 把 4xx/5xx 分类成不同业务异常,并读出对方 error body,别让失败信息被吞。
  • baseUrl 带路径时,拼接段别加前导斜杠,否则会覆盖掉基础路径。
  • 记忆点:RestClient 好写,但真正决定它能不能扛生产的,是那个带连接池和超时的 Bean。
相关推荐
雪隐1 小时前
16GB 显卡跑 Qwen3.8-27B,只要 7 GB:三进制模型 Bonsai 2 部署手记与双格式实测
人工智能·后端
云浪1 小时前
Go Context 到底是什么?
后端·go
摇滚侠1 小时前
《Spring Boot 3:高级与架构设计》第 2 章 IOC容器的高级机制 BeanFactoryPostProcessor 个人理解 6
java·spring boot·笔记·后端
苏三说技术1 小时前
推荐一个牛逼的AgentScope系统
后端
行百里er2 小时前
5 分钟跑起 Redis(Docker 版)
redis·后端·docker
程序员阿鹏2 小时前
简单介绍一下软引用
java·开发语言·jvm·数据结构·后端
回家路上绕了弯2 小时前
如何提升 AI 编码准确性:从“生成代码”到“验证结果”
后端
SL_staff2 小时前
项目延期时如何用四类角色机制实现责任锚定?一线技术实践拆解
java·低代码·团队管理
newerp2 小时前
系统调用与 M/P 解绑
后端·程序员·go