Spring Boot Starter 机制:JDK 8 从零实现
Starter 不是一个特殊运行时容器,也不是“依赖名称以 starter 结尾就能自动创建 Bean”。它首先是 Maven/Gradle 依赖入口;真正的开箱即用来自依赖传递、自动配置候选发现、条件判断、BeanDefinition 注册、配置绑定和 Spring Bean 生命周期共同作用。
本页先用 JDK 8 + Spring Boot 2.7.18 从零实现医院接口客户端 Starter,再单独说明 Boot 3/Java 17 的变化。健康检查、指标和 FailureAnalyzer 的完整生产扩展见Starter 全过程原理。
一、学完后必须会什么
- 区分 Starter、SDK、autoconfigure、依赖管理和自动配置。
- 解释业务项目引入一个依赖后,默认 Bean 如何进入容器。
- 使用 Boot 2
spring.factories注册自动配置。 - 对照 Boot 3
AutoConfiguration.imports和jakarta迁移。 - 使用
@ConfigurationProperties、条件注解和默认让位机制。 - 使用
ApplicationContextRunner测试默认装配、开关关闭、用户覆盖和配置错误。 - 排查“依赖已经引入,但 Bean 没有出现”。
- 判断一个能力是否适合做 Starter,避免把业务规则塞入公共基础设施。
二、先区分五个容易混淆的概念
| 概念 | 负责什么 | 不负责什么 |
|---|---|---|
| SDK | 提供纯 Java 客户端、模型、协议和异常 | 不要求业务项目必须启动 Boot |
| autoconfigure | 配置属性、条件、默认 Bean、监控与诊断 | 不承载特定订单或医院业务规则 |
| starter | 聚合 SDK 和 autoconfigure 依赖 | 通常不写复杂实现代码 |
| 依赖管理/BOM | 约束兼容版本 | 不会把未声明的依赖自动加入项目 |
| 自动配置 | 条件满足时注册默认 BeanDefinition | 不会替业务系统编写领域逻辑 |
最常见误解是“Starter 就是自动配置”。准确说法是:Starter 将 SDK 和自动配置模块带入 classpath;Boot 发现自动配置候选并判断条件;Spring 最终创建 Bean。
三、为什么商业系统需要 Starter
医疗数据平台有十个服务需要调用医院网关。每个服务都复制以下代码:
- HTTP 客户端和连接池。
- 网关地址、连接超时、读取超时和 Token 配置。
- 签名、traceId、错误转换。
- 健康检查、指标和日志。
复制的后果:
- 同一协议出现十份实现,修复安全漏洞要发布十次。
- 各项目依赖版本和超时默认值不一致。
- 有的服务忘记连接池上限,有的服务没有超时。
- Token 字段、错误码和监控指标无法统一。
- 业务开发者必须了解底层客户端装配细节。
Starter 的价值是将成熟的接入约定变成“一个依赖 + 一组配置 + 可覆盖默认 Bean”,同时保留业务系统的接管能力。
四、推荐模块边界
hospital-client-parent
├─ hospital-client-sdk
├─ hospital-client-spring-boot-autoconfigure
├─ hospital-client-spring-boot-starter
└─ hospital-client-sampleflowchart TD
A["业务应用引入 starter"] --> B["starter 传递 autoconfigure"]
B --> C["autoconfigure 依赖 SDK"]
C --> D["Boot 发现 HospitalClientAutoConfiguration"]
D --> E["条件满足后注册 HospitalClient Bean"]
E --> F["业务 Service 构造器注入客户端"]为什么不把三个模块合成一个:
- SDK 可被非 Spring、批处理工具和普通 Java 程序复用。
- autoconfigure 可以独立做上下文测试。
- starter 保持纯依赖入口,升级关系清楚。
- 业务应用不会为了使用纯客户端被迫引入整个 Boot 自动配置体系。
小型内部组件可以暂时合并 starter 与 autoconfigure,但概念职责仍要分清。
五、引入 Starter 后的完整运行过程
flowchart TD
A["Maven 解析 starter 依赖"] --> B["SDK 和 autoconfigure 进入 classpath"]
B --> C["SpringApplication 启动"]
C --> D["@EnableAutoConfiguration 导入选择器"]
D --> E["读取 spring.factories 候选类"]
E --> F["解析 HospitalClientAutoConfiguration"]
F --> G["判断 classpath、属性和已有 Bean"]
G --> H{"全部条件满足"}
H -- "否" --> I["跳过并记录条件结果"]
H -- "是" --> J["注册 BeanDefinition"]
J --> K["refresh 阶段绑定配置并创建 Bean"]
K --> L["业务代码注入使用"]必须分清:
- Maven 引入依赖发生在构建和运行 classpath 形成阶段。
- 发现自动配置类不等于条件一定满足。
- 条件满足注册 BeanDefinition,不等于 Bean 已创建成功。
- 配置绑定、构造器和初始化方法仍可能让 Bean 创建失败。
六、JDK 8 + Boot 2.7 项目结构
hospital-client-sdk/
src/main/java/com/example/hospital/client/HospitalClient.java
hospital-client-spring-boot-autoconfigure/
src/main/java/com/example/hospital/autoconfigure/
HospitalClientProperties.java
HospitalClientAutoConfiguration.java
src/main/resources/META-INF/spring.factories
hospital-client-spring-boot-starter/
pom.xml父工程应统一版本;示例关键属性:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/>
</parent>
<properties>
<java.version>8</java.version>
</properties>七、第一步:编写不依赖 Spring 的 SDK
package com.example.hospital.client;
public class HospitalClient {
private final String baseUrl;
private final int connectTimeoutMs;
private final int readTimeoutMs;
public HospitalClient(String baseUrl,
int connectTimeoutMs,
int readTimeoutMs) {
if (baseUrl == null || baseUrl.trim().isEmpty()) {
throw new IllegalArgumentException("baseUrl must not be blank");
}
this.baseUrl = baseUrl;
this.connectTimeoutMs = connectTimeoutMs;
this.readTimeoutMs = readTimeoutMs;
}
public String describe() {
return baseUrl + ",connect=" + connectTimeoutMs
+ ",read=" + readTimeoutMs;
}
}SDK 不使用 @Component,也不读取 application.yml。它只表达客户端行为和构造参数。这样普通 Java 程序也可以直接 new HospitalClient(...)。
真实 HTTP 客户端还要处理连接池、DNS、TLS、请求签名、响应关闭、错误映射和指标;本页用最小代码观察自动装配行为。
八、第二步:配置属性类
Boot 2.7 使用 javax.validation:
package com.example.hospital.autoconfigure;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
@Validated
@ConfigurationProperties(prefix = "hospital.client")
public class HospitalClientProperties {
private boolean enabled = true;
@NotBlank
private String baseUrl;
@Min(100)
private int connectTimeoutMs = 1000;
@Min(100)
private int readTimeoutMs = 3000;
// getter、setter 省略
}为什么不用许多 @Value:公共组件需要统一前缀、嵌套结构、默认值、类型转换、校验、IDE 元数据和测试,配置属性对象更适合表达完整契约。
配置值来自最终 Environment,不是属性类主动打开 YAML 文件。Binder 根据前缀进行宽松名称匹配和类型转换,再执行校验。
九、第三步:Boot 2 自动配置类
package com.example.hospital.autoconfigure;
import com.example.hospital.client.HospitalClient;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(HospitalClient.class)
@ConditionalOnProperty(
prefix = "hospital.client",
name = "enabled",
havingValue = "true",
matchIfMissing = true)
@EnableConfigurationProperties(HospitalClientProperties.class)
public class HospitalClientAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public HospitalClient hospitalClient(HospitalClientProperties properties) {
return new HospitalClient(
properties.getBaseUrl(),
properties.getConnectTimeoutMs(),
properties.getReadTimeoutMs());
}
}每个注解的职责:
| 注解 | 判断或动作 | 不加的后果 |
|---|---|---|
@Configuration | 声明配置类 | @Bean 方法不会按配置类方式解析 |
proxyBeanMethods=false | 不需要配置类方法间代理调用 | 默认代理会有不必要开销,但不是功能错误 |
@ConditionalOnClass | SDK 类型在 classpath 才参与 | 缺 SDK 时可能出现类加载问题 |
@ConditionalOnProperty | 提供显式启停开关 | 业务项目难以关闭默认能力 |
@EnableConfigurationProperties | 注册并绑定配置属性 Bean | 属性类可能没有进入容器 |
@ConditionalOnMissingBean | 用户 Bean 优先 | 自定义实现可能与默认实现冲突 |
条件判断主要决定是否注册 BeanDefinition,不应该在条件中访问外部网络。自动配置解析阶段连接医院网关,会让启动过程变慢且难以诊断。
十、第四步:Boot 2 spring.factories
路径必须精确:
src/main/resources/META-INF/spring.factories内容:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.hospital.autoconfigure.HospitalClientAutoConfiguration常见错误:
- 文件放到
META-INF/spring/spring.factories,路径多了一层。 - key 拼错,不是
EnableAutoConfiguration的全限定名。 - 自动配置类名或包名错误。
- Maven 打包资源时把文件排除。
- 多模块合并资源时覆盖了其他
spring.factories。
可直接检查 Jar:
jar tf hospital-client-spring-boot-autoconfigure.jar应能看到 META-INF/spring.factories 和自动配置 class。
十一、第五步:Starter 只聚合依赖
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>hospital-client-spring-boot-autoconfigure</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>autoconfigure 模块再依赖 SDK。是否把 SDK 同时直接声明在 starter 中,取决于模块依赖是否希望更直观;不要出现两个不同版本的 SDK。
Starter 一般不需要启动类,不需要 @ComponentScan,也不应该扫描业务项目包。
十二、第六步:业务项目接入
依赖:
<dependency>
<groupId>com.example</groupId>
<artifactId>hospital-client-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>配置:
hospital:
client:
enabled: true
base-url: https://gateway.hospital.example
connect-timeout-ms: 1000
read-timeout-ms: 5000使用:
@Service
public class CollectService {
private final HospitalClient hospitalClient;
public CollectService(HospitalClient hospitalClient) {
this.hospitalClient = hospitalClient;
}
}业务应用没有导入自动配置类,也没有扫描 Starter 包。Bean 能出现是因为 @EnableAutoConfiguration 从声明文件发现候选配置,不是组件扫描碰巧扫到了它。
十三、用户 Bean 为什么能覆盖默认 Bean
@Configuration
public class CustomHospitalClientConfiguration {
@Bean
public HospitalClient hospitalClient() {
return new HospitalClient("http://mock-hospital", 200, 500);
}
}flowchart TD
A["解析用户配置和自动配置"] --> B["判断容器是否已有 HospitalClient"]
B -- "已有" --> C["@ConditionalOnMissingBean 不匹配"]
C --> D["不注册默认 BeanDefinition"]
B -- "没有" --> E["注册 Starter 默认 BeanDefinition"]这不是两个 Bean 创建后再删除一个,而是条件判断时默认 BeanDefinition通常就不注册。条件判断依赖配置解析顺序,因此公共自动配置要使用 Boot 提供的自动配置机制和排序,不要依赖偶然扫描顺序。
十四、Boot 3/Java 17 应改什么
Boot 3 推荐自动配置类:
@AutoConfiguration
@ConditionalOnClass(HospitalClient.class)
@EnableConfigurationProperties(HospitalClientProperties.class)
public class HospitalClientAutoConfiguration {
}声明文件:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports每行一个类名:
com.example.hospital.autoconfigure.HospitalClientAutoConfiguration主要边界:
| Boot 2.7/JDK 8 | Boot 3/Java 17+ |
|---|---|
常见 spring.factories 声明 | 推荐 AutoConfiguration.imports |
@Configuration 可作为自动配置 | 推荐 @AutoConfiguration |
javax.validation.* | jakarta.validation.* |
| Spring Framework 5.3 | Spring Framework 6 |
不能只把 spring.factories 改名就宣布迁移完成。Servlet、Validation、JPA、监控扩展和第三方依赖都要兼容 Jakarta 命名空间和 Java 17。
十五、用 ApplicationContextRunner 测自动配置
公共 Starter 不应只用一个 sample 项目手工启动。ApplicationContextRunner 可以快速创建隔离上下文并断言条件行为。
class HospitalClientAutoConfigurationTest {
private final ApplicationContextRunner contextRunner =
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(
HospitalClientAutoConfiguration.class));
@Test
void createsDefaultClient() {
contextRunner
.withPropertyValues(
"hospital.client.base-url=http://hospital",
"hospital.client.connect-timeout-ms=1000",
"hospital.client.read-timeout-ms=3000")
.run(context -> {
assertThat(context).hasSingleBean(HospitalClient.class);
assertThat(context.getBean(HospitalClient.class).describe())
.contains("http://hospital");
});
}
@Test
void backsOffWhenUserProvidesBean() {
contextRunner
.withPropertyValues("hospital.client.base-url=http://default")
.withBean(HospitalClient.class,
() -> new HospitalClient("http://custom", 200, 500))
.run(context -> {
assertThat(context).hasSingleBean(HospitalClient.class);
assertThat(context.getBean(HospitalClient.class).describe())
.contains("http://custom");
});
}
@Test
void doesNotCreateClientWhenDisabled() {
contextRunner
.withPropertyValues("hospital.client.enabled=false")
.run(context ->
assertThat(context).doesNotHaveBean(HospitalClient.class));
}
}还应覆盖:缺少 baseUrl 时启动失败、超时小于最小值、SDK 类缺失、多个用户 Bean、属性覆盖、Boot 版本兼容。
十六、四个失败实验
16.1 删除 spring.factories
依赖仍在,SDK 类也能手动 new,但自动配置候选无法发现,容器中没有默认 HospitalClient。证明“classpath 有类”与“Boot 知道自动配置类”是两件事。
16.2 设置 enabled=false
自动配置类会出现在条件报告的未匹配部分,默认 Bean 不注册。业务 Service 如果无条件要求该 Bean,应用仍可能因依赖缺失启动失败;可选能力应使用 ObjectProvider 或在更高层同步使用相同条件。
16.3 删除 baseUrl
条件可能已经匹配,但配置绑定校验失败。证明“Positive matches”不代表 Bean 一定创建成功。
16.4 移除 @ConditionalOnMissingBean
用户自定义 Bean 与默认 Bean 同时存在,按类型注入可能出现 NoUniqueBeanDefinitionException,或用户以为已经接管但实际注入结果不确定。
十七、Starter 没生效的证据化排查
flowchart TD
A["业务项目注入失败"] --> B["检查 dependency:tree"]
B --> C["检查运行 Jar 中声明文件"]
C --> D["在 conditions 报告搜索自动配置类"]
D --> E{"是否成为候选"}
E -- "否" --> F["检查依赖、文件路径和类名"]
E -- "是但未匹配" --> G["查看 class、property、bean 条件原因"]
E -- "正向匹配" --> H["检查配置绑定和 Bean 创建异常"]
H --> I["检查用户 Bean 是否让默认配置退让"]命令和端点:
mvn dependency:tree
jar tf your-autoconfigure.jar
java -jar app.jar --debug
curl http://127.0.0.1:8080/actuator/conditions
curl http://127.0.0.1:8080/actuator/configprops| 证据 | 能说明什么 |
|---|---|
| dependency tree 没有 starter | 构建依赖或 scope 有问题 |
| Jar 中没有声明文件 | 资源路径或打包配置错误 |
| conditions 中没有自动配置类 | 未成为候选、被排除或版本机制不兼容 |
| Negative match | 条件明确不满足 |
| Positive match 但启动失败 | 配置绑定、构造器或初始化失败 |
| 只有用户 Bean | 默认配置按设计退让,不是故障 |
十八、生产级 Starter 还要补什么
- 配置元数据和清晰文档。
- 连接池上限、连接超时、读取超时等保守默认值。
- HealthIndicator,但健康检查不能执行昂贵业务调用。
- Micrometer 指标,tag 不能包含患者 ID、订单号等高基数值。
- FailureAnalyzer,将配置错误翻译成可操作提示。
- 敏感配置不进入
toString()和启动日志。 - 优雅关闭客户端连接池和后台线程。
- 版本兼容矩阵、自动配置测试和示例项目。
- 默认关闭有副作用的后台任务,避免“引入依赖就开始消费消息”。
- 明确超时、重试、幂等和线程模型,不能留给每个业务项目猜。
完整实现见Starter 全过程原理和Spring Boot 扩展点。
十九、什么不应该做成 Starter
- 只被一个服务使用、变化很快的业务逻辑。
- 某家医院独有的字段映射和业务判断。
- 无法给出安全默认值、引入后必然产生副作用的能力。
- 需要大量扫描业务包、修改业务 Bean 的“万能框架”。
- 为减少几行配置而增加复杂生命周期和黑盒行为的封装。
适合做 Starter 的通常是稳定、跨服务复用、可配置、可关闭、可覆盖、可观测的基础设施能力。
二十、面试标准回答
Starter 为什么能开箱即用
Starter 首先通过 Maven 聚合 SDK 和 autoconfigure 依赖。Spring Boot 启动时,
@EnableAutoConfiguration导入选择器;Boot 2.6及更早常从spring.factories读取候选类,Boot 2.7已支持imports,Boot 3从AutoConfiguration.imports读取自动配置候选类。配置类经过 classpath、属性和已有 Bean 条件判断后注册 BeanDefinition,容器在refresh()阶段完成配置绑定、依赖注入和 Bean 创建,所以业务代码可以直接注入默认客户端。
为什么要拆 SDK、autoconfigure 和 starter
SDK 保持纯 Java 可独立使用;autoconfigure 集中放属性、条件、默认 Bean、监控和诊断;starter 只提供依赖入口。拆分后职责清晰、测试独立、版本关系可控,也不会让非 Spring 使用者被迫依赖 Boot。
Starter 没生效怎么排查
先用 dependency tree 确认运行时依赖,再检查 autoconfigure Jar 中的声明文件;然后在 debug 或 Actuator conditions 报告搜索目标自动配置,判断未成为候选、被排除还是条件不匹配;条件匹配后继续检查配置绑定、Bean 构造异常和用户 Bean 是否触发默认让位。
面试页只保留标准回答,详细过程回到本页:Spring Boot 面试题。
二十一、关联知识点
本章小结
Starter 的完整链路是:依赖工具把 SDK 与 autoconfigure 放入 classpath,Boot 从版本对应的声明文件发现候选配置,条件注解决定是否注册默认 BeanDefinition,Binder 将 Environment 配置绑定为属性对象,Spring 在刷新阶段创建最终 Bean,业务项目通过自定义 Bean 接管默认实现。
能说出每一步的执行者、输入、输出和失败证据,才算理解 Starter;只记住“Starter 是依赖集合”或“加 @ConditionalOnMissingBean”仍然只是表面。
