自定义 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 示例
@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 的回答重点在于"踩坑经历"和"最佳实践"。光知道怎么写还不够,能说出"踩过什么坑、怎么解决的"才是加分项。