升到 Spring Boot 4 之后,接口上的 @HttpServiceClient("echo") 突然编译不过,或者启动后注入不到 HTTP Service Bean------这不是依赖少了,而是 Framework 7 / Boot 4 已删掉该注解,也不再扫描注册 。本文按官方 rest-client 文档与 issue #47179,把怎么改成 @ImportHttpServices、组级属性怎么配、怎么验 Bean 说清楚。
为什么 Boot4 没有 HttpServiceClient 了?
Spring Boot issue #47179 写得很直:@HttpServiceClient 的支持已在 Spring Framework 7.0 拿掉 ,并在 Boot 侧 #47123 移除;Boot 4.0 不再扫描 HTTP Service 接口客户端。组级 configuration properties 与 group configurer 还在 ,变的是「谁把接口注册成 Bean」------从注解扫描,改成你显式 @ImportHttpServices。
常见现象对照:
| 现象 | 大致原因 |
|---|---|
@HttpServiceClient 编译找不到类型 |
Framework 7 已删除该注解 |
| 升 4.0 后接口 Bean 注入失败 | Boot 不再扫描;未写 @ImportHttpServices |
| 接口上还写死 base URL | 应迁到 spring.http.serviceclient.<group>.* |
| 多组只改了一处 Import | 多组需重复注解,每组各自 group |
决策流可以先看一张图:

图:Boot3 扫描注册 → Boot4 删注解 → 显式 ImportHttpServices → 组配置 → 验收。
边界钉死:不是「注解改名还能自动扫」,而是显式导入;故意不 Import,容器里就不应有对应 Bean。
同系列升级里,WebClient starter 模块化见 168121729,TestRestTemplate 换法见 168206634,Undertow 迁移见 168409414;本稿只盯 HTTP Service 接口客户端注解怎么换,不把那些主题再写一遍。
官方硬点以 rest-client 文档「HTTP Service Interface Clients」 与 #47179 为准。Migration Guide wiki 没有单独的 HttpServiceClient 专节,升级时不要指望在那里逐条对照。
ImportHttpServices 怎么写?
包名固定为:org.springframework.web.service.registry.ImportHttpServices(Framework 7.0 起)。通常挂在主类或配置类上,属性常用:
group:组名,默认"default"types/value:直接列出接口类型basePackages/basePackageClasses:按包扫描接口(仍是你声明的扫描范围,不是 Boot 默认全包扫描)clientType:后端客户端类型;默认UNSPECIFIED→ 用 RestClient
单组:列类型
java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.service.registry.ImportHttpServices;
@SpringBootApplication
@ImportHttpServices(group = "echo", types = EchoClient.class)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
接口本身继续用 @HttpExchange / @GetExchange 等声明路径,不要 再挂已删除的 @HttpServiceClient:
java
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;
@HttpExchange("/api")
public interface EchoClient {
@GetExchange("/ping")
String ping();
}
按包导入
接口多、不想在注解里逐个列类型时:
java
@ImportHttpServices(group = "echo", basePackages = "com.example.clients.echo")
注意:basePackages 只扫你写出的包;没有「Boot 4 默认全工程扫描 HTTP Service」这回事。漏写 Import,就不会有 Bean。
多组:重复注解
#47179 明确:多组用重复 的 @ImportHttpServices(group=..., types=...) 覆盖原先多个 @HttpServiceClient("group") 的场景:
java
@SpringBootApplication
@ImportHttpServices(group = "echo", types = EchoClient.class)
@ImportHttpServices(group = "billing", types = BillingClient.class)
public class Application { }
每组各自对应一份 spring.http.serviceclient.<group>.* 配置,不要共用错组名。
spring.http.serviceclient 配什么?
组级前缀是 spring.http.serviceclient.<group>.*。文档示例方向包括:base URL、默认头、API versioning、redirect、connection/read timeouts、SSL bundles。升级时最常改的是这三项:
yaml
spring:
http:
# 全局:作用于所有 HTTP clients;组级可覆盖
clients:
connect-timeout: 1s
serviceclient:
echo:
base-url: https://echo.example.com
connect-timeout: 2s
read-timeout: 2s
billing:
base-url: https://billing.example.com
read-timeout: 5s
# 未指定 group 时,客户端归入名为 default 的组
default:
base-url: https://default.example.com
实践要点:
- 接口上去掉写死的绝对 URL ,改走组级
base-url,环境差异用 profile / 配置中心覆盖。 - 全局 用
spring.http.clients.*(例如全局connect-timeout);组级 用spring.http.serviceclient.<group>.*覆盖。文档示例就是:全局 1s,echo 组 connect/read 2s。 - 未写
group时,客户端进"default"组------配置键也要写成spring.http.serviceclient.default.*,别找错前缀。
属性与注册的关系可以再压成一张图:

