Skip to content

Spring Boot Starter 全过程原理

Starter 不是“把几个依赖放一起”这么简单。真正能在商业项目里复用的 Starter,必须同时具备依赖聚合、自动配置发现、条件装配、配置绑定、默认 Bean、用户覆盖、监控指标、健康检查、失败诊断和线上排查能力。

一句话先建立直觉:

Starter 是一个场景化接入入口;autoconfigure 提供默认装配;Spring Boot 通过自动配置清单发现配置类,再用条件注解决定是否创建 Bean。

学习目标

学完这一页,你要能说清楚:

  1. Starter、autoconfigure、client SDK、业务项目的边界。
  2. 为什么生产项目推荐拆成 xxx-starterxxx-autoconfigure
  3. Starter 引入后为什么能开箱即用。
  4. Boot 2 的 spring.factories 和 Boot 3 的 AutoConfiguration.imports 怎么写。
  5. @ConfigurationProperties@ConditionalOnClass@ConditionalOnMissingBean@ConditionalOnProperty 各自负责什么。
  6. 自定义 Starter 从 0 到可用需要哪些文件。
  7. 为什么公共 Starter 不能强行创建 Bean。
  8. Starter 怎么支持健康检查、指标、失败诊断。
  9. 自动配置没生效、配置没绑定、Bean 冲突怎么排查。
  10. 商业项目里哪些能力适合沉淀成 Starter。

Starter 解决什么问题

假设公司有 20 个微服务都要调用医院 HIS 接口。没有 Starter 时,每个服务都要重复做:

  1. 引入 HTTP 客户端依赖。
  2. HospitalClient Bean。
  3. 写医院地址、超时、鉴权配置。
  4. 写健康检查。
  5. 写指标。
  6. 写失败重试和日志。
  7. 写启动时配置校验。

结果很容易变成:

问题后果
依赖版本不一致有的服务能跑,有的服务启动失败
Bean 写法不一致超时、重试、鉴权行为不统一
配置项命名不一致运维和接入方难理解
没有健康检查依赖挂了监控不知道
没有失败诊断缺配置时只抛空指针

Starter 的目标是把公共接入方式标准化:

mermaid
flowchart TD
    A["业务项目引入 hospital-spring-boot-starter"] --> B["依赖自动进入 classpath"]
    B --> C["Spring Boot 发现自动配置类"]
    C --> D["绑定 hospital.client 配置"]
    D --> E["按条件创建 HospitalClient"]
    E --> F["注册健康检查和指标"]
    F --> G["业务代码直接注入使用"]

模块边界

生产项目里推荐至少拆三层:

text
hospital-client-sdk
hospital-spring-boot-autoconfigure
hospital-spring-boot-starter
模块职责是否依赖 Spring Boot
hospital-client-sdk真正的客户端能力,比如 HTTP 调用、签名、重试尽量不依赖
hospital-spring-boot-autoconfigure自动配置类、属性绑定、条件装配、健康检查依赖
hospital-spring-boot-starter依赖聚合入口,通常几乎不写代码依赖聚合

为什么要拆?

  1. SDK 可以被非 Spring Boot 项目使用。
  2. autoconfigure 可以单独测试自动配置逻辑。
  3. starter 只做入口,避免职责混乱。
  4. 依赖升级和版本管理更清楚。

Starter 开箱即用全过程

mermaid
flowchart TD
    A["业务项目添加 starter 依赖"] --> B["starter 引入 autoconfigure 和 sdk"]
    B --> C["自动配置声明文件进入 classpath"]
    C --> D["AutoConfigurationImportSelector 读取候选配置"]
    D --> E["条件注解判断"]
    E --> F{"条件匹配吗"}
    F -- "否" --> G["跳过"]
    F -- "是" --> H["绑定配置属性"]
    H --> I["注册默认 BeanDefinition"]
    I --> J["refresh 阶段创建 Bean"]
    J --> K["业务 Service 注入使用"]

关键点:

  1. starter 本身不神奇,真正装配来自 autoconfigure。
  2. 自动配置类必须能被 Spring Boot 发现。
  3. 条件注解决定是否生效。
  4. 最终仍然回到 Spring IOC 创建 Bean。

Boot 2 和 Boot 3 声明文件

Boot 2 写法

文件路径:

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

内容:

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

Boot 3 写法

文件路径:

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

内容:

