ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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

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 相关类就推断为REACTIVEWebFlux 应用如果缺失 Servlet 相关类就推断为NONE纯非 Web 应用否则默认SERVLET。这个推断决定了后续创建什么类型的 ApplicationContext。Servlet 应用对应AnnotationConfigServletWebServerApplicationContextReactive 应用对应AnnotationConfigReactiveWebServerApplicationContext非 Web 应用对应AnnotationConfigApplicationContext。类型判断错误会导致应用启动方式完全不对这在实际项目中偶尔会遇到引入 WebFlux 依赖但没加 WebMvc结果整个项目行为变得非常奇怪。2.3 从spring.factories加载初始化器和监听器接下来是getSpringFactoriesInstances()方法这是 SpringBoot SPI 机制的核心。它做的事情是读取 classpath 下所有META-INF/spring.factories文件按接口类型取出对应的实现类列表再做实例化。private T CollectionT getSpringFactoriesInstances(ClassT type, Class?[] parameterTypes, Object... args) { ClassLoader classLoader getClassLoader(); SetString names new LinkedHashSet(SpringFactoriesLoader.loadFactoryNames(type, classLoader)); ListT 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()方法的完整流程就像一场精心编排的交响乐。我把核心步骤整理成一个时序脉络实际执行顺序大致如下创建并启动StopWatch开始计时通过SpringApplicationRunListeners发布ApplicationStartingEvent事件解析命令行参数准备DefaultApplicationArguments准备Environment并发布ApplicationEnvironmentPreparedEvent打印 Banner创建 ApplicationContext调用ApplicationContextInitializer.initialize()发布ApplicationContextInitializedEvent将主配置类、args、Environment 绑定到 context发布ApplicationPreparedEvent执行refreshContext()这是最核心的一步发布ApplicationStartedEvent执行 Runner发布ApplicationReadyEventStopWatch停止面试时你能说出这些事件名称已经能超过一大半候选人了。但要拿高分还要理解这套事件机制存在的意义它是为了在容器尚未完全就绪时也能把关键节点状态通知到外部的监听器。比如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 创建对应类型的 EnvironmentServlet 应用对应ApplicationServletEnvironment。configureEnvironment()会把命令行参数--server.port8081这类选项转化成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); } SetConfigurableApplicationContext shutdownContexts new LinkedHashSet(); // 注册主配置类 SetClass? 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回调方法参数是封装好的ApplicationArgumentsCommandLineRunner回调方法参数是原始的命令行参数数组两者优先级的排序依据都是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 的配置哪怕你根本没在用它。所以每个自动配置类都标注了一堆条件注解。常见的有ConditionalOnClassclasspath 里有指定类才生效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.javaMyAutoConfiguration.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-phase5s配合 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。如果某个自动配置类显示matchedfalse优先检查条件注解的缺失原因如果类根本不在报告中检查 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 里的服务注册发现、配置中心、网关很多细节都会豁然开朗。面试只是一个验证过程真正收获的是你对自己代码运行的底层逻辑有了掌控感。这份掌控感才是这一路啃源码真正值钱的地方。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进