Spring Framework 7:RestTemplate 弃用后,怎样迁到 RestClient 与 HTTP Service
Spring Framework 7.0 在参考文档里把同步客户端里用了多年的 RestTemplate 标成 deprecated ,官方指向流畅 API 的 RestClient,并说明未来版本会移除。(@Deprecated 注解本身,按 Spring 官方博客的规划放在 7.1,彻底移除计划在 8.0。)文档里 REST 客户端现在是四选一:RestClient、WebClient、已弃用的 RestTemplate,以及基于注解接口的 HTTP Service Clients 。下面按官方 Migrating to RestClient 把迁移步骤和 HTTP Service 接线写清楚。Boot 的 spring.http.clients.* 前缀,以及「RestClient.create() 旁路自动配置」那条线,这里都不展开。
选择四者时也可以简单对照:要同步、要流畅 API → RestClient;要响应式或流式 → WebClient;还在旧代码里、准备迁走 → RestTemplate(仅过渡);要把 HTTP 当成「带注解的 Java 接口」来拥有 → HTTP Service Clients。Framework 7 的信号很明确:新同步代码别再把 RestTemplate 当默认答案。
一、弃用意味着什么、迁移分几步
REST Clients 文档写得很干脆:As of Spring Framework 7.0, RestTemplate is deprecated in favor of RestClient and will be removed in a future version。这是文档层面的弃用:7.0.x 的 RestTemplate 类上还没有 @Deprecated 注解(查 7.0.9 的 jar 确认),Spring 官方博客《The state of HTTP clients in Spring》给的计划是 7.1 正式加注解并标记移除,8.0 删除;Maven Central 上 7.1 目前只有 7.1.0-M2 里程碑。异步或流式场景继续看 WebClient;同步调用的主路径换成 RestClient。
官方建议渐进 迁移,别搞周末大爆炸重写。文档把它写成两大步:先用 RestClient.create(restTemplate) 逐步替换调用,再用 RestClient.Builder 重建基础设施。下面把起点也算上,拆成四步,和配图一致:
- 起点:现有
RestTemplate,业务仍在用模板 API; - 用现有
RestTemplate实例生成RestClient:RestClient.create(restTemplate); - 按组件替换调用写法(先改「怎么发请求」,基础设施先不动);
- 全部流量走
RestClient之后,再用RestClient.Builder重建工厂、拦截器等。ClientHttpRequestFactory与ClientHttpRequestInterceptor可以复用。

