news 2026/10/23 6:28:28

SpringBoot启动流程深度拆解:从SpringApplication到自动配置

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot启动流程深度拆解:从SpringApplication到自动配置

做 Java 开发这些年,面试和被面试的次数我都记不清了。SpringBoot 启动流程这道题,在我经历过的几乎所有 Java 岗位面试里都出现过,问法五花八门,但内核只有一个:你到底懂不懂 SpringBoot 是如何把自己“跑起来”的。很多候选人能背出run()方法、refresh()方法、自动配置这几个名词,可一追问"SpringApplication初始化时到底加载了什么""Environment在哪一步被准备""条件注解的解析时机"就卡壳了。这说明所谓的八股其实没有真正内化成自己的知识体系。这篇文章我把 SpringBoot 的启动流程从头到尾拆一遍,结合我调试过的实际项目经验和面试官视角的考察点,帮你把这套东西彻底吃透,既应付面试,也能真刀真枪排查启动问题。

这道题被问烂了,但它确实是检验 SpringBoot 功底的好题目。一个开发人员如果能把启动流程讲清楚,说明他对 IoC 容器、Bean 生命周期、自动配置、事件机制、内嵌容器这几个核心模块的理解是成体系的。反过来,只会写 Controller 和 Service,从来没看过启动日志、没排查过启动失败的人,在这道题面前基本三句话就露馅。

1. 为什么面试官必问"启动流程"这道题

1.1 面试官到底想考察什么

很多人觉得面试官问启动流程是在考背诵能力,其实不然。一个合格的高级开发或者技术专家问这个问题,通常会从三个层次递进考察。

第一层是宏观认知。你能不能清晰说出 SpringBoot 启动的核心主线:创建 SpringApplication 实例、准备 Environment、创建并刷新 ApplicationContext、启动内嵌 Web 服务器、执行 Runner。这层考察的是你是否真的用 SpringBoot 写过项目,还是只会 CV 代码。

第二层是细节深度。面试官会追问"具体怎么加载自动配置的""spring.factories文件里存的什么""refresh()方法为什么要分那么多步""内嵌 Tomcat 是在哪一步启动的"。这层考察你是否读过源码、是否理解 Spring 容器的核心机制。

第三层是问题定位能力。面试官会抛出现场问题:启动慢、端口冲突、配置不生效、自定义 Starter 不加载、Bean 循环依赖导致启动失败。如果你只懂流程而不会利用对流程的理解去排查,这题只能算答对了一半。

所以在准备这道题时,不要只背流程,要带着"这段代码如果出问题会怎么表现"的思路去学。

1.2 这题和工作实战的关联度

我见过不少面试表现很好、一到线上就抓瞎的人,原因就是他把启动流程当纯面试题背了。实际上,启动流程的理解深度直接影响你排查问题的速度。

举个例子,线上环境遇到"应用启动成功,但注册到 Nacos 后发现服务下线了",如果你知道SmartLifecycle的启动顺序、知道ApplicationRunner和CommandLineRunner的执行时机,就能迅速判断是服务注册动作和健康检查的时序问题,而不是瞎查一堆配置。

再比如"本地启动正常,测试环境启动报NoSuchBeanDefinitionException",如果你了解自动配置的@ConditionalOnMissingBean失效场景,就能第一时间怀疑是不是自定义配置类被 ComponentScan 提前加载导致的。这些问题的排查手段,全都建立在对启动流程的精确理解之上。

2. 启动前夜:SpringApplication 实例化阶段

2.1 从main方法到run()的入口链

大多数 SpringBoot 应用启动的入口代码长这样:

@SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }

这一行SpringApplication.run()做了一件容易被忽略的事:它先new了一个SpringApplication对象,然后再调用这个对象的run()方法。run()是静态方法,内部代码其实等价于:

public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) { return new SpringApplication(primarySource).run(args); }

