Spring7 RestTemplate弃用迁移

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 重建基础设施。下面把起点也算上,拆成四步,和配图一致:

  1. 起点:现有 RestTemplate,业务仍在用模板 API;
  2. 用现有 RestTemplate 实例生成 RestClient:RestClient.create(restTemplate);
  3. 按组件替换调用写法(先改「怎么发请求」,基础设施先不动);
  4. 全部流量走 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 注入路径单独处理。

评审清单可以很短:

  1. 是否还在新代码里 new RestTemplate()?
  2. 捕获异常是否仍写死 HttpClientErrorException?
  3. 用 RestClient.builder() / RestClient.create() 重建之后,工厂是否从 HttpURLConnection 悄悄换成了 JDK HttpClient,行为是否验收过?
  4. 接口客户端是否共享了已治理的那颗 RestClient(超时、拦截器、观测),别再 new 一颗裸客户端。

一份可执行的迁移顺序建议:

  1. 盘点所有 RestTemplate Bean 与 new RestTemplate 点,标出共享的工厂与拦截器;
  2. 为每个 Bean 增加 RestClient.create(restTemplate) 的桥接 Bean(或在门面内私有转换),新代码只注入 RestClient;
  3. 按模块替换 get/post/exchange,补齐 4xx 捕获与编码相关测试;
  4. 删除桥接,改为 RestClient.builder()... 显式配置,确认工厂与拦截器列表与旧版一致;
  5. 对稳定下游引入 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」。

相关推荐
朝朝辞暮i1 小时前
C++ 第 13 课:值传递 —— 为什么函数里改了,外面却没变?
java·c++·算法
弹简特1 小时前
【Java项目-企悦抽】16-抽奖模块01-获取活动完整信息接口实现
java·开发语言·状态模式·springboot
萧瑟余晖1 小时前
Spring 资源与环境配置体系详解
spring
泡茶喝茶写代码2 小时前
A股量化数据工程:从 REST 接口到策略信号(第 1 篇):指数列表与实时行情接入
java·python·股票数据api·股票数据·股票数据api接口·股票api数据接口·股票量化数据接口
Sarvartha2 小时前
接口基础知识
java·开发语言
不才不才不不才3 小时前
Spring 源码系列(29): 20 道高频面试题源码级解析合集
java·后端·spring
索隆zoro3 小时前
Army 的可插拔架构:army-jdbc 与方言模块
java·后端
小蒜学长3 小时前
校园社团招新网站的设计与实现(代码+数据库+LW)
java·数据库·spring boot·后端·校园社团招新网站
Dovis(誓平步青云)4 小时前
多个链接不等于多份证据,新闻核验看板怎样合并来源
java·服务器·前端·javascript·人工智能·pdf·电脑