自定义 Spring Boot Starter

面试官问:"你用过 MyBatis Spring Boot Starter,那你知道它是怎么实现的吗?如果让你自己写一个 starter,你需要做什么?"

候选人小周说:"就是引入依赖吧?应该还需要配置一些东西?"面试官追问:"配置什么?在哪里配置?"小周答不上来。

面试官又问:"你知道 @ConfigurationProperties 吗?"小周说:"知道,就是绑定配置属性的。"面试官追问:"那它和 @Value 有什么区别?什么场景用哪个?"小周支支吾吾。

【面试官心理】

这道题我用来测试候选人是否真的理解 Starter 的工作机制。能说出"Starter 就是一堆依赖"的占 50%,能说出"自动配置类 + 条件注解"的占 30%,能实际动手写过自定义 Starter 的只有 10%。这道题能答到最后的,基本都有框架贡献或内部平台开发经验。

一、从零实现 Starter 🔴

1.1 Starter 的项目结构

一个完整的 Starter 项目由两个模块组成:

ourcompany-ourmodule-starter
├── ourcompany-ourmodule-spring-boot-starter/    # 使用方引入这个
│   ├── pom.xml
│   └── src/main/resources/
│       └── META-INF/
│           └── spring/
│               └── org.springframework.boot.autoconfigure.AutoConfiguration.imports

└── ourcompany-ourmodule-autoconfigure/          # 自动配置逻辑
    ├── pom.xml
    └── src/main/java/
        └── com/ourcompany/ourmodule/autoconfigure/
            ├── OurmoduleAutoConfiguration.java
            └── properties/
                └── OurmoduleProperties.java
⚠️

注意命名规范:使用方引入的是 xxx-spring-boot-starter,而自动配置模块叫 xxx-autoconfigure。这样拆分的好处是:starter 只负责引入依赖,autoconfigure 包含所有配置逻辑,可以单独测试。

1.2 自动配置类写法

第一步:定义配置属性类

// OurmoduleProperties.java
@ConfigurationProperties(prefix = "ourmodule")
public class OurmoduleProperties {
    private boolean enabled = true;
    private String host = "localhost";
    private int port = 8080;
    private List<String> modules = new ArrayList<>();
    private Map<String, String> config = new HashMap<>();

    // getters and setters
}

第二步:编写自动配置类

// OurmoduleAutoConfiguration.java
@Configuration
@ConditionalOnClass(OurmoduleService.class)
@ConditionalOnProperty(prefix = "ourmodule", name = "enabled", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(OurmoduleProperties.class)
public class OurmoduleAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(OurmoduleService.class)
    public OurmoduleService ourmoduleService(OurmoduleProperties properties) {
        return new OurmoduleService(properties);
    }
}

第三步:注册自动配置

# Spring Boot 2.7+:META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.ourcompany.ourmodule.autoconfigure.OurmoduleAutoConfiguration
# Spring Boot 2.7 之前:META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.ourcompany.ourmodule.autoconfigure.OurmoduleAutoConfiguration

1.3 Starter 的 pom.xml 写法

<!-- ourcompany-ourmodule-spring-boot-starter/pom.xml -->
<project>
    <groupId>com.ourcompany</groupId>
    <artifactId>ourcompany-ourmodule-spring-boot-starter</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <dependencies>
        <!-- 引入自动配置模块 -->
        <dependency>
            <groupId>com.ourcompany</groupId>
            <artifactId>ourcompany-ourmodule-autoconfigure</artifactId>
            <version>1.0.0</version>
        </dependency>
        <!-- 引入第三方依赖(可选) -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>
    </dependencies>
</project>
💡

Starter pom.xml 中不写任何 Java 代码。它只负责两件事:1. 依赖自动配置模块;2. 引入使用方需要的传递依赖。这样使用方引入一个 starter,就能获得所有依赖和自动配置。

二、@ConfigurationProperties 深度解析 🟡

2.1 @ConfigurationProperties vs @Value

特性@ConfigurationProperties@Value
松散绑定支持(user-name = userName)不支持
SpEL 表达式不支持支持
默认值支持(类型默认值)不支持(需指定 default)
批量注入支持(绑定一组属性)不支持(逐个指定)
元数据支持(生成 metadata.json)不支持
校验支持(@Validated)不支持
// @ConfigurationProperties 示例
@ConfigurationProperties(prefix = "user")
@Validated
public class UserProperties {
    @Email  // 支持 JSR-303 校验
    private String email;
    private int age;
    private String phoneNumber;  // user.phone-number 自动绑定
}

// @Value 示例
@Value("${user.email}")
private String email;

@Value("#{systemProperties['user.age'] ?: 18}")
private int age;

@Value("default-value")
private String defaultValue;

2.2 启用 ConfigurationProperties 的方式

方式一:@EnableConfigurationProperties(推荐)

@Configuration
@EnableConfigurationProperties(UserProperties.class)
public class AppConfig {
    // UserProperties 会自动注册为 Bean
}

方式二:@ConfigurationPropertiesScan(Spring Boot 2.2+)

@SpringBootApplication
@ConfigurationPropertiesScan
public class Application {
    // 所有 @ConfigurationProperties 类自动注册
}

方式三:@ConfigurationProperties 直接标注在 @Bean 方法上

@Bean
@ConfigurationProperties(prefix = "user")
public UserProperties userProperties() {
    return new UserProperties();
}

三、条件注解组合使用 🟡

3.1 常用条件注解

