Spring Boot 的启动原理是什么?

非常非常好的问题,别看Spring Boot就用一行run方法就启动了,其实内部背后执行了一整套复杂的启动流程。

当年我第一次接触Spring Boot的时候,第一个疑问就是:

Tomcat跑哪儿去了?

怎么不需要配置Tomcat就能把java web程序启动起来了? 当时真觉得很神奇。像我这种老程序员,当时写java web程序时,老麻烦了:

  • 要先下载Tomcat包,解压后配置service.xml文件;
  • 把Tomcat的路径配置在Intellij Idea里;
  • 编写完java代码后,点击run按钮,将代码部署到Tomcat里去;

跟我同一个时代的程序员,当时应该都有一个想法,要是Tomcat是内置的就好了。想不到若干年后,Spring Boot做了这个事情。

我们可以先从【Tomcat哪儿去了】这个问题入手,慢慢铺开来讲。


问题 :Spring Boot 为什么不需要外置的Tomcat?

Spring Boot想把Tomcat内嵌进来并启动,是不能靠自己的,得靠Tomcat自身,就是说Tomcat得自己先去做到一件事:

支持使用方可以通过new的方式,将自己启动起来。

也就是说,容器应该支持从独立进程 变成了可编程对象,直接在代码里启动。事实上在Tomcat 7的时候,内部多了一个叫org.apache.catalina.startup.Tomcat的类,就是用来支持内嵌启动的:

java 复制代码
Tomcat tomcat = new Tomcat();
tomcat.setPort(8080);
tomcat.start();
tomcat.getServer().await();

Spring Boot就是用了这个类,将Tomcat里在代码里直接启动的。当Tomcat提供了这样的能力后,Spring Boot就只需要再做一件事就可以了。就是把Tomcat当成jar包引入进来就可以了。

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

这个 starter 里直接包含了:

xml 复制代码
 <!-- 内嵌 Tomcat -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId> 
</dependency>

而spring-boot-starter-tomcat会把真正的Tomcat jar包tomcat-embed-core.jar引入进来的。

但是由于还有像Jetty和Undertow等容器,因此这块,必须做统一的封装,这个就是ServletWebServerFactory接口的作用。

Spring Boot通过ServletWebServerFactory接口,将内嵌集成Web容器的方式标准化了。

那么到这里,我们可以开始讲代码细节了吗? 不行,因为还差一个上帝的视角。如果你一下子就进入代码细节,很容易迷失方向的。

要理解Spring Boot的启动流程,是得有一些前置的基础知识的。

理解Spring Boot启动流程的前置知识点

Spring Boot(我讲解的是2.7版本)的启动流程本质上是一场精密编排的自动化构建过程。要真正理解它,你需要先理解以下这四个核心概念。

第一个核心机制:Java SPI 与spring.factories

Spring Boot的零配置,始于一套增强版的 Java SPI (Service Provider Interface) 机制。它要解决的问题是:

Spring Boot 怎么知道要加载哪些类?配置都藏在哪?

JDK 原生的 SPI 是查找META-INF/services,而 Spring Boot 2.7 的核心通过SpringFactoriesLoader类,专门扫描所有 jar 包下的META-INF/spring.factories文件。这是一个巨大的清单文件。在spring-boot-autoconfigure包里,这个文件列出了非常多的自动配置类(如RedisAutoConfigurationJdbcTemplateAutoConfiguration)。

那知道这个对理解启动流程有什么作用呢?

有的,因为启动的要处理的第一件事情就是搜集信息 。Boot 会读取这些文件,把所有潜在的扩展点(监听器、初始化器、自动配置类)都加载到内存中备用。

第二个核心机制:按需装配和约定优于配置

在使用SPI把相关的配置类 加载进来后,还需要执行一个按需装配的逻辑。这个是为了解决如下的一个问题:

加载了那么多的配置类,如果全部都启动,会有卡死和冲突的问题。

一定是要过滤 一下的。我们用非常常见的**@ConditionalOnClass @ConditionalOnMissingBean**来说明。

**@ConditionalOnClass的意思是类存在,我才生效;类不存在,我装作没看见。**比如:

java 复制代码
@ConditionalOnClass(RedisTemplate.class)
@Configuration
public class RedisAutoConfiguration {
    ...
}

