问题引入
2024 年 Q2,某电商公司的订单服务在容器化改造后,启动时间从 12 秒飙升到 38 秒。Kubernetes 的滚动更新策略要求 maxUnavailable=0,这意味着每次发布需要先启动新 Pod、通过健康检查、再下线旧 Pod。38 秒的启动时间让一次简单的发布要持续 15 分钟,开发团队苦不堪言。
排查过程充满了困惑:
- 有人怀疑是容器资源限制,加了 CPU 和内存,启动时间只降了 2 秒
- 有人怀疑是网络问题,检查了 DNS 和镜像拉取,都不是瓶颈
- 有人在
@PostConstruct里加了System.currentTimeMillis()打点,发现某个 Bean 的初始化占了 8 秒——但它明明只是从数据库加载了一些配置 - 最终发现,自动配置加载了 147 个配置类,其中 30 多个根本不需要,只是因为在类路径上而已
SpringBoot 的 main 方法里只有一行代码:
java
SpringApplication.run(Application.class, args);
但这行代码背后,SpringBoot 完成了应用类型推断、环境准备、容器创建、自动配置加载、单例 Bean 实例化、内嵌服务器启动等数十个步骤。面试追问下去:
SpringApplication.run()到底做了什么?每一步的输入和输出是什么?Environment是怎么组装出来的?为什么命令行参数能覆盖application.yml?- 自动配置在启动的哪个阶段加载?内嵌 Tomcat 又是在哪个阶段启动的?
ApplicationRunner和CommandLineRunner有什么区别?执行时机是什么时候?- 如果启动耗时过长,应该从哪里入手排查?
本文从 SpringApplication 的构造到 ApplicationReadyEvent 的发布,穿透启动流程的每一个环节。
核心概念
1. SpringApplication 的角色
SpringApplication 是 SpringBoot 应用的启动引导类,它不是 Spring Framework 的一部分,而是 SpringBoot 对 Spring 容器启动过程的封装和编排。
java
// SpringApplication 的核心职责
public class SpringApplication {
// 1. 推断应用类型(Servlet / Reactive / None)
private WebApplicationType webApplicationType;
// 2. 启动前的扩展点
private List<ApplicationContextInitializer<?>> initializers;
private List<ApplicationListener<?>> listeners;
// 3. 主类(用于包扫描和配置推断)
private Class<?> mainApplicationClass;
// 4. 启动入口
public ConfigurableApplicationContext run(String... args) {
// ... 九步启动流程
}
}
可以把 SpringApplication 理解为一场音乐会的指挥家——它自己不演奏乐器,但负责确定演出类型(交响乐团还是室内乐)、调音(准备环境)、安排乐手就位(创建容器)、指挥演奏(刷新上下文)。
2. 应用类型推断
SpringBoot 2.0 引入了 WebApplicationType 枚举,根据类路径推断应用类型:
java
public enum WebApplicationType {
NONE, // 非 Web 应用(批处理、定时任务)
SERVLET, // 传统 Servlet Web 应用(Tomcat/Jetty/Undertow)
REACTIVE // 响应式 Web 应用(Netty)
}
推断逻辑(WebApplicationType.deduceFromClasspath()):
类路径存在 reactor.core.publisher.Flux
AND 存在 org.springframework.web.reactive.DispatcherHandler
AND 不存在 org.springframework.web.servlet.DispatcherServlet
→ REACTIVE
类路径存在 javax.servlet.Servlet(或 jakarta.servlet.Servlet)
AND 存在 org.springframework.web.context.ConfigurableWebApplicationContext
→ SERVLET
否则 → NONE
读图导引:推断顺序是先检查 Reactive 再检查 Servlet。如果类路径上同时有 Spring MVC 和 Spring WebFlux,SpringBoot 会优先判定为 SERVLET 类型(因为 DispatcherServlet 的存在),除非显式排除 spring-webmvc。
3. ApplicationContextInitializer 与 ApplicationListener
这是 SpringBoot 留给开发者的两个核心扩展点:
| 扩展点 | 触发时机 | 用途 |
|---|---|---|
ApplicationContextInitializer |
Context 创建后、刷新前 | 修改 Context 的初始状态(如注册自定义的 BeanDefinition、修改 Environment) |
ApplicationListener |
启动全生命周期 | 监听 Spring 事件(EnvironmentPreparedEvent、ContextRefreshedEvent 等) |
java
// 自定义 Initializer:在刷新前修改 Environment
public class CustomInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext context) {
ConfigurableEnvironment env = context.getEnvironment();
env.getPropertySources().addFirst(new MapPropertySource("custom",
Collections.singletonMap("custom.key", "customValue")));
}
}
注册方式:在 META-INF/spring.factories 或 META-INF/spring/org.springframework.context.ApplicationContextInitializer.imports 中声明:
org.springframework.context.ApplicationContextInitializer=\
com.example.CustomInitializer
4. Environment 与 PropertySource
Environment 是 Spring 的属性源抽象,内部包含多个 PropertySource,按优先级排列:
PropertySources(按优先级从高到低)
├── ServletConfigPropertySource // Servlet 配置
├── ServletContextPropertySource // ServletContext 参数
├── JndiPropertySource // JNDI 属性
├── MapPropertySource (systemProperties) // System.getProperties()
├── SystemEnvironmentPropertySource // System.getenv()
├── RandomValuePropertySource // random.*
├── OriginTrackedMapPropertySource (application-dev.yml)
├── OriginTrackedMapPropertySource (application.yml)
└── MapPropertySource (defaultProperties)
读图导引:高优先级的 PropertySource 会覆盖低优先级的同名属性。PropertySourcesPropertyResolver 在解析属性时,按顺序遍历 PropertySource 列表,第一个匹配的键值即为结果。
5. SpringApplicationRunListener
这是 SpringBoot 特有的启动监听器接口,比 ApplicationListener 更贴近启动流程:
java
public interface SpringApplicationRunListener {
void starting(); // 启动开始时
void environmentPrepared(ConfigurableEnvironment environment); // Environment 准备完成
void contextPrepared(ConfigurableApplicationContext context); // Context 创建完成
void contextLoaded(ConfigurableApplicationContext context); // Context 加载完成
void started(ConfigurableApplicationContext context); // 启动完成
void running(ConfigurableApplicationContext context); // 运行中
void failed(ConfigurableApplicationContext context, Throwable exception); // 启动失败
}
事件序列:
starting() ──▶ environmentPrepared() ──▶ contextPrepared() ──▶ contextLoaded() ──▶ started() ──▶ running()
│ │
└── 发布 ApplicationStartingEvent └── 发布 ApplicationStartedEvent
└── 发布 ApplicationReadyEvent
读图导引:SpringApplicationRunListener 是 SpringBoot 启动的"里程碑"系统,每个方法对应一个确定的启动阶段。开发者可以在特定阶段插入自定义逻辑(如在 environmentPrepared 时动态修改配置)。
6. ApplicationRunner vs CommandLineRunner
两者都是在容器启动完成后执行的回调接口,用于执行初始化逻辑:
| 对比维度 | ApplicationRunner | CommandLineRunner |
|---|---|---|
| 参数类型 | ApplicationArguments(结构化参数) |
String... args(原始参数数组) |
| 参数解析 | 自动解析 --key=value 和 --flag |
不解析,原始字符串 |
| 执行时机 | 都在 refreshContext 之后、running 之前 |
同上 |
| 排序方式 | @Order 或 Ordered 接口 |
@Order 或 Ordered 接口 |
| 执行顺序 | 所有 Runner 按 @Order 排序后先执行 ApplicationRunner,再执行 CommandLineRunner |
同上 |
java
@Component
@Order(1)
public class CacheWarmUpRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
// 解析 --warmup.cache=users,orders
List<String> caches = args.getOptionValues("warmup.cache");
// 预热缓存...
}
}
原理分析
1. 启动全景:九步流程
读图导引:粉色节点是启动流程的核心——refresh Context 阶段承载了自动配置加载、Bean 实例化和内嵌服务器启动三大重任。黄色节点 onRefresh 是 Web 应用独有的扩展点,非 Web 应用跳过此步。
2. Step 1:构造 SpringApplication
java
public SpringApplication(Class<?>... primarySources) {
// 1. 保存主配置类
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// 2. 推断应用类型
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 3. 加载所有 ApplicationContextInitializer
// 从 META-INF/spring.factories 读取 key=org.springframework.context.ApplicationContextInitializer
setInitializers((Collection) getSpringFactoriesInstances(
ApplicationContextInitializer.class));
// 4. 加载所有 ApplicationListener
// 从 META-INF/spring.factories 读取 key=org.springframework.context.ApplicationListener
setListeners((Collection) getSpringFactoriesInstances(
ApplicationListener.class));
// 5. 推断主类(通过堆栈跟踪找到 main 方法所在的类)
this.mainApplicationClass = deduceMainApplicationClass();
}
推断主类的技巧:
java
private Class<?> deduceMainApplicationClass() {
try {
StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
for (StackTraceElement stackTraceElement : stackTrace) {
if ("main".equals(stackTraceElement.getMethodName())) {
return Class.forName(stackTraceElement.getClassName());
}
}
} catch (ClassNotFoundException ex) {
// ignore
}
return null;
}
SpringBoot 通过构造一个 RuntimeException 获取调用栈,然后找到方法名为 main 的栈帧——这个技巧确保了即使 SpringApplication.run() 被包装在另一个类的静态方法中调用,也能正确推断出用户的主类。
3. Step 2-3:准备 Environment
java
ConfigurableEnvironment environment = prepareEnvironment(listeners, args);
prepareEnvironment 的内部流程:
读图导引:Environment 的准备是"层层叠加"的过程。每添加一个 PropertySource,都在列表的尾部追加,但由于解析时从前向后遍历,后添加的高优先级源会覆盖前面的同名属性。
配置文件加载与 profile 激活:
java
// ConfigFileApplicationListener 的处理逻辑
void load() {
// 1. 加载默认配置文件:application.yml / application.properties
// 2. 根据 spring.profiles.active 加载 profile 专属配置
// 3. 高优先级文件覆盖低优先级文件
}
profile 的激活优先级:
SpringApplication.setAdditionalProfiles()ConfigurableEnvironment.setActiveProfiles()- JVM 参数
-Dspring.profiles.active=prod - 环境变量
SPRING_PROFILES_ACTIVE=prod application.yml中的spring.profiles.active
暗面:profile 的继承关系容易让人困惑。application-prod.yml 不会继承 application.yml 的全部属性——如果 application.yml 定义了 server.port=8080,而 application-prod.yml 没有定义 server.port,最终生效的仍然是 8080。但如果 application-prod.yml 显式定义了 server.port=80,则覆盖默认值。这不是继承,而是"叠加后覆盖"。
4. Step 4-5:创建与准备 Context
java
// Step 4: 创建 Context
context = createApplicationContext();
// Step 5: 准备 Context
prepareContext(context, environment, listeners, args, printedBanner);
Context 类型选择:
| 应用类型 | Context 类型 |
|---|---|
| SERVLET | AnnotationConfigServletWebServerApplicationContext |
| REACTIVE | AnnotationConfigReactiveWebServerApplicationContext |
| NONE | AnnotationConfigApplicationContext |
prepareContext 的核心操作:
java
private void prepareContext(ConfigurableApplicationContext context,
ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments, Banner printedBanner) {
// 1. 将 Environment 关联到 Context
context.setEnvironment(environment);
// 2. 应用后处理(注册 BeanNameGenerator、ResourceLoader 等)
postProcessApplicationContext(context);
// 3. 执行所有 ApplicationContextInitializer
applyInitializers(context);
// 4. 发布 ApplicationContextInitializedEvent
listeners.contextPrepared(context);
// 5. 注册 SpringBoot 特殊 Bean
// - springApplicationArguments(命令行参数)
// - springBootBanner(Banner 对象)
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
// 6. 加载主类 BeanDefinition(后续 refresh 时实例化)
Set<Object> sources = getAllSources();
load(context, sources.toArray(new Object[0]));
// 7. 发布 ApplicationPreparedEvent
listeners.contextLoaded(context);
}
读图导引:prepareContext 是 Context 的"出厂设置"阶段。Initializer 在此阶段执行,可以对 Context 做最后的修改。主类的 BeanDefinition 在此注册,但尚未实例化——实例化要到 refresh() 阶段才发生。
5. Step 6:刷新 Context(核心)
java
refreshContext(context);
这最终调用到 AbstractApplicationContext.refresh(),是 Spring Framework 的核心方法:
java
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// 1. 准备刷新(记录启动时间、初始化属性源)
prepareRefresh();
// 2. 获取/刷新 BeanFactory
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// 3. 准备 BeanFactory
prepareBeanFactory(beanFactory);
try {
// 4. 子类扩展(Web 应用注册 ServletContextAwareProcessor)
postProcessBeanFactory(beanFactory);
// 5. 【关键】执行 BeanFactoryPostProcessor
// ConfigurationClassPostProcessor 解析 @Configuration 类
// AutoConfigurationImportSelector 加载自动配置类
invokeBeanFactoryPostProcessors(beanFactory);
// 6. 注册 BeanPostProcessor
registerBeanPostProcessors(beanFactory);
// 7. 初始化国际化和事件广播器
initMessageSource();
initApplicationEventMulticaster();
// 8. 【关键】子类扩展:Web 应用在此创建并启动内嵌服务器
onRefresh();
// 9. 注册监听器
registerListeners();
// 10. 【关键】实例化所有非懒加载的单例 Bean
finishBeanFactoryInitialization(beanFactory);
// 11. 完成刷新,发布 ContextRefreshedEvent
finishRefresh();
}
// ... catch and handle exceptions
}
}
读图导引:三步"关键"调用构成了刷新阶段的核心骨架。第 5 步加载自动配置,第 8 步启动内嵌服务器,第 10 步实例化业务 Bean。这三步的顺序是严格设计的——自动配置必须在 Bean 实例化前完成,内嵌服务器必须在 Bean 实例化后启动(因为某些 Bean 的初始化可能依赖于服务器的就绪状态)。
自动配置在哪个阶段加载?
在 invokeBeanFactoryPostProcessors() 阶段,ConfigurationClassPostProcessor 会解析所有 @Configuration 类,包括通过 @Import(AutoConfigurationImportSelector.class) 引入的自动配置类。AutoConfigurationImportSelector 读取 AutoConfiguration.imports 文件,获取候选配置类列表,然后根据条件注解过滤,最终将生效的配置类注册为 BeanDefinition。
内嵌 Tomcat 在哪个阶段启动?
java
// ServletWebServerApplicationContext.onRefresh()
@Override
protected void onRefresh() {
super.onRefresh();
try {
createWebServer(); // 创建并启动内嵌 Web 服务器
} catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}
onRefresh() 在 finishBeanFactoryInitialization() 之前执行,这意味着内嵌服务器在业务 Bean 实例化前就已经启动。但有些 Bean 的初始化(如 @PostConstruct 方法)可能依赖于 ServletContext,因此 SpringBoot 在 createWebServer() 时通过 getSelfInitializer() 注册了一个 ServletContextInitializer,确保后续 Servlet 和 Filter 的注册能正确执行。
单例 Bean 的实例化:
java
// DefaultListableBeanFactory.preInstantiateSingletons()
@Override
public void preInstantiateSingletons() throws BeansException {
// 1. 遍历所有 BeanDefinition 名称
List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);
for (String beanName : beanNames) {
// 2. 获取合并后的 BeanDefinition
RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
// 3. 条件:非抽象、单例、非懒加载
if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
// 4. 如果是 FactoryBean,先获取 FactoryBean 本身
if (isFactoryBean(beanName)) {
// ...
} else {
// 5. 普通 Bean:走 getBean → createBean 流程
getBean(beanName);
}
}
}
}
这是 Spring IOC 容器的标准行为:按 BeanDefinition 的注册顺序,逐个实例化非懒加载的单例 Bean。如果 Bean A 依赖 Bean B,B 会先被实例化——这就是 Spring 的依赖注入保证。
6. Step 7-9:启动完成与 Runner 执行
java
// SpringApplication.run() 的后半段
// Step 7: 启动完成
afterRefresh(context, applicationArguments);
listeners.started(context); // 发布 ApplicationStartedEvent
// Step 8: 执行 Runner
callRunners(context, applicationArguments);
// Step 9: 运行中
listeners.running(context); // 发布 ApplicationReadyEvent
// 返回 Context
return context;
事件发布的完整时序:
读图导引:纵向观察时间轴,事件从准备阶段一直贯穿到运行阶段。每个事件都是开发者可以介入的"钩子"。注意 callRunners() 在 started() 之后、running() 之前——这意味着 Runner 执行时,容器已完全就绪但尚未发布最终的就绪事件。
7. 启动时间的暗面
SpringBoot 启动时间主要由以下几部分构成:
| 阶段 | 典型耗时 | 优化空间 |
|---|---|---|
| 构造 SpringApplication | < 100ms | 小 |
| 准备 Environment | 100-500ms | 中(减少配置文件解析) |
| 加载 BeanDefinition | 500-2000ms | 大(减少扫描路径、排除不必要的自动配置) |
| 自动配置条件评估 | 200-1000ms | 大(@EnableAutoConfiguration(exclude)) |
| 实例化单例 Bean | 500-3000ms | 大(懒初始化、减少 @PostConstruct 重逻辑) |
| 内嵌服务器启动 | 300-1000ms | 中(预编译 JSP、减少 Servlet 注册) |
| 执行 Runner | 视业务而定 | 大(异步化初始化逻辑) |
排查启动瓶颈的工具:
SpringBoot 2.4+ 提供了启动事件追踪:
yaml
# application.yml
spring:
output:
ansi:
enabled: always
以及 SpringApplicationStartup API(SpringBoot 2.4+ / Spring Framework 5.3+):
java
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(Application.class);
// 使用 BufferingApplicationStartup 记录启动步骤
app.setApplicationStartup(new BufferingApplicationStartup(10000));
app.run(args);
}
}
访问 /actuator/startup(需暴露该端点)可查看详细的启动步骤耗时。
懒初始化:SpringBoot 2.2+ 支持全局懒初始化:
yaml
spring:
main:
lazy-initialization: true
开启后,所有单例 Bean 不会在启动时实例化,而是在第一次被依赖时创建。这可以显著缩短启动时间,但代价是第一次请求的延迟会增加("冷启动"效应)。生产环境通常不建议全局开启,而是对特定的重 Bean 使用 @Lazy 注解。
实战/源码
1. 源码走读:SpringApplication.run() 主流程
java
public ConfigurableApplicationContext run(String... args) {
// 1. 启动计时器
long startTime = System.nanoTime();
// 2. 创建 BootstrapContext(SpringBoot 2.4+ 的启动上下文)
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
configureHeadlessProperty();
// 3. 获取 SpringApplicationRunListeners
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
// 4. 包装命令行参数
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 5. 准备 Environment
ConfigurableEnvironment environment = prepareEnvironment(
listeners, bootstrapContext, applicationArguments);
configureIgnoreBeanInfo(environment);
// 6. 打印 Banner
Banner printedBanner = printBanner(environment);
// 7. 创建 ApplicationContext
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
// 8. 准备 Context
prepareContext(bootstrapContext, context, environment,
listeners, applicationArguments, printedBanner);
// 9. 【核心】刷新 Context
refreshContext(context);
// 10. 刷新后处理
afterRefresh(context, applicationArguments);
// 11. 记录启动时间
Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass)
.logStarted(getApplicationLog(), timeTakenToStartup);
}
// 12. 发布 started 事件
listeners.started(context, timeTakenToStartup);
// 13. 执行 Runner
callRunners(context, applicationArguments);
} catch (Throwable ex) {
// 14. 异常处理
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}
try {
// 15. 发布 running 事件
Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
listeners.ready(context, timeTakenToReady);
} catch (Throwable ex) {
// ...
}
return context;
}
2. 自定义启动监听器
场景:在应用启动完成后发送告警通知或注册服务发现。
java
@Component
public class StartupEventListener {
@EventListener(ApplicationReadyEvent.class)
public void onApplicationReady(ApplicationReadyEvent event) {
// 应用完全就绪后执行
ConfigurableApplicationContext context = event.getApplicationContext();
String appName = context.getEnvironment().getProperty("spring.application.name");
System.out.println("应用 " + appName + " 启动完成,准备注册到服务发现...");
}
@EventListener(ApplicationFailedEvent.class)
public void onApplicationFailed(ApplicationFailedEvent event) {
// 启动失败时执行
Throwable exception = event.getException();
System.err.println("应用启动失败: " + exception.getMessage());
// 发送告警...
}
}
更细粒度的 SpringApplicationRunListener:
java
public class CustomRunListener implements SpringApplicationRunListener {
public CustomRunListener(SpringApplication application, String[] args) {
// 构造方法必须有两个参数
}
@Override
public void starting(ConfigurableBootstrapContext bootstrapContext) {
System.out.println("[启动] 应用即将启动...");
}
@Override
public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext,
ConfigurableEnvironment environment) {
System.out.println("[启动] Environment 准备完成,active profiles: " +
Arrays.toString(environment.getActiveProfiles()));
}
@Override
public void started(ConfigurableApplicationContext context, Duration timeTaken) {
System.out.println("[启动] 应用启动耗时: " + timeTaken.toMillis() + "ms");
}
}
注册到 META-INF/spring.factories:
org.springframework.boot.SpringApplicationRunListener=\
com.example.CustomRunListener
3. 启动耗时分析实战
方案一:Spring Boot Actuator 启动端点(2.4+)
yaml
management:
endpoints:
web:
exposure:
include: startup,metrics
POST /actuator/startup 获取启动步骤详情:
json
{
"timeline": {
"startTime": "2024-06-01T10:00:00.000Z",
"events": [
{
"endTime": "2024-06-01T10:00:01.234Z",
"duration": "1.234s",
"startupStep": {
"name": "spring.beans.instantiate",
"description": "Instantiating bean: dataSource"
}
}
]
}
}
方案二:自定义启动计时
java
@Component
public class StartupTimeLogger implements ApplicationListener<ApplicationReadyEvent> {
@Autowired
private ApplicationContext context;
@Override
public void onApplicationReady(ApplicationReadyEvent event) {
// 获取 BeanFactory 中的启动信息
// 或使用自定义的 StopWatch 在关键步骤打点
}
}
方案三:JVM 参数查看类加载耗时
bash
java -verbose:class -jar app.jar | grep -E "Loaded|Unloaded"
类加载是启动耗时的隐形杀手。SpringBoot 应用通常要加载 8000-15000 个类,每个类加载涉及磁盘 I/O、字节码验证、解析,累积起来可达数秒。SpringBoot 3.x 的 AOT 编译可以将启动时的类加载开销前置到编译期。
4. 启动优化实践
优化一:排除不必要的自动配置
java
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class,
HibernateJpaAutoConfiguration.class,
JacksonAutoConfiguration.class
})
public class Application { }
如果应用是纯 API 网关或消息消费者,不需要数据源和 JPA,排除这些配置可以节省数百毫秒的自动配置评估时间。
优化二:懒初始化 + 关键 Bean 预热
yaml
spring:
main:
lazy-initialization: true
java
@Component
@Lazy(false) // 这个 Bean 仍然启动时实例化
public class CriticalService {
// 关键服务需要预热
}
优化三:异步化初始化逻辑
java
@Component
public class AsyncInitializer {
@Async
@EventListener(ApplicationReadyEvent.class)
public void init() {
// 耗时操作(如缓存预热、配置加载)异步执行
// 不阻塞主启动流程
}
}
优化四:SpringBoot 3.x AOT 编译
SpringBoot 3.x 支持 AOT(Ahead-of-Time)编译,配合 GraalVM 原生镜像:
xml
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
</plugin>
AOT 编译的优势:
- 启动时无需类路径扫描和反射解析,BeanDefinition 在编译期确定
- GraalVM 原生镜像启动时间从秒级降到毫秒级(典型值 50-200ms)
- 内存占用降低 50%+
代价:
- 失去运行时的动态性(如基于反射的自动配置、CGlib 动态代理受限)
- 需要显式配置反射清单、资源清单、动态代理清单
- 编译时间大幅增加(可达数分钟)
- 部分库(如某些序列化框架)不完全兼容
常见问题
Q1:SpringApplication.run() 和 new AnnotationConfigApplicationContext() 有什么区别?
AnnotationConfigApplicationContext 是 Spring Framework 的原生 API,需要手动注册配置类、手动刷新:
java
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext();
ctx.register(AppConfig.class);
ctx.refresh();
SpringApplication.run() 是 SpringBoot 的高级封装,额外做了:推断应用类型、准备 Environment、加载 Initializer/Listener、打印 Banner、自动配置加载、内嵌服务器启动、执行 Runner。可以理解为 SpringApplication.run() = new AnnotationConfigApplicationContext() + SpringBoot 的"启动编排"。
Q2:内嵌 Tomcat 在启动的哪个阶段启动?为什么启动后立刻能接收请求?
内嵌 Tomcat 在 refreshContext() 的 onRefresh() 子阶段启动(第 8 步),位于 finishBeanFactoryInitialization()(第 10 步,实例化单例 Bean)之前。Tomcat 启动后会立即开始监听端口,但此时 Spring 容器尚未完成 Bean 实例化。SpringBoot 通过 TomcatStarter(一个 ServletContainerInitializer)确保 Tomcat 在接收第一个请求前,Spring 容器已经完成刷新。具体而言,TomcatStarter.onStartup() 会等待 Context 刷新完成才注册 DispatcherServlet。
Q3:ApplicationRunner 和 CommandLineRunner 的执行顺序?
两者都在容器刷新完成后执行,但 ApplicationRunner 优先于 CommandLineRunner。同一类型的 Runner 按 @Order 排序。如果 Runner 抛出异常,SpringBoot 会将应用状态标记为失败,但已启动的 Web 服务器不会自动关闭——这是生产环境中需要注意的边界。
Q4:如何在启动过程中获取 Environment?
三种方式:
- 实现
EnvironmentAware接口(在 Bean 实例化后注入) - 监听
ApplicationEnvironmentPreparedEvent(最早可获取时机) - 在
ApplicationContextInitializer中通过context.getEnvironment()获取
最早的方式是在 SpringApplicationRunListener.environmentPrepared() 或 ApplicationEnvironmentPreparedEvent 监听器中获取,此时配置文件已加载、profile 已激活。
Q5:启动慢怎么系统排查?
排查清单:
- 开启
debug: true查看自动配置报告,排除不必要的自动配置 - 使用
BufferingApplicationStartup+/actuator/startup定位耗时步骤 - 检查
@PostConstruct和InitializingBean.afterPropertiesSet()中是否有阻塞操作 - 检查类路径扫描范围是否过大(
@ComponentScan是否扫到了不必要的包) - 使用
-verbose:class查看类加载耗时,考虑使用 CDS(Class Data Sharing)或 AOT - 检查 JVM 参数:
-Xms和-Xmx设置是否合理,避免启动时频繁 GC
Q6:SpringBoot 3.x 的 AOT 编译和传统的 JIT 有什么区别?
JIT(Just-In-Time)是运行时编译,字节码在 JVM 运行过程中逐步编译为机器码。AOT(Ahead-of-Time)是编译期编译,在构建时将 Java 字节码编译为原生机器码(通过 GraalVM Native Image)。
SpringBoot 3.x 的 AOT 处理不仅仅是 GraalVM 原生镜像,还包括:
- 在构建期生成
BeanDefinition的静态元数据,替代运行时的组件扫描 - 生成
reflect-config.json、proxy-config.json等 GraalVM 配置 - 生成优化的
ApplicationContextInitializer,替代运行时的条件评估
AOT 模式适合: Serverless(冷启动敏感)、CLI 工具、边缘计算。不适合:需要动态代理频繁变化、大量使用反射和字节码增强的场景。
Q7:为什么有些 Bean 在启动时没有被创建?
可能原因:
- 标注了
@Lazy,需要首次被依赖时才创建 - 作用域是
prototype,每次获取时才创建 - 条件注解不匹配(如
@ConditionalOnProperty条件不满足) - 被
@Profile限制,当前未激活对应 profile - 被其他自动配置的
@ConditionalOnMissingBean覆盖
总结
SpringBoot 的启动流程是一出精心编排的交响乐:
- 前奏(构造阶段):推断应用类型、加载扩展点、确定主类——为演出选定乐器和指挥
- 调音(Environment 准备):层层叠加 PropertySource,命令行参数覆盖配置文件——调好每一个音准
- 就位(Context 创建与准备):创建舞台、Initializer 做最后调整、主类注册到节目单
- 演奏(refresh 核心):自动配置加载(总谱分发)、BeanPostProcessor 就位(乐手准备)、内嵌服务器启动(舞台灯光打开)、单例 Bean 实例化(正式演奏)
- 谢幕(Runner 执行与事件发布):安可曲目(初始化逻辑)、观众掌声(ApplicationReadyEvent)
理解启动流程的关键在于把握三个"里程碑":invokeBeanFactoryPostProcessors(自动配置加载)、onRefresh(内嵌服务器启动)、finishBeanFactoryInitialization(单例 Bean 实例化)。这三个步骤的顺序是严格设计的,任何一步的提前或延后都会导致启动失败或行为异常。
生产环境中的启动优化,本质是减少"不必要的工作":排除不需要的自动配置、推迟非关键 Bean 的初始化、将阻塞操作移出主线程。SpringBoot 3.x 的 AOT 编译则是一条更激进的优化路径——用编译期的确定性替代运行时的动态性,代价是牺牲部分灵活性。