Spring Boot Starter 设计思考:分离模式是否适用于所有场景?
在 Spring Boot 生态中,starter + autoconfigure 的分离设计已成为广泛采用的标准模式。但当我们深入分析其在实际项目中的应用时,或许可以思考一下:这种设计是否真的是恰当的?
一、当前模式的分析
1. 官方及绝大多数流行框架的模式
当前大多数 Starter 采用这样的结构,其中 Starter 本身是一个空的聚合包,不包含任何实际代码:
xml
<!-- mybatis-spring-boot-starter (空壳聚合包) -->
<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</dependency>
</dependencies>
同时在 autoconfigure 模块中需要重新声明依赖:
xml
<!-- mybatis-spring-boot-autoconfigure -->
<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
这种设计带来的现象:
- Starter 模块作为纯粹的依赖聚合空壳,不包含任何业务逻辑
- 实际的自动配置逻辑位于独立的 autoconfigure 模块
- 相同的依赖需要在两个位置声明,造成配置冗余
2. 自动配置类独立存在的思考
以 Redis 自动配置为例:
java
// org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
考虑这样的使用场景:
xml
<!-- 用户只引入了基础库,未使用完整 starter -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
可能出现的情况:
- 自动配置类检测到基础类存在,开始配置流程
- 但由于缺少某些必要组件,配置可能无法完全生效
- 这种"部分配置"的状态可能带来不确定性
二、自动配置类的本质思考
1. 自动配置类与 Starter 的天然联系
从功能角度看,自动配置类与 Starter 之间存在着紧密的关联:
- 自动配置类的目的是让 Starter 能够开箱即用
- Starter 的价值很大程度上通过自动配置类来体现
- 两者在逻辑上是一个完整的功能单元
2. 关于 Condition 注解的思考
在当前的分离模式下,条件注解看起来是必要的:
java
@ConditionalOnClass({RedisTemplate.class, JedisConnection.class})
但如果我们换一个角度思考:当自动配置类位于 Starter 内部时,这些条件检查是否还有必要?
如果自动配置类直接放在 Starter 包内:
java
@Configuration
public class RedisAutoConfiguration {
// 由于 Starter 已经包含了所有必要依赖,
// @ConditionalOnClass 可能变得冗余
}
个人看法:
用户引入 Starter 的行为本身就表明了使用意图,Starter 的依赖管理已经确保了所需类的存在。在这种情况下,过多的 @ConditionalOnClass 检查反而可能增加了不必要的复杂性。
三、社区现状的分析
1. 广泛采用的模式
MyBatis、MyBatis-Plus、PageHelper 等流行项目都采用了与 Spring 官方相似的结构:
xml
<!-- mybatis-spring-boot-starter -->
<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</dependency>
</dependencies>
这种一致性主要基于:
- 学习成本考虑:开发者已经熟悉这种组织结构
- 生态一致性:保持与其他组件的统一结构
- 风险规避:遵循经过验证的实践方案
2. Spring 官方的特殊考量
Spring 团队采用集中式的 autoconfigure 设计,可能基于以下考虑:
- 统一管理:协调大量第三方库的适配
- 版本控制:统一管理配置类的兼容性
- 框架生态:维护整个 Spring Boot 生态的一致性
但这种针对框架级别的设计考量,并不一定适用于所有的自研 Starter 场景。
四、自研 Starter 的替代方案思考
1. 内聚式设计
对于自研的 Starter,可以考虑采用更加内聚的设计:
company-spring-boot-starter/
├── src/main/java
│ └── com/company/autoconfigure/
│ ├── CompanyAutoConfiguration.java
│ └── CompanyProperties.java
├── src/main/resources
│ └── META-INF
│ ├── spring.factories
│ └── spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
└── pom.xml
2. 简化的依赖管理
只需要在 Starter 中声明一次依赖:
xml
<dependencies>
<dependency>
<groupId>com.company</groupId>
<artifactId>company-core</artifactId>
</dependency>
<!-- 其他运行时依赖 -->
</dependencies>
这样可以避免:
- 独立的 autoconfigure 模块创建和维护
- 重复的依赖声明
- optional 依赖的复杂管理
3. 合理的条件注解使用
在内聚式设计中,可以更加有选择地使用条件注解:
java
@Configuration
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class CompanyAutoConfiguration {
@Bean
@ConditionalOnMissingBean // 保留:允许用户自定义
public CompanyService companyService() {
return new DefaultCompanyService();
}
@Bean
@ConditionalOnProperty(prefix = "company", name = "feature.enabled")
public FeatureService featureService() {
return new FeatureService();
}
}
个人建议:
- 保留
@ConditionalOnMissingBean尊重用户自定义 - 保留
@ConditionalOnProperty支持功能开关 - 保留
@AutoConfigureAfter确保配置顺序 - 酌情减少
@ConditionalOnClass的使用
五、对传统分离模式的重新审视
分离模式的实际价值有限
从实际应用角度来看,传统分离模式的优势场景相当有限:
- Spring 官方场景:仅当需要统一管理几十个不同组件的配置和版本时,集中式的 autoconfigure 包才有其管理上的便利性
- 第三方组件场景:对于大多数独立的第三方组件来说,这种分离带来的更多是复杂性而非价值
内聚式设计的普适优势
相比之下,内聚式设计在大多数场景下都表现出更好的特性:
- 架构简洁:减少不必要的模块分层,降低系统复杂度
- 维护便利:所有相关代码集中在同一模块,便于理解和修改
- 依赖明确:避免重复的依赖声明,减少配置错误可能性
- 意图清晰:Starter 作为一个完整的功能单元,职责单一明确
六、总结与建议
经过对 Spring Boot Starter 设计模式的深入分析,我认为传统的分离模式在大多数应用场景下可能并非最优选择。
核心观点:
- 自动配置类本质上与其对应的 Starter 是高度内聚的,强行分离反而引入不必要的复杂性
- 对于绝大多数项目(特别是自研 Starter),将自动配置类直接包含在 Starter 包内是更加简单直接的设计
- 只有在类似 Spring 官方需要统一管理大量第三方组件配置的特殊场景下,集中式的 autoconfigure 设计才有其合理性
实践建议:
在开发自研 Starter 时,建议优先考虑内聚式设计。这种设计不仅减少了模块间的依赖关系,简化了构建配置,还使得代码结构更加直观易懂。只有当确实需要支持高度灵活的可选依赖组合时,才考虑采用传统的分离模式。
技术决策应当基于实际需求和具体场景,而非盲目遵循某种"标准做法"。在理解现有设计背后的考量的同时,保持批判性思维,选择最适合自己项目的技术方案,这才是技术人应有的态度。