RestTemplate 废弃后迁移 RestClient 实战:Spring Boot 4.2 升级指南

导读 :10 月 22 日 Spring Boot 4.2.0-RC1 发布,跟着的 Spring Framework 7.1.0-RC1 正式给 RestTemplate 打上了 @Deprecated(since="7.1", forRemoval=true)。我手上几个定制项目里 RestTemplate 调用少说上百处,这篇是我梳理的迁移步骤和对照表,照着改基本不用再翻文档。
适用版本:Spring Boot 4.2.0-RC1 / Spring Framework 7.1.0-RC1(废弃从这里开始);RestClient 从 Spring Framework 6.1(Boot 3.2)起就有,所以本文的迁移代码在 Boot 3.2+ 上同样能跑。HTTP Interface 的 @ImportHttpServices 和 spring.http.serviceclient.* 配置需要 Boot 4.0+。


一、到底废弃到什么程度了

先把事实摆清楚,免得被标题党吓到。

项目 状态 信源
RestTemplate 类 @Deprecated(since="7.1", forRemoval=true),Javadoc 写明 "For removal in 8.0" Spring Framework 7.1.0-RC1 Javadoc
RestTemplateBuilder、RestTemplateCustomizer、RestTemplateAutoConfiguration 随之废弃,迁移到 RestClient.Builder / RestClientCustomizer Spring Boot 4.2.0-M1 Release Notes
TestRestTemplate 废弃,改用 RestTestClient(@AutoConfigureRestTestClient) 同上
MockRestServiceServerAutoConfiguration 废弃 同上

几点我的判断:

  • 现在不改不会挂。forRemoval 是给 8.0 准备的,4.2 里照常能用,只是编译会刷一屏 deprecation 警告
  • 新代码别再写 RestTemplate 了。官方 Javadoc 原话是 RestClient 才是"新的高级特性的重点",以后新功能(比如 API 版本、HTTP Interface 分组)都只往 RestClient 上加
  • 老代码可以渐进迁移。RestTemplate 和 RestClient 共用底层(请求工厂、拦截器、消息转换器),这是能平滑迁的关键

⚠️ 版本状态:截至 2026-10-23,4.2 还是 RC1,正式版尚未发布。Spring Boot 一般 5 月、11 月各发一个小版本,4.2 GA 我推测在 11 月,以官方发布为准。Spring 7.1 参考文档里有一句 "As of Spring Framework 7.0, RestTemplate is deprecated",和 Javadoc 的 since="7.1" 对不上,我以 Javadoc 注解为准。


二、先摸清项目里有多少 RestTemplate

动手前先统计影响面。在项目根目录执行:

bash 复制代码
# 统计每个文件里 RestTemplate 相关调用的次数,按次数倒序
grep -rn --include=*.java -E "RestTemplate|getForObject|postForObject|getForEntity|postForEntity|\.exchange\(" src/ \
  | awk -F: '{print $1}' | sort | uniq -c | sort -rn | head -30
bash 复制代码
# 找出所有创建 RestTemplate 的地方(Bean 定义、new、Builder)
grep -rn --include=*.java -E "new RestTemplate|RestTemplateBuilder|@Bean[^)]*RestTemplate" src/

我的经验,一个典型 Spring Boot 项目的 RestTemplate 分三类:

  1. 一两个配置类:定义 RestTemplate Bean,加拦截器、超时、消息转换器
  2. 一堆调用点 :getForObject / postForObject / exchange
  3. 测试 :MockRestServiceServer 或 TestRestTemplate

迁移顺序也按这个来,但先改调用点,后改配置,这是官方迁移指南推荐的顺序,理由下一节说。


三、第一步:用现有 RestTemplate 包一个 RestClient

RestClient.create(RestTemplate) 会复用 RestTemplate 的请求工厂、拦截器、消息转换器。这样配置类一行不动,调用点就能一个个换。

原来的配置,大部分老项目长这样:

java 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

@Configuration
public class HttpClientConfig {

