10 月 22 日 Spring Boot 4.2.0-RC1 发布,Spring Framework 7.1.0-RC1 正式给
RestTemplate打上了@Deprecated(since="7.1", forRemoval=true)。这篇是我梳理的渐进迁移步骤和方法对照表。适用版本:Spring Boot 4.2.0-RC1 / Spring Framework 7.1.0-RC1;RestClient 迁移代码在 Boot 3.2+ 可用,
@ImportHttpServices需要 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 分三类:
- 一两个配置类:定义 RestTemplate Bean,加拦截器、超时、消息转换器
- 一堆调用点 :
getForObject/postForObject/exchange - 测试 :
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 用的是基于 JDKHttpClient的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 客户端不急着动。
参考链接
- Spring Framework 7.1 RestTemplate Javadoc(@Deprecated since 7.1)
- Spring Framework 7.1 REST Clients 参考文档(含迁移对照表)
- Spring Boot 4.2.0-M1 Release Notes
- Spring Boot 4.2.0-M2 Release Notes
- Spring Boot 4.2.0-RC1 Release Notes
- Spring Boot 参考文档:Calling REST Services
- spring-framework issue #35620:RestClient 异常行为讨论
迁移中遇到新坑欢迎评论区补充。