SpringBoot 深度指南
SpringBoot 深度指南
本文面向已经用过 SpringBoot、准备高级/资深 Java 岗位面试的同学。目标不是罗列注解和配置项,而是讲清楚"启动过程到底做了什么""自动装配这条链路每一步具体在干什么""条件装配为什么会踩坑""内嵌容器和 starter 机制背后的设计思路是什么"。源码分析以 Spring Boot 3.x(JDK 17) 为基准版本,涉及版本演进的地方会同时讲清楚 2.x 的历史做法。文中引用的类名、方法名(如
SpringApplication#run、AutoConfigurationImportSelector、SpringBootCondition#matches等)均为真实存在且相对稳定的 API,用简化代码/伪代码还原其调用链路,但不标注具体源码行号——源码行号会随小版本变动,面试和实际排查都应该以本地反编译/源码跳转为准,本文只保证调用关系和语义方向是对的,细节建议对照本地源码复核。
目录
每节标题保持简短,核心结论以 要点提示放在每节正文开头。
- 一、SpringApplication.run() 启动流程全貌
- 二、自动装配原理
- 三、@Conditional 条件装配体系
- 四、内嵌容器原理
- 五、starter 机制
- 六、常见面试高频问题清单
- 结语:面试回答的通用框架
一、SpringApplication.run() 启动流程全貌
要点:
SpringApplication#run的本质是"准备环境 → 创建并刷新 Spring 容器 → 执行用户扩展点"这三段式流程,SpringApplicationRunListeners作为事件广播器贯穿全程,把启动过程切成一个个可观测、可扩展的时间点,而不是一段不可插手的黑盒逻辑。
1.1 应用类型推断与环境准备
new SpringApplication(primarySources) 构造阶段就会做几件影响后续流程的事:
- 推断应用类型(
WebApplicationType):通过检测 classpath 上是否存在特定类(如DispatcherServlet、ConfigurableWebApplicationContext属于 Servlet 生态,org.springframework.web.reactive.DispatcherHandler属于 Reactive 生态)来判断当前是SERVLET、REACTIVE还是NONE,这个判断结果决定了后续创建哪一种ApplicationContext实现(如ServletWebServerApplicationContext)。 - 加载
ApplicationContextInitializer和ApplicationListener:这一步已经在使用自动装配同源的加载机制——通过SpringFactoriesLoader(3.x 里对这些"经典扩展点"仍然保留了spring.factories的加载方式,这属于自动装配主流程之外的另一套加载逻辑,不要和第二节的AutoConfiguration.imports混淆)从 classpath 上所有 jar 包的META-INF/spring.factories里读取配置好的实现类。
run() 方法调用后的主干流程大致如下(简化还原,非逐行摘抄):
public ConfigurableApplicationContext run(String... args) {
// 1. 计时 + 创建 SpringApplicationRunListeners(事件广播器)
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting(...);
try {
// 2. 封装启动参数,准备/绑定 Environment(读取配置文件、系统属性、命令行参数等)
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, ..., applicationArguments);
// 3. 打印 Banner
Banner printedBanner = printBanner(environment);
// 4. 按第 1.1 节推断出的应用类型,创建对应的 ApplicationContext 实现
context = createApplicationContext();
// 5. 准备容器:设置 Environment、执行 ApplicationContextInitializer、注册启动类为 BeanDefinition
prepareContext(..., context, environment, listeners, applicationArguments, printedBanner);
// 6. 真正触发 Spring 容器刷新(BeanFactory 初始化、BeanDefinition 加载、Bean 实例化、
// 内嵌容器创建与启动都发生在这一步,详见第四节)
refreshContext(context);
afterRefresh(context, applicationArguments);
listeners.started(context, ...);
// 7. 执行用户定义的 CommandLineRunner / ApplicationRunner
callRunners(context, applicationArguments);
} catch (Throwable ex) {
handleRunFailure(context, ex, listeners);
throw ...;
}
listeners.ready(context, ...);
return context;
}
几个容易被面试追问的细节:
| 步骤 | 关键点 |
|---|---|
| 推断应用类型 | 靠 classpath 探测特定类是否存在,不是靠配置文件声明 |
| 准备 Environment | 此时 ApplicationContext 还没创建,但 Environment 已经可用——这也是为什么某些扩展点(如 EnvironmentPostProcessor)能在容器创建之前就读到/修改配置 |
| 创建 Context | 类型在构造阶段已推断好,这里只是 new 出对应实现类,不涉及 Bean 加载 |
| refreshContext | 真正的 AbstractApplicationContext#refresh() 模板方法在这里被调用,BeanFactoryPostProcessor(自动装配的候选类过滤发生在这个阶段,见第二节)、BeanPostProcessor、Bean 实例化、内嵌容器启动全部在这一步完成 |
| CommandLineRunner / ApplicationRunner | 在容器刷新完成之后才执行,二者区别仅在于 run 方法的参数类型(String[] 还是封装过的 ApplicationArguments),多个 Runner 之间可以用 @Order 或实现 Ordered 控制执行顺序 |
1.2 SpringApplicationRunListeners 事件广播机制
SpringApplicationRunListeners 是对多个 SpringApplicationRunListener 的聚合封装,负责在启动流程的关键节点广播事件,让框架内部组件和用户代码都能在不侵入主流程的前提下"插一脚"。核心事件节点(3.x 中的典型顺序,具体事件类型建议对照本地源码复核):
| 事件节点 | 触发时机 | 典型用途 |
|---|---|---|
starting |
run() 刚开始,Environment 还未创建 |
记录启动时间、做最早期的初始化 |
environmentPrepared |
Environment 已准备好,ApplicationContext 还未创建 |
动态修改/补充配置项(如 EnvironmentPostProcessor 的典型触发点) |
contextPrepared |
ApplicationContext 已创建,尚未加载 BeanDefinition |
较少使用的早期扩展点 |
contextLoaded |
BeanDefinition 已加载完成,refresh() 尚未调用 |
在容器刷新之前对 BeanDefinition 做最后调整 |
started |
refresh() 已完成,Runner 尚未执行 |
容器内的 Bean 已经可用 |
ready |
所有 Runner 执行完毕,应用完全就绪 | 对外暴露"服务已就绪"的信号,常用于健康检查/注册中心上报就绪状态 |
failed |
启动过程中任意阶段抛出异常 | 统一的失败处理/清理逻辑 |
这套机制本质上是观察者模式在启动流程上的应用:SpringApplicationRunListener 的实现类同样通过 META-INF/spring.factories 注册(不是 3.x 的 AutoConfiguration.imports,因为它不属于"自动配置类"这个扩展点类型),Spring Boot 官方内置实现是 EventPublishingRunListener,它把这些"启动阶段事件"转换成标准的 ApplicationEvent 广播出去,用户只要实现 ApplicationListener<XxxEvent> 并注册为 Bean(或者在 spring.factories 里注册,因为有些事件在容器创建 Bean 之前就已经触发,普通 Bean 形式的 Listener 根本来不及监听到),就能在不修改启动主干代码的前提下介入启动过程的任意阶段——这也是"约定优于配置"在框架扩展点设计上的一个典型体现:框架把关键时间点都广播出来,扩展逻辑以插件形式接入,而不是让使用者去改框架源码。
二、自动装配原理
要点:自动装配的主干链路是
@SpringBootApplication→@EnableAutoConfiguration→@Import(AutoConfigurationImportSelector.class)→ 从配置文件读取候选自动配置类全列表 → 经过@Conditional体系逐个过滤 → 只有条件满足的配置类才会真正生效并把 Bean 注册进容器。这是 SpringBoot 面试出现频率最高的问题,必须能完整讲出这条链路,而不是只会说"自动装配就是自动帮你配置好 Bean"。
2.1 @SpringBootApplication 与 @EnableAutoConfiguration
@SpringBootApplication 是一个组合注解,核心由三个注解组成:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration // 本质是 @Configuration,标识这是一个配置类
@EnableAutoConfiguration // 自动装配的真正入口
@ComponentScan(...) // 扫描启动类所在包及子包下的组件
public @interface SpringBootApplication {
...
}
三者分工非常清晰:@SpringBootConfiguration 让启动类本身成为一个可以定义 @Bean 的配置类;@ComponentScan 负责"能扫到的组件"(@Component/@Service/@Controller 等标准 Spring 组件);而自动装配的真正机制在 @EnableAutoConfiguration 里:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
...
}
关键在 @Import(AutoConfigurationImportSelector.class)——这把自动装配的入口接入了 Spring 原生的 @Import 机制。@Import 除了能直接导入一个配置类,还支持导入实现了 ImportSelector 接口的类,容器在解析配置类时会调用其 selectImports() 方法,方法返回的类名数组会被当作额外的配置类继续解析。AutoConfigurationImportSelector 正是利用了这个扩展点,把"到底要导入哪些自动配置类"这个决策权从静态注解声明变成了运行时动态计算。
2.2 AutoConfigurationImportSelector 加载候选配置类
AutoConfigurationImportSelector(实现了 DeferredImportSelector,是 ImportSelector 的子接口,语义是"延迟到所有其他配置类都处理完之后再处理",这一点在第 2.3 节和 3.3 节还会用到)的核心逻辑可以简化理解为:
public class AutoConfigurationImportSelector implements DeferredImportSelector {
public String[] selectImports(AnnotationMetadata metadata) {
// 1. 从注解上取出用户通过 exclude/excludeName 显式排除的类
// 2. 加载候选自动配置类全列表(本节重点)
List<String> configurations = getCandidateConfigurations(metadata, attributes);
// 3. 去重
configurations = removeDuplicates(configurations);
// 4. 应用 exclude 排除
configurations = removeExcludedConfigurations(configurations, exclusions);
// 5. 经过条件过滤(第 2.3 节 / 第三节),只保留真正生效的
configurations = getConfigurationClassFilter().filter(configurations);
return StringUtils.toStringArray(configurations);
}
}
第 2 步"加载候选自动配置类全列表"这个动作,底层依赖 ImportCandidates(3.x)或历史上的 SpringFactoriesLoader(2.x),这正是版本差异最大的地方。
Spring Boot 2.x 与 3.x 自动装配配置文件的区别
| 维度 | Spring Boot 2.x | Spring Boot 3.x |
|---|---|---|
| 配置文件路径 | META-INF/spring.factories |
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports |
| 文件格式 | key=value1,value2,... 的 properties 格式,一个大文件承载所有扩展点(自动配置类、ApplicationContextInitializer、ApplicationListener、SpringApplicationRunListener 等都挤在同一个文件里,靠不同的 key 区分) |
一个扩展点类型对应一个独立的文件,自动配置类的候选列表单独放在 AutoConfiguration.imports 里,每行一个全限定类名,不需要 key,也不需要处理逗号分隔 |
| 加载方式 | SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class, classLoader),按 key(EnableAutoConfiguration 全限定名)取出对应的 value 列表 |
ImportCandidates.load(AutoConfiguration.class, classLoader),直接按约定路径读取整份文件内容 |
| 是否兼容旧写法 | —— | 3.x 中 spring.factories 里的 EnableAutoConfiguration key 已不再被自动装配主流程读取(历史遗留的其他扩展点如 ApplicationListener 仍然走 spring.factories,这一点在 1.2 节已经提到),自定义 starter 若沿用旧文件会导致自动配置类加载不到,这是很多老 starter 升级 3.x 时踩的第一个坑 |
为什么 3.x 要做这个拆分:spring.factories 在 2.x 后期已经变成了一个职责严重混杂的"大杂烩"文件——同一个文件里既登记自动配置类,又登记 ApplicationContextInitializer、ApplicationListener、失败分析器 FailureAnalyzer 等等十几种不同类型的扩展点,靠 key 区分类型、靠逗号分隔多个值,文件本身没有任何结构化的分组,人工维护和阅读成本都比较高,而且自动配置类的候选列表通常是这些扩展点里条目数量最多、变动最频繁的一类(每个 starter 至少贡献一到几个自动配置类),和其他扩展点混在一起不利于工具化处理(如 IDE 索引、构建期校验)。3.x 把"自动配置类候选列表"这个用量最大、语义最单一的扩展点单独拆成一个文件,文件格式也从 key=value 简化成纯粹的"一行一个类名",语义更直接、加载逻辑也更简单(不需要再按 key 查找和处理逗号分隔),这是"职责单一、按扩展点类型拆分配置"这一工程思路在自动装配上的具体落地;而其他数量较少、变动不频繁的扩展点(如 ApplicationContextInitializer)3.x 仍然保留在 spring.factories 里,并没有全部拆分,说明这次改造是有针对性地拆分"最需要拆"的那一类,而不是无差别推翻整个机制。
2.3 候选类的条件过滤
getCandidateConfigurations 拿到的是全量候选列表——不管当前项目实际用没用到 Redis、RabbitMQ、DataSource,只要对应的 starter jar 在 classpath 上,它的自动配置类全限定名就会出现在这份列表里(这份列表来自所有 jar 包里的 AutoConfiguration.imports 文件汇总,不区分是否真的会生效)。真正决定"这个自动配置类是否会被实例化、注册 Bean 进容器"的,是 @Conditional 体系在容器解析 BeanDefinition 阶段做的逐个过滤——这也是为什么 SpringBoot 能做到"约定优于配置、有依赖就自动生效、没依赖就自动跳过":并不是它聪明地"感知"到了你需要什么,而是先无差别列出所有可能相关的配置类,再用条件注解把不满足条件的挨个筛掉,剩下的才是最终生效的自动配置类。这条过滤逻辑的细节在第三节展开。
三、@Conditional 条件装配体系
要点:
@Conditional的底层是Condition接口的matches()方法,在容器解析BeanDefinition(还没到实例化 Bean 那一步)阶段就会被调用;@ConditionalOnClass/@ConditionalOnMissingBean/@ConditionalOnProperty分别检测 classpath、容器里已有的 Bean、配置项这三类完全不同的运行时信息,共同点是都要"先有一个可以查询的现场"才能做判断。
3.1 Condition 接口与 matches 方法
@Conditional 注解本身只接受一个 Condition 数组:
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
ConditionContext:提供了查询当前"现场"所需的一切——BeanFactory(可以查询容器里已注册的 BeanDefinition,@ConditionalOnMissingBean就是靠这个)、Environment(可以读配置项,@ConditionalOnProperty靠这个)、ClassLoader(可以探测某个类是否能被加载,@ConditionalOnClass靠这个)、当前的ResourceLoader等。matches()返回true表示条件满足,这个配置类/这个@Bean方法才会被保留在容器的解析结果里;返回false则这个BeanDefinition直接被跳过,根本不会进入后续的实例化流程。
调用时机上,matches() 是在 ConfigurationClassParser 解析配置类、把候选类转换成 BeanDefinition 的阶段被调用的(具体承载类是 ConditionEvaluator,内部会调用 SpringBootCondition#matches 这类 SpringBoot 提供的基类实现),远早于 Bean 的实例化和依赖注入阶段。这个时机很关键:意味着条件判断依据的是"当前已经确定下来的 BeanDefinition 集合和 Environment",而不是"运行时已经创建好的 Bean 实例"——如果条件判断依赖的信息本身还没确定(比如某个 Bean 是否存在,取决于另一个还没解析完的配置类),就会引出第 3.3 节的加载顺序问题。
SpringBoot 提供的所有 @ConditionalOnXxx 注解,底层实现类基本都继承自 SpringBootCondition(实现了 Condition 接口),并在 matches() 里统一做了日志输出(记录"这个条件为什么满足/不满足",这也是为什么加上 --debug 启动参数或者 ConditionEvaluationReport 能打印出一份完整的自动配置生效/未生效报告及原因,排查自动装配问题时非常有用)。
3.2 三种常见条件注解的判断逻辑
| 注解 | 判断依据 | 简化判断逻辑 | 典型用途 |
|---|---|---|---|
@ConditionalOnClass |
classpath 上是否存在指定的类 | 通过 ClassLoader 尝试加载指定的类全限定名(通常用类名字符串而非 .class 直接引用,避免这个类本身不存在时配置类本身都无法编译/加载),能加载成功即条件满足 |
判断某个第三方依赖是否被引入,如 @ConditionalOnClass(RedisOperations.class) |
@ConditionalOnMissingBean |
容器(BeanFactory)里当前是否已经不存在指定类型/名称的 Bean |
按类型(或指定 name)查询已注册的 BeanDefinition,一个都查不到才满足条件;查询范围默认包含当前正在解析的这一批里已经确定要注册的 BeanDefinition |
让自动配置类"让路"给用户自定义的同类型 Bean,实现默认值可覆盖 |
@ConditionalOnProperty |
指定的配置项是否存在、值是否等于期望值 | 从 Environment 里读取 prefix + name 对应的配置值,和 havingValue(未指定时默认判断值非 false 即可,具体行为建议对照注解文档确认)比较,matchIfMissing 控制配置项完全缺失时的默认判断结果 |
让用户能通过配置文件里的一个开关,显式启用/禁用某个自动配置,如 spring.redis.enabled=false |
@ConditionalOnClass 用的是静态的、探测式的判断(有没有这个类,编译期无法确定要不要加载它,所以设计上必须做成运行时反射式探测),而 @ConditionalOnMissingBean 和 @ConditionalOnProperty 用的是动态的、依赖当前解析进度/配置状态的判断——尤其是 @ConditionalOnMissingBean,它的判断结果会随着"容器当前已经确定注册哪些 Bean"这个动态过程而变化,这正是第 3.3 节要讨论的坑的根源。
3.3 @ConditionalOnMissingBean 的加载顺序坑
要点:
@ConditionalOnMissingBean要求"判断时容器里确实还没有这个 Bean",如果自动配置类先于用户自定义配置类被解析,判断那一刻用户的 Bean 还没注册进去,条件会误判为"缺失"从而让自动配置类的默认 Bean 抢先注册,等用户配置类真正被解析时反而会因为同类型/同名 Bean 已存在而冲突或被忽略——这就是"自定义配置没能覆盖自动配置"的典型根因。
@ConditionalOnMissingBean 之所以在实践中通常表现为"用户自定义的 @Bean 能顺利覆盖自动配置的默认 Bean",前提是自动配置类要在解析顺序上排在用户配置类之后,如果这个前提被打破,就会出现"明明写了同类型的 @Bean,容器里用的却还是自动配置类给的默认实现"这种诡异现象。SpringBoot 从两个层面来保证这个前提:
AutoConfigurationImportSelector实现的是DeferredImportSelector而不是普通ImportSelector:DeferredImportSelector的语义是"推迟处理"——ConfigurationClassParser在处理@Import时,普通ImportSelector返回的类会被立刻递归解析,而DeferredImportSelector返回的类会被先收集起来,等当前这一轮所有其他配置类(包括用户自己用@Configuration/@Bean写的配置类)都处理完之后,才统一处理。这意味着自动配置类的BeanDefinition天然就比用户自定义配置类的BeanDefinition晚注册,@ConditionalOnMissingBean在自动配置类里判断"容器里有没有这个 Bean"时,用户的 Bean 大概率已经先一步注册进去了,条件不满足,自动配置类里的默认 Bean 就会被跳过——这就是覆盖生效的本质。- 自动配置类之间还有
@AutoConfiguration的before/after(对应历史上的@AutoConfigureBefore/@AutoConfigureAfter/@AutoConfigureOrder)做进一步排序:多个自动配置类之间也存在依赖关系(比如某个自动配置类要在另一个提供了基础 Bean 的自动配置类之后再解析,才能正确判断"这个基础 Bean 是否已存在"),这套注解只解决自动配置类内部的相对顺序,不改变"所有自动配置类整体上晚于用户配置类"这个由DeferredImportSelector保证的大前提。
经典的踩坑场景:如果用户的 @Bean 定义不是写在一个能被正常扫描到、且在合理时机解析的 @Configuration 类里,而是写在某个本身也需要经过复杂条件判断、或者本身被其他机制推迟加载的地方(比如用户自己也写了一个 DeferredImportSelector 导入的配置类,和自动配置类"抢跑道"),就可能出现用户 Bean 的注册时机反而晚于(或与)自动配置类同批处理,导致 @ConditionalOnMissingBean 判断时用户 Bean 还不存在,自动配置的默认 Bean 抢先注册,最终用户定义的同类型 Bean 因为 Spring 容器默认不允许重复定义而报 BeanDefinitionOverrideException(如果开启了允许覆盖,则可能是后解析的悄悄覆盖前面的,行为随 spring.main.allow-bean-definition-overriding 配置和解析顺序变化,实际报错/覆盖行为建议结合具体版本和配置项验证,不要凭经验直接下结论)。实践中的正确姿势:把自己的 Bean 定义放在正常的、能被 @ComponentScan 扫到的 @Configuration 类里(不要人为引入额外的延迟加载机制),依赖框架保证的"自动配置类整体上最后解析"这个默认顺序即可,一般不需要用户自己操心加载顺序。
四、内嵌容器原理
要点:内嵌 Tomcat 不是"额外起了一个进程再把应用扔进去",而是
ServletWebServerApplicationContext在refresh()流程中,把创建/启动 Web 服务器这件事也纳入了 Spring 容器生命周期管理——TomcatServletWebServerFactory作为一个普通 Bean 被容器管理,容器刷新到特定阶段时调用它创建出Tomcat对象并启动,Web 服务器的生命周期因此和 Spring 容器的生命周期绑定在一起。
4.1 ServletWebServerApplicationContext 如何创建内嵌 Tomcat
第一节提到,refreshContext() 最终调用的是 AbstractApplicationContext#refresh() 这个经典模板方法。这个模板方法有一个专门预留给子类扩展的钩子方法 onRefresh(),普通的 Spring 容器(如 AnnotationConfigApplicationContext)对这个钩子基本不做特殊处理,而 ServletWebServerApplicationContext 重写了 onRefresh(),在这里插入了创建内嵌 Web 服务器的逻辑:
// 简化还原调用关系,非逐行摘抄
class ServletWebServerApplicationContext extends GenericWebApplicationContext {
protected void onRefresh() {
super.onRefresh();
try {
createWebServer();
} catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}
private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = getServletContext();
if (webServer == null && servletContext == null) {
// 从容器里取出 ServletWebServerFactory 类型的 Bean
// (由第五节讲到的 Web 相关自动配置类注册进去,如 TomcatServletWebServerFactory)
ServletWebServerFactory factory = getWebServerFactory();
this.webServer = factory.getWebServer(getSelfInitializer());
}
...
// 触发内嵌服务器真正启动监听端口
this.webServer.start();
}
}
getWebServerFactory() 从容器里按类型查找 ServletWebServerFactory 的 Bean——这个 Bean 从哪来?正是 ServletWebServerFactoryAutoConfiguration(属于自动装配的一部分,条件是 classpath 上要有对应容器的类,如 Tomcat 的 @ConditionalOnClass({ Servlet.class, Tomcat.class, ... })) 根据 classpath 上实际存在哪种容器实现(Tomcat/Jetty/Undertow)装配出对应的 TomcatServletWebServerFactory/JettyServletWebServerFactory/UndertowServletWebServerFactory。TomcatServletWebServerFactory#getWebServer() 内部会 new Tomcat()、配置 Connector(监听端口)、创建 Context、把 Spring 的 DispatcherServlet 注册为 Tomcat 内部的一个 Servlet,最后 tomcat.start()——这一整套原本需要独立部署一个 Tomcat 进程、把 war 包扔进 webapps 目录才能做的事情,被完整地"内联"进了 Java 代码里,作为 Spring 容器 refresh() 生命周期的一部分执行。
与外置容器部署的本质区别
| 维度 | 传统 war 包 + 外置容器 | SpringBoot 内嵌容器 |
|---|---|---|
| 进程关系 | Tomcat 是独立进程/服务,应用是被 Tomcat 加载的 war 包,容器先于应用存在 | Tomcat 的核心类库作为普通依赖被打进应用 jar 包,是应用创建并管理的一个对象,应用进程先启动,容器是应用生命周期里的一个组件 |
| 生命周期归属 | 容器生命周期独立于应用,一个容器可以同时部署多个 war 应用 | 容器生命周期和 Spring 容器(ApplicationContext)绑定,refresh() 里创建启动,容器 close() 时一并关闭,一个 Tomcat 实例只服务这一个应用 |
| 部署产物 | war 包,需要预先装好 Tomcat 环境 | 可执行 jar 包(内嵌了 Tomcat 依赖),java -jar 即可运行,不需要提前装容器 |
| 版本管理 | 容器版本由运维统一管理,应用代码和容器版本解耦但也可能不一致(本地开发用的 Tomcat 版本和线上不一致是经典坑) | 容器版本作为 Maven/Gradle 依赖管理,和应用代码一起打包、一起做版本控制,本地和线上版本天然一致 |
| 典型适用场景 | 传统企业级部署、需要在一台机器上跑多个应用共享容器资源 | 微服务、容器化(Docker/K8s)部署,一个进程一个服务,配合镜像天然契合"一份产物到处运行" |
这个设计带来的核心收益是"微服务化部署的一致性":每个服务都是一个独立的、自带运行时环境的可执行 jar,不再需要维护一套独立于代码版本的容器环境,这也是 SpringBoot 能和容器化生态(Docker/K8s)如此契合的根本原因之一。
五、starter 机制
要点:一个 starter 本质上是"一份 Maven/Gradle 依赖坐标(帮你把相关的第三方库版本锁定、传递依赖梳理好)+ 一个或多个自动配置类(按条件自动装配出可直接使用的 Bean)+ 一份
spring-configuration-metadata.json(给 IDE 提供配置项的自动补全和文档提示)"这三者的组合,本质是把"引入依赖 → 手动配置 Bean → 记住每个配置项怎么写"这套原本需要开发者自己做的事情,提前做成一份可复用的"约定"。
5.1 starter 的三个组成部分
| 组成部分 | 作用 | 典型例子 |
|---|---|---|
| 依赖坐标(pom / build.gradle) | 把使用这个功能所需的全部第三方依赖版本梳理清楚、锁定兼容版本,避免使用者自己排查依赖冲突 | spring-boot-starter-data-redis 依赖了 spring-data-redis、lettuce-core 等,版本已经和当前 SpringBoot 版本做过兼容性验证 |
| 自动配置类 | 按第二、三节讲的条件装配机制,检测到相关依赖存在、且用户没有自定义同类型 Bean 时,自动注册出开箱即用的 Bean | RedisAutoConfiguration 自动装配出 RedisTemplate、StringRedisTemplate 等 |
spring-configuration-metadata.json |
记录这个 starter 支持哪些配置项(前缀、类型、默认值、说明文字),IDE(IntelliJ IDEA / STS)读取这份元数据后,在 application.yml/application.properties 里输入配置项时能提供自动补全和悬浮说明 |
spring.redis.host、spring.redis.port 等配置项的补全提示 |
"约定优于配置"在这里的具体体现是:使用者只需要做两件事——引入一个 starter 依赖、在配置文件里按约定好的前缀填几个配置项(甚至可以不填,全部用默认值),剩下的"到底要不要装配这些 Bean""这些 Bean 之间怎么互相依赖注入""默认值该给多少"全部由 starter 内部的自动配置类事先决定好,使用者不需要手写一堆 @Bean 方法去 new 出 RedisTemplate 并配置好序列化器、连接池等一堆样板代码。
5.2 以 Redis Starter 为例
一个简化还原的自动配置类大致长这样(真实的 RedisAutoConfiguration 会更复杂,涉及连接工厂的选择、序列化器配置等,这里只还原核心骨架,具体实现建议对照本地 spring-boot-autoconfigure 依赖源码复核):
@AutoConfiguration
@ConditionalOnClass(RedisOperations.class) // classpath 上要有 spring-data-redis 相关类,否则整个配置类不生效
@EnableConfigurationProperties(RedisProperties.class) // 绑定 spring.redis.* 前缀的配置项
public class RedisAutoConfiguration {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate") // 用户没有自定义同名 Bean 时才生效,让路给用户自定义
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) {
return new StringRedisTemplate(connectionFactory);
}
}
这个例子完整体现了自动装配的几个关键机制在一个真实场景里如何配合:@ConditionalOnClass(RedisOperations.class) 保证"没引入 Redis 相关依赖的项目,这个配置类整体上不会生效,不会因为多余的自动配置类报类找不到的错误";@EnableConfigurationProperties(RedisProperties.class) 把 spring.redis.host/spring.redis.port 等配置项绑定到一个强类型的 RedisProperties 对象上(这也是 spring-configuration-metadata.json 里那些配置项提示的数据来源);@ConditionalOnMissingBean 保证用户如果自己定义了一个名为 redisTemplate 的 RedisTemplate Bean(比如想自定义序列化方式,这在实践中非常常见——默认的 JDK 序列化会导致 Redis 里存的是不可读的字节,很多项目会自定义 RedisTemplate 换成 JSON 序列化),自动配置的默认实现会自动让路,不会产生冲突。
六、常见面试高频问题清单
要点:这一节把前五节的内容浓缩成可以直接在面试现场组织语言的答案,但建议真正理解链路后用自己的话说出来,而不是背诵。
6.1 SpringBoot 自动装配原理讲一下
参考答案骨架(对应第二节):启动类上的 @SpringBootApplication 组合了 @EnableAutoConfiguration,后者通过 @Import(AutoConfigurationImportSelector.class) 接入 Spring 的 @Import 扩展机制;AutoConfigurationImportSelector 实现了 DeferredImportSelector,会在容器解析配置类时被延迟调用,读取所有 jar 包里登记的自动配置类候选列表(3.x 是 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports);这份候选列表是全量的,不管用没用到都会先列出来,真正决定哪些配置类生效的是 @Conditional 体系(@ConditionalOnClass/@ConditionalOnMissingBean/@ConditionalOnProperty 等)在解析 BeanDefinition 阶段做的逐个过滤;最终只有条件全部满足的自动配置类才会真正把它内部定义的 @Bean 注册进容器。
6.2 SpringBoot 2.x 和 3.x 自动装配配置文件的区别
参考答案骨架(对应 2.2 节):2.x 用 META-INF/spring.factories,一个 key=value 格式的大文件承载所有扩展点,自动配置类通过 EnableAutoConfiguration 这个 key 登记;3.x 把自动配置类候选列表拆成独立的 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件,每行一个类名,不再需要 key 和逗号分隔。原因是自动配置类是数量最多、变动最频繁的一类扩展点,和其他扩展点混在一个大文件里职责混杂、不利于维护,3.x 把它单独拆分出来,加载逻辑也更简单直接;其他数量较少的扩展点(如 ApplicationContextInitializer)3.x 仍保留在 spring.factories 里。实践影响:老 starter 升级到 3.x 时如果只在 spring.factories 里登记自动配置类而没有补充 AutoConfiguration.imports 文件,会导致自动配置类完全加载不到。
6.3 @ConditionalOnMissingBean 什么时候会踩坑
参考答案骨架(对应 3.3 节):@ConditionalOnMissingBean 依赖"判断那一刻容器里确实还没有这个 Bean"这个前提,这个前提由 AutoConfigurationImportSelector 实现 DeferredImportSelector(自动配置类整体上晚于用户普通配置类解析)来保证。如果用户自定义 Bean 的注册时机因为某种原因(比如自己也用了会被推迟处理的机制、或者配置类本身没被正常扫描到)晚于或恰好与自动配置类同批处理,@ConditionalOnMissingBean 判断时会误判为"缺失",导致自动配置的默认 Bean 抢先注册,用户自定义的 Bean 反而可能因为重复定义报错或被忽略。实践建议:自定义 Bean 老老实实放在能被 @ComponentScan 正常扫到的配置类里,不要引入额外的延迟加载机制去和自动配置类"抢跑道"。
6.4 SpringBoot 怎么内嵌 Tomcat
参考答案骨架(对应第四节):ServletWebServerApplicationContext 重写了 AbstractApplicationContext#refresh() 预留的 onRefresh() 钩子方法,在容器刷新过程中调用 createWebServer(),从容器里取出由 Web 相关自动配置类装配好的 ServletWebServerFactory 类型 Bean(如 TomcatServletWebServerFactory,按 classpath 上实际存在哪种容器实现来选择),调用其 getWebServer() 创建出内嵌的 Tomcat 对象,配置好 Connector/Context 并把 DispatcherServlet 注册进去,最后启动监听端口。和传统 war 包部署到外置容器的本质区别是:外置容器模式下容器进程独立存在、先于应用启动,应用只是被容器加载的产物;内嵌容器模式下 Tomcat 的核心类库是应用的一个普通依赖,容器实例的创建和生命周期完全被 Spring 容器管理,两者共生共灭,最终产物是一个可以 java -jar 直接运行的可执行 jar,不需要提前搭好容器环境,这也是 SpringBoot 天然契合微服务和容器化部署的原因之一。
6.5 自己写一个 starter 需要做哪些事
参考答案骨架(对应第五节):第一,新建一个模块,梳理清楚这个 starter 要引入的第三方依赖,锁定好兼容版本;第二,写自动配置类,用 @ConditionalOnClass 判断相关依赖是否存在、用 @ConditionalOnMissingBean 让路给用户自定义、用 @EnableConfigurationProperties 绑定一个强类型的配置项 Bean 承接 application.yml 里对应前缀的配置;第三,在 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 里登记这个自动配置类的全限定名(3.x 写法,2.x 要写在 spring.factories 里);第四,如果希望配置项在 IDE 里有自动补全提示,引入 spring-boot-configuration-processor 依赖,编译时会自动生成 spring-configuration-metadata.json(一般不需要手写)。命名习惯上,官方 starter 是 spring-boot-starter-xxx,第三方/自定义 starter 建议按官方推荐的 xxx-spring-boot-starter 命名以示区分。
结语:面试回答的通用框架
回答 SpringBoot 相关问题时,比较稳妥的表达顺序是:这个机制解决了什么具体问题 → 入口/触发点在哪里(哪个注解、哪个方法、哪个生命周期钩子)→ 中间经过了哪些关键步骤/关键类 → 最终产生了什么效果 → 有什么容易踩的坑或版本差异。比如被问"自动装配原理",不要只回答"SpringBoot 会自动帮你配置好常用的 Bean",而是完整讲出"解决的是减少样板配置代码的问题 → 入口是 @EnableAutoConfiguration 上的 @Import(AutoConfigurationImportSelector.class) → 中间经过全量候选列表加载(2.x/3.x 配置文件不同)和 @Conditional 条件过滤两个关键步骤 → 最终只有条件满足的自动配置类会把 Bean 注册进容器 → 常见坑是 @ConditionalOnMissingBean 依赖解析顺序,容易在自定义配置没能正确覆盖默认配置时排查很久"——这个"问题 → 入口 → 关键步骤 → 效果 → 坑/版本差异"的框架同样适用于启动流程、条件装配、内嵌容器、starter 机制等本文涉及的几乎所有 SpringBoot 面试题。

浙公网安备 33010602011771号