意思是,只有当你的项目依赖里有RedisTemplate这个类,我这份 Redis 自动配置才会加载。

Boot是通用框架,不知道你到底要不要用Redis,因此只有条件满足了,才会去加载的。这就是按需装配。

而**@ConditionalOnMissingBean**呢,则是说,你没配,我帮你配,你配了,则听你的。比如:

java 复制代码
@ConditionalOnMissingBean(DataSource.class)
@Bean
public DataSource dataSource() {
    ...
}

意思是,如果容器里还没有DataSource ,我就帮你创建一个默认的,但是如果你自己配置了:

java 复制代码
@Bean
public DataSource myDataSource() {
    ...
}

则不创建了。这就是约定优于配置。

第三个核心机制:观察者模式与生命周期

这个是为了解决如下的问题:

启动流程这么长,如何解耦各个阶段?又如何干预?

你千万不要以为Boot的启动流程是流水账一样的,它其实更像是:

每到一个阶段,就发一个事件, 谁关心这个事件,谁就出来干活。

这就是设计模式中的观察者模式

而其中最关键的就是【阶段性广播员】EventPublishingRunListener启动过程中的每个阶段 ,都是它负责去广播,发布事件,而这个时候,所有注册的监听器都会收到。注意,这里是所有的监听器都会收到阶段性事件的,但只有监听这个事件类型的监听器会执行

那启动过程中到底有哪些关键的阶段呢? 如下:

  • ApplicationStartingEvent(启动开始,还没开始干活)
  • ApplicationEnvironmentPreparedEvent(环境准备好了,配置读进去了)
  • ApplicationContextInitializedEvent(容器创建了,初始化器执行了)
  • ApplicationPreparedEvent(Bean 定义加载了,但还没实例化)
  • ApplicationStartedEvent(启动完成,可以对外服务了)

上面我写的这段一定要理解哈,因为源码里充满了multicastEvent()。如果你不理解这个机制,就会觉得代码非常跳跃,明明在这里执行,怎么突然跑到那个类里去了?理解了事件,你才能把复杂的长流程切割成**阶段,**然后按照阶段去理解,就相对清晰很多了。

最后终于可以启动Spring的容器了

Spring Boot 复杂的启动流程中,最终的目的就是为了启动一个 Spring 的ApplicationContext。这里一定要理解好,Spring Boot底层也是基于Spring的,因此你可以简单如下理解:

Spring Boot本质上只是帮你准备好一切,最终真正干活的,还是Spring Framework。

最终就是想通过调用Spring中ApplicationContext的refresh方法,将Spring容器彻底激活。好,到了这里,可以用最简洁的语言来描述Spring Boot的启动流程了。

  • 构建 Environment ;
  • 创建合适的 ApplicationContext;
  • 调用 refresh()。

从源代码的角度来分析启动流程了

注意,由于篇幅问题,我这里只会列出入口的部分,你顺着入口一步一步看就可以了。只要你用我上面介绍的知识,我相信你再次去分析SpringApplication.run()里底层实现,就不会那么晕了。

在此之前,我先用一张整体的架构图,帮你建立整体的架构认知。

核心就两步:

  1. 构造阶段:推断应用类型、加载扩展组件;
  2. 运行阶段:准备环境、创建容器、刷新容器、启动完成。

SpringApplication.run()对应的代码入口在这里:

java 复制代码
public ConfigurableApplicationContext run(String... args) {
    // 1. 触发启动事件
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting(bootstrapContext, this.mainApplicationClass);
    
    try {
        // 2. 准备环境(加载 application.properties/yml)
        ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
        
        // 3. 创建 ApplicationContext(根据应用类型创建不同的上下文)
        context = createApplicationContext();
        
        // 4. 准备上下文(注册启动类、执行初始化器)
        prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
        
        // 5. 刷新上下文(核心!启动 Tomcat、实例化 Bean)
        refreshContext(context);
        
        // 6. 触发启动完成事件
        listeners.started(context, timeTakenToStartup);
        
        // 7. 执行 CommandLineRunner/ApplicationRunner
        callRunners(context, applicationArguments);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, listeners);
    }
    
    return context;
}

其中的refreshContext(context),就是 Tomcat 真正启动的地方,如果你对里面的代码感兴趣,可以去看ServletWebServerApplicationContext里面的onRefresh方法实现就可以了。

