
1. 从RestTemplate到RestClient一次迟到的“拨乱反正”如果你和我一样是从Spring 3.x甚至更早版本一路用过来的老开发者那么对RestTemplate这个名字一定感情复杂。它曾是Spring生态中调用HTTP服务的绝对主力简单、直接但也因其“重量级”和略显过时的设计而备受诟病。尤其是在微服务、云原生和响应式编程大行其道的今天一个阻塞式的、基于模板模式的客户端总让人觉得与时代有些脱节。Spring Boot 3.2带来的全新RestClient在我看来不是一次简单的API更新而是一次迟到的、对HTTP客户端使用体验的“拨乱反正”。它没有选择另起炉灶搞一套全新的异步或响应式API那有WebClient而是精准地瞄准了那些依然需要同步、阻塞式调用的场景用一套更现代、更流畅、更符合Java开发者直觉的API重新定义了同步HTTP客户端该有的样子。这就像给你的旧车换上了一台更高效、更平顺的新发动机驾驶体验提升立竿见影但操作习惯几乎无需改变。2. RestClient核心设计哲学流畅Fluent与不可变Immutable要理解RestClient首先要抓住它的两个核心设计哲学流畅API和不可变构建。这直接决定了它的使用方式和优势所在。2.1 告别链式“污染”不可变构建器的优势RestTemplate时代我们经常这样写代码RestTemplate template new RestTemplate(); template.setMessageConverters(...); template.setInterceptors(...); // 然后使用template进行各种请求这种方式的问题是RestTemplate实例本身是可变的mutable。如果你在某个地方修改了它的配置比如添加了一个全局拦截器那么所有使用这个RestTemplate的地方都会受到影响。在多线程环境或复杂的应用结构中这很容易引发难以追踪的配置污染问题。RestClient彻底改变了这一点。它采用了建造者模式Builder Pattern来创建客户端并且构建出的RestClient实例是不可变的Immutable。// 创建一个基础配置的、不可变的RestClient RestClient baseClient RestClient.builder() .baseUrl(https://api.example.com) .defaultHeader(User-Agent, MyApp/1.0) .build(); // 基于baseClient创建一个添加了认证头的新客户端原baseClient不受影响 RestClient authClient baseClient.mutate() .defaultHeader(Authorization, Bearer new-token) .build();这里的mutate()方法会返回一个新的建造者让你在现有配置基础上进行修改最终生成一个全新的RestClient实例。原来的baseClient配置纹丝不动。这种不可变性带来了巨大的好处线程安全和配置隔离。你可以放心地创建具有不同用途如对内服务、对外API、带特殊认证等的客户端实例而不用担心它们相互干扰。2.2 行云流水的调用体验流畅API详解流畅APIFluent API意味着方法调用可以像链条一样连接下去让代码读起来就像一句完整的句子。RestClient将这一点发挥到了极致。一个完整的GET请求示例如下String result restClient.get() .uri(/users/{id}, 123) // 设置URI和路径变量 .header(X-Custom-Header, value) // 设置请求头 .accept(MediaType.APPLICATION_JSON) // 设置Accept头 .retrieve() // 执行请求并准备检索响应 .body(String.class); // 将响应体转换为指定类型让我们拆解这个链条restClient.get(): 指定HTTP方法返回一个RequestHeadersUriSpec对象代表一个待配置的请求。.uri(...): 设置请求路径。支持路径变量{id}和UriBuilder的完整功能。.header(...)/.accept(...): 设置请求头。这些方法可以链式调用多次。.retrieve():关键方法。它触发实际的HTTP调用并返回一个ResponseSpec对象用于处理响应。注意此时网络请求已经发生。.body(String.class): 从ResponseSpec中提取响应体并尝试用配置的HttpMessageConverter将其转换为指定的Java类型这里是String。这种链式调用不仅写起来顺畅更重要的是它通过类型系统清晰地定义了每个阶段能做什么比如只有在.retrieve()之后才能处理响应减少了误用的可能性。注意.retrieve()方法是同步且阻塞的。它会一直等待直到收到完整的HTTP响应或超时。这也是RestClient与异步的WebClient的核心区别之一。RestClient专注于简化同步阻塞调用如果你需要非阻塞或响应式编程WebClient仍然是更佳选择。3. 实战从基础调用到高级特性全解析理解了设计哲学我们进入实战环节。我将通过一个模拟的用户服务API展示RestClient从入门到精通的完整使用路径。3.1 基础配置与实例创建首先在Spring Boot 3.2项目中你不需要引入额外依赖spring-boot-starter-web已经包含了RestClient。创建RestClient有多种方式适应不同场景方式一使用建造者从头构建import org.springframework.web.client.RestClient; Configuration public class AppConfig { Bean public RestClient myRestClient() { return RestClient.builder() .baseUrl(https://jsonplaceholder.typicode.com) // 基础URL .defaultHeader(Accept, MediaType.APPLICATION_JSON_VALUE) // 默认头 .defaultStatusHandler(HttpStatusCode::is4xxClientError, (request, response) - { // 默认的4xx错误处理 throw new MyClientException(Client error: response.getStatusCode()); }) .requestInterceptor((request, body, execution) - { // 请求拦截器可用于日志、计量等 log.info(Sending request to: {}, request.getURI()); return execution.execute(request, body); }) .build(); } }方式二从RestTemplate平滑迁移如果你有现有的RestTemplate并且配置了复杂的消息转换器或拦截器可以快速转换RestTemplate oldTemplate ... // 你现有的RestTemplate RestClient restClient RestClient.builder(oldTemplate).build();RestClient.builder(RestTemplate)会拷贝RestTemplate的ClientHttpRequestFactory、消息转换器列表和拦截器实现零成本迁移。方式三使用Spring Boot的自动配置Spring Boot 3.2为RestClient提供了自动配置。你可以在application.yml中配置spring: restclient: base-url: https://api.example.com default-headers: accept: application/json user-agent: MySpringBootApp然后通过Autowired注入一个预配置的RestClient实例。这种方式最适合简单的、全局统一的HTTP客户端需求。3.2 各种HTTP方法的调用模式RestClient支持所有标准的HTTP方法调用模式高度一致。GET 请求获取资源// 1. 获取简单响应体 User user restClient.get() .uri(/users/{id}, userId) .retrieve() .body(User.class); // 2. 获取完整的ResponseEntity包含状态码、头信息等 ResponseEntityUser responseEntity restClient.get() .uri(/users/{id}, userId) .retrieve() .toEntity(User.class); // 注意是 toEntity // 3. 处理可能为空的响应404 Not Found User user restClient.get() .uri(/users/{id}, 9999) // 假设ID不存在 .retrieve() .onStatus(status - status HttpStatus.NOT_FOUND, (request, response) - { // 专门处理404返回null而不是抛异常 return; }) .body(User.class); // 如果404这里body为nullPOST 请求创建资源// 1. 发送JSON体并期望返回创建的对象 User newUser new User(John Doe, johnexample.com); User createdUser restClient.post() .uri(/users) .contentType(MediaType.APPLICATION_JSON) .body(newUser) // 自动使用Jackson序列化 .retrieve() .body(User.class); // 2. 发送表单数据 MultiValueMapString, String formData new LinkedMultiValueMap(); formData.add(username, john); formData.add(password, secret); String result restClient.post() .uri(/login) .contentType(MediaType.APPLICATION_FORM_URLENCODED) .body(formData) .retrieve() .body(String.class);PUT 与 PATCH 请求更新资源// PUT - 替换整个资源 User updatedUser new User(Jane Doe, janeexample.com); restClient.put() .uri(/users/{id}, userId) .contentType(MediaType.APPLICATION_JSON) .body(updatedUser) .retrieve() // PUT请求可能没有响应体但retrieve()仍会触发请求并检查状态 .toBodilessEntity(); // 明确表示不期待响应体 // PATCH - 部分更新资源需服务端支持 MapString, Object patchData Map.of(email, new-emailexample.com); restClient.patch() .uri(/users/{id}, userId) .contentType(MediaType.APPLICATION_JSON) .body(patchData) .retrieve() .toBodilessEntity();DELETE 请求删除资源// 简单删除 restClient.delete() .uri(/users/{id}, userId) .retrieve() .toBodilessEntity(); // 带条件的删除例如匹配ETag restClient.delete() .uri(/users/{id}, userId) .header(If-Match, \some-etag-value\) .retrieve() .toBodilessEntity();3.3 请求与响应的精细化控制RestClient提供了细粒度的控制能力远超RestTemplate的简单exchange方法。请求头Headers的灵活设置String result restClient.get() .uri(/data) .header(Authorization, Bearer token) .header(X-Request-ID, UUID.randomUUID().toString()) // 链式添加多个头 .accept(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML) // 设置Accept服务端可择一返回 .ifNoneMatch(\v1.0\) // 条件请求如果匹配ETag则返回304 .ifModifiedSince(Instant.now().minus(Duration.ofDays(1))) // 条件请求 .retrieve() .body(String.class);URI构建的多种姿势// 1. 简单路径 .uri(/api/v1/users) // 2. 使用路径变量 (推荐自动编码) .uri(/users/{id}/posts/{postId}, userId, postId) // 3. 使用URI模板和Map MapString, Object uriVars Map.of(id, userId, type, admin); .uri(/users/{id}/type/{type}, uriVars) // 4. 使用UriBuilder进行复杂构建 .uri(uriBuilder - uriBuilder .path(/search) .queryParam(q, searchTerm) .queryParam(page, page) .queryParam(size, size) .build())响应处理的三种模式.retrieve()方法返回的ResponseSpec提供了三种主要的响应处理方式.body(ClassT): 最常用只关心成功时的响应体。遇到4xx/5xx状态码会抛出RestClientException子类。.toEntity(ClassT): 获取完整的ResponseEntityT包含状态码、头信息和响应体。同样遇到错误状态码会抛异常。.toBodilessEntity(): 当你只关心请求是否成功如DELETEPUT不期待响应体时使用。错误处理Status Handlers的进化这是RestClient相比RestTemplate的一大改进。错误处理更加声明式和集中化。// 方式1在创建RestClient时定义默认错误处理器全局 RestClient client RestClient.builder() .baseUrl(https://api.example.com) .defaultStatusHandler(HttpStatusCode::is4xxClientError, (request, response) - { throw new MyCustomClientErrorException(response.getStatusCode(), Client error occurred); }) .defaultStatusHandler(HttpStatusCode::is5xxServerError, (request, response) - { throw new MyCustomServerErrorException(response.getStatusCode(), Server is down); }) .build(); // 方式2在单次请求中覆盖或添加特定的错误处理器局部 User user client.get() .uri(/users/{id}, id) .retrieve() .onStatus(status - status HttpStatus.NOT_FOUND, (request, response) - { // 专门处理404例如返回一个默认值或记录日志 log.warn(User with id {} not found, id); // 注意这里不抛异常retrieve()会继续执行body()可能返回null }) .onStatus(status - status HttpStatus.FORBIDDEN, (request, response) - { // 处理403例如抛出一个业务特定的异常 throw new InsufficientPrivilegeException(No permission to access user); }) .body(User.class); // 如果404被处理且未抛异常这里可能为null这种设计将错误处理逻辑从业务代码中剥离使得核心业务流获取用户更加清晰。你可以根据不同的API、不同的错误码定义非常精细的恢复或失败策略。3.4 消息转换器Message Converters与拦截器InterceptorsRestClient继承了Spring强大的HttpMessageConverter机制并提供了灵活的拦截器接口。消息转换器的配置与使用默认情况下RestClient会使用Spring上下文中的一组默认消息转换器如MappingJackson2HttpMessageConverter用于JSON。你也可以自定义RestClient customClient RestClient.builder() .messageConverters(converters - { // 清除默认的添加自己的 converters.clear(); // 添加一个支持JSON的Jackson转换器并配置属性 ObjectMapper mapper new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); converters.add(new MappingJackson2HttpMessageConverter(mapper)); // 添加String转换器 converters.add(new StringHttpMessageConverter()); }) .build();当你使用.body(Object)发送请求或.body(ClassT)接收响应时会自动选择合适的转换器。拦截器实现横切关注点拦截器是RestClient的另一个强大功能用于在请求发出前和响应返回后执行通用逻辑。RestClient loggedClient RestClient.builder() .requestInterceptor((request, body, execution) - { // 1. 请求前记录日志、添加签名、计量开始 long startTime System.nanoTime(); String requestId UUID.randomUUID().toString(); request.getHeaders().add(X-Request-ID, requestId); log.info(Outgoing request [{}]: {} {}, requestId, request.getMethod(), request.getURI()); // 2. 执行请求 ClientHttpResponse response execution.execute(request, body); // 3. 响应后记录耗时、处理通用错误 long duration (System.nanoTime() - startTime) / 1_000_000; log.info(Incoming response [{}]: {} in {} ms, requestId, response.getStatusCode(), duration); // 4. 可以在这里包装response甚至重试逻辑需谨慎 return response; }) // 可以添加多个拦截器按添加顺序执行 .requestInterceptor(new BasicAuthenticationInterceptor(user, password)) .build();常见的拦截器用途包括认证自动添加Authorization头。日志记录所有请求和响应的摘要信息。链路追踪注入Trace ID、Span ID。重试实现简单的重试机制注意复杂的重试建议用RetryTemplate或Resilience4j等专业库。指标收集记录请求耗时、状态码分布等。4. 避坑指南与性能调优实战在实际项目中使用RestClient你可能会遇到一些“坑”。下面是我从实际项目中总结的经验和解决方案。4.1 连接池配置避免性能瓶颈的隐形杀手默认情况下RestClient底层使用的SimpleClientHttpRequestFactory或基于Apache HttpClient、OkHttp等可能没有配置连接池或者配置不合理这在并发请求下会成为性能瓶颈。问题场景你的服务需要高频调用下游某个APIQPS达到几百。使用默认配置的RestClient后发现经常出现连接超时、响应缓慢监控显示TCP连接数不断创建和销毁开销巨大。解决方案为RestClient配置一个带连接池的ClientHttpRequestFactory。这里以Apache HttpClient 5为例添加依赖如果尚未引入dependency groupIdorg.apache.httpcomponents.client5/groupId artifactIdhttpclient5/artifactId /dependency配置连接池和HttpClientConfiguration public class RestClientConfig { Bean public RestClient highPerformanceRestClient() { // 1. 创建连接池管理器 PoolingHttpClientConnectionManager connectionManager new PoolingHttpClientConnectionManager(); connectionManager.setMaxTotal(200); // 整个连接池最大连接数 connectionManager.setDefaultMaxPerRoute(50); // 每个路由目标主机默认最大连接数 // 可以针对特定主机设置 // connectionManager.setMaxPerRoute(new HttpRoute(new HttpHost(api.slow.com)), 20); // 2. 配置请求超时、连接超时等 RequestConfig requestConfig RequestConfig.custom() .setConnectTimeout(Timeout.ofSeconds(5)) // 连接超时 .setResponseTimeout(Timeout.ofSeconds(10)) // 响应超时 .build(); // 3. 构建HttpClient CloseableHttpClient httpClient HttpClients.custom() .setConnectionManager(connectionManager) .setDefaultRequestConfig(requestConfig) .evictExpiredConnections() // 驱逐过期连接 .evictIdleConnections(TimeValue.ofMinutes(1)) // 驱逐空闲连接 .build(); // 4. 使用HttpComponentsClientHttpRequestFactory ClientHttpRequestFactory requestFactory new HttpComponentsClientHttpRequestFactory(httpClient); // 5. 构建RestClient return RestClient.builder() .requestFactory(requestFactory) .baseUrl(https://api.example.com) .build(); } }关键参数解析setMaxTotal(200): 连接池总共最多持有200个连接。根据你的应用线程数和下游服务能力调整。setDefaultMaxPerRoute(50): 对于同一个host:port路由最多同时有50个连接。这是防止对单一服务连接数过多。setConnectTimeout: 建立TCP连接的超时时间。setResponseTimeout: 从请求发出到收到完整响应的超时时间含数据传输。evictIdleConnections: 定期清理空闲连接释放资源。将配置好的RestClient注入使用Service public class UserService { private final RestClient restClient; public UserService(Qualifier(highPerformanceRestClient) RestClient restClient) { this.restClient restClient; } // ... 使用restClient进行调用 }实测心得连接池配置没有银弹需要根据实际压测结果调整。一个常见的起始点是MaxPerRoute设置为你的应用最大并发线程数MaxTotal设置为MaxPerRoute * 下游服务数量。务必监控生产环境的连接池状态如活跃连接数、空闲连接数、等待队列长度。4.2 超时控制区分连接、读取与总超时超时配置不当是线上故障的常见原因。RestClient本身不直接设置超时它依赖于底层的ClientHttpRequestFactory。误区很多人只设置一个“超时时间”但HTTP请求涉及多个阶段需要分别控制。正确配置以Apache HttpClient 5为例RequestConfig requestConfig RequestConfig.custom() .setConnectTimeout(Timeout.ofSeconds(3)) // 连接超时与服务器建立TCP连接的最长时间 .setConnectionRequestTimeout(Timeout.ofSeconds(1)) // 从连接池获取连接的超时时间 .setResponseTimeout(Timeout.ofSeconds(10)) // 响应超时从连接建立成功到收到完整响应包的最长时间 .build();连接超时网络不通、服务器端口未开放时触发。应设置较短如2-5秒。连接请求超时从连接池获取连接等待时间。如果连接池耗尽请求会在此处等待。通常设置很短1-2秒超时立即失败避免长时间阻塞。响应超时服务器处理过慢或网络传输慢时触发。根据下游API的SLA服务等级协议设置如5-30秒。在RestClient中统一处理超时异常RestClient client RestClient.builder() .requestFactory(...) // 使用配置了超时的factory .defaultStatusHandler(HttpStatusCode::is5xxServerError, (request, response) - { // 处理服务器错误 }) .build(); // 使用try-catch捕获可能的超时异常通常是RestClientException的子类 try { client.get() .uri(/slow-api) .retrieve() .body(String.class); } catch (ResourceAccessException e) { // RestClientException的子类通常包装了IO超时等异常 if (e.getCause() instanceof SocketTimeoutException) { log.error(读取响应超时, e); // 执行降级逻辑如返回缓存、默认值 return fallbackData; } else if (e.getCause() instanceof ConnectTimeoutException) { log.error(连接服务器超时, e); throw new ServiceUnavailableException(下游服务不可达); } throw e; }4.3 响应式编程混用下的线程阻塞风险这是一个在Spring WebFlux响应式项目中容易踩的坑。问题场景你的应用是使用Spring WebFlux构建的响应式应用但某个服务需要调用一个只提供同步HTTP接口的下游系统。你直接在Service的某个方法中使用了RestClient进行阻塞调用。Service public class ReactiveUserService { private final RestClient restClient; private final ReactiveUserRepository reactiveRepo; public MonoUser getUserWithExternalData(String userId) { // 从响应式仓库获取用户 return reactiveRepo.findById(userId) .flatMap(user - { // 错误在响应式链中进行阻塞调用 ExternalData data restClient.get() // 这是一个阻塞操作 .uri(/external-api/data/{id}, user.getExternalId()) .retrieve() .body(ExternalData.class); user.setExternalData(data); return Mono.just(user); }); } }在响应式编程中所有操作都应在非阻塞的线程如Netty的EventLoop线程上执行。上述代码中的restClient.get().retrieve()是阻塞的它会挂起当前线程直到收到响应。如果这个阻塞操作发生在EventLoop线程上会严重拖慢整个应用的响应能力甚至导致线程饥饿。解决方案将阻塞调用调度到专门的、用于阻塞任务的线程池上。Service public class ReactiveUserService { private final RestClient restClient; private final ReactiveUserRepository reactiveRepo; private final Scheduler blockingScheduler; // 专门用于阻塞任务的调度器 public ReactiveUserService(RestClient restClient, ReactiveUserRepository reactiveRepo) { this.restClient restClient; this.reactiveRepo reactiveRepo; // 创建一个有边界的弹性线程池专门处理阻塞IO this.blockingScheduler Schedulers.boundedElastic(); } public MonoUser getUserWithExternalData(String userId) { return reactiveRepo.findById(userId) .flatMap(user - Mono.fromCallable(() - { // 将阻塞调用包装在Callable中 return restClient.get() .uri(/external-api/data/{id}, user.getExternalId()) .retrieve() .body(ExternalData.class); }) .subscribeOn(blockingScheduler) // **关键**在阻塞调度器上执行 .doOnNext(data - user.setExternalData(data)) .thenReturn(user) // 转换回User的Mono ); } }核心要点Mono.fromCallable(): 将阻塞的RestClient调用包装成一个Callable。.subscribeOn(blockingScheduler): 指定这个Callable在blockingScheduler这个专门的线程池上执行从而不阻塞响应式主线程。Schedulers.boundedElastic(): Reactor提供的弹性线程池适用于阻塞任务。它会根据需要创建线程但有上限防止创建过多线程。4.4 文件上传与下载处理流式数据RestClient同样支持流式数据的传输比如文件上传和下载。文件上传Multipart Form Data// 假设要上传一个文件和一个JSON字段 Resource fileResource new FileSystemResource(/path/to/file.txt); MultiValueMapString, Object parts new LinkedMultiValueMap(); parts.add(file, fileResource); // 文件部分 parts.add(metadata, Map.of(name, myfile, type, text)); // JSON部分会自动序列化 String response restClient.post() .uri(/upload) .contentType(MediaType.MULTIPART_FORM_DATA) // 关键设置Content-Type .body(parts) .retrieve() .body(String.class);Spring会自动处理MultiValueMap将文件部分编码为multipart/form-data格式并将JSON对象序列化为表单字段。大文件下载流式处理到磁盘restClient.get() .uri(/download/large-file.zip) .accept(MediaType.APPLICATION_OCTET_STREAM) .retrieve() .toEntity(Resource.class) // 获取ResponseEntityResource .subscribe(responseEntity - { Resource resource responseEntity.getBody(); try (InputStream inputStream resource.getInputStream(); FileOutputStream outputStream new FileOutputStream(/local/path/file.zip)) { // 使用缓冲区流式拷贝避免内存溢出 byte[] buffer new byte[8192]; int bytesRead; while ((bytesRead inputStream.read(buffer)) ! -1) { outputStream.write(buffer, 0, bytesRead); } } catch (IOException e) { log.error(Failed to download file, e); } });对于超大文件使用.toEntity(Resource.class)获取响应体作为Resource然后通过流式IO写入本地文件这是最安全、最省内存的方式。千万不要用.body(String.class)或.body(byte[].class)去接收大文件。5. 与RestTemplate、WebClient的深度对比与选型建议Spring生态现在有三个主要的HTTP客户端RestTemplate、WebClient和RestClient。如何选择特性RestTemplate (Legacy)WebClient (Reactive)RestClient (New Sync)编程模型同步、阻塞式异步、非阻塞(响应式)同步、阻塞式底层技术基于 JDKHttpURLConnection或 Apache HttpClient基于 Project Reactor 和 Netty基于RestTemplate的底层设施但API现代化API 风格模板方法略显冗长流畅的、函数式的流畅的、函数式的配置方式可变实例易污染不可变建造者不可变建造者错误处理通过ResponseErrorHandler通过onStatus处理器通过onStatus处理器更清晰线程模型每个请求阻塞一个线程事件驱动少量线程处理高并发每个请求阻塞一个线程适用场景传统Servlet应用简单同步调用微服务、高并发、响应式全栈应用传统Servlet应用升级需要现代API的同步调用与阻塞式库集成学习成本低但API老旧中高需理解响应式编程低API直观类似WebClientSpring Boot 3.x已标记为Deprecated(不推荐使用)推荐用于响应式栈推荐用于同步栈选型决策树你的应用是响应式的吗使用Spring WebFlux是- 毫不犹豫选择WebClient。它是响应式栈的原生选择。否- 进入第2步。你的应用是传统的Servlet应用吗使用Spring MVC是且是新项目或重大重构- 选择RestClient。它提供了现代、流畅的API错误处理更友好是RestTemplate的完美替代品。是但已有大量RestTemplate代码改动成本高- 可以暂时继续使用RestTemplate但新模块建议用RestClient。长远看RestTemplate终将被淘汰。否是其他类型应用如批处理、命令行工具- 选择RestClient。它的同步阻塞模型更简单直观适合脚本或顺序任务。迁移策略 从RestTemplate迁移到RestClient相对平滑因为底层基础设施相同。主要工作是重写调用代码的API。你可以利用RestClient.builder(oldTemplate)快速创建一个具有相同配置的RestClient实例然后逐步替换各个调用点。我个人在将团队的一个老项目从RestTemplate迁移到RestClient后最直观的感受是代码可读性大幅提升错误处理逻辑更加集中和清晰再也不用在业务代码里到处写try-catch来处理不同的HTTP状态码了。对于仍在维护传统Spring MVC项目的团队来说RestClient是技术栈更新一个非常好的切入点它能带来立竿见影的代码质量改善而迁移成本却可控。