    @Bean
    public RestTemplate restTemplate() {
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setConnectTimeout(2000);
        factory.setReadTimeout(5000);
        RestTemplate restTemplate = new RestTemplate(factory);
        // 统一加 traceId 请求头
        restTemplate.getInterceptors().add((request, body, execution) -> {
            request.getHeaders().add("X-Trace-Id", MDC.get("traceId"));
            return execution.execute(request, body);
        });
        return restTemplate;
    }
}

过渡期只加一个 Bean:

java 复制代码
import org.springframework.web.client.RestClient;

@Bean
public RestClient legacyRestClient(RestTemplate restTemplate) {
    // 复用 restTemplate 的超时、拦截器、消息转换器
    return RestClient.create(restTemplate);
}

调用点注入 legacyRestClient,逐个改写。全部改完,再回头把配置挪到 RestClient.Builder 上(第五节)。先改调用点的原因:配置和调用一起改,出了问题分不清是超时没配对还是调用写错了;底层完全不变、只换 API 写法,测试一跑就知道对不对。


四、第二步:调用点改写对照表

下面是官方迁移指南里的对照表,我挑了日常最常用的部分,并补了我们项目里最常见的写法。

4.1 方法对照

RestTemplate RestClient
getForObject(url, Class, vars...) get().uri(url, vars...).retrieve().body(Class)
getForEntity(url, Class, vars...) get().uri(url, vars...).retrieve().toEntity(Class)
postForObject(url, req, Class, vars...) post().uri(url, vars...).body(req).retrieve().body(Class)
postForEntity(url, req, Class, vars...) post().uri(url, vars...).body(req).retrieve().toEntity(Class)
postForLocation(url, req, vars...) post().uri(url, vars...).body(req).retrieve().toBodilessEntity().getLocation()
put(url, req, vars...) put().uri(url, vars...).body(req).retrieve().toBodilessEntity()
delete(url, vars...) delete().uri(url, vars...).retrieve().toBodilessEntity()
exchange(url, method, entity, Class, vars...) method(m).uri(url, vars...).headers(h -> ...).body(obj).retrieve().toEntity(Class)
exchange(..., ParameterizedTypeReference, ...) ....retrieve().toEntity(ParameterizedTypeReference)
execute(url, method, callback, extractor, vars...) method(m).uri(url, vars...).exchange(ExchangeFunction)

⚠️ HttpEntity 和 RequestEntity 在 RestClient 里没有对应参数,headers 和 body 要拆开分别塞进 headers(...) 和 body(...)。这是改写时最容易漏 header 的地方。

4.2 实际改写示例

假设我们要调一个统一返回 Result<T> 的内部服务。先定义 DTO:

java 复制代码
// 下游服务的统一返回结构
public record Result<T>(int code, String msg, T data) {}

public record UserDTO(Long id, String name, String deptId) {}

public record UserQuery(String deptId, int pageNo, int pageSize) {}

GET + 路径变量

java 复制代码
// 改写前
UserDTO user = restTemplate.getForObject(
        "http://user-service/api/users/{id}", UserDTO.class, userId);

// 改写后
UserDTO user = restClient.get()
        .uri("http://user-service/api/users/{id}", userId)
        .retrieve()
        .body(UserDTO.class);

POST + 泛型返回 + 自定义 header(老代码里最长的一种)

java 复制代码
// 改写前
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("X-Org-Id", orgId);
HttpEntity<UserQuery> entity = new HttpEntity<>(query, headers);
ResponseEntity<Result<List<UserDTO>>> resp = restTemplate.exchange(
        "http://user-service/api/users/search",
        HttpMethod.POST,
        entity,
        new ParameterizedTypeReference<Result<List<UserDTO>>>() {});
List<UserDTO> users = resp.getBody().data();

// 改写后
Result<List<UserDTO>> result = restClient.post()
        .uri("http://user-service/api/users/search")
        .contentType(MediaType.APPLICATION_JSON)
        .header("X-Org-Id", orgId)
        .body(query)
        .retrieve()
        .body(new ParameterizedTypeReference<Result<List<UserDTO>>>() {});
List<UserDTO> users = result.data();

行数差不多,但不用再手搓 HttpEntity,读起来是从上到下一条请求的样子,我更喜欢这种。查询参数也别再手动拼 ?a=1&b=2,用 uri(uriBuilder -> uriBuilder.path("/api/users").queryParam("deptId", deptId).build())。


