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.importsjakarta 迁移。
  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”仍然只是表面。