text
com.example.hospital.autoconfigure.HospitalClientAutoConfiguration

对比:

对比项Boot 2Boot 3
常见 JDKJDK 8、JDK 11JDK 17+
声明文件META-INF/spring.factoriesMETA-INF/spring/...AutoConfiguration.imports
写法key-value每行一个配置类
老项目是否常见非常常见新项目推荐

你不能只会 Boot 3。很多商业系统仍然是 JDK 8 + Spring Boot 2.x,面试和维护老项目时必须能看懂 spring.factories

从 0 写一个医院客户端 Starter

下面用“医院接口客户端”举例,比短信 Demo 更贴近商业系统。

1. SDK 模块

java
package com.example.hospital.client;

public class HospitalClient {
    private final String baseUrl;
    private final int connectTimeoutMillis;
    private final String accessKey;

    public HospitalClient(String baseUrl, int connectTimeoutMillis, String accessKey) {
        this.baseUrl = baseUrl;
        this.connectTimeoutMillis = connectTimeoutMillis;
        this.accessKey = accessKey;
    }

    public String queryPatient(String patientId) {
        return "query " + patientId + " from " + baseUrl;
    }

    public boolean ping() {
        return baseUrl != null && baseUrl.startsWith("http");
    }
}

SDK 只表达客户端能力,不关心 Spring Boot 怎么装配。

2. 配置属性类

java
package com.example.hospital.autoconfigure;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "hospital.client")
public class HospitalClientProperties {
    private boolean enabled = true;
    private String baseUrl;
    private int connectTimeoutMillis = 3000;
    private String accessKey;

    public boolean isEnabled() {
        return enabled;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    public String getBaseUrl() {
        return baseUrl;
    }

    public void setBaseUrl(String baseUrl) {
        this.baseUrl = baseUrl;
    }

    public int getConnectTimeoutMillis() {
        return connectTimeoutMillis;
    }

    public void setConnectTimeoutMillis(int connectTimeoutMillis) {
        this.connectTimeoutMillis = connectTimeoutMillis;
    }

    public String getAccessKey() {
        return accessKey;
    }

    public void setAccessKey(String accessKey) {
        this.accessKey = accessKey;
    }
}

配置项示例:

yaml
hospital:
  client:
    enabled: true
    base-url: https://his.example.com
    connect-timeout-millis: 3000
    access-key: ${HOSPITAL_ACCESS_KEY}

为什么用 @ConfigurationProperties,而不是一堆 @Value

  1. 一组配置有统一前缀。
  2. 支持嵌套对象。
  3. 支持类型转换。
  4. 可以加校验。
  5. IDE 可以生成配置提示。

3. 自动配置类

java
package com.example.hospital.autoconfigure;

import com.example.hospital.client.HospitalClient;
import org.springframework.boot.autoconfigure.AutoConfiguration;
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;

@AutoConfiguration
@ConditionalOnClass(HospitalClient.class)
@EnableConfigurationProperties(HospitalClientProperties.class)
@ConditionalOnProperty(
        prefix = "hospital.client",
        name = "enabled",
        havingValue = "true",
        matchIfMissing = true
)
public class HospitalClientAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public HospitalClient hospitalClient(HospitalClientProperties properties) {
        if (properties.getBaseUrl() == null || properties.getBaseUrl().trim().isEmpty()) {
            throw new HospitalClientConfigException("hospital.client.base-url 不能为空");
        }
        return new HospitalClient(
                properties.getBaseUrl(),
                properties.getConnectTimeoutMillis(),
                properties.getAccessKey()
        );
    }
}

每个注解的意义:

注解作用
@AutoConfiguration表示这是 Boot 自动配置类
@ConditionalOnClassSDK 存在时才装配
@EnableConfigurationProperties开启配置属性绑定
@ConditionalOnProperty支持配置开关
@ConditionalOnMissingBean用户自定义 Bean 优先,默认 Bean 让位

4. Boot 3 自动配置声明

文件:

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

内容:

text
com.example.hospital.autoconfigure.HospitalClientAutoConfiguration

如果这个文件漏了,业务项目引入 starter 后,自动配置类根本不会进入候选列表。

5. starter 模块 pom

xml
<dependencies>
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>hospital-spring-boot-autoconfigure</artifactId>
        <version>${project.version}</version>
    </dependency>
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>hospital-client-sdk</artifactId>
        <version>${project.version}</version>
    </dependency>
