SpringBoot启动流程科普:一行代码背后的12步魔法!

难度:⭐⭐ | 适合人群:想轻松理解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启动流程有清晰的认识!

如果这篇文章对你有帮助,请:

  • 👍 点赞支持
  • ⭐ 收藏备用
  • 🔄 转发分享
  • 💬 评论交流

感谢阅读,期待下次再见! 👋