@ConditionalOnBean(DataSource.class)           // 容器中存在某个 Bean
@ConditionalOnMissingBean(DataSource.class)    // 容器中不存在某个 Bean
@ConditionalOnClass(DataSource.class)          // classpath 中存在某个类
@ConditionalOnMissingClass("com.alibaba.druid.DruidDataSource")  // classpath 中不存在某个类
@ConditionalOnProperty(prefix = "xxx", name = "enabled", havingValue = "true")  // 配置属性满足条件
@ConditionalOnResource(resources = "classpath:init.sql")         // 某个资源文件存在
@ConditionalOnWebApplication                   // 是 Web 应用
@ConditionalOnNotWebApplication                // 非 Web 应用
@ConditionalOnExpression("${xxx.enabled:true}")  // SpEL 表达式

3.2 组合条件的实战用法

@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnProperty(prefix = "http.client", name = "enabled", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(HttpClientProperties.class)
public class HttpClientAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(RestTemplate.class)
    public RestTemplate restTemplate(HttpClientProperties properties) {
        RestTemplateBuilder builder = new RestTemplateBuilder();
        return builder
            .setConnectTimeout(Duration.ofMillis(properties.getConnectTimeout()))
            .setReadTimeout(Duration.ofMillis(properties.getReadTimeout()))
            .build();
    }
}
⚠️

@ConditionalOnMissingBean 是最容易踩坑的条件注解。它的语义是"如果容器中没有这个 Bean,就注册一个"。但在自动配置中加 @ConditionalOnMissingBean,意味着用户自定义的 Bean 优先级更高。这个设计是为了让用户能够覆盖自动配置的默认行为,但也很容易导致"配置不生效"的排查困难。

四、多环境配置 🟢

4.1 profile 条件激活

@Configuration
@ConditionalOnProperty(prefix = "ourmodule", name = "enabled", havingValue = "true")
@Profile("prod")
public class OurmoduleProdConfiguration {
    @Bean
    public OurmoduleService prodService(OurmoduleProperties properties) {
        // 生产环境专用逻辑
        return new OurmoduleService(properties, Environment.PROD);
    }
}

@Configuration
@ConditionalOnProperty(prefix = "ourmodule", name = "enabled", havingValue = "true")
@Profile({"dev", "test"})
public class OurmoduleDevConfiguration {
    @Bean
    public OurmoduleService devService(OurmoduleProperties properties) {
        return new OurmoduleService(properties, Environment.DEV);
    }
}

4.2 application.yml 多环境配置

# application.yml(公共配置)
ourmodule:
  enabled: true
  host: ${OURMODULE_HOST:localhost}

---
# application-dev.yml
ourmodule:
  port: 8080
  modules:
    - module-a
    - module-b

---
# application-prod.yml
ourmodule:
  port: 443
  modules:
    - module-a
    - module-b
    - module-c

五、生产避坑

5.1 踩过的真实坑

场景:我们发布了一个内部 HTTP Client Starter,配置了 @ConditionalOnMissingBean(RestTemplate.class)。结果用户在测试环境自定义了一个 RestTemplate Bean 覆盖了默认的,但是没覆盖另一个 ClientHttpRequestFactory Bean,导致默认的连接池配置没有生效。

根因:自动配置注册了两个 Bean:RestTemplate 和 ClientHttpRequestFactory。用户只覆盖了 RestTemplate,没有覆盖连接池配置。

解法:使用 @ConditionalOnSingleCandidate 或者在文档中明确说明需要同时覆盖哪些 Bean。

5.2 最佳实践

@Configuration
public class OurmoduleAutoConfiguration {

    private final OurmoduleProperties properties;

    // 构造器注入,确保属性已绑定
    public OurmoduleAutoConfiguration(OurmoduleProperties properties) {
        this.properties = properties;
    }

    @Bean
    @ConditionalOnMissingBean(OurmoduleService.class)
    public OurmoduleService ourmoduleService() {
        return new OurmoduleService(properties);
    }

    // 提供配置元数据,让 IDE 能够提示配置项
}
💡

生成 spring-configuration-metadata.json 元数据文件非常重要。使用 spring-boot-configuration-processor,它会在编译时自动生成 metadata,帮助使用方在 IDE 中获得配置提示。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

六、面试标准回答

6.1 P5 级别

"自定义 Starter 需要两个模块:autoconfigure 模块包含自动配置类,starter 模块依赖 autoconfigure 模块。使用方只需要引入 starter 依赖,就能自动获得配置。"

6.2 P6 级别

"实现自定义 Starter 需要三步:1. 定义 @ConfigurationProperties 配置属性类;2. 编写 @Configuration 自动配置类,使用 @Conditional 系列条件注解决定是否生效;3. 在 META-INF/spring.factories(2.x)或 AutoConfiguration.imports(3.x)中注册自动配置类。

@ConfigurationProperties 支持松散绑定和批量注入,比 @Value 更适合绑定一组相关配置。启用方式可以是 @EnableConfigurationProperties 或 @ConfigurationPropertiesScan。"

6.3 P7 级别

"设计 Starter 时最大的坑是 Bean 覆盖策略。我之前设计的内部 Starter,因为没有正确使用 @ConditionalOnMissingBean,导致用户的自定义 Bean 和自动配置的 Bean 冲突,花了大量时间排查。

最佳实践是:1. 使用 @ConditionalOnMissingBean 而非 @Primary 来让用户覆盖;2. 使用 spring-boot-configuration-processor 生成元数据;3. 自动配置类放在 autoconfigure 模块,starter 只做依赖聚合;4. @ConditionalOnMissingBean 必须放在 @Bean 方法上才有意义,放在 @Configuration 类上没有效果。"

【面试官心理】

P7 的回答重点在于"踩坑经历"和"最佳实践"。光知道怎么写还不够,能说出"踩过什么坑、怎么解决的"才是加分项。