五、第三步:把配置迁到 RestClient.Builder

调用点全换完,删掉 RestTemplate Bean,改成用 Boot 自动配置的 RestClient.Builder。

5.1 超时用配置文件

Boot 4.x 的全局 HTTP 客户端属性(Boot 4.1.2 参考文档确认):

yaml 复制代码
spring:
  http:
    clients:
      connect-timeout: 2s
      read-timeout: 5s

这组属性对 Boot 自动配置的 HTTP 客户端都生效,不用再自己 new 请求工厂。

5.2 拦截器和公共 header 用 RestClientCustomizer

java 复制代码
import org.slf4j.MDC;
import org.springframework.boot.web.client.RestClientCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration(proxyBeanMethods = false)
public class RestClientConfig {

    // 对所有注入的 RestClient.Builder 生效
    @Bean
    public RestClientCustomizer traceIdCustomizer() {
        return builder -> builder.requestInterceptor((request, body, execution) -> {
            String traceId = MDC.get("traceId");
            if (traceId != null) {
                request.getHeaders().add("X-Trace-Id", traceId);
            }
            return execution.execute(request, body);
        });
    }
}

⚠️ RestClientCustomizer 的包名我是按 Boot 3.x 的 org.springframework.boot.web.client 写的。Boot 4.0 做了模块拆分,部分类换了包,升级后以 IDE 自动导入为准。

5.3 每个下游服务一个 RestClient

Boot 提供的 RestClient.Builder 是 prototype 作用域,每次注入都是新实例,所以可以放心在里面设 baseUrl,不会互相污染:

java 复制代码
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;

@Component
public class UserServiceClient {

    private final RestClient restClient;

    public UserServiceClient(RestClient.Builder builder) {
        this.restClient = builder
                .baseUrl("http://user-service")
                .defaultHeader("X-Caller", "order-service")
                .build();
    }

    public UserDTO getUser(Long id) {
        return restClient.get()
                .uri("/api/users/{id}", id)
                .retrieve()
                .body(UserDTO.class);
    }
}

⚠️ 默认请求工厂的差异:直接 RestClient.create()、classpath 上又没有 Apache HttpClient / Jetty / Reactor Netty 时,RestClient 用的是基于 JDK HttpClient 的 JdkClientHttpRequestFactory,而 RestTemplate 默认是基于 HttpURLConnection 的 SimpleClientHttpRequestFactory。官方提醒这可能带来 HTTP 层面的细微差异(比如对某些 header、重定向的处理)。迁移后联调时如果遇到"老的能通新的不通",先看这里。


六、异常处理:这里最容易出 bug

6.1 默认行为

RestClient 和 RestTemplate 一样,4xx、5xx 默认抛异常,异常都是 RestClientException 的子类。老代码里常见 catch (HttpClientErrorException.NotFound e) { return null; } 这种写法,迁移时别急着删,针对每个 catch 写一个用例确认抛出的异常类型没变。Spring 官方 issue #35620 里也有人在问两者异常是否完全一致,官方文档目前没给逐一映射表。

6.2 推荐写法:onStatus 就地处理

java 复制代码
UserDTO user = restClient.get()
        .uri("/api/users/{id}", id)
        .retrieve()
        // 404 单独转成业务上的"用户不存在"
        .onStatus(status -> status.value() == 404, (request, response) -> {
            throw new UserNotFoundException(id);
        })
        // 其他 4xx 转成业务异常,带上下游返回的 body 方便排查
        .onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {
            String body = new String(response.getBody().readAllBytes(), StandardCharsets.UTF_8);
            throw new BizException("调用用户服务失败: " + response.getStatusCode() + " " + body);
        })
        .body(UserDTO.class);

6.3 全局统一:defaultStatusHandler

每个调用都写 onStatus 太啰嗦,可以在 builder 上挂一个默认处理器:

java 复制代码
this.restClient = builder
        .baseUrl("http://user-service")
        .defaultStatusHandler(HttpStatusCode::isError, (request, response) -> {
            String body = new String(response.getBody().readAllBytes(), StandardCharsets.UTF_8);
            throw new BizException("user-service " + request.getURI().getPath()
                    + " 返回 " + response.getStatusCode() + ": " + body);
        })
        .build();