为什么要拆成两步?因为SpringApplication对象在构造阶段就开始干活了,很多关键信息是在构造函数里准备好的,比如判断应用类型、加载初始化器和监听器、定位主配置类。如果你只盯着run()方法,就漏掉了最前面的初始化逻辑。

SpringApplication的构造函数核心如下:

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) { this.resourceLoader = resourceLoader; this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources)); this.webApplicationType = WebApplicationType.deduceFromClasspath(); this.bootstrapRegistryInitializers = new ArrayList<>(getSpringFactoriesInstances(BootstrapRegistryInitializer.class)); setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); this.mainApplicationClass = deduceMainApplicationClass(); }

这短短几行代码,信息量非常大。

2.2 WebApplicationType 的类型推断逻辑

WebApplicationType.deduceFromClasspath()这个方法很巧妙。它的作用是判断当前应用是哪种 Web 应用类型,枚举值有NONE、SERVLET、REACTIVE三种。判断依据不是配置,而是 classpath 里是否存在特定类。

static WebApplicationType deduceFromClasspath() { if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null) && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) { return WebApplicationType.REACTIVE; } for (String className : SERVLET_INDICATOR_CLASSES) { if (!ClassUtils.isPresent(className, null)) { return WebApplicationType.NONE; } } return WebApplicationType.SERVLET; }

一眼就能看懂:如果 classpath 里有org.springframework.web.reactive.DispatcherHandler且没有 SpringMVC 相关类,就推断为REACTIVE(WebFlux 应用);如果缺失 Servlet 相关类就推断为NONE(纯非 Web 应用);否则默认SERVLET。

这个推断决定了后续创建什么类型的 ApplicationContext。Servlet 应用对应AnnotationConfigServletWebServerApplicationContext,Reactive 应用对应AnnotationConfigReactiveWebServerApplicationContext,非 Web 应用对应AnnotationConfigApplicationContext。类型判断错误会导致应用启动方式完全不对,这在实际项目中偶尔会遇到:引入 WebFlux 依赖但没加 WebMvc,结果整个项目行为变得非常奇怪。

2.3 从spring.factories加载初始化器和监听器

接下来是getSpringFactoriesInstances()方法,这是 SpringBoot SPI 机制的核心。它做的事情是读取 classpath 下所有META-INF/spring.factories文件,按接口类型取出对应的实现类列表,再做实例化。

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) { ClassLoader classLoader = getClassLoader(); Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader)); List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names); AnnotationAwareOrderComparator.sort(instances); return instances; }

值得注意的细节有三个。

SpringFactoriesLoader.loadFactoryNames 会扫描所有 jar 包里的 spring.factories,不是只扫当前应用自己的。这就是为什么引入一个第三方 Starter 后,它的监听器、初始化器能被自动注册进当前应用的启动流程。

实例化后会对结果排序,排序规则是@Order注解和Ordered接口。面试如果有追问,你要能答出这里的排序是按照AnnotationAwareOrderComparator执行的。

从 SpringBoot 2.7 开始,官方逐步用META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件替代自动配置的加载,但spring.factories里的 Initializer、Listener、ApplicationContextInitializer 加载机制依然保留。很多老项目直到升级时才发现不兼容,这里后面展开说。

deduceMainApplicationClass()的定位逻辑也很有意思。它通过遍历当前线程的堆栈,找到第一个包含public static void main方法且方法签名匹配的类,作为主配置类。这个类会作为后续自动配置的扫描起点之一。所以你的main方法所在的类,天然就是@SpringBootApplication扫包路径的根。

3. 启动主流程:run() 方法的层层拆解

3.1 启动时间线:事件发布与监听器机制