</dependencies>

starter 模块一般不写复杂代码,只负责让业务项目少引依赖。

6. 业务项目使用

java
import com.example.hospital.client.HospitalClient;
import org.springframework.stereotype.Service;

@Service
public class PatientService {
    private final HospitalClient hospitalClient;

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

    public String getPatient(String patientId) {
        return hospitalClient.queryPatient(patientId);
    }
}

业务项目不需要写 HospitalClient@Bean。如果需要接管,也可以自己定义:

java
import com.example.hospital.client.HospitalClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CustomHospitalClientConfig {

    @Bean
    public HospitalClient hospitalClient() {
        return new HospitalClient("https://custom.example.com", 1000, "custom-key");
    }
}

因为自动配置用了 @ConditionalOnMissingBean,默认 Bean 会让位。

加上健康检查

商业 Starter 不能只创建客户端,还要能被监控系统理解。

java
package com.example.hospital.autoconfigure;

import com.example.hospital.client.HospitalClient;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;

public class HospitalClientHealthIndicator implements HealthIndicator {
    private final HospitalClient hospitalClient;

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

    @Override
    public Health health() {
        try {
            if (hospitalClient.ping()) {
                return Health.up().withDetail("hospitalClient", "available").build();
            }
            return Health.down().withDetail("hospitalClient", "unavailable").build();
        } catch (Exception ex) {
            return Health.down(ex).build();
        }
    }
}

自动配置中注册:

java
@Bean
@ConditionalOnClass(HealthIndicator.class)
@ConditionalOnMissingBean(name = "hospitalClientHealthIndicator")
public HealthIndicator hospitalClientHealthIndicator(HospitalClient hospitalClient) {
    return new HospitalClientHealthIndicator(hospitalClient);
}

注意:健康检查必须轻量、带超时,不能做全量业务查询。

加上指标

java
package com.example.hospital.autoconfigure;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;

public class HospitalClientMetrics {
    private final Counter success;
    private final Counter failed;

    public HospitalClientMetrics(MeterRegistry registry) {
        this.success = Counter.builder("hospital_client_success_total")
                .description("医院接口调用成功次数")
                .register(registry);
        this.failed = Counter.builder("hospital_client_failed_total")
                .description("医院接口调用失败次数")
                .register(registry);
    }

    public void success() {
        success.increment();
    }

    public void failed() {
        failed.increment();
    }
}

指标适合看整体趋势,日志适合查单个请求。公共 Starter 最好提供核心指标,避免每个业务项目各写各的。

加上失败诊断

缺少 base-url 时,如果只抛 NullPointerException,接入方体验很差。可以定义异常:

java
public class HospitalClientConfigException extends RuntimeException {
    public HospitalClientConfigException(String message) {
        super(message);
    }
}

失败分析器:

java
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;

public class HospitalClientFailureAnalyzer
        extends AbstractFailureAnalyzer<HospitalClientConfigException> {

    @Override
    protected FailureAnalysis analyze(Throwable rootFailure,
                                      HospitalClientConfigException cause) {
        return new FailureAnalysis(
                "医院客户端配置不完整:" + cause.getMessage(),
                "请在 application.yml 中配置 hospital.client.base-url",
                cause
        );
    }
}

Boot 2 常见声明:

properties
org.springframework.boot.diagnostics.FailureAnalyzer=\
com.example.hospital.autoconfigure.HospitalClientFailureAnalyzer

Boot 3 也可以通过对应的工厂加载机制注册。核心思想是:把启动失败原因翻译成人能看懂的提示。

条件注解组合策略

成熟 Starter 不会只写一个条件注解。

mermaid
flowchart TD
    A["自动配置类"] --> B{"SDK类存在吗"}
    B -- "否" --> X["跳过"]
    B -- "是" --> C{"enabled=true吗"}
    C -- "否" --> X
    C -- "是" --> D{"用户已有Bean吗"}
    D -- "有" --> E["默认Bean让位"]
    D -- "没有" --> F["创建默认Bean"]

推荐组合:

目标推荐注解
依赖存在才启用@ConditionalOnClass
配置开关控制@ConditionalOnProperty
用户可覆盖@ConditionalOnMissingBean
Web 场景才启用@ConditionalOnWebApplication
有某个基础 Bean 再启用@ConditionalOnBean

不这样会怎样

