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。