Skip to content

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 全过程原理。

一、学完后必须会什么 ​

  1. 区分 Starter、SDK、autoconfigure、依赖管理和自动配置。
  2. 解释业务项目引入一个依赖后,默认 Bean 如何进入容器。
  3. 使用 Boot 2 spring.factories 注册自动配置。
  4. 对照 Boot 3 AutoConfiguration.imports 和 jakarta 迁移。
  5. 使用 @ConfigurationProperties、条件注解和默认让位机制。
  6. 使用 ApplicationContextRunner 测试默认装配、开关关闭、用户覆盖和配置错误。
  7. 排查“依赖已经引入,但 Bean 没有出现”。
  8. 判断一个能力是否适合做 Starter,避免把业务规则塞入公共基础设施。

二、先区分五个容易混淆的概念 ​

概念负责什么不负责什么
SDK提供纯 Java 客户端、模型、协议和异常不要求业务项目必须启动 Boot
autoconfigure配置属性、条件、默认 Bean、监控与诊断不承载特定订单或医院业务规则
starter聚合 SDK 和 autoconfigure 依赖通常不写复杂实现代码
依赖管理/BOM约束兼容版本不会把未声明的依赖自动加入项目
自动配置条件满足时注册默认 BeanDefinition不会替业务系统编写领域逻辑

最常见误解是“Starter 就是自动配置”。准确说法是:Starter 将 SDK 和自动配置模块带入 classpath;Boot 发现自动配置候选并判断条件;Spring 最终创建 Bean。

三、为什么商业系统需要 Starter ​

医疗数据平台有十个服务需要调用医院网关。每个服务都复制以下代码:

  • HTTP 客户端和连接池。
  • 网关地址、连接超时、读取超时和 Token 配置。
  • 签名、traceId、错误转换。
  • 健康检查、指标和日志。

复制的后果:

  1. 同一协议出现十份实现,修复安全漏洞要发布十次。
  2. 各项目依赖版本和超时默认值不一致。
  3. 有的服务忘记连接池上限,有的服务没有超时。
  4. Token 字段、错误码和监控指标无法统一。
  5. 业务开发者必须了解底层客户端装配细节。

Starter 的价值是将成熟的接入约定变成“一个依赖 + 一组配置 + 可覆盖默认 Bean”,同时保留业务系统的接管能力。

四、推荐模块边界 ​

text
hospital-client-parent
├─ hospital-client-sdk
├─ hospital-client-spring-boot-autoconfigure
├─ hospital-client-spring-boot-starter
└─ hospital-client-sample
mermaid
flowchart 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 后的完整运行过程 ​

mermaid
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["业务代码注入使用"]

必须分清:

  1. Maven 引入依赖发生在构建和运行 classpath 形成阶段。
  2. 发现自动配置类不等于条件一定满足。
  3. 条件满足注册 BeanDefinition,不等于 Bean 已创建成功。
  4. 配置绑定、构造器和初始化方法仍可能让 Bean 创建失败。

六、JDK 8 + Boot 2.7 项目结构 ​

text
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

父工程应统一版本;示例关键属性:

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 ​

java
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:

java
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 自动配置类 ​

java
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不需要配置类方法间代理调用默认代理会有不必要开销,但不是功能错误
@ConditionalOnClassSDK 类型在 classpath 才参与缺 SDK 时可能出现类加载问题
@ConditionalOnProperty提供显式启停开关业务项目难以关闭默认能力
@EnableConfigurationProperties注册并绑定配置属性 Bean属性类可能没有进入容器
@ConditionalOnMissingBean用户 Bean 优先自定义实现可能与默认实现冲突

条件判断主要决定是否注册 BeanDefinition,不应该在条件中访问外部网络。自动配置解析阶段连接医院网关,会让启动过程变慢且难以诊断。

十、第四步:Boot 2 spring.factories ​

路径必须精确:

text
src/main/resources/META-INF/spring.factories

内容:

properties
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.hospital.autoconfigure.HospitalClientAutoConfiguration