错误做法后果
starter 里写业务规则公共组件和业务强耦合,其他项目无法复用
不加 @ConditionalOnMissingBean用户无法替换默认实现,或者 Bean 冲突
默认线程池无界高并发下 OOM 或线程数暴涨
自动配置类做网络连接启动慢,甚至启动失败
配置没有开关出问题时无法临时关闭能力
不提供健康检查依赖故障无法被监控发现
不提供失败诊断接入方只能看一堆堆栈

自动配置没生效怎么排查

mermaid
flowchart TD
    A["Starter 引入后 Bean 没有出现"] --> B["确认依赖是否真的引入"]
    B --> C["查看 jar 中是否有自动配置声明"]
    C --> D["打开 debug=true 或 actuator conditions"]
    D --> E{"自动配置类在候选列表吗"}
    E -- "否" --> F["检查 spring.factories 或 imports 路径"]
    E -- "是" --> G{"条件匹配吗"}
    G -- "否" --> H["检查 classpath、enabled、Web环境"]
    G -- "是" --> I{"是否已有用户Bean"}
    I -- "是" --> J["默认Bean让位"]
    I -- "否" --> K["检查配置绑定和Bean创建异常"]

排查清单:

  1. mvn dependency:tree 看 starter 是否真的引入。
  2. 打开 jar,确认 META-INF/spring.factoriesAutoConfiguration.imports 存在。
  3. 类名是否写错,包名是否改过。
  4. debug=true 看条件报告。
  5. /actuator/conditions 看 Positive matches 和 Negative matches。
  6. 是否配置了 enabled=false
  7. 是否缺少 SDK 依赖导致 @ConditionalOnClass 不匹配。
  8. 是否已经有用户自定义 Bean,导致默认 Bean 让位。
  9. 配置绑定是否失败,例如字符串绑定 int。
  10. Bean 创建时是否访问外部服务导致启动失败。

商业项目适合沉淀成 Starter 的能力

能力是否适合原因
统一鉴权客户端适合多服务共用,配置和指标统一
医院接口客户端适合多服务复用,接入和监控标准化
Redis Key 规范工具适合统一前缀、序列化、过期策略
MQ 生产者封装适合统一重试、traceId、消息头
业务订单规则不适合和具体业务强绑定
某个 Controller不适合容易污染业务路由
数据库表结构强依赖逻辑谨慎多项目表结构可能不一致

判断标准:

  1. 是否多个项目都会用。
  2. 是否属于基础设施而不是业务规则。
  3. 是否可以通过配置差异化。
  4. 是否能提供默认值并允许覆盖。
  5. 是否需要统一监控和排查。

面试标准回答

Starter 是什么

Starter 是 Spring Boot 的场景化依赖入口,通常负责聚合依赖,并引入 autoconfigure 模块。autoconfigure 模块提供自动配置类、配置属性类、条件注解和默认 Bean。业务项目引入 Starter 后,Spring Boot 读取自动配置声明文件,条件满足就创建默认 Bean,所以能开箱即用。

Starter 和自动配置什么关系

Starter 主要负责依赖入口,自动配置负责真正装配 Bean。starter 把 SDK 和 autoconfigure 带进 classpath,Spring Boot 再通过 spring.factoriesAutoConfiguration.imports 发现自动配置类,经过条件注解判断后把默认 Bean 注册到容器。

为什么要拆 starter 和 autoconfigure

starter 最好只做依赖聚合,autoconfigure 放自动配置类、属性类、条件类和默认 Bean。这样职责清楚,自动配置逻辑可以单独测试,SDK 也可以独立复用,避免 starter 模块变成大杂烩。

自定义 Starter 关键点

关键点包括:定义清晰的配置前缀,使用 @ConfigurationProperties 承载配置;用 @ConditionalOnClass 判断依赖是否存在;用 @ConditionalOnProperty 提供开关;用 @ConditionalOnMissingBean 尊重用户自定义;按 Boot 2/3 版本写正确的自动配置声明文件;提供健康检查、指标和失败诊断。

Starter 没生效怎么排查

先确认依赖是否引入,再看 jar 里是否有 spring.factoriesAutoConfiguration.imports,然后打开 debug=true/actuator/conditions 看自动配置类是否在候选列表、是否被 exclude、条件是否匹配、是否已有用户 Bean、配置绑定或 Bean 创建是否异常。

关联知识点