有一个默认工厂差异,文档特意点名:classpath 上没有 Apache / Jetty 等其它客户端时,RestClient 倾向 JdkClientHttpRequestFactory(JDK HttpClient),而 RestTemplate 倾向 SimpleClientHttpRequestFactory(HttpURLConnection)。迁移后若出现细微 HTTP 行为差别,先对齐同一套 ClientHttpRequestFactory,再查业务代码。
为什么官方还要强调「先改调用、后改基础设施」?因为很多代码库里 RestTemplate 不是一处 new 出来的:有的在 @Bean 里挂了超时与拦截器,有的被多个门面共用,还有测试里 mock 了 RestTemplate。先用 RestClient.create(restTemplate),等于在同一套请求工厂与拦截器 上换皮(Javadoc 列出继承的属性:ClientHttpRequestFactory、HttpMessageConverters、ClientHttpRequestInterceptor、ClientHttpRequestInitializer、UriBuilderFactory 和 error handler;本地用 Spring 7.0.9 试过,RestTemplate 上加的拦截器头会原样带到桥接出来的 RestClient 上,而裸的 RestClient.create() 走 JDK HttpClient,User-Agent 都不一样);行为面尽量贴近旧客户端,评审也更好做 diff。等调用侧都迁完,再动 Builder,避免「API 换了、底层工厂也换了、异常类型也变了」三次变更叠在同一个 PR。
RestClient 创建后是线程安全的,文档写明可在多线程间共享。这一点与「每个请求 new 一个客户端」的坏习惯相反。迁移时顺手把「方法内临时 new RestTemplate()」收成注入的单例(或至少是共享的 RestClient),比只改方法名更有长期价值。
二、调用怎么对照着改
下面按文档对照表挑最常见的几条。每个片段 import 齐全,可单独阅读。
GET 只要 body:
java
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestTemplate;
public class GetBodyMigrate {
public String legacy(RestTemplate rt, String url) {
return rt.getForObject(url, String.class);
}
public String modern(RestClient client, String url) {
return client.get()
.uri(url)
.retrieve()
.body(String.class);
}
}
GET 要状态与头:getForEntity → toEntity:
java
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestTemplate;
public class GetEntityMigrate {
public ResponseEntity<String> legacy(RestTemplate rt, String url) {
return rt.getForEntity(url, String.class);
}
public ResponseEntity<String> modern(RestClient client, String url) {
return client.get()
.uri(url)
.retrieve()
.toEntity(String.class);
}
}
POST 带 JSON body:
java
import org.springframework.http.MediaType;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestTemplate;
public class PostMigrate {
public record Pet(String name) {}
public Pet legacy(RestTemplate rt, String url, Pet pet) {
return rt.postForObject(url, pet, Pet.class);
}
public Pet modern(RestClient client, String url, Pet pet) {
return client.post()
.uri(url)
.contentType(MediaType.APPLICATION_JSON)
.body(pet)
.retrieve()
.body(Pet.class);
}
}
DELETE:
java
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestTemplate;
public class DeleteMigrate {
public void legacy(RestTemplate rt, String url) {
rt.delete(url);
}
public void modern(RestClient client, String url) {
client.delete()
.uri(url)
.retrieve()
.toBodilessEntity();
}
}
渐进桥接时,基础设施仍可来自旧实例:
java
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestTemplate;
public class BridgeFromTemplate {
public RestClient bridge(RestTemplate restTemplate) {
// 官方渐进第一步:先从现有 RestTemplate 得到 RestClient
return RestClient.create(restTemplate);
}
public RestClient rebuild() {
// 调用都迁完后,再用 Builder 显式重建
return RestClient.builder()
.baseUrl("https://api.example.com")
.build();
}
}
retrieve() 本身是无操作,必须落到 body / toEntity / toBodilessEntity 等终端方法才有副作用。从模板方法迁到流畅 API 时,漏写终端调用很常见:编译能过,逻辑上却是个空操作。
URI 变量写法也一一对应:模板方法里的 getForObject(url, Class, Object...) 变成 get().uri(url, args...).retrieve().body(Class);Map 形式的变量同理传到 uri(String, Map)。需要 URI 实例时用 uri(URI),文档提醒:String URL 默认会编码,而 URI 或函数形式提供的地址按「不二次编码」处理。迁代码时若旧逻辑依赖某种编码细节,这里要回归。
head / options / put / patch 在对照表里都有等价链,模式相同:动词方法 → uri →(可选)body / headers → retrieve → 终端转换。postForLocation 这类「只要 Location 头」的用法,对应到 toBodilessEntity() 再取 getLocation()。不必一次改完所有冷门方法;按调用频率排序,先清 get/post/exchange 三大户。
若旧代码大量使用 RestTemplate.exchange(RequestEntity, Class),文档脚注写明:要把 method、URI、headers、body 拆到 RestClient 的 method / uri / headers / body 上。这比「找一个同名方法」略烦,却是流畅 API 的代价,换来中间态可测、可插 status handler。
三、错误处理:别假设 4xx 行为完全一样
文档对异常层次的表述是:RestClient 与 RestTemplate 在抛错行为上一致,层次顶部都是 RestClientException。RestTemplate 对 4xx 始终抛 HttpClientErrorException;RestClient 默认遇到 4xx/5xx 同样抛 RestClientException 的子类(Javadoc 写的是 RestClientResponseException,4xx 的具体类型就是 HttpClientErrorException 及其 NotFound、Unauthorized 等子类;用 Spring 7.0.9 实测 404、401,RestClient.create()、RestClient.create(restTemplate) 与 RestTemplate 抛出的类型完全相同)。差别在于 RestClient 可以用 onStatus (单次请求)或 Builder 上的 defaultStatusHandler(全局)自定义,灵活度更高。
java
import org.springframework.http.HttpStatusCode;
import org.springframework.web.client.RestClient;
public class StatusHandlerExample {
public String fetchOrCustom(RestClient client, String url) {
return client.get()
.uri(url)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, (request, response) -> {
throw new IllegalStateException(
"client error: " + response.getStatusCode());
})
.body(String.class);
}
}
迁移回归时建议单独测:404/401 是否仍被上层按旧类型捕获。默认处理器下,旧的 catch (HttpClientErrorException ex) 仍然接得住;一旦你在 onStatus / defaultStatusHandler 里改抛自定义异常,或者换了请求工厂(文档提醒 SimpleClientHttpRequestFactory 在读取 401 这类错误响应的状态时可能直接抛异常),就要重新回归。想兜底可以改捕获更宽的 RestClientException,也可以用 onStatus 把行为收成你熟悉的形状。需要完全自己消化响应时,可用 exchange(...)。文档说明此时 不会 应用 status handlers,因为你已经拿到完整响应。
测试策略上,不必一上来就对真实下游做全量回归。可以先固定 ClientHttpRequestFactory(或测试用 mock 服务器),用同一组请求断言:状态码、关键响应头、反序列化字段。旧测试若 mock 的是 RestTemplate,迁移期允许暂时保留「桥接出来的 RestClient」走真实序列化栈,只替换调用侧;等基础设施重建阶段,再把 mock 点移到 RestClient.Builder 或 HTTP Service 接口。
四、下一步:HTTP Service 接口客户端
调用都迁到 RestClient 之后,可以把「URL、方法、入参出参」收成接口,交给 HttpServiceProxyFactory 生成代理。文档示例(GitHub 风格仓库服务)如下。
java
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;
@HttpExchange(url = "/repos/{owner}/{repo}", accept = "application/vnd.github.v3+json")
public interface RepositoryService {
@GetExchange
Repository getRepository(@PathVariable String owner, @PathVariable String repo);
record Repository(String name, String description) {}
}
接线(用上面的 RepositoryService,这一段要单独编译得有它):
java
import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.RestClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
public class HttpServiceWiring {
public RepositoryService repositoryService(RestClient restClient) {
RestClientAdapter adapter = RestClientAdapter.create(restClient);
HttpServiceProxyFactory factory = HttpServiceProxyFactory
.builderFor(adapter)
.build();
return factory.createClient(RepositoryService.class);
}
}
业务代码只依赖接口,不再散落 uri / retrieve / body。文档强调:这适合由「懂该 REST API 的团队」收敛输入输出类型、Javadoc 与方法签名,调用方拿现成 Java API 即可。同一套机制也可挂 WebClientAdapter 或(跟着 RestTemplate 一起走弃用路线的)RestTemplateAdapter。新代码优先 RestClientAdapter。