6.4 下游 HTTP 200 但业务码失败

内部服务经常是 HTTP 永远 200、靠 Result.code 表示成败。这种用 exchange 自己拿响应:

java 复制代码
UserDTO user = restClient.get()
        .uri("/api/users/{id}", id)
        .exchange((request, response) -> {
            Result<UserDTO> result = response.bodyTo(new ParameterizedTypeReference<Result<UserDTO>>() {});
            if (result == null || result.code() != 0) {
                throw new BizException("user-service 业务失败: " + (result == null ? "空响应" : result.msg()));
            }
            return result.data();
        });

⚠️ exchange 里 onStatus 和 defaultStatusHandler 都不生效,状态码要自己判断。


七、顺手把 Feign 式调用换成 HTTP Interface

如果项目里对内部服务的调用很多,我更推荐直接上 HTTP Interface,写法跟 OpenFeign 很像,但不用额外依赖:

java 复制代码
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.web.service.annotation.PostExchange;

@HttpExchange("/api/users")
public interface UserApi {

    @GetExchange("/{id}")
    UserDTO getUser(@PathVariable Long id);

    @PostExchange("/search")
    Result<List<UserDTO>> search(@RequestHeader("X-Org-Id") String orgId, @RequestBody UserQuery query);
}

Boot 4.x 用 @ImportHttpServices 注册,按分组配置地址和超时:

java 复制代码
@SpringBootApplication
@ImportHttpServices(group = "user", types = UserApi.class)
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}
yaml 复制代码
spring:
  http:
    serviceclient:
      user:
        base-url: http://user-service
        connect-timeout: 2s
        read-timeout: 3s

然后直接注入 UserApi 用。分组级的 header、拦截器用 RestClientHttpServiceGroupConfigurer:

java 复制代码
@Bean
RestClientHttpServiceGroupConfigurer callerHeaderConfigurer() {
    return groups -> groups.forEachClient((group, clientBuilder) ->
            clientBuilder.defaultHeader("X-Caller", "order-service"));
}

💡 没有分组的接口会被归到名为 default 的组里。老项目用 Boot 3.x 的话没有 @ImportHttpServices,需要手动 HttpServiceProxyFactory.builderFor(RestClientAdapter.create(restClient)).build().createClient(UserApi.class)。


八、测试:MockRestServiceServer 照样能用

MockRestServiceServer.bindTo(RestClient.Builder) 从 Spring Framework 6.1 就支持,老测试改动很小:

java 复制代码
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;

import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.client.RestClient;

class UserServiceClientTest {

    @Test
    void getUser() {
        RestClient.Builder builder = RestClient.builder();
        // 先 bind 再 build,mock 才能拦到请求
        MockRestServiceServer server = MockRestServiceServer.bindTo(builder).build();
        UserServiceClient client = new UserServiceClient(builder);

        server.expect(requestTo("http://user-service/api/users/1001"))
              .andRespond(withSuccess("{\"id\":1001,\"name\":\"张三\",\"deptId\":\"D01\"}",
                      MediaType.APPLICATION_JSON));

        UserDTO user = client.getUser(1001L);

        assertThat(user.name()).isEqualTo("张三");
        server.verify();
    }
}

集成测试里的 TestRestTemplate 在 Boot 4.2 被废弃,官方给的替代是 RestTestClient 配合 @AutoConfigureRestTestClient,这部分我还没在 RC1 上实际跑过,暂不展开。


九、Boot 4.2 里其他值得注意的变化

升 4.2 不只是 RestTemplate,顺手把 Release Notes 里会影响我们的列一下(M1 到 RC1 汇总):