run()方法的完整流程就像一场精心编排的交响乐。我把核心步骤整理成一个时序脉络,实际执行顺序大致如下:

  1. 创建并启动StopWatch,开始计时
  2. 通过SpringApplicationRunListeners发布ApplicationStartingEvent事件
  3. 解析命令行参数,准备DefaultApplicationArguments
  4. 准备Environment,并发布ApplicationEnvironmentPreparedEvent
  5. 打印 Banner
  6. 创建 ApplicationContext
  7. 调用ApplicationContextInitializer.initialize(),发布ApplicationContextInitializedEvent
  8. 将主配置类、args、Environment 绑定到 context,发布ApplicationPreparedEvent
  9. 执行refreshContext(),这是最核心的一步
  10. 发布ApplicationStartedEvent,执行 Runner
  11. 发布ApplicationReadyEvent,StopWatch停止

面试时你能说出这些事件名称已经能超过一大半候选人了。但要拿高分,还要理解这套事件机制存在的意义:它是为了在容器尚未完全就绪时,也能把关键节点状态通知到外部的监听器。比如ApplicationStartingEvent发布时,ApplicationContext都还没创建,但 LoggingApplicationListener 已经能在这个时机初始化日志系统了。

SpringApplicationRunListeners其实是对SpringApplicationRunListener的封装,默认实现是EventPublishingRunListener,它的工作就是遍历当前 SpringApplication 注册的 ApplicationListener,逐个调用对应的事件处理方法。

3.2 Environment 准备阶段在干什么

Environment 是 Spring 框架里代表应用运行环境的对象,内部持有PropertySource列表和ConversionService。启动流程里对它的准备分为两步:prepareEnvironment()和后续的 context 绑定。

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, DefaultApplicationArguments arguments) { ConfigurableEnvironment environment = getOrCreateEnvironment(); configureEnvironment(environment, arguments.getSourceArgs()); listeners.environmentPrepared(environment); bindToSpringApplication(environment); return environment; }

这几步分别做了这些事:

getOrCreateEnvironment()按 WebApplicationType 创建对应类型的 Environment,Servlet 应用对应ApplicationServletEnvironment。

configureEnvironment()会把命令行参数--server.port=8081这类选项转化成SimpleCommandLinePropertySource,并且设置默认的 profile。

environmentPrepared()发布ApplicationEnvironmentPreparedEvent。这里有个实用价值极高的细节:SpringBoot 内置的ConfigDataEnvironmentPostProcessor就是在这个监听环节读取application.yml/application.properties/.yaml配置文件,并加载到 Environment 里的。也就是说,配置文件的加载时机在 Environment 准备阶段,早于 ApplicationContext 的创建。

如果你曾经困惑过"为什么application.yml里定义的spring.profiles.active能影响 Environment 默认值",原因就在这里:profile 的激活逻辑也是在这个阶段完成的。

Banner 的打印就在 Environment 准备完之后。它允许自定义 banner 文字,SpringBoot 还支持由图片生成字符画,这个特性后来被做成了各种在线 banner 生成器,挺有意思的,但底层原理不外乎是读取配置的文件路径,然后用字符数组映射像素灰度,纯属锦上添花的功能。

3.3 ApplicationContext 创建与前置准备动作

Environment 就绪后,下一步是创建 ApplicationContext 并做前置准备。这段我直接贴关键代码,你对着看更容易理解:

context = createApplicationContext(); context.setApplicationStartup(this.applicationStartup); if (this.beanNameGenerator != null) { context.getBeanFactory().registerSingleton(AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR, this.beanNameGenerator); } if (this.resourceLoader != null) { if (context instanceof GenericApplicationContext gac) { gac.setResourceLoader(this.resourceLoader); } } if (this.addConversionService) { context.getBeanFactory().setConversionService(context.getEnvironment().getConversionService()); } // 初始化器 applyInitializers(context); listeners.contextPrepared(context); // 将主配置类注册为 BeanDefinition if (this.logStartupInfo) { logStartupInfo(context.getParent() == null); logStartupProfileInfo(context); } Set<ConfigurableApplicationContext> shutdownContexts = new LinkedHashSet<>(); // 注册主配置类 Set<Class<?>> coreSources = new LinkedHashSet<>(); coreSources.addAll(this.primarySources); for (Class<?> source : coreSources) { if (isComponent(source)) { context.getBeanFactory().registerSingleton(source.getName(), source); } else { AnnotatedBeanDefinitionReader reader = new AnnotatedBeanDefinitionReader(context.getBeanFactory()); reader.register(source); } }

