敲代码离不开springboot,少了springboot谁还来替我当牛马------ai
欢迎来到 Spring Boot 的"后台控制室"~
刚开始、小白的你是否曾有过这样的错觉:
"我就加了一个
@SpringBootApplication注解,连application.properties都没怎么写,Redis 连接池有了,MySQL 驱动配好了,Tomcat 启动了,甚至连 Swagger 文档都生成了......这难道不是魔法吗?"
这就是预定大于配置的魅力,"工业化流水线"的极致体现!
如果把传统的 SSM 框架比作**"手工打造跑车"** (你需要自己选引擎、装轮胎、接线路),那么 Spring Boot 就是**"全自动无人工厂"**。你只需要按下启动按钮(Main 方法),工厂内部的机械臂(SPI 机制)就会根据预设的图纸(Starter),自动把零件组装好。
今天,我们要拆掉工厂的围墙,看看里面的机械臂到底是怎么运作的。我们要手撕源码,彻底搞懂 SPI (Service Provider Interface) 和 条件装配 的黑盒逻辑。
核心痛点:为什么它"全知全能"?
场景一:配置地狱的终结者
在 Spring Boot 之前,为了整合 MyBatis,你需要:
- 引入 jar 包。
- 写
SqlSessionFactoryBean。 - 配置
MapperScannerConfigurer。 - 写一堆 XML 或 Java Config。
现在 :引入 mybatis-spring-boot-starter,搞定。
疑问:它怎么知道我要连 MySQL 还是 Oracle?它怎么知道我的 Mapper 接口在哪?
场景二:启动速度的秘密
Spring 容器要扫描成千上万个类。如果每个 Starter 里的几百个配置类都无脑加载,启动得慢成蜗牛。
事实 :哪怕你引入了 50 个 Starter,启动依然飞快。
疑问:它是如何做到"只加载我需要的",而把不需要的统统过滤掉的?
生活化比喻:
- 传统 Spring = 相亲角 。
大妈(容器)拿着大喇叭喊:"谁实现了 DataSource 接口?站出来!"
几百个人(类)同时举手,大妈得一个个问:"你有 MySQL 驱动吗?有 HikariCP 吗?"累得半死。- Spring Boot SPI = 猎头公司的 VIP 名单 。
大妈(容器)不去现场喊人,而是直接去META-INF目录下拿一份加密名单 (spring.factories或.imports)。
名单上写着:"如果classpath下有 MySQL 驱动,就请'MySQL 配置专员'上岗;如果有 Redis 驱动,就请'Redis 配置专员'上岗。"
结果:大妈只看名单,按需招人,效率极高,绝不浪费口舌。
源码追踪:从 main() 到 loadFactories() 的奇幻漂流
让我们戴上显微镜,跟随 SpringApplication.run() 的脚步,看看魔法是如何发生滴
1. 入口:SpringApplication.run()
如果这都感觉陌生,只能拉出去了------人还是有点底线!
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return new SpringApplication(primarySource).run(args);
}
2. 准备环境:prepareEnvironment()
这一步主要加载配置文件,暂时跳过。
3. 创建上下文:createApplicationContext()
根据类型创建 AnnotationConfigServletWebServerApplicationContext。
4. 关键步骤 :prepareContext() -> load()
这里会调用 load 方法,将你的主配置类加载进去。
5. 核心高潮 :refreshContext() -> invokeBeanFactoryPostProcessors()
在容器刷新过程中,有一个至关重要的处理器:ConfigurationClassPostProcessor 。
它会解析 @Configuration 类。而你的 @SpringBootApplication 本质上包含了 @EnableAutoConfiguration。
重点来了! @EnableAutoConfiguration 导入了一个关键的选择器:
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration { ... }
6. 揭秘时刻:AutoConfigurationImportSelector.selectImports()
这个类是真正的"猎头总管"。它的核心逻辑如下(简化版伪代码):
public String[] selectImports(AnnotationMetadata annotationMetadata) {
// 1. 获取所有候选配置类的名单
// 在 Spring Boot 2.7+ 中,优先读取 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
// 在旧版本中,读取 META-INF/spring.factories
List<String> configurations = getCandidateConfigurations(annotationMetadata, AutoConfigurationEntry.class);
// 2. 【去重与排序】确保顺序正确
configurations = sort(configurations, autoConfigurationMetadata);
// 3. 【核心过滤】这就是性能优化的关键!
// 使用 @ConditionalOnXxx 注解过滤掉不满足条件的配置类
configurations = filter(configurations, autoConfigurationMetadata);
// 4. 返回最终需要加载的类名数组
return StringUtils.toStringArray(configurations);
}
看到了吗? 所谓的"自动装配",就是读取名单 -> 排序 -> 过滤 -> 加载的过程。
机制详解:Java SPI vs Spring SPI
在这之前,我问一句你敢答应吗?"Spring Boot 的 SPI 和 Java 原生的 SPI 有什么区别?" 这是一个坑,跳进去就出不来!下面说一下历程,不得不说,新设计真的很睿智~
Java 原生 SPI (ServiceLoader)
- 位置 :
META-INF/services/接口全限定名 - 内容:直接列出实现类的全限定名。
- 缺点 :
- 无延迟加载 :一旦调用
ServiceLoader.load(),所有实现类会被实例化。哪怕你只用其中一个,其他的也会报错或浪费资源。 - 无条件过滤:无法根据环境(有没有某个类、有没有某个 Bean)来决定加载谁。
- Key-Value 缺失:只能存类名,不能存元数据。
- 无延迟加载 :一旦调用
2. Spring SPI (SpringFactoriesLoader)
- 位置 :
META-INF/spring.factories(Boot 2.x) 或META-INF/spring/*.imports(Boot 3.x)。 - 内容 :
Key=Value格式。Key 是接口/注解,Value 是实现类列表(逗号分隔)。 - 进化优势 :
- 解耦:通过 Key 分类管理,不仅仅是 SPI,还能管理监听器、初始化器等。
- 延迟与过滤 :Spring 拿到名单后,不会立即实例化 ,而是先解析类上的
@Conditional 注解。只有条件满足,才真正加载并实例化。 - 有序性 :支持
@AutoConfigureBefore,@AutoConfigureAfter 控制加载顺序。
忍不住感叹 :
Java 原生的 SPI 就像"盲目招聘",只要简历投进来(在文件里),HR 就全部发 Offer 入职,不管公司需不需要。
Spring 的 SPI 就像"精准猎头",拿到简历后,先做背景调查(检查 ClassPath、检查 Bean),符合条件的才发 Offer。这就是为什么 Spring Boot 能加载上百个 Starter 却不卡顿的原因。
条件装配:性能优化的"守门员"
如果没有条件装配,引入 redis-starter 就会强制加载 Redis 配置,哪怕你根本没装 Redis 驱动,启动直接报 ClassNotFoundException。
条件注解 是 Spring Boot 的灵魂。它们在 parse 阶段起作用,直接拦截不符合条件的配置类。
常用条件注解大赏
| 注解 | 含义 | 典型应用场景 |
|---|---|---|
@ConditionalOnClass |
类路径下有指定类时才生效 | RedisAutoConfiguration 只有在 Jedis.class 存在时才加载。 |
@ConditionalOnMissingBean |
容器中没有指定 Bean 时才生效 | 用户自己定义了 DataSource,自动配置就不创建了,避免冲突。 |
@ConditionalOnProperty |
配置文件中有指定属性时才生效 | server.port=8080 存在时才配置 Tomcat 端口。 |
@ConditionalOnWebApplication |
当前是 Web 环境时才生效 | 只有 Web 项目才加载 SpringMVC 相关配置。 |
@ConditionalOnSingleCandidate |
容器中只有一个该类型的 Bean 时生效 | 确保不会产生歧义。 |
源码级过滤逻辑
在 AutoConfigurationImportSelector 的 filter 方法中,Spring 会利用 OnClassCondition, OnBeanCondition 等内部类,模拟加载环境,判断条件是否成立。不成立的配置类,连 Class 都不会被加载到 JVM 中,这才是极致的性能优化。
实战演练:手写一个"防脱发"Starter
光看不练假把式。我们来手写一个自定义 Starter:corn**-care-spring-boot-starter** 。
功能 :自动检测项目中是否有 Shampoo 类,如果有,自动创建一个 CornCareService Bean,并打印"正在..."。⚠️ 代码手动修改过,但是不影响看
第一步:创建普通 Maven 模块 hair-care-spring-boot-starter
<dependencies>
<!-- ............ -->
<!-- 核心依赖:提供自动装配能力 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<optional>true</optional>
</dependency>
<!-- 可选依赖:模拟业务库,实际使用时由引入方决定要不要加 -->
<dependency>
<groupId>com.example</groupId>
<artifactId>shampoo-lib</artifactId>
<optional>true</optional>
</dependency>
<!-- 方便测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
第二步:编写业务逻辑类 (Service)
package com.example.haircare.service;
public class CornCareService {
public void care() {
System.out.println(" [HairCare] 检测到活动,正在为您自动获取金币... 丝滑!");
}
}
第三步:编写自动配置类 (AutoConfiguration) ------ 核心中的核心
package com.example.haircare.autoconfigure;
import com.example.haircare.service.CornCareService;
import com.example.shampoo.Shampoo; // 假设这是第三方库的类
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
// 1. 只有当 classpath 下有 Shampoo 类时,这个配置类才生效
@ConditionalOnClass(Shampoo.class)
// 2. 只有当配置文件开启了 hair.care.enabled=true (默认 true) 时才生效
@ConditionalOnProperty(prefix = "corn.care", name = "enabled", havingValue = "true", matchIfMissing = true)
public class CornCareAutoConfiguration {
// 3. 只有当容器中没有 CornCareService 这个 Bean 时,才创建默认的
// 这样用户就可以自己定义一个 Bean 来覆盖默认行为
@Bean
@ConditionalOnMissingBean(CornCareService.class)
public CornCareService cornCareService() {
return new CornCareService();
}
}
第四步:注册 SPI 名单 (魔法发生的地方)
在 src/main/resources 下创建目录结构:
META-INF/spring/ (Spring Boot 3.x 推荐) 或 META-INF/ (2.x)。
文件路径 :src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
(注:如果是 Spring Boot 2.7 及以下,文件名为 META-INF/spring.factories)
com.example.haircare.autoconfigure.HairCareAutoConfiguration
(如果是 spring.factories 格式,则是:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.haircare.autoconfigure.CornrCareAutoConfiguration)
这就相当于告诉 Spring Boot:"嘿,我是 HairCareAutoConfiguration,启动的时候记得看看我符不符合条件,符合就把我加载了。"
第五步:在另一个项目中测试
新建一个 Spring Boot 项目 demo-app。
<dependency>
<groupId>com.example</groupId>
<artifactId>corn-care-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
<!-- 只有引入了这个,Shampoo 类才会存在,自动配置才会生效 -->
<dependency>
<groupId>com.example</groupId>
<artifactId>shampoo-lib</artifactId>
<version>1.0.0</version>
</dependency>
2. 编写测试代码
@SpringBootApplication
@RestController
public class DemoApplication {
@Autowired(required = false) // 允许为 null,测试用
private CornCareService cornCareService;
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@GetMapping("/hair")
public String doCorn() {
if (cornCareService != null) {
cornCareService.care();
return "参与成功!";
} else {
return "未检测到活动,领取服务未启动。";
}
}
}
3. 验证场景
-
场景 A :引入了
shampoo-lib。- 结果:访问
/hair-> 输出"护发成功!",控制台打印"正在为您自动参与..."。 - 原理:
@ConditionalOnClass(Shampoo.class)成立,配置类加载,Bean 创建。
- 结果:访问
-
场景 B :移除
shampoo-lib依赖。- 结果:访问
/hair-> 输出"未检测到活动..."。 - 原理:
Shampoo类不存在,配置类直接被跳过,零开销。
- 结果:访问
-
场景 C:用户自定义 Bean。
@Configuration
public class MyConfig {
@Bean
public CornCareService myCustomService() {
return () -> System.out.println(" 自定义高端活动 SPA!");
}
} -
结果:输出"自定义高端活动 SPA!"。
-
原理:
@ConditionalOnMissingBean检测到已有 Bean,默认配置不执行。
interview题:SPI 与自动装配
Q1: Spring Boot 的自动装配原理是什么?
核心是 SPI 机制 + 条件注解。
- Spring Boot 启动时,
@EnableAutoConfiguration通过AutoConfigurationImportSelector加载META-INF/spring.factories(或.imports) 中配置的所有自动配置类。 - 加载过程中,利用
@ConditionalOnClass,@ConditionalOnMissingBean等注解进行按需过滤。 - 只有满足条件的配置类才会被解析并注册到 Spring 容器中,从而实现"约定优于配置"的自动化装配。
Q2: 为什么要自定义 Starter?和普通依赖有什么区别?
- 普通依赖:只是引入了 Jar 包,用户需要手动配置 Bean、XML 或 Java Config,容易出错且重复劳动。
- 自定义 Starter :将"依赖 + 自动配置逻辑"打包。用户只需引入 Starter,无需任何配置即可使用默认功能,同时也保留了通过
@ConditionalOnMissingBean进行自定义扩展的能力。这是封装复用 和降低接入成本的最佳实践。
Q3: Spring Boot 2.7 和 3.0 在 SPI 机制上有什么变化?
- 2.7 及以前 :主要使用
META-INF/spring.factories文件,Key 是EnableAutoConfiguration,Value 是类列表。所有类型的 SPI 都在一个文件里,解析时需要遍历 Key。 - 3.0 (Jakarta EE) :为了性能和规范,废弃了
spring.factories用于自动配置。改为在META-INF/spring/目录下,为每个接口单独建立文件,如org.springframework.boot.autoconfigure.AutoConfiguration.imports。 - 优势:减少了文件解析的 IO 开销,不需要读取整个大文件再过滤 Key,直接读取目标文件即可,启动速度进一步提升。
Q4: 如果两个 Starter 都配置了同一个 Bean,会发生什么?如何解决?
- 现象 :Spring 容器启动报错
BeanDefinitionOverrideException(默认不允许覆盖) 或者后面的覆盖前面的 (取决于配置)。 - 解决 :
- 最佳实践 :在自动配置类中使用
@ConditionalOnMissingBean。这样如果用户或其他 Starter 已经定义了该 Bean,当前的自动配置就会失效,避免冲突。 - 调整顺序 :使用
@AutoConfigureBefore或@AutoConfigureAfter明确加载顺序。 - 允许覆盖 :设置
spring.main.allow-bean-definition-overriding=true(不推荐,容易掩盖问题)。
- 最佳实践 :在自动配置类中使用
- "自动装配不是魔法,是基于'约定优于配置'(Convention over Configuration)的极致工程化实现。理解 SPI,你就理解了 Spring 生态疯狂扩张的基石。"
- "好的 Starter 设计,应该像空气一样:平时感觉不到它的存在(零配置),但当你需要时,它无处不在(自动生效);当你想定制时,它随时退让(@ConditionalOnMissingBean)。"
- "不要为了炫技而滥用自动装配。如果一个配置逻辑复杂多变,显式的 Java Config 往往比隐式的魔法更易于维护和调试。透明,永远是架构的第一原则。"