前几天升级 Spring Boot 依赖时,我顺手看了一眼 4.2 的 Release Notes。

看到一行熟悉的名字:

RestTemplate

这次不是增加功能,而是:

deprecated。

准确来说,Spring Framework 已经明确将 RestTemplate 标记为弃用,并推荐迁移到 RestClient;刚发布的 Spring Boot 4.2.0-M1 进一步把 RestTemplateAutoConfiguration、RestTemplateBuilder、TestRestTemplate 等配套基础设施一起放进了弃用名单。

我看完之后第一反应其实不是:

“Spring 又造了一个 HTTP 客户端。”

而是打开公司一个老项目搜了一下:

RestTemplate

结果:

47处。

支付、短信、用户中心、地图服务、物流查询……

基本哪儿都有。

于是花了点时间把其中一条调用链改成了 RestClient。

改完之后我的感觉是:

这次确实可以开始迁了。


RestTemplate的问题不是不能用,而是时代已经过去了

先说一句公道话。

RestTemplate 并不好用吗?

其实挺好用。

很多 Java 项目现在可能还是这种代码:

 

UserResponse response =        restTemplate.getForObject(                ”http://user-service/api/users/{id}”,                UserResponse.class,                userId        );

POST:

 

ResponseEntity response =        restTemplate.postForEntity(                url,                request,                PayResponse.class        );

干了这么多年 Java,基本不用查文档就能写出来。

所以这几年哪怕 WebClient 已经很成熟,我也一直没有为了“新”强行把同步项目全部改成 Reactor。

原因很简单:

我的业务本来就是:

 

Controller   ↓Service   ↓调用第三方HTTP   ↓等待结果   ↓继续处理

非得为了发一个 HTTP 请求变成:

Mono

很多时候反而把简单事情复杂化了。

而 RestClient 比较讨巧的一点就在这里:

它还是同步 HTTP Client。

不需要改响应式编程。

Spring 官方现在对三者的定位也很直接:

RestClient同步、Fluent API WebClient非阻塞、Reactive RestTemplate旧的同步Template API,已弃用

所以对绝大多数普通 Spring MVC 项目来说,RestClient 才是最自然的接班人。


第一个接口改完,我发现代码确实顺眼了一些

以前项目里有一个物流查询:

 

public LogisticsResult query(        String trackingNo) {     String url =            logisticsUrl            + ”/api/tracking/”            + trackingNo;     ResponseEntity response =            restTemplate.getForEntity(                    url,                    LogisticsResult.class            );     return response.getBody();}

换成 RestClient:

 

public LogisticsResult query(        String trackingNo) {     return restClient            .get()            .uri(                ”/api/tracking/{trackingNo}”,                trackingNo            )            .retrieve()            .body(LogisticsResult.class);}

第一眼可能觉得:

好像也没少多少代码。

但真正舒服的是后面的组合。

比如 POST:

 

PayResponse response =        restClient                .post()                .uri(”/api/pay”)                .contentType(                    MediaType.APPLICATION_JSON                )                .body(request)                .retrieve()                .body(PayResponse.class);

加 Header:

 

restClient    .get()    .uri(”/api/orders/{id}”, orderId)    .header(        ”Authorization”,        ”Bearer ” + token    )    .retrieve()    .body(OrderResponse.class);

整个调用顺序基本就是 HTTP 请求真正发生的顺序:

 

GETURLHeaderBody发送Response

Spring 官方对 RestClient 的定位也是“同步、Fluent API 的 HTTP Client”,同时继续使用 Spring 的消息转换等基础设施。

没有什么学习成本。


真正的项目不要到处RestClient.create()

网上 Demo 很容易看到:

RestClient restClient =        RestClient.create();

然后直接用了。

自己玩没问题。

项目里我不会这么干。

因为真实系统调用第三方服务,一定会逐渐出现:

 

统一地址 Authorization TraceId User-Agent 日志 超时 监控 错误处理

这些东西如果散落在几十个 Service 里面,两个月以后又会变成另一个 RestTemplate 项目。

我现在一般会按外部系统分别创建 Client:

 

@Configurationpublic class HttpClientConfig {     @Bean    RestClient logisticsRestClient(            RestClient.Builder builder) {         return builder                .baseUrl(                    ”https://logistics.example.com”                )                .defaultHeader(                    HttpHeaders.ACCEPT,                    MediaType.APPLICATION_JSON_VALUE                )                .requestInterceptor(                    new TraceIdInterceptor()                )                .build();    }}

业务代码里只注入:

private final RestClient        logisticsRestClient;

这样 Logistics Service 根本不用知道:

域名是什么Header怎么加TraceId怎么传底层HTTP Client是谁

Spring Boot 本身会提供 RestClient.Builder 的自动配置能力,所以这种写法也更符合 Boot 的使用方式。

我现在基本遵循一个原则:

一个外部系统,一个明确的 HTTP Client 配置。

别搞一个万能:

restClient

全公司随便调用。

最后一定失控。


还有一个我以前经常写错的地方:4xx和5xx不能都当异常结束

比如支付服务返回:

HTTP/1.1 409 Conflict

Body:

 

{  ”code”: ”ORDER_ALREADY_PAID”,  ”message”: ”订单已经支付”}

这在 HTTP 层是 409。

但业务上它未必意味着:

系统故障

可能只是:

用户重复点击了支付。

如果把所有非 2xx 都粗暴转换成:

RuntimeException

后面监控会非常难看。

我现在会把 HTTP 错误和业务错误分开处理。

例如:

 

PayResponse response =        restClient            .post()            .uri(”/api/pay”)            .body(request)            .retrieve()            .onStatus(                status ->                    status.value() == 409,                (req, res) -> {                    throw new                        OrderAlreadyPaidException();                }            )            .body(PayResponse.class);

真正的:

 

500502503

则进入另外的异常处理链路。

这看起来只是 API 写法变化。

实际上顺手把以前项目里一个长期存在的问题也解决了:

 

HTTP失败业务失败

两个概念最好一开始就分开。


更让我感兴趣的是,下一步连Client实现类都可以不写

把 RestTemplate 改成 RestClient 后,我又顺着 Spring 文档看了一下 HTTP Service Client。

这个东西其实更有意思。

以前写远程用户服务:

 

@Componentpublic class UserClient {     private final RestClient restClient;     public UserResponse getUser(            Long userId) {         return restClient                .get()                .uri(                    ”/users/{id}”,                    userId                )                .retrieve()                .body(UserResponse.class);    }}

现在可以直接定义:

 

@HttpExchange(”/users”)public interface UserClient {     @GetExchange(”/{id}”)    UserResponse getUser(            @PathVariable Long id    );}

就这一份接口。

Spring 可以通过代理生成真正的 HTTP Client 实现。

官方当前也把 HTTP Service Client 和 RestClient、WebClient 一起列为 REST 调用方案,它本质上就是通过带注解的 Java Interface 描述远程 HTTP API,再生成代理。

这套写法让我第一时间想到:

@FeignClient

但区别是:

这是 Spring Framework 自己的能力。

如果项目只是需要:

声明式REST Client

以后是不是还一定需要额外引入 OpenFeign,就值得重新评估了。

至少新项目我会优先试 Spring 自己这套。


迁移不用一次改完,这一点很重要

47 个 RestTemplate 调用,如果让我一个版本全部改掉,我肯定不会干。

这种基础组件最怕:

代码看着更漂亮上线以后第三方请求全出问题

Spring 官方其实已经给了一个很实际的渐进迁移方案。

可以直接从已有的 RestTemplate 创建:

RestClient restClient =        RestClient.create(                restTemplate        );

这样原来已经配置好的:

RequestFactory Interceptor MessageConverter

等基础设施还可以继续利用,然后先逐步把调用 API 从:

restTemplate.getForObject(...)

替换成:

restClient    .get()    .retrieve()

等所有调用迁完,再重新整理 RestClient.Builder 配置。Spring 官方迁移指南就是建议分阶段做,而不是一次推倒重来。

我觉得这个方式比较符合真实公司项目。

先改:

低风险第三方接口

再改:

内部服务调用

支付、订单这类核心链路放最后。

别为了追版本搞“大扫除式重构”。


测试也开始换代了

还有一个信号挺明显。

Spring Boot 4.2.0-M1 不只弃用了:

RestTemplateBuilder

连:

TestRestTemplate

也一起进入弃用流程。

官方推荐的一个明显替代方案叫:

RestTestClient

例如:

RestTestClient client =        RestTestClient                .bindToController(                    new OrderController(                        orderService                    )                )                .build();

然后:

client.get()        .uri(”/orders/10001”)        .exchange()        .expectStatus()        .isOk()        .expectBody(OrderResponse.class);

它比较实用的一点是:

既可以:

不启动真实Server测试Controller

也可以:

连接真实HTTP Server做端到端测试

Spring Framework 官方把它定义成建立在 RestClient 之上的测试客户端。

所以这次变化不像以前那种:

新增一个类,你愿意用就用。

从生产调用,到 Boot 自动配置,再到测试工具,Spring 整条链路都开始往:

RestClient

迁移了。

这个信号其实已经相当明确。


现在要不要马上改?

如果项目还在:

Spring Boot 2.xSpring Boot 3.x

而且 RestTemplate 跑得非常稳定,我不会建议今天看完文章,明天就开一个:

refactor: replace all RestTemplate

然后改 300 个文件。

没必要。

而且 Spring Boot 4.2.0-M1 本身还是Milestone 版本,不是让生产系统为了这件事马上升级的。

但如果现在正在做:

新项目 新的第三方接口 新的微服务调用 Spring Boot 4.x升级

我不会再新增:

new RestTemplate()

了。

直接用:

RestClient

如果接口比较多,再进一步考虑:

@HttpExchange

这会是我现在的选择。

Spring Framework 当前文档已经明确写着:RestTemplate 被弃用,并将在未来版本移除;Spring Boot 4.2 又开始清理它周围的自动配置和测试基础设施。

所以这次不是:

“RestTemplate马上不能运行了。”

真正的意思是:

Spring已经不希望新代码继续往这个方向长了。

这两个说法差别很大。

老项目不用慌。

新代码就别再往旧路上加了。

我已经把项目里的:

47处RestTemplate

列进了迁移清单。

不会一次全改。

但下一次再新增 HTTP 调用,代码大概率会从这一行开始:

restClient    .get()

而不是那个陪了很多 Java 程序员十几年的:

restTemplate    .getForObject(...)

一个类真正开始退场,往往不是它被删除的那一天。

而是:

我们写下一段新代码时,已经不再选择它。

扫码领红包

微信赞赏支付宝扫码领红包

发表回复

后才能评论