这里有个容易被忽略的点:isComponent(source)判断主类是否为@Component的子注解类。@SpringBootApplication本身复合了@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan,而@SpringBootConfiguration又标注了@Configuration,所以它最终会走reader.register()分支,把主配置类解析为AnnotatedBeanDefinition,并触发它的配置类后处理逻辑。

applyInitializers()做的事是遍历之前从spring.factories里加载的ApplicationContextInitializer,依次执行它们的initialize()方法。这是外部自定义扩展的经典时机,常见用途包括给 ApplicationContext 添加自定义 Lifecycle 处理器、注册自定义 BeanFactoryPostProcessor 等。

3.4 refresh() 方法:Spring 容器的核心引擎

refreshContext()是 SpringBoot 启动流程里最重的一块。它直接封装了AbstractApplicationContext.refresh(),这个方法是整个 Spring 容器的核心引擎,我从源码层面拆给你看:

@Override public void refresh() throws BeansException, IllegalStateException { synchronized (this.startupShutdownMonitor) { // 1. 准备刷新,设置启动时间、活跃标志、初始化属性源 prepareRefresh(); // 2. 获取新的 BeanFactory,销毁旧的、创建新的 ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); // 3. 给 BeanFactory 准备标准特性 prepareBeanFactory(beanFactory); // 4. 子类扩展点(Servlet 容器等在此处理) postProcessBeanFactory(beanFactory); // 5. 调用 BeanFactoryPostProcessor invokeBeanFactoryPostProcessors(beanFactory); // 6. 注册 BeanPostProcessor registerBeanPostProcessors(beanFactory); // 7. 初始化消息源 initMessageSource(); // 8. 初始化事件广播器 initApplicationEventMulticaster(); // 9. 模板方法,子类实现(如内嵌容器启动时机) onRefresh(); // 10. 注册监听器 registerListeners(); // 11. 实例化所有非懒加载的单例 Bean finishBeanFactoryInitialization(beanFactory); // 12. 完成刷新,发布事件,启动生命周期处理器 finishRefresh(); // 13. 清理缓存 resetCommonCaches(); } }

这 13 步你必须得记住,面试官最喜欢从这里抠细节。我挑几个容易出彩的讲解要点。

第 5 步invokeBeanFactoryPostProcessors会分类处理BeanDefinitionRegistryPostProcessor和BeanFactoryPostProcessor,并且先处理实现了 PriorityOrdered 和 Ordered 接口的,在处理普通顺序的。@Configuration类的解析、@ComponentScan扫描包路径的注册,都是在这一步完成的。

第 6 步注册的 BeanPostProcessor 不是立即生效的,而是先注册到 BeanFactory 里,等第 11 步实例化 Bean 的时候才会被调用。BeanPostProcessor 的postProcessBeforeInitialization和postProcessAfterInitialization就是在这个阶段被整合进 Bean 生命周期的。

第 8 步initApplicationEventMulticaster默认注册一个SimpleApplicationEventMulticaster。如果你在代码里用@Async却配置了自定义事件监听器,发现事件不触发,十有八九是这个广播器没有配置 TaskExecutor 导致异步策略失效。

第 9 步onRefresh()是关键中的关键。对于 Servlet 应用,ServletWebServerApplicationContext重写了这个方法,在这里创建并启动内嵌 Tomcat/Jetty/Undertow。这里要注意一个时序:内嵌 Web 服务器的启动发生在单例 Bean 实例化之前。也就是说 Spring 容器先把服务器端"架起来"了,但业务 Bean 这时候还没全部创建完。这也是为什么端口号没配置时,Tomcat 默认 8080 启动失败,会在这一阶段就报出端口占用异常,而不会等你业务代码跑完。

第 11 步finishBeanFactoryInitialization负责实例化所有非懒加载单例 Bean。Spring 里的getBean()创建逻辑、循环依赖的处理、AOP 代理的生成,全部集中在这。如果启动过程中遇到循环依赖、Bean 初始化异常,日志会定位在这个阶段。

第 12 步finishRefresh()发布ContextRefreshedEvent,并调用LifecycleProcessor.onRefresh(),激活所有SmartLifecycle组件的 start 方法。这个机制解释了为什么很多框架的"自动启动"逻辑(比如注册到注册中心、开启定时任务)都放在 SmartLifecycle 里实现。

3.5 启动完成后的 Runner 回调与事件收尾

容器刷新完成、内嵌服务器已经监听端口,但这时候 SpringBoot 还没有完全启动完。接下来还有两个 Runner 的回调:

  • ApplicationRunner:回调方法参数是封装好的ApplicationArguments
  • CommandLineRunner:回调方法参数是原始的命令行参数数组

两者优先级的排序依据都是@Order注解,值越小越先执行。这个机制非常适合做数据预热、初始化缓存、启动后自检等任务。

这两个 Runner 执行完之后,SpringBoot 会发布ApplicationReadyEvent。注意这里有个隐含的时序关系:内嵌服务器启动和端口监听早于ApplicationReadyEvent,也就是说其他服务检测到你的端口通了,并不代表你的业务 ApplicationRunner 已经执行完。如果正准备接入微服务注册中心,这个时间差可能导致流量过早打进来,业务组件尚未初始化完成。解决方案通常是给注册中心增加 readiness 探针,或者在 ApplicationRunner 里完成核心依赖初始化后再触发注册。

4. 自动配置:启动流程中最亮眼的设计

4.1 @EnableAutoConfiguration 的 import 机制

自动配置是 SpringBoot 相对传统 Spring 最大的体验升级,也是面试必考点。@SpringBootApplication之所以能"开箱即用",核心在于它复合了@EnableAutoConfiguration这个注解。

@EnableAutoConfiguration的关键是通过@Import(AutoConfigurationImportSelector.class)引入一个 ImportSelector。AutoConfigurationImportSelector会在容器刷新阶段执行,它的selectImports()方法负责返回需要自动注册的配置类全限定名列表。

前面提到从 SpringBoot 2.7 开始,自动配置类的注册信息从META-INF/spring.factories迁移到了META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,格式就是一个类名一行。到 SpringBoot 3.x,已经强制要求使用新格式。升级老项目时遇到自动配置不生效,第一反应就是检查这个 imports 文件是否被正确迁移了。

在selectImports()之后还有筛选逻辑,它会根据当前应用的 classpath 和配置,通过AutoConfigurationImportFilter过滤掉不需要的配置类。这一步是从几十上百个自动配置类中挑出当前应用真正的有效配置的关键。

4.2 条件注解是如何在启动阶段完成"精密装配"

自动配置类不可能无脑全部生效,否则引入一个 Redis 依赖,你就自动获得所有 Redis 的配置,哪怕你根本没在用它。所以每个自动配置类都标注了一堆条件注解。常见的有:

  • @ConditionalOnClass:classpath 里有指定类才生效
  • @ConditionalOnMissingBean:容器中不存在某类型的 Bean 才生效
  • @ConditionalOnProperty:配置文件中存在指定配置项才生效
  • @ConditionalOnWebApplication:当前是 Web 应用才生效

条件注解的解析发生在ConfigurationClassPostProcessor处理配置类阶段。这里有一个很经典的坑:@ConditionalOnMissingBean的生效时机非常早,如果你在自定义配置类里通过@Bean注册了一个和自动配置同类型的 Bean,但你的自定义配置类执行顺序晚于自动配置类,那自动配置类会先看到"容器里还没有这个 Bean",于是也注册了一个,结果就出现两个同类型 Bean,或者因为你的 Bean 晚注册导致自动配置的 Bean 覆盖了你的。

解决思路有两个。第一,自定义配置类标注@AutoConfigureBefore或@AutoConfigureAfter,控制和自动配置类的加载顺序;第二,用@ConditionalOnMissingBean时明确指定要覆盖的具体类型,而不是笼统地匹配接口。

4.3 自定义 Starter 自动配置的实战写法

理解了自动配置的机制,写一个自定义 Starter 就顺理成章了。我分享一个我经常用的最小结构,你可以直接参考:

my-starter/ ├── src/main/resources/ │ └── META-INF/ │ ├── spring.factories # SpringBoot 2.6 及之前 │ └── spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports └── src/main/java/com/example/starter/ ├── MyAutoConfiguration.java └── MyService.java

MyAutoConfiguration.java的典型形态:

@AutoConfiguration @ConditionalOnClass(MyService.class) @ConditionalOnProperty(prefix = "my.starter", name = "enabled", havingValue = "true", matchIfMissing = true) @EnableConfigurationProperties(MyStarterProperties.class) public class MyAutoConfiguration { @Bean @ConditionalOnMissingBean public MyService myService() { return new MyService(); } }

AutoConfiguration.imports文件里写入:

com.example.starter.MyAutoConfiguration

需要注意三点。第一,自动配置类的@Bean方法返回的类,最好不要被业务代码直接 ComponentScan 扫到,否则条件注解会因为 Bean 已存在而不触发。第二,自动配置类一般放在独立的包路径下,不要和业务代码同包。第三,@AutoConfiguration注解从 SpringBoot 2.7 开始取代了原来的@Configuration标注方式,语义更加明确:标注了这个注解的类才是自动配置候选。

5. 启动过程的踩坑记录与排查实战

5.1 启动慢的排查思路

SpringBoot 启动慢是个经典头疼问题。我遇到过一个项目,启动要 3 分钟,排查后的结论是 Bean 实例化阶段做了太多重量级初始化。具体排查方法是利用启动日志里Starting Application和Started Application之间的耗时,再结合refresh()全过程生成的ApplicationStartup记录。

SpringBoot 2.4 之后引入了@EnableStartupProxy或者激活启动记录的功能,可以采集到每次 Bean 创建的耗时。更粗暴的做法是用-Dspring.lifecycle.timeout-per-shutdown-phase=5s配合 JFR 观察热点方法。实践中减慢启动主要就这几类原因:

  • 扫描包路径过大,ComponentScan 扫到了大量无关类
  • Bean 初始化做了远程调用、外部 IO 或线程等待
  • 数据源等资源需要外部服务就绪才能初始化
  • 自动配置加载了大量不需要的配置类(虽然条件注解已过滤,但条件判断本身也有成本)

针对这些,我的建议是先精简扫描范围,给主配置类增加显式scanBasePackages,不要默认扫整个包树。其次是尽量把长耗时的初始化挪到ApplicationRunner或者@Async异步任务里,让 SpringBoot 主流程快速结束。再次是合理利用spring.autoconfigure.exclude排除不用的自动配置类。

5.2 版本升级带来的启动行为变化

从 SpringBoot 2.x 升 3.x,启动流程上最大的变化就是自动配置加载方式从spring.factories迁移到了AutoConfiguration.imports。我在 3.0 升级早期踩过一个坑:项目依赖的某个第三方 Starter 还是旧的 2.x 打包方式,自动配置完全没有生效,结果项目里所有@ConfigurationProperties和自定义 Bean 全部加载不进来,应用能启动但功能全部缺失。

排查这类问题有个系统化方法。启动时加上--debug参数,SpringBoot 会打印所有自动配置的匹配报告(Positive matches 和 Negative matches)。如果某个自动配置类显示matched=false,优先检查条件注解的缺失原因;如果类根本不在报告中,检查 imports 文件是否被正确加载。

另外,SpringBoot 3.x 基于 Spring Framework 6,从javax.*包切换到了jakarta.*包。如果你的项目代码里还直接用javax.servlet.Filter、javax.annotation.Resource这些 API,启动时会报ClassNotFoundException或NoClassDefFoundError。别慌,这是包名迁移导致的基础问题,不是流程理解错了。

5.3 端口冲突与应用未真正未就绪

"端口被占用"应该是新手遇到最多的启动异常了。这个异常本质上出现在onRefresh()阶段,内嵌 Tomcat 启动时绑定端口失败。排查手段包括:netstat -ano | findstr 端口(Windows)或lsof -i:端口(Linux/macOS)查看占用进程,或者换一个随机端口的做法验证问题是否出在固定端口配置上。

但端口冲突只是表象,更深层的问题是:如果你因为某些原因让 Tomcat 启动了但没有真正监听,客户端的连接就会被拒绝。这个"启动成功但不可用"的状态特别难受。排查技巧是看ApplicationReadyEvent有没有发布。如果日志出现了Started Application但没出现ApplicationReadyEvent相关日志,说明启动流程卡在了 Runner 或者 SmartLifecycle 阶段,此时应用对外表现可能端口已通但接口全部超时。

6. 面试速记清单与答题节奏

这道题如果出现在面试里,我建议你按下面的节奏来答题,既不会说漏,也不会让面试官觉得你在背稿。

先一句话定位:SpringApplication.run()本质是创建 SpringApplication 实例并执行 run 方法,其中核心是创建并刷新 Spring IoC 容器,同时按需启动内嵌 Web 服务器。

然后按三层结构展开。第一层讲初始化:WebApplicationType 推断、从 spring.factories 加载 Initializer 和 Listener、定位主配置类。第二层讲 run 主流程:启用监听器、准备 Environment、加载配置文件、打印 Banner、创建 ApplicationContext、初始化器回调、refresh() 容器刷新、内嵌服务器启动、Bean 实例化、Runner 回调、发布就绪事件。第三层落点自动配置:AutoConfigurationImportSelector 从 imports 文件加载自动配置类,经条件注解过滤后注册到容器,同时通过 @ConditionalOnMissingBean 等注解实现"默认配置+自定义覆盖"。

这样答完,宏观、细节、扩展三层都有了,80% 的面试官会在这个基础上追加一两个源码级追问。如果被追问到 Spring 的循环依赖、BeanPostProcessor、动态代理、事务失效这类相关话题,就看你之前积累的深度了。

这里我想特别强调一点,背这些流程不是为了在面试官面前表演背诵能力,而是为了在写代码时心里有那张地图。你知道了ApplicationEnvironmentPreparedEvent是配置文件的加载时机,就不会在@PostConstruct里依赖未初始化的 Environment 属性;你知道了onRefresh()是内嵌服务器启动的时机,就不会在ApplicationRunner里做服务发现时默认其他服务还没就绪。

7. 我个人实操中的几点体会

7.1 用"启动日志 + 源码断点"交叉学习法吃透流程

我向很多同事推荐过一个笨但极其有效的方法:在本地跑一个最小 SpringBoot 工程,然后在SpringApplication的run()方法里打满断点,一步步跟进去看。第一次可能看两三个小时,但比单纯看源码解析文章效果好十倍。

具体操作是:在SpringApplication.run()的第一行断点,然后 F7 进入方法内部,观察StopWatch如何启动、SpringApplicationRunListeners如何创建、Environment 如何准备、ApplicationContext 如何 create。在AbstractApplicationContext.refresh()的 13 个方法处分别打上断点,观察每一步之后 BeanFactory 的内容变化。这个过程中你顺便就把 ApplicationContext 的继承体系、BeanFactoryPostProcessor 的执行机制全看明白了。

7.2 利用启动事件做应用自检和优雅上下线

理解了启动流程中的事件机制后,你会发现很多线上运维需求其实可以用事件去优雅实现。比如在ApplicationReadyEvent之后通过一个ApplicationRunner做健康检查:检查核心数据源连通性、检查缓存组件状态、校验关键配置是否存在,如果检查不通过直接打印严重告警日志,并把启动结果上报给监控系统。

优雅关闭则可以利用SmartLifecycle的stop()方法顺序,先停止接受新请求,再处理完存量请求,最后销毁容器。这套在微服务架构里几乎是标配操作,实现的前提就是你对启动和关闭流程的事件时序有准确理解。

整体来看,SpringBoot 的启动流程这条八股路线,值得每一个 Java 开发者认真啃透。它不是什么面试表演题,而是理解整个 Spring 生态的一把万能钥匙。你把这套流程吃透了,再去看 SpringCloud 里的服务注册发现、配置中心、网关,很多细节都会豁然开朗。面试只是一个验证过程,真正收获的是你对自己代码运行的底层逻辑有了掌控感。这份掌控感,才是这一路啃源码真正值钱的地方。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/23 6:28:21

一级部门负责人全景运作模型:目标解码、资源配置与跨部门协同

1. 一级部门负责人的真实处境&#xff1a;为什么需要一套“全景运作模型库”1.1 从“骨干员工”到“部门负责人”&#xff0c;大多数学管理的人其实是靠直觉在撑我见过太多这样的晋升路径&#xff1a;一个业务能力强、执行力硬的骨干&#xff0c;因为带过一两个项目、业绩亮眼&…

作者头像 李华
网站建设 2026/10/19 7:33:41

从SSH到Linux命令实战:OverTheWire Bandit通关思路与技巧全解析

1. 先聊聊Wargames和Bandit到底是个啥如果你在Linux学习和安全入门的圈子里混过一段时间&#xff0c;大概率会听到过OverTheWire这个平台&#xff0c;而Bandit正是它最出名、也最适合新手的一个Wargame系列。所谓Wargames&#xff0c;直译过来是“战争游戏”&#xff0c;说白了…

作者头像 李华
网站建设 2026/10/22 20:05:19

Java安卓聊天服务端实战:Socket协议、粘包处理与离线消息设计

简介&#xff1a;这是一份基于Java的安卓简易聊天应用服务端设计源码&#xff0c;面向初入移动开发与服务端编程的开发者&#xff0c;可用于学习聊天服务端的用户管理、消息分发等核心逻辑。压缩包共38个文件&#xff0c;包含29个Java源代码、2个XML配置、2个properties属性配置…

作者头像 李华
网站建设 2026/10/19 9:34:48

C#实时监听Excel并批量导入SQL Server实战方案

上周有个做产线的朋友找我&#xff0c;说他们车间每天会固定导出几份Excel报表&#xff0c;希望我能把这些报表“实时”同步到SQL Server里&#xff0c;方便后面的看板和统计查询直接用数据库。这种需求我接过不止一次&#xff0c;C#、Excel、SQL数据库这套组合在工厂上位机、运…

作者头像 李华
网站建设 2026/10/19 9:34:52

Spring AI MCP连接超时排查:Reactor Mono异常原理与修复

项目正要上线&#xff0c;第一轮真实的业务对话却卡了整整20秒&#xff0c;随后控制台抛出一段让人血压升高的异常&#xff1a;reactor.core.Exceptions$ReactorException: java.util.concurrent.TimeoutException: Did not observe any item or terminal signal within 20000m…

作者头像 李华
网站建设 2026/10/19 9:34:48

Redis配置文件核心参数详解与生产实践

接手一个Redis实例&#xff0c;第一件事永远是翻配置文件。我见过太多人直接用默认配置跑生产环境&#xff0c;结果内存被打满、数据丢了、主从裂了才回头查资料。Redis的配置文件&#xff08;redis.conf&#xff09;是它所有行为的底层逻辑&#xff0c;你不管是做缓存、做分布…

作者头像 李华