1. Spring Boot启动流程的常见误区
很多人以为Spring Boot的启动流程就是简单的"加载配置→创建上下文→刷新上下文→启动内嵌容器→运行应用"这五个步骤。这种认知在面试中往往会被面试官连环追问到哑口无言。实际上,每个步骤背后都隐藏着复杂的机制和设计考量。
我在面试候选人时发现,90%的人对SpringApplication.run()的理解停留在表面。他们不知道这个方法内部会先创建SpringApplication实例,再调用其run方法。更关键的是,很少有人能说清楚SpringApplication构造函数中执行的deduceWebApplicationType()和getSpringFactoriesInstances()这两个关键操作的意义。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 启动流程的五个核心阶段详解
2.1 准备阶段:SpringApplication实例化
在new SpringApplication(primarySources)这一步,Spring Boot会做三件重要事情:
-
推断Web应用类型:通过
WebApplicationType.deduceFromClasspath()判断是Servlet应用(Spring MVC)、Reactive应用(WebFlux)还是非Web应用。这个判断基于类路径下是否存在特定的类,比如如果同时存在javax.servlet.Servlet和org.springframework.web.reactive.DispatcherHandler,会优先判断为Reactive应用。 -
加载ApplicationContextInitializer:通过
getSpringFactoriesInstances从META-INF/spring.factories加载所有配置的初始化器。这里有个坑:初始化器的执行顺序取决于它们在文件中的声明顺序,但不同jar包中的spring.factories加载顺序是不确定的。 -
加载ApplicationListener:同样通过
spring.factories机制加载应用监听器。这些监听器会在后续的启动事件中被触发。
提示:自定义初始化器时,可以通过
@Order注解或实现Ordered接口来控制执行顺序,避免依赖文件声明顺序。
2.2 运行阶段:SpringApplication.run()
run()方法才是真正的启动入口,它的核心流程如下:
java复制public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<Exception> exceptions = new ArrayList<>();
configureHeadlessProperty();
// 1. 获取并启动监听器
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
// 2. 准备环境
ConfigurableEnvironment environment = prepareEnvironment(listeners, args);
configureIgnoreBeanInfo(environment);
// 3. 打印Banner
Banner printedBanner = printBanner(environment);
// 4. 创建应用上下文
context = createApplicationContext();
// 5. 准备上下文
prepareContext(context, environment, listeners, printedBanner);
// 6. 刷新上下文
refreshContext(context);
// 7. 刷新后处理
afterRefresh(context, args);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass)
.logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
// 8. 执行Runner
callRunners(context, args);
}
catch (Throwable ex) {
handleRunFailure(context, listeners, exceptions, ex);
throw new IllegalStateException(ex);
}
listeners.running(context);
return context;
}
2.3 环境准备:不只是读取配置文件
prepareEnvironment()方法远比想象中复杂:
-
创建环境对象:根据应用类型创建
StandardServletEnvironment、StandardReactiveWebEnvironment或StandardEnvironment -
配置PropertySources:按顺序添加以下配置源:
- 系统属性(System.getProperties())
- 系统环境变量(System.getenv())
- 随机属性(random.*)
- 应用配置文件(application.properties/yml)
- 其他
PropertySourceLoader加载的配置
-
处理Profile:解析
spring.profiles.active和spring.profiles.include,激活对应的Profile配置
常见坑点:当多个配置源包含相同属性时,后加载的会覆盖先加载的。我曾遇到过一个案例:系统环境变量意外覆盖了应用配置,导致生产环境连上了测试数据库。
2.4 上下文创建与准备
createApplicationContext()根据Web应用类型创建不同的应用上下文:
- Servlet应用:
AnnotationConfigServletWebServerApplicationContext - Reactive应用:
AnnotationConfigReactiveWebServerApplicationContext - 非Web应用:
AnnotationConfigApplicationContext
prepareContext()方法会:
- 将环境绑定到上下文
- 后置处理Bean名称生成器
- 初始化Bean定义读取器
- 执行所有
ApplicationContextInitializer的initialize方法 - 发布
ApplicationContextInitializedEvent事件
2.5 上下文刷新:最复杂的阶段
refreshContext()最终会调用AbstractApplicationContext.refresh(),这是整个Spring框架的核心方法。它包含12个关键步骤:
- 准备刷新:初始化启动时间、活跃状态标志
- 获取BeanFactory:创建或刷新内部的
DefaultListableBeanFactory - 准备BeanFactory:配置标准BeanFactory特性(类加载器、EL解析器等)
- 后置处理BeanFactory:执行所有
BeanFactoryPostProcessor - 注册BeanPostProcessor:找到并注册所有
BeanPostProcessor - 初始化MessageSource:国际化支持
- 初始化事件广播器:
ApplicationEventMulticaster - 初始化特殊Bean:留给子类实现
- 注册监听器:找到所有
ApplicationListener并注册 - 完成BeanFactory初始化:初始化所有非懒加载的单例Bean
- 完成刷新:发布
ContextRefreshedEvent - 销毁临时Bean:清理缓存的Bean元数据
3. 面试中常见的连环追问
3.1 关于自动配置的深度问题
"Spring Boot是如何实现自动配置的?"这个基础问题后,通常会接着问:
-
@EnableAutoConfiguration背后的@Import(AutoConfigurationImportSelector.class)是如何工作的?- 它会从
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports加载所有自动配置类 - 通过
AutoConfigurationImportSelector.getCandidateConfigurations()实现
- 它会从
-
自动配置类上的
@Conditional系列注解是如何起作用的?- 核心是
ConditionEvaluator类,它会在解析配置类时评估所有条件注解 - 常见的条件注解包括:
@ConditionalOnClass:类路径下存在指定类@ConditionalOnMissingBean:容器中不存在指定类型的Bean@ConditionalOnProperty:配置属性满足条件
- 核心是
-
如何自定义自动配置且确保它在官方自动配置之后加载?
- 在
src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports中定义自己的自动配置类 - 使用
@AutoConfigureAfter或@AutoConfigureBefore指定顺序
- 在
3.2 内嵌容器启动流程
"Tomcat是如何被内嵌启动的?"这个问题可以拆解为:
-
ServletWebServerFactoryAutoConfiguration如何提供TomcatServletWebServerFactory?- 它通过
@ConditionalOnClass检查Servlet和Tomcat类的存在 - 通过
@ConditionalOnMissingBean确保没有其他ServletWebServerFactory时才会生效
- 它通过
-
内嵌Tomcat的初始化过程:
TomcatServletWebServerFactory.getWebServer()创建Tomcat实例- 配置Connector(默认监听8080端口)
- 添加
TomcatStarter作为ServletContainerInitializer - 启动Tomcat线程
-
如何自定义内嵌容器配置?
- 通过
server.*属性配置(如server.port=8081) - 自定义
WebServerFactoryCustomizerBean - 完全替换
ServletWebServerFactory实现
- 通过
3.3 启动性能优化相关
"如何优化Spring Boot应用的启动速度?"可以考察:
-
延迟初始化:
spring.main.lazy-initialization=true- 优点:减少启动时间
- 缺点:可能导致首次请求延迟
-
排除不必要的自动配置:
@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})- 需要精确知道哪些自动配置可以安全排除
-
使用AOT(Ahead-Of-Time)编译:
- Spring Native项目
- 通过GraalVM生成原生镜像
- 显著减少启动时间和内存占用
-
优化组件扫描:
- 使用明确的
@ComponentScan路径 - 避免扫描过大的包范围
- 使用明确的
4. 实际案例:诊断启动问题
去年我遇到一个典型的启动问题:应用在生产环境启动时间从正常的15秒突然增加到2分钟。通过以下步骤最终定位到问题:
-
添加
--debug参数启动,查看自动配置报告- 发现大量未使用的自动配置类被加载
-
使用
SpringApplication.setBannerMode(Banner.Mode.OFF)禁用Banner- 节省了约500ms
-
通过
Logger.getLogger("org.springframework.boot")设置DEBUG日志- 发现
ConfigurationClassPostProcessor耗时异常
- 发现
-
最终定位到原因:一个新引入的库包含了数百个
@Configuration类,且这些类都使用了@ComponentScan- 解决方案:在该库的配置类上使用明确的
@ComponentScan路径
- 解决方案:在该库的配置类上使用明确的
这个案例教会我:启动性能问题往往不是单一原因造成的,需要系统性地排查各个可能的影响因素。
