Spring Boot 3.2 RestClient:现代化同步HTTP客户端深度解析与实践指南
1. 项目概述为什么我们需要一个新的 RestClient如果你和我一样在过去几年里深度使用 Spring Boot 进行微服务或分布式系统开发那么对于发起 HTTP 请求这件事你一定有过不少纠结。从最早的RestTemplate到后来社区力推的WebClient再到各种第三方封装选择不少但痛点也一直存在。RestTemplate简单直接但功能上总觉得差那么点意思尤其是在响应式和非阻塞编程成为趋势的今天它显得有些“老派”。WebClient功能强大响应式编程范儿很酷但对于一个简单的同步 HTTP 调用来说它的学习曲线和代码量又显得有点“杀鸡用牛刀”。就在这种背景下Spring Boot 3.2 带来了一个让我眼前一亮的特性RestClient。它不是对旧框架的修修补补而是一个全新的、现代化的同步 HTTP 客户端旨在成为RestTemplate的继任者同时吸收了WebClient在 API 设计上的诸多优点。我第一次在官方文档里看到它时感觉就像 Spring 团队终于听到了我们这些一线开发者的心声——我们需要一个既简单易用又功能强大、符合现代 Java 编码习惯的 HTTP 客户端。简单来说RestClient 的定位非常清晰为同步 HTTP 调用提供一套流畅、声明式的 API。它底层默认基于我们熟悉的HttpClientJDK 11性能有保障。它的 API 设计借鉴了WebClient的Builder模式和链式调用写起来非常流畅同时又去掉了响应式那些复杂的Mono、Flux概念回归同步世界的直观。对于绝大多数日常的 REST API 调用、第三方服务集成场景RestClient 提供了一个近乎完美的选择。它解决了“我想简单快速地发个请求但又不想用那个略显过时的RestTemplate”的核心矛盾。2. 核心特性与设计哲学深度解析2.1 流畅的链式 API从“怎么做”到“做什么”RestClient 最直观的改变就是其 API 设计。我们来回想一下RestTemplate的典型用法你需要先创建一个实例或者注入一个配置好的 Bean然后调用getForObject、postForEntity这类方法传入 URL、请求体、响应类型等一堆参数。代码逻辑是“命令式”的告诉框架“一步一步怎么做”。而 RestClient 采用了“流畅接口”和“建造者模式”。你通过RestClient.create()或RestClient.builder()开始然后像搭积木一样通过链式调用一步步声明你的请求设置基础 URL、添加默认头信息、配置拦截器、定义错误处理最后才执行请求并处理响应。整个代码读起来更像是在描述“我要做什么”而不是“我该如何做”。举个例子一个带认证和错误处理的 GET 请求在 RestClient 中可能长这样MyResponse response restClient.get() .uri(/api/v1/users/{id}, userId) .header(Authorization, Bearer token) .retrieve() .onStatus(status - status.value() 404, (request, response) - { throw new UserNotFoundException(User not found with id: userId); }) .body(MyResponse.class);这段代码从上到下清晰地表达了意图获取get某个资源uri携带认证头header检索响应retrieve针对 404 状态码进行特殊处理onStatus最后将响应体转换为特定类型body。这种声明式的风格极大地提升了代码的可读性和可维护性。2.2 强大的响应处理与错误处理机制错误处理一直是 HTTP 客户端编程中的繁琐环节。RestTemplate默认在遇到 4xx/5xx 状态码时会抛出HttpClientErrorException或HttpServerErrorException你需要用try-catch来包裹或者配置一个自定义的ResponseErrorHandler后者配置起来并不直观。RestClient 在这方面做了大幅改进引入了更精细、更灵活的错误处理策略。核心方法是retrieve()后可以链式调用的onStatus方法。这个方法接受一个PredicateHttpStatusCode来判断哪些状态码需要被视作错误以及一个RestClient.ResponseSpec.ErrorHandler来处理这个错误。你可以为不同的状态码定义不同的处理逻辑。String result restClient.get() .uri(/api/items/{id}, itemId) .retrieve() .onStatus(status - status.is4xxClientError(), (req, resp) - { // 处理所有4xx错误例如记录日志或抛出自定义业务异常 log.warn(Client error occurred for request: {}, req.getURI()); throw new BusinessException(Client request error); }) .onStatus(status - status.value() 503, (req, resp) - { // 专门处理503服务不可用 throw new ServiceUnavailableException(Backend service is down); }) .body(String.class);这种设计的好处是错误处理逻辑和正常的业务逻辑在代码结构上是分离且清晰的。你可以针对不同的 API、不同的错误类型进行定制而不是一股脑地捕获一个通用的异常再去里面做instanceof判断。对于响应体的处理body(ClassT)方法会自动利用配置的HttpMessageConverter进行转换和 Spring MVC 中的体验保持一致非常顺手。2.3 灵活的配置与扩展能力RestClient 的配置入口是RestClient.Builder。通过这个建造者你可以集中配置一些全局行为然后基于此创建出具有特定配置的RestClient实例。这种设计非常适合在微服务环境中为不同的下游服务配置不同的客户端实例。1. 基础配置示例Bean public RestClient orderServiceClient() { return RestClient.builder() .baseUrl(http://order-service:8080) .defaultHeader(X-Client-ID, my-app) .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .requestInterceptor(new LoggingInterceptor()) .build(); } Bean public RestClient paymentServiceClient() { return RestClient.builder() .baseUrl(https://api.payment.com) .defaultHeader(Authorization, Basic encodeCredentials(user, pass)) .requestInitializer(request - request.getHeaders().setAccept(List.of(MediaType.APPLICATION_JSON))) .build(); }这里我为订单服务和支付服务分别创建了独立的RestClientBean。每个客户端都有自己的基础 URL、默认请求头和拦截器。这种细粒度的配置比在全局使用一个RestTemplate并动态修改请求参数要清晰和安全得多。2. 核心配置项解析baseUrl: 设置所有请求的默认基础路径后续的uri()调用都是相对路径。defaultHeader/defaultHeaders: 设置每个请求都会携带的默认头信息如认证令牌、内容类型等。requestInterceptor: 添加请求拦截器。这是非常强大的扩展点你可以在这里统一添加签名、链路追踪TraceId、日志记录、重试逻辑等。拦截器接收ClientHttpRequest允许你修改请求头、体甚至 URI。requestFactory: 自定义用于创建底层连接的ClientHttpRequestFactory。大多数情况下默认的基于 JDKHttpClient的工厂已经足够但在需要特殊代理或 SSL 配置时可以在这里进行定制。messageConverters: 配置用于序列化请求体和反序列化响应体的转换器列表。Spring Boot 会自动配置好一套常用的如 JSON 用的MappingJackson2HttpMessageConverter你也可以自定义或调整顺序。实操心得拦截器的正确使用姿势拦截器是复用通用逻辑的利器。一个常见的模式是创建一个“认证拦截器”从线程上下文或安全上下文中获取当前用户的令牌并自动添加到请求头中。但要注意拦截器的执行顺序以及避免在拦截器中进行复杂的阻塞操作以免影响客户端性能。另外对于需要重试的场景更推荐使用 Spring Retry 等专用框架在拦截器外层或通过ExchangeFilterFunction如果使用WebClient风格的过滤器来实现而不是在拦截器内写循环重试逻辑。3. 从零到一RestClient 的完整实操指南3.1 环境准备与基础依赖要使用 RestClient首先确保你的项目是基于 Spring Boot 3.2 或更高版本。如果你是从旧版本升级需要检查相关依赖的兼容性。对于新项目直接使用 Spring Initializr 生成即可。核心依赖就是spring-boot-starter-web它已经包含了 Spring MVC 和相关的 HTTP 客户端支持。如果你是一个纯 WebFlux 项目只有spring-boot-starter-webflux那么默认的同步RestClient可能不可用你需要额外添加spring-boot-starter-web依赖或者考虑直接使用WebClient。Maven 依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependencyGradle 依赖implementation org.springframework.boot:spring-boot-starter-web无需其他特殊依赖。Spring Boot 的自动配置会为我们准备好RestClient.BuilderBean我们可以直接注入它来创建自定义的RestClient实例。3.2 四种创建与配置方式详解根据不同的使用场景RestClient 提供了多种创建方式灵活度很高。方式一最简创建适用于临时、简单的请求RestClient restClient RestClient.create();这种方式创建的客户端没有任何默认配置如基础 URL、默认头。适合在方法内部发起一次性的、配置简单的请求。但不推荐作为 Bean 注入因为缺乏统一配置。方式二通过 Builder 创建推荐用于定义可复用的客户端这是最常用、最推荐的方式尤其是在需要定义多个面向不同服务的客户端时。import org.springframework.web.client.RestClient; Configuration public class RestClientConfig { Bean public RestClient weatherApiClient(RestClient.Builder builder) { return builder .baseUrl(https://api.weather.com/v3) .defaultHeader(api-key, your-api-key-here) .build(); } Bean public RestClient internalUserServiceClient(RestClient.Builder builder) { return builder .baseUrl(http://user-service:8081) .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .requestInterceptor(new RequestIdInterceptor()) .build(); } }在配置类中我们可以注入 Spring 自动配置好的RestClient.Builder。这个 Builder 本身可能已经携带了一些全局配置例如通过application.properties配置的代理。我们在其基础上为不同的目标服务添加特定的配置baseUrl,defaultHeader等然后build()出独立的RestClientBean。这样在业务代码中我们可以通过Autowired注入weatherApiClient或internalUserServiceClient来使用代码意图非常清晰。方式三自定义全局 Builder统一修改默认行为如果你想修改所有通过RestClient.builder()创建的客户端的默认行为可以自定义一个RestClient.BuilderBean。Bean public RestClient.Builder restClientBuilder() { return RestClient.builder() .requestInterceptor(new MetricsInterceptor()) // 全局监控拦截器 .requestFactory(new HttpComponentsClientHttpRequestFactory()); // 切换为 Apache HttpClient }定义了这个 Bean 后项目中任何通过RestClient.builder()或注入RestClient.Builder的地方都会使用你这个经过自定义的 Builder 实例。注意这会覆盖 Spring Boot 的默认 Builder。方式四从 RestTemplate 迁移平滑升级如果你有现有的RestTemplateBean并且已经对其进行了复杂的配置如自定义转换器、错误处理器RestClient 提供了一个便捷的迁移路径Bean public RestClient customRestClient(RestTemplate oldRestTemplate) { return RestClient.builder(oldRestTemplate).build(); }RestClient.builder(RestTemplate)会从RestTemplate中拷贝其配置的ClientHttpRequestFactory、MessageConverter列表等。这是一个快速将旧项目升级到新 API 的桥梁但长期来看建议还是按照 RestClient 的方式重新配置以利用其新特性。3.3 发起各类 HTTP 请求的代码实录让我们通过一系列具体的代码示例看看如何使用 RestClient 完成常见的 HTTP 操作。假设我们有一个UserServiceClientBean其baseUrl配置为http://localhost:8080/api。1. GET 请求获取资源Service public class UserService { private final RestClient userRestClient; // 注入配置好的客户端 public UserService(Qualifier(userServiceClient) RestClient userRestClient) { this.userRestClient userRestClient; } // 示例1获取对象自动反序列化 public User getUserById(Long id) { return userRestClient.get() .uri(/users/{id}, id) // 路径参数 .retrieve() .body(User.class); // 自动转换为User对象 } // 示例2获取列表 public ListUser getAllUsers() { User[] users userRestClient.get() .uri(/users) .retrieve() .body(User[].class); // 注意返回数组或使用 ParameterizedTypeReference return Arrays.asList(users); } // 示例3带查询参数的GET请求 public ListUser getUsersByCondition(String name, String status) { return List.of(userRestClient.get() .uri(uriBuilder - uriBuilder .path(/users/search) .queryParam(name, name) .queryParam(active, status) .build()) .retrieve() .body(User[].class)); } }注意处理泛型集合当反序列化ListUser这类泛型集合时直接使用.body(List.class)会丢失泛型信息导致 Jackson 反序列化失败或类型不安全。正确的做法是使用ParameterizedTypeReferenceListUser users restClient.get() .uri(/users) .retrieve() .body(new ParameterizedTypeReferenceListUser() {});2. POST 请求创建资源public User createUser(UserCreateRequest request) { return userRestClient.post() .uri(/users) .contentType(MediaType.APPLICATION_JSON) .body(request) // 请求体会被自动序列化为JSON .retrieve() .body(User.class); } // 如果不需要响应体只关心状态码 public void createUserSimple(UserCreateRequest request) { userRestClient.post() .uri(/users) .body(request) .retrieve() .toBodilessEntity(); // 忽略响应体只返回ResponseEntityVoid }3. PUT/PATCH 请求更新资源// PUT - 替换整个资源 public User updateUser(Long id, UserUpdateRequest request) { return userRestClient.put() .uri(/users/{id}, id) .body(request) .retrieve() .body(User.class); } // PATCH - 部分更新资源 public void patchUserEmail(Long id, String newEmail) { MapString, String patchData Map.of(email, newEmail); userRestClient.patch() .uri(/users/{id}, id) .body(patchData) .retrieve() .toBodilessEntity(); }4. DELETE 请求删除资源public void deleteUser(Long id) { userRestClient.delete() .uri(/users/{id}, id) .retrieve() .toBodilessEntity(); }5. 交换模式获取完整响应实体retrieve()方法是一种高级抽象方便我们直接获取响应体。但有些时候我们需要访问响应的状态码、头信息等元数据。这时可以使用exchange方法它返回一个ClientResponse对象提供了对 HTTP 响应的完全控制权。public User getUserWithDetails(Long id) { ClientResponse response userRestClient.get() .uri(/users/{id}, id) .accept(MediaType.APPLICATION_JSON) .exchange(); // 注意这里不是retrieve() HttpStatusCode statusCode response.getStatusCode(); HttpHeaders headers response.getHeaders(); if (statusCode.is2xxSuccessful()) { return response.body(User.class); } else if (statusCode HttpStatus.NOT_FOUND) { log.warn(User {} not found. Headers: {}, id, headers); return null; } else { // 处理其他错误可以读取错误响应体 String errorBody response.body(String.class); throw new ServiceException(Failed to get user, status: statusCode , body: errorBody); } }exchange提供了最大的灵活性但代价是需要手动处理更多细节如关闭响应资源虽然ClientResponse通常会自动处理。对于大多数常见场景retrieve()配合onStatus的错误处理已经足够优雅和简洁。4. 高级特性与生产级应用实践4.1 拦截器实战实现统一认证与日志拦截器是 RestClient 实现横切关注点的核心组件。让我们实现两个实用的拦截器。1. 认证拦截器自动注入 JWT Token假设我们的系统使用 JWT 进行服务间认证Token 存储在SecurityContext或ThreadLocal中。Component public class JwtTokenInterceptor implements ClientHttpRequestInterceptor { Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { // 从安全上下文或自定义Holder中获取当前令牌 String token SecurityContextHolder.getContext().getAuthentication() ! null ? SecurityContextHolder.getContext().getAuthentication().getCredentials().toString() : TokenHolder.getCurrentToken(); if (token ! null !token.isBlank()) { request.getHeaders().setBearerAuth(token); // 便捷方法等同于 set(Authorization, Bearer token) } // 添加请求ID用于链路追踪 request.getHeaders().set(X-Request-ID, UUID.randomUUID().toString()); // 继续执行请求链 return execution.execute(request, body); } }然后在配置RestClient时添加这个拦截器.requestInterceptor(new JwtTokenInterceptor())。这样所有通过该客户端发起的请求都会自动携带认证令牌无需在每个调用处重复设置。2. 日志与监控拦截器Component Slf4j public class LoggingMetricsInterceptor implements ClientHttpRequestInterceptor { Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { String requestId request.getHeaders().getFirst(X-Request-ID); String method request.getMethod().name(); String uri request.getURI().toString(); long startTime System.currentTimeMillis(); log.debug(Outgoing request [{}]: {} {}, requestId, method, uri); try { ClientHttpResponse response execution.execute(request, body); long duration System.currentTimeMillis() - startTime; log.debug(Received response [{}]: Status {} in {} ms, requestId, response.getStatusCode(), duration); // 可以在这里记录指标例如发送到Micrometer Metrics.counter(http.client.requests, method, method, uri, uri, status, String.valueOf(response.getStatusCode().value())) .increment(); return response; } catch (IOException e) { long duration System.currentTimeMillis() - startTime; log.error(Request failed [{}]: {} {} after {} ms, requestId, method, uri, duration, e); Metrics.counter(http.client.errors, method, method, uri, uri, exception, e.getClass().getSimpleName()) .increment(); throw e; } } }这个拦截器记录了请求的耗时、状态并集成监控指标。注意拦截器的执行顺序很重要通常认证拦截器应该在日志拦截器之前执行以确保日志能记录到完整的请求头信息。4.2 消息转换器处理非 JSON 数据虽然 JSON 是主流但有时我们仍需处理 XML、表单数据或自定义格式。RestClient 通过HttpMessageConverter来处理这些转换。1. 发送表单数据public void login(String username, String password) { // 方式一使用 MultiValueMap MultiValueMapString, String formData new LinkedMultiValueMap(); formData.add(username, username); formData.add(password, password); String result restClient.post() .uri(/login) .contentType(MediaType.APPLICATION_FORM_URLENCODED) .body(formData) .retrieve() .body(String.class); // 方式二使用字符串拼接简单场景 // String formBody username URLEncoder.encode(username) password URLEncoder.encode(password); // .contentType(MediaType.APPLICATION_FORM_URLENCODED) // .body(formBody) }Spring 默认配置了FormHttpMessageConverter来处理application/x-www-form-urlencoded数据。2. 发送 Multipart 文件public void uploadProfilePicture(Long userId, MultipartFile file) throws IOException { // 构建 multipart 数据 MultiValueMapString, Object parts new LinkedMultiValueMap(); parts.add(file, new ByteArrayResource(file.getBytes()) { Override public String getFilename() { return file.getOriginalFilename(); } }); parts.add(userId, userId); restClient.post() .uri(/users/{id}/avatar, userId) .contentType(MediaType.MULTIPART_FORM_DATA) .body(parts) .retrieve() .toBodilessEntity(); }这里利用了ByteArrayResource并重写getFilename()方法来构建文件部分。Spring 的MultipartHttpMessageConverter会处理这种格式。3. 自定义消息转换器假设你需要和一个老系统通信它使用 XML。Configuration public class RestClientConfig { Bean public RestClient xmlServiceClient(RestClient.Builder builder) { // 创建支持XML的转换器 MarshallingHttpMessageConverter xmlConverter new MarshallingHttpMessageConverter(); Jaxb2Marshaller marshaller new Jaxb2Marshaller(); marshaller.setPackagesToScan(com.example.xml.model); // 你的XML模型类包 xmlConverter.setMarshaller(marshaller); xmlConverter.setUnmarshaller(marshaller); return builder .baseUrl(http://legacy-system/api) .messageConverters(converters - { converters.add(0, xmlConverter); // 添加到列表开头优先于JSON转换器 // 也可以完全替换 converters.clear(); converters.add(xmlConverter); }) .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_XML_VALUE) .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) .build(); } }这样使用xmlServiceClient发起的请求其Accept和Content-Type头都会是 XML并且序列化/反序列化会使用 JAXB。4.3 连接池与超时配置性能调优要点默认情况下RestClient 使用 JDK 自带的HttpClient其连接池行为由系统属性控制。在生产环境中我们通常需要更精细的控制。通过application.yml配置全局 HTTP 客户端属性spring: application: name: my-service # 这些配置会影响通过 RestClient.builder() 创建的客户端底层使用的 HttpClient http: client: connect-timeout: 2s # 建立TCP连接的超时时间 read-timeout: 5s # 从服务器读取数据的超时时间 write-timeout: 5s # 向服务器发送数据的超时时间JDK HttpClient支持 connection-timeout: 500ms # 从连接池获取连接的超时时间 max-connections: 200 # 连接池最大总连接数 max-connections-per-route: 50 # 到每个目标主机的最大连接数 keep-alive: 30s # 空闲连接的存活时间 compression: on # 是否启用压缩gzip/deflate # 代理配置根据实际需要 # proxy: # host: proxy.example.com # port: 8080 # username: user # password: pass # non-proxy-hosts: localhost|127.*|[::1]这些配置属性会被 Spring Boot 的HttpClientAutoConfiguration捕获并应用到自动配置的HttpClientBean 上进而影响所有基于此HttpClient的RestClient实例。为特定客户端配置独立的超时如果需要对某个特定的下游服务设置不同的超时你需要自定义ClientHttpRequestFactory。Bean public RestClient slowExternalServiceClient() { // 使用HttpComponentsClientHttpRequestFactory需要引入Apache HttpClient依赖 HttpClient httpClient HttpClientBuilder.create() .setMaxConnTotal(100) .setMaxConnPerRoute(20) .setConnectionTimeToLive(30, TimeUnit.SECONDS) .setDefaultRequestConfig(RequestConfig.custom() .setConnectTimeout(5000) // 5秒连接超时 .setSocketTimeout(30000) // 30秒读写超时 .build()) .build(); HttpComponentsClientHttpRequestFactory factory new HttpComponentsClientHttpRequestFactory(httpClient); return RestClient.builder() .baseUrl(http://very-slow-external-api.com) .requestFactory(factory) .build(); }重要提示超时策略connect-timeout设置过短可能导致网络波动时连接失败。对于内网服务1-2秒足够对于公网API可以考虑3-5秒。read-timeout这是最重要的超时之一。必须根据下游服务的 SLA服务等级协议来设置。例如一个快速查询接口可设为1-2秒一个批处理接口可能需要30秒或更长。永远不要不设置读超时否则一个慢响应或挂起的连接会永久占用你的线程池资源导致应用雪崩。连接池大小max-connections,max-connections-per-route需要根据应用的并发量和下游服务的承受能力来调整。一个通用的起始公式是max-connections-per-route 并发线程数 * 2。监控客户端的连接池使用情况是关键。5. 常见问题、性能调优与迁移指南5.1 高频问题排查手册在实际使用 RestClient 时你可能会遇到以下典型问题。这里提供一个快速排查指南。问题现象可能原因排查步骤与解决方案抛出HttpClientErrorException或HttpServerErrorException这是RestClient的默认行为当响应状态码为 4xx 或 5xx 且未被onStatus处理时抛出。1. 检查是否使用了retrieve()。2. 检查是否通过onStatus处理了特定的错误状态码。3. 使用exchange()方法手动检查状态码和响应体进行更灵活的错误处理。反序列化失败抛出HttpMessageNotReadableException1. 响应体格式与声明的 Java 类型不匹配如 JSON 返回了数组但用User.class接收。2. 缺少对应的HttpMessageConverter如处理 XML 时。3. JSON 字段与 Java 对象字段名/类型不匹配。1.开启调试日志logging.level.org.springframework.web.clientDEBUG查看原始响应内容。2. 使用.body(String.class)先获取原始字符串检查其格式。3. 确认是否正确配置了消息转换器如 XML。4. 检查 Java 对象的字段注解如JsonProperty是否与 JSON 字段对应。请求超时SocketTimeoutException1. 未配置read-timeout或配置过短。2. 下游服务响应缓慢或网络延迟高。3. 连接池耗尽请求在队列中等待。1. 检查spring.http.client.read-timeout配置根据下游服务性能适当调大。2. 使用链路追踪工具如 SkyWalking, Zipkin分析下游服务耗时。3. 监控连接池指标调整max-connections和max-connections-per-route。无法注入RestClient.Builder1. 项目是纯 WebFlux 应用未引入spring-boot-starter-web。2. 自定义了RestClient.BuilderBean 但类型不匹配。1. 确保依赖中包含spring-boot-starter-web。2. 检查是否有多个RestClient.BuilderBean 定义导致注入冲突。使用Primary注解指定主 Bean。拦截器未生效1. 拦截器未正确添加到RestClient.Builder。2. 拦截器顺序问题被后续拦截器覆盖了修改。3. 使用了不同的RestClient实例。1. 确认拦截器 Bean 已被 Spring 管理并在配置客户端时通过.requestInterceptor()添加。2. 拦截器按添加顺序执行检查逻辑。3. 确保业务代码中注入的是你配置了拦截器的那个RestClientBean。POST/PUT 请求体为 null在调用.body(Object)时传入的对象为null。RestClient 不会将null对象序列化为请求体。如果需要发送空的 JSON 对象{}可以传入Collections.emptyMap()或一个空对象实例。如果需要发送null值需要更底层的操作通常不建议。5.2 从 RestTemplate 平滑迁移的策略如果你有一个正在使用RestTemplate的大型项目全面迁移到RestClient可能需要一个渐进的过程。策略一并行运行逐步替换这是风险最低的策略。不要一次性替换所有RestTemplate代码。引入依赖确保升级到 Spring Boot 3.2。创建新客户端为新的服务调用或模块直接使用RestClient进行开发。旧代码迁移当需要修改或重构某个使用了RestTemplate的旧类时顺便将其迁移到RestClient。可以借助RestClient.builder(oldRestTemplate)来快速创建一个行为一致的客户端减少配置迁移成本。最终清理当所有相关代码都迁移完毕后删除旧的RestTemplateBean 定义和相关配置。策略二创建适配器层如果项目结构清晰可以创建一个“HTTP 客户端门面”或适配器接口。public interface ApiClient { T T getForObject(String url, ClassT responseType, Object... uriVariables); // ... 其他方法 } // 旧实现委托给RestTemplate Service Primary // 初始阶段使用此实现 ConditionalOnProperty(name http.client.impl, havingValue rest-template, matchIfMissing true) public class RestTemplateApiClient implements ApiClient { private final RestTemplate restTemplate; // 实现方法... } // 新实现使用RestClient Service ConditionalOnProperty(name http.client.impl, havingValue rest-client) public class RestClientApiClient implements ApiClient { private final RestClient restClient; // 实现方法... }这样业务代码依赖于ApiClient接口。通过一个配置开关如http.client.impl可以在不修改业务代码的情况下在RestTemplate和RestClient之间切换实现平滑迁移和回滚。迁移注意事项错误处理RestTemplate默认抛异常RestClient的retrieve()也需要配置onStatus或使用exchange来达到类似效果。这是迁移时需要重点修改的部分。URI 构建RestClient的uri()方法更灵活支持String模板和UriBuilder。迁移时注意路径参数和查询参数的写法变化。响应类型处理ListT等泛型集合时RestClient需要使用ParameterizedTypeReference而RestTemplate有专用的ParameterizedTypeReference参数重载方法概念一致但 API 略有不同。5.3 性能监控与最佳实践将 RestClient 用于生产环境监控是必不可少的。1. 启用 Micrometer 指标如果你使用了 Spring Boot Actuator 和 Micrometer例如与 Prometheus 集成RestClient 的指标会自动通过RestClient.Builder的observationRegistry集成。确保你的配置中包含了相关依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependency dependency groupIdio.micrometer/groupId artifactIdmicrometer-registry-prometheus/artifactId /dependency然后在application.yml中启用指标management: endpoints: web: exposure: include: metrics,prometheus metrics: tags: application: ${spring.application.name}你可以在/actuator/metrics/http.client.requests端点看到 HTTP 客户端的详细指标包括请求数量、耗时、状态码分布等。2. 连接池监控如果使用 Apache HttpClient 作为底层实现可以暴露其连接池指标到 Micrometer。Bean public MeterBinder httpClientMetrics(HttpClient httpClient) { return new HttpClientMetrics(httpClient, my-http-client); }3. 日志记录为org.springframework.web.client设置DEBUG级别日志可以在开发阶段看到详细的请求和响应信息包括头信息和体注意敏感信息。在生产环境建议设置为WARN或ERROR并结合拦截器记录摘要日志和错误。最佳实践总结为每个下游服务创建独立的客户端 Bean配置不同的超时、重试和拦截策略。合理设置超时连接超时、读超时、写超时必须根据网络环境和下游服务 SLA 明确设置。使用连接池并监控其使用情况避免连接泄漏或耗尽。实现统一的拦截器用于认证、链路追踪、日志和监控。精细化错误处理利用onStatus将不同的 HTTP 错误状态映射到不同的业务异常。监控告警对客户端错误率、延迟、超时率设置监控告警。考虑重试机制对于网络抖动或下游服务瞬时故障可以在拦截器层或使用 Spring Retry 实现有策略的重试注意幂等性。从我个人的迁移和使用体验来看RestClient 带来的代码清晰度和可维护性的提升是巨大的。它用一种更现代、更符合直觉的方式解决了我们日常开发中高频的 HTTP 通信需求。虽然初期需要一点学习成本来熟悉其 API 和配置方式但一旦上手你就会发现它几乎能优雅地处理所有场景。对于新项目毫无疑问应该直接采用 RestClient对于老项目制定一个渐进式的迁移计划逐步享受它带来的便利是完全值得的。