接口多、目标主机多时,Framework 提供分组配置:@ImportHttpServices 按 group 声明接口(或按包扫描),再声明 HttpServiceGroupConfigurer(常见实现形态是 RestClientHttpServiceGroupConfigurer)为各组定制 RestClient.Builder / 代理工厂。文档写明 Spring Boot、Spring Security、Spring Cloud 会通过 HttpServiceGroupConfigurer 接入各自能力。具体 YAML 键名以各项目文档为准,这里不展开。
错误处理仍落在底层客户端:例如给 RestClient.Builder 配 defaultStatusHandler,再交给 Adapter;HTTP Service 代理不会另搞一套互不相干的异常模型。
方法参数方面,文档列出了常用绑定:@PathVariable、@RequestParam、@RequestHeader、@RequestBody、@RequestPart、@CookieValue 等;也支持动态传入 URI 或 HttpMethod 覆盖注解上的声明。参数默认不能为 null,除非注解 required=false 或参数被判定为 optional。这一点和控制器侧习惯一致,却容易在「可选查询参数」上踩坑,迁移接口时要把可空语义写进方法签名。
返回值在 RestClient 适配下是同步模型:void、HttpHeaders、业务类型、ResponseEntity<T> 等。需要原始流时,RestClientAdapter 额外支持 InputStream / ResponseEntity<InputStream>。别把 WebClient 那套 Mono/Flux 返回值写进仅挂了 RestClientAdapter 的接口,那是 ReactorHttpExchangeAdapter 的事。
自定义参数可实现 HttpServiceArgumentResolver,把某个查询对象拆成多个 request parameter;工厂构建时 .customArgumentResolver(...) 注册。这比在每个方法上堆十几个 @RequestParam 更干净,也适合留给「拥有该 API 的团队」维护。
规模化时,@ImportHttpServices(group=..., types=...) 或按包扫描声明多个 group;多个配置类可以共同贡献同一个 HttpServiceProxyRegistry。同类型接口出现在多个 group 时,不能只靠类型注入,要用 registry 按 group 名取客户端。文档给了 registry.getClient("echo1", EchoService.class) 这种写法。提前规划 group 边界(通常一主机一组,或按认证方式分组),能少掉后期的歧义 Bean。
五、迁移范围与评审清单
- 弃用迁移 :重点是
RestTemplate→RestClient对照表、create(restTemplate)渐进、工厂差异、onStatus。 - HTTP Service:迁移完成后再考虑的接口化客户端,别当成「RestClient 入门」。
- 不在迁移范围内的 :Boot 全局 HTTP 客户端属性前缀(Boot 4 里是
spring.http.clients.*,见《Boot4统一http.clients配置客户端》),以及「静态RestClient.create()不走自动配置」(见《RestClient.create()绕过超时与虚拟线程配置》,那篇讲的是 Boot 3.5 下spring.http.client.*超时对它不生效)。这些是配置装配问题,和 Framework 7 的弃用迁移是两回事。若团队里仍有人随手create()却指望容器级超时生效,应回到 Boot 的 Builder 注入路径单独处理。
评审清单可以很短:
- 是否还在新代码里
new RestTemplate()? - 捕获异常是否仍写死
HttpClientErrorException? - 用
RestClient.builder()/RestClient.create()重建之后,工厂是否从HttpURLConnection悄悄换成了 JDKHttpClient,行为是否验收过? - 接口客户端是否共享了已治理的那颗
RestClient(超时、拦截器、观测),别再 new 一颗裸客户端。
一份可执行的迁移顺序建议:
- 盘点所有
RestTemplateBean 与new RestTemplate点,标出共享的工厂与拦截器; - 为每个 Bean 增加
RestClient.create(restTemplate)的桥接 Bean(或在门面内私有转换),新代码只注入RestClient; - 按模块替换 get/post/exchange,补齐 4xx 捕获与编码相关测试;
- 删除桥接,改为
RestClient.builder()...显式配置,确认工厂与拦截器列表与旧版一致; - 对稳定下游引入 HTTP Service 接口,把散落 URI 收进接口模块;需要分组时再上
@ImportHttpServices。
这样拆开后,每个 PR 都有清晰的回滚面:调用替换出问题,不必同时怀疑工厂;工厂重建出问题,调用侧已相对稳定。到 7.1 起编译器会对 RestTemplate 给出弃用告警(7.0.x 里还只是文档层面的弃用),趁 Framework 7 升级窗口清掉,比拖到「未来版本移除」时半夜救火便宜。
最后说一下版本口径:以上按 Spring Framework 7.0 文档表述,没有绑定具体的 Boot 修订号。升级时以你工程实际 BOM 为准,再对照同一份 REST Clients 参考页做回归。
六、小结
Spring Framework 7.0 给同步 HTTP 客户端画了明确路线:RestTemplate 弃用,迁到 RestClient;需要接口化时用 @HttpExchange / @GetExchange + RestClientAdapter + HttpServiceProxyFactory。迁移可以分四步:现有 RestTemplate、RestClient.create(restTemplate)、对照替换调用、Builder 重建基础设施,并留意默认 ClientHttpRequestFactory 差异与 onStatus 带来的错误处理弹性。先把调用与异常契约收干净,再上 HTTP Service 分组,比「一边弃用一边继续摊大饼 RestTemplate 工具类」可持续得多。
封面建议:白底,画面中央是四步箭头(RestTemplate → create(rt) → 替换调用 → Builder),旁边小字标注 Jdk 与 Simple 请求工厂的差异;右下角一个小框画出 Interface + @GetExchange → Proxy;整体蓝橙配色,标题文字为「RestTemplate 迁到 RestClient」。