图:Import + serviceclient 属性汇入 Bean;全局 clients 可被组级覆盖;configurer 可选。
默认 RestClient,什么时候才用 WebClient?
Framework 侧:clientType 默认 UNSPECIFIED,使用 RestClient 。要走 WebClient,需显式改成 WEB_CLIENT(并准备好对应栈),例如:
java
@ImportHttpServices(
group = "echo",
types = EchoClient.class,
clientType = ImportHttpServices.ClientType.WEB_CLIENT
)
Boot 侧提供两类 group configurer Bean(声明即由 auto-config 应用):
RestClientHttpServiceGroupConfigurer------ RestClient 后端WebClientHttpServiceGroupConfigurer------ WebClient 后端
Starter 对照(Migration Guide starters 表):spring-boot-starter-restclient(命令式 RestClient/RestTemplate)、spring-boot-starter-webclient(响应式 WebClient)。生产侧 starter 怎么换,见已发文 168121729;不要 编造「classpath 里有 WebFlux 就自动把所有 HTTP Service 切成 WebClient」------默认仍是 RestClient,切 WebClient 要显式 clientType + 对应 starter/configurer。
可选定制(RestClient 后端加默认头,一句示例即可):
java
@Bean
RestClientHttpServiceGroupConfigurer echoHeaders() {
return groups -> groups.filterByName("echo")
.forEachClient((group, builder) ->
builder.defaultHeader("X-Client", "boot4-demo"));
}
WebClient 后端同理换 WebClientHttpServiceGroupConfigurer。超时等组级项继续走 spring.http.serviceclient.* / spring.http.clients.*,不要 在本文里往 WebClient.Builder 上堆 responseTimeout / connectTimeout 写法(既有禁写项)。
怎么确认 HTTP Service Bean 注册成功?
建议按「能注入 → 能打到 URL → 故意失败对照」三步验收,比只看启动日志安心:
1. 正常路径:注入并调用
java
@RestController
@RequiredArgsConstructor
class PingController {
private final EchoClient echoClient;
@GetMapping("/local-ping")
String localPing() {
return echoClient.ping();
}
}
启动后访问本机入口,确认请求落到 spring.http.serviceclient.echo.base-url 指向的地址(可用 mock server / WireMock,或临时改 base-url 看落点)。
2. 负向对照:故意不 Import
临时去掉 @ImportHttpServices(或去掉某一组),应出现 无对应 Bean (NoSuchBeanDefinitionException 一类)。加回后恢复。这一步专门用来钉死「Boot 4 不再扫描」------避免有人以为「放在 component-scan 包下就会自动注册」。
3. 依赖与后端栈
bash
# Maven 示例:确认 restclient / webclient 按你的选型出现
mvn -q dependency:tree -Dincludes=org.springframework.boot:spring-boot-starter-restclient
mvn -q dependency:tree -Dincludes=org.springframework.boot:spring-boot-starter-webclient
默认走 RestClient 时,应能看到 restclient 相关坐标;只有显式 WEB_CLIENT 时才去对齐 webclient starter。测试侧客户端换法见 168206634,与本稿生产 HTTP Service 注册是两条线。
4. 一眼检查表
| 检查项 | 期望 |
|---|---|
源码中 @HttpServiceClient |
无(已删) |
@ImportHttpServices |
有;多组则重复注解 |
spring.http.serviceclient.<group>.base-url |
与组名一致 |
| 故意不 Import | 注入失败 |
| 默认后端 | RestClient(未改 clientType) |
边界与不要踩的坑
- 不要把
@HttpServiceClient写成仍可用 ,也不要假设「以后官方会加回同名注解」。#47179 只提到若类似能力有用会收集反馈,不等于承诺加回同名 API。 - 不要写「扫描默认全开」 。Boot 4 明确不再扫描;必须
@ImportHttpServices(types或你声明的basePackages)。 - Migration Guide 无 HttpServiceClient 专节 ------硬点以 rest-client.html + #47179 为准,别谎称 wiki 专节逐条列出。
- 禁写
WebClient.Builder上的responseTimeout/connectTimeout迁移教条;超时用文档中的spring.http.clients.*/spring.http.serviceclient.*。 - 勿抢主线 :Undertow 没了怎么迁见 168409414;WebClient starter、TestRestTemplate 各有专文。本稿不展开 SSRF /
InetAddressFilter长文,也不写活动名。 - 多模块工程里,Import 往往挂在「组装 HTTP 客户端的配置模块」而不是每个业务 jar 上;升级时全局搜索旧注解名,比只改主类更不容易漏组。
从 Boot3 迁过来的改法顺序
建议按固定顺序改,避免「注解删了、配置还指着旧组名」或「配置改了、Import 漏了」:
- 全仓库搜索
@HttpServiceClient、旧 import。业务模块、内部 starter、示例工程一并扫。 - 删掉旧注解 ,在组装客户端的配置类或主类上补
@ImportHttpServices;多组就写多条。 - 把写死在接口/注解上的绝对 URL 挪走 ,落到
spring.http.serviceclient.<group>.base-url;路径片段仍留在@HttpExchange。 - 对齐组名 :旧
@HttpServiceClient("echo")的字符串,就是新注解group = "echo",也是配置里的echo段。改名要三处一起改。 - 确认后端栈 :多数工程保持默认 RestClient +
spring-boot-starter-restclient;只有明确要响应式客户端时再设clientType = WEB_CLIENT并加 webclient starter。 - 跑正负向验收:能注入调用;去掉 Import 应失败;改 base-url 看落点。
Boot 3 常见「接口上挂 @HttpServiceClient、指望组件扫描自动注册」在 Boot 4 整条失效。迁移 MR 里不妨把「搜索结果截图 / 旧注解清零」写进描述,减少漏模块。
若你们以前把 URL 写在接口常量或自定义注解里,升级时容易只改一处。更稳妥的是:注册看 Import,地址看 serviceclient 属性,路径看 HttpExchange------三层职责分开,环境差异只动配置。
旧写法 → 新写法对照
| Boot 3 方向 | Boot 4 对应 |
|---|---|
接口或配置上的 @HttpServiceClient("echo") |
删除;改为 @ImportHttpServices(group="echo", types=...) |
| 依赖组件扫描自动发现接口 | 必须显式 Import;可用 basePackages 限定范围 |
多组多个 @HttpServiceClient |
多条可重复的 @ImportHttpServices |
| 组 URL / 超时(若已用 serviceclient 属性) | 前缀仍为 spring.http.serviceclient.<group>.*(保留并核对) |
| 未分组客户端 | 归入 default 组;配置键用 ...serviceclient.default.* |
这张表可以贴进升级手册:评审时按行勾,比口头说「按文档改」更不容易漏。
配置中心与多环境注意点
spring.http.serviceclient.* 经常不只在仓库 yml 里,还在 Nacos、K8s ConfigMap、启动参数里。只改 Git、不改运行时配置,会出现「本地能调、预发仍打到旧域名」的漂移。建议:
- 每个环境一份组级
base-url(或占位符 + 环境变量),组名保持一致 ,避免 profile 里组名拼错导致落到空配置的default。 - 超时先用文档示例量级做冒烟,再按下游 SLA 调;全局
spring.http.clients.*适合「全集群统一下限」,组级适合「某个慢下游单独放宽」。 - 新增一组客户端时,先加 Import,再加配置,最后写注入点------顺序反了,本地用假配置也能「看起来能启动」,一到联调才发现 Bean 都没有。
HTTP Service 这条适合小步 MR:先编译通过并完成 Import,再单独收拢 URL 与超时,别和一个 MR 里混进 Undertow / Jackson(见系列内链)。
FAQ
为什么 Boot4 没有 HttpServiceClient 了?
Framework 7 删除该注解,Boot 4(#47123 / #47179)不再扫描注册。改用 @ImportHttpServices,组配置与 group configurer 仍保留。
ImportHttpServices 怎么写多组?
对每个组写一条 @ImportHttpServices(group=..., types=...)(或 basePackages),注解可重复;组名与 spring.http.serviceclient.<group> 对齐。
spring.http.serviceclient 配什么?
至少配 base-url;常用再加 connect-timeout / read-timeout。全局默认走 spring.http.clients.*,组级覆盖。未指定 group 时组名为 default。
接口上的 URL 还要写死吗?
绝对地址建议去掉,改组级 base-url;路径仍留在 @HttpExchange / @GetExchange 上。
怎么确认 HTTP Service Bean 注册成功?
启动后注入调用;再故意去掉 Import 应无 Bean;改 base-url 看请求落点;依赖树确认 RestClient / WebClient 后端与选型一致。
小结 checklist
- 删除所有
@HttpServiceClient;改org.springframework.web.service.registry.ImportHttpServices - 单组:
group+types或basePackages;多组:重复注解 - 配置:
spring.http.serviceclient.<group>.base-url|connect-timeout|read-timeout;全局spring.http.clients.* - 默认 RestClient;WebClient 须显式
clientType+ 对应 configurer/starter - 验收:能注入;故意不 Import 无 Bean;依赖树与 base-url 落点核对
- 文档锚点:rest-client.html + #47179(勿依赖 Migration Guide 专节)
按「删旧注解 → 显式 Import → 组级属性 → 正负向验收」改完,这类 Boot4 HTTP Service 注册失败通常一次就能闭环,比在依赖里猜「是不是少了某个自动配置」省事。