常见错误:

  • 文件放到 META-INF/spring/spring.factories,路径多了一层。
  • key 拼错,不是 EnableAutoConfiguration 的全限定名。
  • 自动配置类名或包名错误。
  • Maven 打包资源时把文件排除。
  • 多模块合并资源时覆盖了其他 spring.factories。

可直接检查 Jar:

bash
jar tf hospital-client-spring-boot-autoconfigure.jar

应能看到 META-INF/spring.factories 和自动配置 class。

十一、第五步:Starter 只聚合依赖 ​

xml
<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,也不应该扫描业务项目包。

十二、第六步:业务项目接入 ​

依赖:

xml
<dependency>
    <groupId>com.example</groupId>
    <artifactId>hospital-client-spring-boot-starter</artifactId>
    <version>1.0.0</version>
</dependency>

配置:

yaml
hospital:
  client:
    enabled: true
    base-url: https://gateway.hospital.example
    connect-timeout-ms: 1000
    read-timeout-ms: 5000

使用:

java
@Service
public class CollectService {
    private final HospitalClient hospitalClient;

    public CollectService(HospitalClient hospitalClient) {
        this.hospitalClient = hospitalClient;
    }
}

业务应用没有导入自动配置类,也没有扫描 Starter 包。Bean 能出现是因为 @EnableAutoConfiguration 从声明文件发现候选配置,不是组件扫描碰巧扫到了它。

十三、用户 Bean 为什么能覆盖默认 Bean ​

java
@Configuration
public class CustomHospitalClientConfiguration {
    @Bean
    public HospitalClient hospitalClient() {
        return new HospitalClient("http://mock-hospital", 200, 500);
    }
}
mermaid
flowchart TD
    A["解析用户配置和自动配置"] --> B["判断容器是否已有 HospitalClient"]
    B -- "已有" --> C["@ConditionalOnMissingBean 不匹配"]
    C --> D["不注册默认 BeanDefinition"]
    B -- "没有" --> E["注册 Starter 默认 BeanDefinition"]

这不是两个 Bean 创建后再删除一个,而是条件判断时默认 BeanDefinition通常就不注册。条件判断依赖配置解析顺序,因此公共自动配置要使用 Boot 提供的自动配置机制和排序,不要依赖偶然扫描顺序。

十四、Boot 3/Java 17 应改什么 ​

Boot 3 推荐自动配置类:

java
@AutoConfiguration
@ConditionalOnClass(HospitalClient.class)
@EnableConfigurationProperties(HospitalClientProperties.class)
public class HospitalClientAutoConfiguration {
}

声明文件:

text
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

每行一个类名:

text
com.example.hospital.autoconfigure.HospitalClientAutoConfiguration

主要边界:

Boot 2.7/JDK 8Boot 3/Java 17+
常见 spring.factories 声明推荐 AutoConfiguration.imports
@Configuration 可作为自动配置推荐 @AutoConfiguration
javax.validation.*jakarta.validation.*
Spring Framework 5.3Spring Framework 6

不能只把 spring.factories 改名就宣布迁移完成。Servlet、Validation、JPA、监控扩展和第三方依赖都要兼容 Jakarta 命名空间和 Java 17。

十五、用 ApplicationContextRunner 测自动配置 ​

公共 Starter 不应只用一个 sample 项目手工启动。ApplicationContextRunner 可以快速创建隔离上下文并断言条件行为。

java
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 没生效的证据化排查 ​

mermaid
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 是否让默认配置退让"]

命令和端点:

bash
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 还要补什么 ​

  1. 配置元数据和清晰文档。
  2. 连接池上限、连接超时、读取超时等保守默认值。
  3. HealthIndicator,但健康检查不能执行昂贵业务调用。
  4. Micrometer 指标,tag 不能包含患者 ID、订单号等高基数值。
  5. FailureAnalyzer,将配置错误翻译成可操作提示。
  6. 敏感配置不进入 toString() 和启动日志。
  7. 优雅关闭客户端连接池和后台线程。
  8. 版本兼容矩阵、自动配置测试和示例项目。
  9. 默认关闭有副作用的后台任务,避免“引入依赖就开始消费消息”。
  10. 明确超时、重试、幂等和线程模型,不能留给每个业务项目猜。

完整实现见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”仍然只是表面。