Lesson 56 · Spring 生态核心原理
Spring Boot 自动装配:@EnableAutoConfiguration 原理
面试官:Spring Boot 为什么能"开箱即用"?
"面试官:你项目里用 Spring Boot,那你说说 Spring Boot 为什么能'开箱即用'?自动装配的原理是什么?你自己写过 Starter 吗?"
这三个问题是 Spring Boot 面试的"三连击",层层递进。大多数候选人能说出"约定大于配置",但真正能把自动装配链路讲清楚的并不多。让我们从一个最常见的现象入手:
@SpringBootApplication public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } } // 没有 web.xml,没有 DispatcherServlet 配置,没有内嵌 Tomcat 配置 // 但启动后:Tomcat 跑在 8080,DispatcherServlet 已注册,JSON 序列化就绪 // 这一切是怎么发生的?
答案就藏在一个注解里:@SpringBootApplication。它不是单一注解,而是一个"组合拳"——三个核心注解的聚合体。让我们逐层拆解。
自动装配是 Spring Boot 的核心机制,理解它意味着你不仅会"用"框架,还理解框架"怎么帮你做事"。面试官通过这个问题可以判断:你是否具备阅读框架源码的能力、是否能自定义 Starter 扩展框架、是否在遇到问题时能从原理层面定位而非只会搜索报错信息。
@SpringBootApplication:三位一体的组合注解
打开 @SpringBootApplication 的源码,你会看到它身上贴了三个核心注解:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration // ① 本质就是 @Configuration,标记当前类是配置类 @EnableAutoConfiguration // ② 开启自动装配 —— 今天的核心主角 @ComponentScan( // ③ 扫描当前包及其子包下的 @Component excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) } ) public @interface SpringBootApplication { @AliasFor(annotation = EnableAutoConfiguration.class) Class<?>[] exclude() default {}; // 排除指定的自动配置类 @AliasFor(annotation = EnableAutoConfiguration.class) String[] excludeName() default {}; }
| 注解 | 作用 | 面试重点 |
|---|---|---|
@SpringBootConfiguration |
标记当前类为配置类(等同于 @Configuration) |
让 Spring Boot 知道从哪里开始加载配置 |
@EnableAutoConfiguration |
开启自动装配,从 META-INF 中加载所有自动配置类 | 核心机制,后面三站全部围绕它展开 |
@ComponentScan |
扫描启动类所在包及子包下的组件 | 为什么启动类要放在根包下 |
面试官追问:为什么启动类一般放在根包下?
@ComponentScan 默认扫描当前包及其所有子包。如果启动类放在 com.example.app 下,那么 com.example.app.service、com.example.app.controller 都会被扫到。如果放在 com.example.app.controller 包下,service 包里的 Bean 就注册不进来。
@EnableAutoConfiguration:自动装配的加载链路
这是整个自动装配的核心。让我们打开 @EnableAutoConfiguration 的源码:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @AutoConfigurationPackage // 记录自动配置的基础包路径 @Import(AutoConfigurationImportSelector.class) // ← 关键!导入选择器 public @interface EnableAutoConfiguration { String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration"; Class<?>[] exclude() default {}; String[] excludeName() default {}; }
核心在于 @Import(AutoConfigurationImportSelector.class)。这个 Selector 负责从 classpath 下加载所有自动配置类。加载链路如下:
public class AutoConfigurationImportSelector implements DeferredImportSelector { @Override public String[] selectImports(AnnotationMetadata metadata) { // 1. 获取所有候选配置类的全限定名 List<String> configurations = getCandidateConfigurations(metadata, attributes); // └→ 内部调用 SpringFactoriesLoader.loadFactoryNames() // 读取 META-INF/spring.factories 文件 // key = EnableAutoConfiguration.class.getName() // 2. 去重 configurations = removeDuplicates(configurations); // 3. 排除用户 exclude 的类 Set<String> exclusions = getExclusions(metadata, attributes); configurations.removeAll(exclusions); // 4. 排序(按 @AutoConfigureOrder / @AutoConfigureBefore / After) sort(configurations); return configurations.toArray(new String[0]); } }
在 Spring Boot 2.x 中,配置类列表写在 META-INF/spring.factories 里:
# Spring Boot 2.x 的注册方式 org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\ org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,\ org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\ # ... 共 130+ 个自动配置类
从 Spring Boot 3.0 开始,spring.factories 机制被废弃,改用新文件 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,每行一个全限定类名,不再需要那个长长的 key。底层加载类也从 SpringFactoriesLoader 换成了 ImportCandidates。如果你维护的自定义 Starter 需要同时兼容 2.x 和 3.x,两个文件都要保留。
# 新格式:每行一个全限定名,更简洁
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
@EnableAutoConfiguration → @Import(AutoConfigurationImportSelector) → 读取 META-INF 下的注册文件 → 拿到所有自动配置类的全限定名 → 去重、排除、排序 → 返回给 Spring 容器注册。
@Conditional 条件装配:按需激活的守门员
spring.factories 里注册了 130 多个自动配置类,但你的应用不可能全部需要。如果全部加载,不仅浪费内存,还会因为缺少依赖而报错。@Conditional 系列注解就是这些配置类的"守门员"——只有满足条件,配置类才会生效。
| 注解 | 生效条件 | 典型用法 |
|---|---|---|
@ConditionalOnClass | classpath 下存在指定类 | 有 Tomcat 才装配 WebMvcAutoConfiguration |
@ConditionalOnMissingClass | classpath 下不存在指定类 | 用户未引入反应式框架时装配 Servlet 栈 |
@ConditionalOnBean | 容器中存在指定 Bean | 有 DataSource 才装配 JdbcTemplate |
@ConditionalOnMissingBean | 容器中不存在指定 Bean | 用户没自定义时才装配默认实现 |
@ConditionalOnProperty | 配置属性满足条件 | spring.cache.type=redis 时才装配 Redis 缓存 |
@ConditionalOnWebApplication | 当前是 Web 应用 | 只在 Web 环境下注册 DispatcherServlet |
@Configuration @ConditionalOnClass(ObjectMapper.class) // classpath 下有 Jackson 才生效 public class JacksonAutoConfiguration { @Bean @ConditionalOnMissingBean // 用户没自己定义 ObjectMapper 时才装配 public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) { return builder.createXmlMapper(false).build(); } }
这里有一个面试常考的设计思想:@ConditionalOnMissingBean 体现了 Spring Boot 的"约定配置 + 用户覆盖"哲学。框架提供一个合理的默认实现,但用户一旦自定义了同类型 Bean,框架的默认实现就自动退让。
@Configuration @ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }) @EnableConfigurationProperties(DataSourceProperties.class) public class DataSourceAutoConfiguration { @Configuration @ConditionalOnMissingBean(DataSource.class) // 用户没配 DataSource 才走这里 @Conditional(EmbeddedDatabaseCondition.class) // classpath 下有 H2/HSQL/Derby static class EmbeddedDatabaseConfiguration { @Bean public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.H2).build(); } } @Configuration @ConditionalOnProperty(prefix = "spring.datasource", name = "url") static class PooledDataSourceConfiguration { // 用户配了 spring.datasource.url 才走 HikariCP 连接池 } }
面试官追问:@Conditional 的底层原理是什么?
每个 @Conditional 注解都关联一个 Condition 实现类。Spring 在注册 Bean 之前,会调用 Condition.matches(ConditionContext, AnnotatedTypeMetadata) 方法,返回 true 才注册,返回 false 就跳过。例如 @ConditionalOnClass 对应的 OnClassCondition 内部通过 ClassLoader 检查指定类是否存在于 classpath。
130+ 自动配置类全部被加载到候选列表中,但最终是否注册到 Spring 容器,由 @Conditional 系列注解决定。条件不满足 → 整个配置类被跳过,就像它不存在一样。
自动装配全流程:从注解到 Bean 注册
前面几站拆解了各个环节,现在把它们串成一张完整的流程图。面试时能在脑中复现这张图,你就赢了。
在 application.properties 中加一行 debug=true,启动时会打印 Condition Evaluation Report,列出所有候选自动配置类及其匹配/不匹配原因。这是排查"为什么某个 Bean 没被注入"的利器。
自定义 Starter:从原理到实战
面试高频追问:"你自己写过 Starter 吗?写一个 Starter 需要几步?" 答案是 5 步。我们以一个"短信发送 Starter"为例,完整走一遍。
Starter 的本质是什么?
Starter 是一个空壳 Maven 模块,它本身不包含代码,只做一件事:引入"自动配置模块"的依赖。自动配置模块才是真正包含 @Configuration 类和 Bean 定义的地方。Spring 官方推荐将两者分开,便于用户按需排除。
第 1 步:创建自动配置模块 sms-spring-boot-autoconfigure
<!-- 自动配置模块的 pom --> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure</artifactId> </dependency> </dependencies>
第 2 步:定义配置属性类
@ConfigurationProperties(prefix = "sms") public class SmsProperties { private String accessKey; // sms.access-key private String secretKey; // sms.secret-key private String signName = "默认签名"; // sms.sign-name // getter / setter 省略 }
第 3 步:编写自动配置类
@Configuration @ConditionalOnClass(SmsClient.class) // classpath 下有 SmsClient 才生效 @ConditionalOnProperty( // 配置了 sms.access-key 才生效 prefix = "sms", name = "access-key", matchIfMissing = false ) @EnableConfigurationProperties(SmsProperties.class) public class SmsAutoConfiguration { @Bean @ConditionalOnMissingBean // 用户没自定义时才装配默认实现 public SmsClient smsClient(SmsProperties props) { return new DefaultSmsClient( props.getAccessKey(), props.getSecretKey(), props.getSignName() ); } @Bean @ConditionalOnMissingBean public SmsTemplate smsTemplate(SmsClient client) { return new SmsTemplate(client); // 封装发送短信的便捷方法 } }
第 4 步:注册自动配置类
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.example.sms.autoconfigure.SmsAutoConfiguration
com.example.sms.autoconfigure.SmsAutoConfiguration
第 5 步:创建 Starter 模块 sms-spring-boot-starter
<!-- Starter 模块:一个 Bean 都没有,只做依赖引入 --> <dependencies> <dependency> <groupId>com.example</groupId> <artifactId>sms-spring-boot-autoconfigure</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>com.example</groupId> <artifactId>sms-sdk</artifactId> <version>1.0.0</version> <!-- 引入底层 SDK --> </dependency> </dependencies>
使用方只需引入 Starter,在 application.yml 中配置属性即可:
sms: access-key: AKID2024XXXX secret-key: SK2024YYYY sign-name: "我的公司"
@Service public class OrderService { @Autowired private SmsTemplate smsTemplate; // 自动注入,零配置 public void notifyShipped(String phone, String orderNo) { smsTemplate.send(phone, "您的订单 " + orderNo + " 已发货"); } }
1. 创建 autoconfigure 模块(写 @Configuration + @Conditional) → 2. 定义 @ConfigurationProperties 绑定配置 → 3. 在 META-INF 注册自动配置类 → 4. 创建 starter 空壳模块聚合依赖 → 5. 使用方引入 starter + 写配置,开箱即用。
总结:关键注解速查 + Starter 清单
自动装配核心注解速查
| 注解 | 作用 | 面试关键词 |
|---|---|---|
@SpringBootApplication | 三合一组合注解 | @Configuration + @EnableAutoConfiguration + @ComponentScan |
@EnableAutoConfiguration | 开启自动装配 | @Import(AutoConfigurationImportSelector) |
AutoConfigurationImportSelector | 加载候选配置类 | 读 META-INF → 去重 → 排除 → 排序 |
@ConditionalOnClass | classpath 有指定类才生效 | 按需加载,避免缺依赖报错 |
@ConditionalOnMissingBean | 容器中没有指定 Bean 才装配 | 默认实现 + 用户覆盖 |
@ConditionalOnProperty | 配置属性满足条件才生效 | 功能开关、条件启用 |
@ConfigurationProperties | 绑定外部配置到 Java 对象 | 类型安全的配置读取 |
自定义 Starter 五步清单
| 步骤 | 模块 | 关键动作 |
|---|---|---|
| 1 | autoconfigure | 编写 @Configuration 类,用 @Conditional 控制装配 |
| 2 | autoconfigure | 定义 @ConfigurationProperties 绑定用户配置 |
| 3 | autoconfigure | META-INF 注册自动配置类(2.x 用 spring.factories,3.x 用 imports) |
| 4 | starter | 创建空壳模块,聚合 autoconfigure + 底层 SDK 依赖 |
| 5 | 使用方 | 引入 starter 依赖 + 配置属性 = 开箱即用 |
提到"在 application.properties 中设置 debug=true 可以查看 Condition Evaluation Report"会让面试官眼前一亮。这份报告会列出所有候选的自动配置类,标明 Positive matches(条件通过)和 Negative matches(条件未通过),以及每个条件不满足的具体原因——是排查自动装配问题的终极武器。
面试 30 秒总结模板
"Spring Boot 的自动装配基于 @EnableAutoConfiguration,它通过 @Import 导入 AutoConfigurationImportSelector,从 META-INF/spring.factories 或 imports 文件中加载所有候选自动配置类。每个配置类通过 @Conditional 系列注解(如 @ConditionalOnClass、@ConditionalOnMissingBean)进行条件判断,只有满足条件的配置类才会被注册到 Spring 容器。自定义 Starter 的核心就是:写一个 autoconfigure 模块定义条件和 Bean,再用一个 starter 空壳模块聚合依赖。"
自动装配公式(一句话记忆):
@EnableAutoConfiguration + META-INF 注册 + @Conditional 过滤 = 开箱即用