难度:⭐⭐ | 适合人群:想轻松理解SpringBoot启动原理的开发者
💥 开场:最简单的SpringBoot应用
时间: 某个周一早上
地点: 办公室
人物: 我,一个刚接触SpringBoot的新人
我看着屏幕上的代码:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
我: "就这?一行代码就能启动整个应用?" 🤔
哈吉米路过: "对啊,简单吧?"
我: "那传统Spring不是要写一堆XML配置吗?"
南北绿豆也凑过来: "是啊,以前要写几百行配置呢。"
我打开了传统Spring项目的配置:
<!-- web.xml -->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring-mvc.xml</param-value>
</init-param>
</servlet>
<!-- spring-mvc.xml -->
<context:component-scan base-package="com.example"/>
<mvc:annotation-driven/>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"/>
<property name="suffix" value=".jsp"/>
</bean>
<!-- applicationContext.xml -->
<bean id="dataSource" class="...">
<!-- 一堆配置 -->
</bean>
<bean id="sqlSessionFactory" class="...">
<!-- 一堆配置 -->
</bean>
<!-- 还有更多配置文件... -->
我: "天啊,这得写多久?" 😱
阿西噶阿西: "所以SpringBoot才这么火啊!它把这些都自动帮你做了。"
我: "那这一行代码背后到底做了什么?" 💡
哈吉米: "来,我给你讲讲这12步魔法..."
🎯 第一问:传统Spring vs SpringBoot
传统Spring启动步骤
1. 配置web.xml
2. 配置spring-mvc.xml
3. 配置applicationContext.xml
4. 配置数据源
5. 配置MyBatis
6. 配置事务管理器
7. 配置视图解析器
8. 部署到Tomcat
9. 启动Tomcat
10. 加载配置文件
11. 创建容器
12. 才能开始用
耗时: 半天到一天
配置文件: 几百行到上千行
心情: 😫 崩溃
SpringBoot启动步骤
// 1. 写一个类
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args); // 2. 一行代码
}
}
// 3. 运行main方法
// 4. 启动成功!
耗时: 3分钟
配置文件: 0行(或几行yaml)
心情: 😎 舒服
为什么这么简单?
南北绿豆: "因为SpringBoot做了三件事:"
1. 内嵌Tomcat
- 不需要单独部署
- 直接运行main方法
2. 自动配置
- 不需要写配置文件
- 根据依赖自动配置
3. 起步依赖
- 不需要管理版本
- 一个Starter搞定
这就是"约定大于配置"的魅力! ✨
🚀 第二问:SpringApplication.run()的12步魔法
整体流程图
main方法启动
↓
SpringApplication.run(Application.class, args)
↓
┌─────────────────────────────────────┐
│ 准备阶段(步骤1-5) │
│ ├─ 创建SpringApplication对象 │
│ ├─ 推断应用类型 │
│ ├─ 加载初始化器 │
│ ├─ 加载监听器 │
│ └─ 推断主启动类 │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 启动阶段(步骤6-12) │
│ ├─ 启动监听器 │
│ ├─ 准备环境变量 │
│ ├─ 打印Banner │
│ ├─ 创建容器 │
│ ├─ 准备容器 │
│ ├─ 刷新容器(自动配置) │
│ └─ 启动完成回调 │
└─────────────────────────────────────┘
↓
应用启动成功
步骤1-2:创建SpringApplication对象
// 这一行代码分两步
SpringApplication.run(Application.class, args);
↓
// 第一步:创建SpringApplication对象
SpringApplication app = new SpringApplication(Application.class);
// 第二步:调用run方法
app.run(args);
创建对象时做了什么?
阿西噶阿西: "主要做三件事。"
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
// 1. 推断应用类型
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 2. 加载初始化器(从spring.factories)
setInitializers(getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 3. 加载监听器(从spring.factories)
setListeners(getSpringFactoriesInstances(ApplicationListener.class));
// 4. 推断主启动类
this.mainApplicationClass = deduceMainApplicationClass();
}
推断应用类型:
// SpringBoot会自动判断是哪种应用
static WebApplicationType deduceFromClasspath() {
// 检查是否有WebFlux的类
if (ClassUtils.isPresent("org.springframework.web.reactive.DispatcherHandler", null)) {
return WebApplicationType.REACTIVE; // 响应式Web应用
}
// 检查是否有Servlet的类
if (ClassUtils.isPresent("org.springframework.web.servlet.DispatcherServlet", null)) {
return WebApplicationType.SERVLET; // 传统Web应用
}
return WebApplicationType.NONE; // 非Web应用
}
哈吉米: "SpringBoot会根据你的依赖自动判断应用类型!"
步骤3-5:加载扩展点
从哪里加载?
spring.factories文件:
# ApplicationContextInitializer
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\
...
# ApplicationListener
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
...
南北绿豆: "这和自动配置加载一样,都是从spring.factories读取!"
步骤6-7:启动监听器
public ConfigurableApplicationContext run(String... args) {
// 1. 创建计时器
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 2. 创建启动上下文
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
// 3. 配置Headless模式
configureHeadlessProperty();
// 4. 获取运行监听器
SpringApplicationRunListeners listeners = getRunListeners(args);
// 5. 发布启动事件
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
// ... 后续步骤
}
}
阿西噶阿西: "这时候会发布ApplicationStartingEvent事件,告诉大家'我要启动了'!"
步骤8:准备环境变量
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 准备Environment(环境变量)
ConfigurableEnvironment environment = prepareEnvironment(listeners,
bootstrapContext, applicationArguments);
// ...
}
Environment包含什么?
Environment
├─ 系统环境变量(System.getenv())
├─ 系统属性(System.getProperties())
├─ application.yml配置
├─ application.properties配置
├─ 命令行参数
└─ Profile配置
哈吉米: "所有配置在这一步加载完成!"
步骤9:打印Banner
// 打印Banner
Banner printedBanner = printBanner(environment);
就是启动时看到的这个:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.7.10)
南北绿豆: "这个可以自定义哦!"
src/main/resources/banner.txt
// 写点自己的:
=================================
欢迎使用我的SpringBoot应用
作者:XXX
=================================
步骤10:创建ApplicationContext
// 根据应用类型创建不同的容器
context = createApplicationContext();
创建哪种容器?
protected ConfigurableApplicationContext createApplicationContext() {
// 根据webApplicationType决定
switch (this.webApplicationType) {
case SERVLET:
return new AnnotationConfigServletWebServerApplicationContext(); // Web应用
case REACTIVE:
return new AnnotationConfigReactiveWebServerApplicationContext(); // 响应式
default:
return new AnnotationConfigApplicationContext(); // 普通应用
}
}
阿西噶阿西: "就是我们之前讲的ApplicationContext容器!"
步骤11:准备上下文
// 准备容器
prepareContext(bootstrapContext, context, environment, listeners,
applicationArguments, printedBanner);
做了什么?
1. 设置Environment
2. 执行ApplicationContextInitializer
3. 发布ApplicationContextInitializedEvent事件
4. 注册启动参数Bean
5. 注册Banner Bean
6. 加载源(主启动类)
7. 发布ApplicationPreparedEvent事件
哈吉米: "这一步把容器准备好,但Bean还没创建!"
步骤12:刷新上下文(核心!)
// 刷新容器(最重要的一步)
refreshContext(context);
这一步做什么?
南北绿豆: "这就是ApplicationContext的refresh()方法!还记得我们讲过的吗?"
refreshContext()
↓
调用 AbstractApplicationContext.refresh()
↓
12个子步骤:
1. 准备刷新
2. 获取BeanFactory
3. 准备BeanFactory
4. BeanFactory后置处理
5. 执行BeanFactoryPostProcessor
6. 注册BeanPostProcessor
7. 初始化国际化
8. 初始化事件广播器
9. 刷新(留给子类扩展)
10. 注册监听器
11. 实例化所有单例Bean ← 自动配置在这里!
12. 完成刷新
阿西噶阿西: "步骤11最关键!所有Bean在这里创建,自动配置在这里生效!"
🎨 第三问:关键时刻 - 自动配置加载
在哪一步加载的?
哈吉米: "在步骤12的刷新上下文中!"
refreshContext()
↓
AbstractApplicationContext.refresh()
↓
invokeBeanFactoryPostProcessors() ← 这一步
↓
处理@Configuration类
↓
处理@Import注解
↓
AutoConfigurationImportSelector.selectImports()
↓
读取spring.factories
↓
加载133个自动配置类
↓
条件注解过滤
↓
注册符合条件的Bean
南北绿豆: "还记得我们之前讲的自动配置原理吗?就是在这里生效的!"
整体时间线
步骤1-5:准备SpringApplication(瞬间)
↓
步骤6-9:准备环境(几十毫秒)
↓
步骤10:创建容器(瞬间)
↓
步骤11:准备容器(几十毫秒)
↓
步骤12:刷新容器(大部分时间)
├─ 加载自动配置
├─ 创建所有Bean
├─ 依赖注入
├─ AOP代理
└─ 初始化回调
↓
启动完成
阿西噶阿西: "90%的启动时间都在步骤12!"
📡 第四问:启动事件机制
7种启动事件
哈吉米: "SpringBoot启动过程会发布7种事件。"
1. ApplicationStartingEvent
- 时机:刚开始启动,环境还没准备
- 用途:最早期的初始化
2. ApplicationEnvironmentPreparedEvent
- 时机:环境准备好了,但容器还没创建
- 用途:修改环境配置
3. ApplicationContextInitializedEvent
- 时机:容器创建了,但Bean还没加载
- 用途:容器初始化
4. ApplicationPreparedEvent
- 时机:容器准备好了,但还没刷新
- 用途:刷新前的最后准备
5. ApplicationStartedEvent
- 时机:容器刷新完成,但Runner还没执行
- 用途:启动完成的通知
6. ApplicationReadyEvent
- 时机:所有准备完成,应用可以接收请求
- 用途:启动完全完成
7. ApplicationFailedEvent
- 时机:启动失败
- 用途:失败处理
监听器使用
@Component
public class MyApplicationListener implements ApplicationListener<ApplicationReadyEvent> {
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
System.out.println("=================================");
System.out.println(" 应用启动完成!");
System.out.println(" 启动耗时:" + event.getTimeTaken().toMillis() + "ms");
System.out.println("=================================");
}
}
输出:
=================================
应用启动完成!
启动耗时:2847ms
=================================
🔧 第五问:实用扩展点
CommandLineRunner
场景: 启动时执行一些初始化操作
@Component
public class DataInitRunner implements CommandLineRunner {
@Autowired
private UserService userService;
@Override
public void run(String... args) throws Exception {
System.out.println(">>> 开始初始化数据...");
// 检查是否有管理员账号
if (userService.findByUsername("admin") == null) {
User admin = new User("admin", "admin123");
userService.save(admin);
System.out.println(" 创建默认管理员账号");
}
System.out.println("<<< 数据初始化完成");
}
}
执行时机: 容器刷新完成后,ApplicationReadyEvent之前
ApplicationRunner
和CommandLineRunner类似,但参数更友好:
@Component
public class MyApplicationRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
// 获取命令行参数
System.out.println(">>> 命令行参数:");
// 获取选项参数(--key=value)
Set<String> optionNames = args.getOptionNames();
for (String name : optionNames) {
System.out.println(" --" + name + "=" + args.getOptionValues(name));
}
// 获取非选项参数
List<String> nonOptionArgs = args.getNonOptionArgs();
System.out.println(" 其他参数:" + nonOptionArgs);
}
}
启动时传参:
java -jar app.jar --server.port=8081 --debug=true arg1 arg2
输出:
>>> 命令行参数:
--server.port=[8081]
--debug=[true]
其他参数:[arg1, arg2]
多个Runner的执行顺序
@Component
@Order(1) // 优先级高,先执行
public class FirstRunner implements CommandLineRunner {
@Override
public void run(String... args) {
System.out.println("1. FirstRunner执行");
}
}
@Component
@Order(2)
public class SecondRunner implements CommandLineRunner {
@Override
public void run(String... args) {
System.out.println("2. SecondRunner执行");
}
}
输出:
1. FirstRunner执行
2. SecondRunner执行
🐛 第六问:常见问题
问题1:启动慢怎么办?
排查方法:
// 1. 开启启动日志
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(Application.class);
app.setLogStartupInfo(true); // 打印启动信息
app.run(args);
}
}
查看哪个步骤慢:
Started Application in 5.234 seconds (JVM running for 6.123)
// 具体耗时分布
Creating beans: 3.5s ← 大部分时间在创建Bean
Loading auto-configuration: 1.2s
Other: 0.5s
优化方案:
方案1:懒加载
spring:
main:
lazy-initialization: true # 启用懒加载
效果:
- 启动快(不创建Bean)
- 首次访问慢(这时才创建)
方案2:排除不需要的自动配置
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class, // 不用数据库
RedisAutoConfiguration.class // 不用Redis
})
public class Application {
}
方案3:组件扫描优化
// ❌ 不推荐:扫描范围太大
@SpringBootApplication
@ComponentScan("com") // 扫描整个com包
// ✅ 推荐:精确扫描
@SpringBootApplication
@ComponentScan("com.example.myapp") // 只扫描自己的包
问题2:启动失败怎么排查?
看日志:
***************************
APPLICATION FAILED TO START
***************************
Description:
Failed to configure a DataSource: 'url' attribute is not specified and no embedded
datasource could be configured.
Reason: Failed to determine a suitable driver class
Action:
Consider the following:
If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.
If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).
哈吉米: "SpringBoot的错误信息很友好,会告诉你:**
Description:问题描述
Reason:失败原因
Action:建议的解决方案
常见失败原因:
1. 缺少依赖
- 解决:检查pom.xml
2. 配置错误
- 解决:检查application.yml
3. 端口被占用
- 解决:换端口或kill占用进程
4. Bean循环依赖
- 解决:参考循环依赖文章
5. 自动配置冲突
- 解决:排除冲突的自动配置
问题3:如何自定义启动过程?
自定义Banner:
src/main/resources/banner.txt
=================================
我的SpringBoot应用
Version: 1.0.0
Author: XXX
=================================
自定义初始化器:
public class MyInitializer implements ApplicationContextInitializer {
@Override
public void initialize(ConfigurableApplicationContext context) {
System.out.println(">>> 自定义初始化器执行");
// 可以修改容器配置
}
}
// 注册方式1:spring.factories
org.springframework.context.ApplicationContextInitializer=\
com.example.MyInitializer
// 注册方式2:代码注册
SpringApplication app = new SpringApplication(Application.class);
app.addInitializers(new MyInitializer());
app.run(args);
自定义监听器:
public class MyListener implements ApplicationListener<ApplicationStartedEvent> {
@Override
public void onApplicationEvent(ApplicationStartedEvent event) {
System.out.println(">>> 自定义监听器:应用启动完成");
}
}
// 注册(同上)
💡 知识点总结
SpringBoot启动核心要点
✅ 一行代码,12步魔法
- SpringApplication.run()
- 准备阶段(1-5步)
- 启动阶段(6-12步)
✅ 自动推断
- 应用类型(Web/Reactive/None)
- 主启动类
✅ 自动加载
- 初始化器(Initializer)
- 监听器(Listener)
- 自动配置类(AutoConfiguration)
✅ 核心步骤
- 步骤8:准备环境变量
- 步骤10:创建容器
- 步骤12:刷新容器(Bean创建、自动配置)
✅ 事件机制
- 7种启动事件
- 监听器监听事件
- 可以在各个阶段插入逻辑
✅ 扩展点
- ApplicationContextInitializer
- ApplicationListener
- CommandLineRunner
- ApplicationRunner
启动流程简化版
1. new SpringApplication() - 创建启动器
├─ 推断应用类型
├─ 加载初始化器
└─ 加载监听器
2. run() - 执行启动
├─ 发布starting事件
├─ 准备环境(加载配置)
├─ 打印Banner
├─ 创建容器
├─ 准备容器
├─ 刷新容器
│ ├─ 加载自动配置
│ ├─ 创建Bean
│ └─ 依赖注入
├─ 执行Runner
└─ 发布ready事件
3. 启动完成
记忆口诀
一行代码启动Boot,
十二步骤要记牢。
创建对象推断类型,
加载扩展和监听。
准备环境打横幅,
创建容器来刷新。
自动配置这时加,
所有Bean都创建。
Runner执行做初始化,
发布事件告完成。
🤔 面试常考
Q1: SpringBoot启动流程?
A:
简化版回答:
1. 创建SpringApplication对象(加载初始化器和监听器)
2. 执行run方法
3. 准备环境(加载配置)
4. 创建并刷新ApplicationContext
5. 在刷新过程中加载自动配置,创建Bean
6. 执行CommandLineRunner
7. 发布ApplicationReadyEvent
8. 启动完成
详细版可以说12步骤。
Q2: 自动配置在哪一步加载?
A:
在refreshContext()的invokeBeanFactoryPostProcessors()阶段:
1. 处理@Configuration类
2. 处理@Import注解
3. AutoConfigurationImportSelector.selectImports()
4. 读取spring.factories
5. 加载自动配置类
6. 条件注解过滤
7. 注册Bean
Q3: CommandLineRunner和ApplicationRunner的区别?
A:
相同点:
- 都在容器刷新完成后执行
- 都可以获取启动参数
不同点:
- CommandLineRunner:参数是String[]
- ApplicationRunner:参数是ApplicationArguments(更友好)
推荐使用ApplicationRunner。
💬 写在最后
从一行代码到12步魔法,我们轻松理解了SpringBoot的启动流程:
- 🪄 理解了启动的12个步骤
- 📡 掌握了7种启动事件
- 🔧 学会了扩展点的使用
- 🐛 知道了常见问题的排查
这篇科普文,希望能让你对SpringBoot启动流程有清晰的认识!
如果这篇文章对你有帮助,请:
- 👍 点赞支持
- ⭐ 收藏备用
- 🔄 转发分享
- 💬 评论交流
感谢阅读,期待下次再见! 👋