java 复制代码
@Override
protected void onRefresh() {
    super.onRefresh();
    try {
        createWebServer();  // 创建 Web 容器
    }
    catch (Throwable ex) {
        throw new ApplicationContextException("Unable to start web server", ex);
    }
}

如果你是第一次看这段代码,会有个疑问,里面的ServletWebServerFactory是哪来的? 这个就是我们上面提到的自动装配和按需装配的思想,从ServletWebServerFactoryConfiguration得到的。并结合ConditionalOnClass和ConditionalOnMissingBean以及EnableAutoConfiguration注解,以及AutoConfigurationImportSelector类来完成的。

你只需要顺着SpringApplication.run()的入口,结合我们上面的前置知识,大概看个几周时间,基本就能记住一些细节和了解到一些设计方法和设计思想了。

写在最后

Spring Boot 的启动流程看起来复杂,实际上核心思路很清晰:

  1. 推断应用类型(SERVLET/xxxxxxx)
  2. 加载自动配置
  3. 创建 ApplicationContext(根据类型创建不同的上下文)
  4. 刷新容器 (在 onRefresh() 钩子中启动 Tomcat)
  5. 触发启动完成事件(执行用户自定义任务)

Spring Boot 能把传统 SSH 项目的部署方式从war 包 + 外置容器简化到jar 包直接运行,核心就是这套 自动配置 + 内嵌容器 的架构设计。

2014 年 Spring Boot刚发布的时候,很多人觉得这不就是把Tomcat 打成 jar 包吗。但真正理解了启动原理后你会发现,这背后是一整套精巧的设计:

  • SPI 机制的扩展点设计;
  • 观察者模式的使用;
  • 条件注解的按需加载;
  • 工厂模式的容器抽象;

多读源代码,对编码水平和技术设计能力水平的提升,是有好处的。


最近在知乎出了

  • 「应付6000万会员的秒杀系统专栏」
  • 「几亿用户,百万并发的C端商品系统实战」
  • 「技术团队DDD领域驱动设计三年落地实战」
  • 「应付亿级用户规模的支付系统代码实战」
  • 「应付亿级用户的会员体系代码实战」

专栏,感兴趣的可以订阅一下。至于知识星球的,可以搜:

  • 老码头的技术浮生录

它是一个能实际帮你解决难题的星球。有问题的,找知心的Sam哥,支持无限次语音一对一解决你遇到的难题。「另外后续我新写的所有对外的付费专栏,在星球内都是免费的,且可以拿到所有源代码。」

当前星球里免费看的专栏是:

  • 「应付6000万会员的秒杀系统专栏」
  • 「几亿用户,百万并发的C端商品系统实战」
  • 「技术团队DDD领域驱动设计三年落地实战」
  • 「应付亿级用户规模的支付系统代码实战」
  • 「应付亿级用户的会员体系代码实战」

知识星球内后续将推出20+个付费专栏,覆盖电商全链路:

选购线 用户会员营销线 中后台
购物车服务 营销系统 订单系统
商品服务 用户系统 支付系统
菜单服务 结算服务

从前台选购到中后台结算,星球成员全部免费,后续新增也不额外收费。

我的知乎账号:

  • SamDeepThinking
相关推荐
南部余额1 小时前
Spring WebClient 从入门到精通
java·后端·spring
CodeStats1 小时前
从 CPU 指令到 JVM 进程:彻底讲透 Java 执行 main 方法时,类加载、主线程、栈帧入栈的完整底层逻辑
java·linux·开发语言
摇滚侠1 小时前
Spring 零基础入门到进阶 基于注解管理 Bean 38-43
xml·java·后端·spring·intellij-idea
SamDeepThinking1 小时前
我们当年是如何真实落地BFF的?
java·后端·架构
码语智行2 小时前
Shapefile获取空间数据和中心点坐标
java·arcgis
caoyc2 小时前
RAG 赛道全景扫描:ragflow 一骑绝尘、微软谷歌跟进乏力、下半场属于 Agent
java
Asmewill2 小时前
Centos系统docker时间同步方案
后端
屋外雨大,惊蛰出没2 小时前
深入浅出Spring Boot
java·spring boot·ioc·aop