变化 影响 处理
spring-boot-starter-amqp 拆分 用 RabbitMQ 的项目 AMQP 0.9 改 spring-boot-starter-rabbitmq,AMQP 1.0 用 spring-boot-starter-amqp-rabbitmq
Tomcat 三个 servlet 属性迁到 server.tomcat.servlet.* 配了 use-relative-redirects 等的项目 改属性名;use-relative-redirects 默认变为 true
management.metrics.observations.ignored-meters 删除 依赖 LongTaskTimer 指标的监控 需要的话设 management.metrics.observations.include-active-long-task-timer=true
Buildpacks 默认 builder 换成 ubuntu-resolute-builder(Ubuntu 26.04) 用 spring-boot:build-image 打镜像的 默认 run image 无 shell,要 shell 就显式指定 builder
Docker Compose 支持 MySQL/MariaDB 随机 root 密码 本地用 Docker Compose 起库的 可以去掉 compose 里的明文密码
4.1 中废弃的 API 被移除 所有项目 先在 4.1 上清掉 deprecation 警告再升
依赖升级 所有项目 Framework 7.1.0-RC1、Security 7.2.0-RC1、Data 2026.1.0-RC1、Hibernate 7.4.13.Final、MySQL Connector/J 26.7.0

十、迁移速查清单

步骤 做什么 验证
1 grep 统计 RestTemplate 调用点和定义处 列出清单
2 加 RestClient.create(restTemplate) 过渡 Bean 启动正常
3 逐个改调用点,HttpEntity 拆成 headers() + body() 每改一个跑对应用例
4 核对 catch 的异常类型 补 4xx/5xx 用例
5 超时改 spring.http.clients.*,拦截器改 RestClientCustomizer 联调看 header、超时
6 删 RestTemplate Bean 全局 grep 无残留
7 高频内部调用改 HTTP Interface(可选) 接口用例通过

十一、常见问题

Q:4.2 发布后 RestTemplate 还能用多久?

Javadoc 写的是 8.0 删除。Spring Framework 8.0 什么时候出,目前没有官方时间表,我不乱猜。能确定的是 4.2 和后续 4.x 都还能用,只是有警告。

Q:直接全局替换能不能一把梭?

不建议。exchange 那种带 HttpEntity 的写法、以及依赖特定异常类型的 catch,正则替换很容易漏 header 或改坏错误处理。我的做法是让 Claude Code 按文件批量改,但每个文件改完都要过一遍对应的单测。

Q:WebClient 要不要也换?

不用。WebClient 没被废弃,异步、流式场景官方仍然推荐它。只是很多项目为了同步调用硬引了 WebFlux 再 .block(),这种可以换成 RestClient,顺便把 webflux 依赖去掉。

Q:Spring Cloud OpenFeign 怎么办?

OpenFeign 不在这次废弃范围内,继续能用。新服务我会优先用 HTTP Interface,老的 Feign 客户端不急着动。


参考链接


我们组下个迭代就准备开始迁了,踩到新坑会回来补。觉得这份对照表有用的话,点赞 👍 收藏 📌 一下,迁移的时候翻出来照着改。

你们项目里 RestTemplate 有多少处?打算一次性迁还是渐进迁?评论区说说,顺便分享你遇到过的 RestClient 和 RestTemplate 行为不一致的地方。

相关推荐
wuminyu1 小时前
JVM规范学习总结-JVM规范基本概念
java·linux·c语言·jvm·c++
java_nnnn1 小时前
分布式系统进阶:数据库分库分表、微服务与架构演进总结
数据库·redis·微服务·架构
时间静止不是简史5 小时前
生产事故排查:Redis 宕机导致补偿队列消费线程死亡,通话录音未上送业务平台
java·redis
2301_32241428048 小时前
基于SpringBoot+Vue的休闲零食超市管理系统的设计与实现59004-计算机毕设 原创(免费领源码+部署教程+开发环境)
java·vue.js·spring boot·redis·mysql·决策树·推荐算法
金銀銅鐵10 小时前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(上)
java·后端·ai编程
福建佰胜张工11 小时前
SMC 薄型气爪 MHF2-16D1 深度解析:平行开闭、低剖面结构在自动化装配中的选型与落地
java·服务器·安全·自动化
念何架构之路11 小时前
go-grpc设计思想
开发语言·后端·golang
catoop11 小时前
API Key 设计与安全生成方案:结构拆解、数据库设计与 Java 实现
java·数据库·安全
system_error_x11 小时前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端