免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

SpringBoot启动流程详解:从main方法到内嵌Tomcat就绪

SpringBoot启动流程详解:从main方法到内嵌Tomcat就绪 1. 为什么值得把启动流程啃透——面试只是最表面的理由如果你搞过一段时间SpringBoot大概率被问过SpringBoot的启动流程是什么这类问题。说实话这个问题在面试里出现的频率非常高但它绝对不只是面试题那么简单。我在实际生产环境里排查过好几次诡异故障——应用启动后端口迟迟不监听、某些Bean在启动阶段莫名报错、配置项总是被意外覆盖——最后追根溯源都得回到启动流程上找原因。SpringBoot的启动流程本质上回答了一个核心问题一个只有几十行代码的main方法是怎么变成一个包含内嵌Tomcat、数据库连接池、各种自动配置、Bean管理体系、Web接口全部就绪的完整应用的。这中间不是SpringBoot自己施法完成的而是SpringFramework的IoC容器启动逻辑和SpringBoot的自动装配机制、环境准备机制、内嵌服务器机制按特定顺序组合运行的产物。把这条线理顺你对整个Spring生态的理解会上一个台阶。这篇文章我会从SpringApplication的构造开始沿着run方法一路往下走把每一步做了什么、为什么在这个时机做、哪些地方容易出问题都拆开讲清楚。内容偏原理但我尽量用大白话和实际案例说话保证你能一边看一边在脑子里把整个执行链路串起来。2. 从main方法到SpringApplication初始化——你以为的起点不是真起点很多人在项目里看到这样的代码觉得应用就是从这行开始跑的SpringBootApplication public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } }这句话本身没问题但启动流程真正开始的位置要往前推一步。SpringApplication.run是一个静态方法它内部先要创建一个SpringApplication实例然后再调用实例的run方法。也就是说启动流程的第一段其实是SpringApplication对象的构造过程。2.1 SpringApplication构造阶段的关键推断逻辑SpringApplication的构造函数会做四件关键事情推断WebApplicationType通过classpath下是否存在org.springframework.web.reactive.DispatcherHandler、org.springframework.web.servlet.DispatcherServlet、org.springframework.web.context.ConfigurableWebApplicationContext等类将应用类型判定为NONE、SERVLET或REACTIVE。这个过程用的是ClassNameUtils的预设类名集合做存在性检查不是new出来再看性能开销极小。加载BootstrapRegistryInitializer从spring.factories中读取所有BootstrapRegistryInitializer实现这个是SpringBoot 2.4.0之后引入的新机制用于在启动早期阶段做额外注册。加载ApplicationContextInitializer同样从spring.factories中读取这些初始化器会在ApplicationContext准备完成后、refresh之前被调用。她们负责做一些定制化操作比如设置Environment的默认profile、注册额外的BeanFactoryPostProcessor等。加载ApplicationListener从spring.factories中读取所有的ApplicationListener实现这些监听器会在启动流程的不同阶段收到事件通知比如ApplicationStartedEvent、ApplicationReadyEvent、ApplicationFailedEvent。这四步的共同点是读取并缓存配置都是在为后续的run阶段做准备。你可能注意到这里文件都在用spring.factories——虽说SpringBoot 2.7开始推荐用AutoConfiguration.imports文件加载自动配置类但ApplicationListener、ApplicationContextInitializer、EnvironmentPostProcessor这些基础设施至今仍主要走spring.factories。2.2 构造阶段的坑为什么说主启动类位置不能乱放我在排查一些项目问题时发现很多人对主启动类放哪个包不太在意。其实主启动类的位置直接决定了默认的组件扫描根路径——SpringBootApplication上内置的ComponentScan如果不显式指定basePackages默认就以主启动类所在包为扫描根。所以我见过的情况是把主启动类放在顶层包结果扫描范围一下铺到整个项目启动时加载了大量不该加载的Bean启动速度慢不说偶尔还会出现Bean类型冲突。更隐蔽的一个坑是主启动类被放在某个子包里导致兄弟包里的RestController、Service全部没扫到表现就是启动成功但接口404。这种问题排查起来最费时间因为你第一反应不会去怀疑启动类的摆放位置。实际上启动类放在哪本质上是配置了扫描边界这个边界在启动阶段直接影响后续的ComponentScan执行范围。3. run方法的前半段监听器、环境与Banner如何交织实例构造完成之后SpringApplication.run(String... args)方法就登场了。这个方法的内容在SpringBoot不同版本里略有差异但主干逻辑相当稳定。我以SpringBoot 2.7.x为例按执行顺序拆开说。3.1 启动计时与运行监听器run方法最先做的事情是创建StopWatch。这个名字很直白就是个秒表。之后调用SpringApplicationRunListeners对象的starting()方法。SpringApplicationRunListeners不是单个监听器而是一组SpringApplicationRunListener的集合。它和ApplicationListener的区别在于SpringApplicationRunListener专门监听SpringApplication启动过程中的九个阶段starting、environmentPrepared、contextPrepared、contextLoaded、started、ready、failed等更偏启动生命周期ApplicationListener则是SpringFramework的通用事件机制监听的是ApplicationEvent。这个阶段最常见的用途是实现启动耗时打点。我在自己的基础框架里就实现过一个SpringApplicationRunListener在starting阶段记录启动时间、在ready阶段计算总耗时并输出到监控日志。要注册这个监听器还是在spring.factories里加配置org.springframework.boot.SpringApplicationRunListenercom.example.support.CustomRunListener然后写一个构造方法签名匹配SpringApplication, String[]的实现类即可。3.2 Environment准备阶段配置来源的合并顺序接下来是配置准备的重头戏。prepareEnvironment方法会做以下这些事根据启动参数创建ApplicationArguments对象供后续代码通过getApplicationArguments()获取main方法的原始参数。创建或获取ConfigurableEnvironment对于Servlet类型的Web应用通常是StandardServletEnvironment。配置环境的一些必要属性spring.main.*系列配置会在这一步被读取并应用比如spring.main.web-application-type、spring.main.lazy-initialization等。如果有EnvironmentPostProcessor会在这里统一执行。这是非常强大的扩展点配置中心、加密配置解密器基本都是靠它实现的。很多人在这一步栽过的跟头是配置覆盖顺序搞混。SpringBoot的PropertySource是有优先级的大概从高到低是这样优先级配置来源最高Devtools全局配置~/.spring-boot-devtools.properties高TestPropertySource注解测试场景高命令行参数--server.port8081这种高SPRING_APPLICATION_JSON内嵌JSON中ServletConfig / ServletContext参数中JNDI属性中Java System PropertiesSystem.getProperties()中操作系统环境变量中RandomValuePropertySourcerandom.*占位符低jar包外的application-{profile}.properties/yml低jar包内的application-{profile}.properties/yml更低jar包外的application.properties/yml最低jar包内的application.properties/yml注意一个容易被忽略的点同名的application.ymljar包外的配置会覆盖jar包内的。这个机制在生产环境非常有用——不用重新打包就能调整配置——但也经常造成本地跑得好好的服务器上配置不生效的奇怪现象。3.3 Banner、Headless属性与ApplicationContext创建环境准备完成后run方法会打印Banner。这一步的扩展点在Banner接口上你想自定义启动图案可以实现这个接口并配置spring.banner.location指向自己的banner文件。热词里提到的SpringBoot banner生成器就是这样来的——它生成的其实就是一个文本文件SpringBoot启动时会把它打印到控制台。另外spring.main.banner-modeoff可以关掉它某些生产环境为了日志干净会这么设置。紧接着是一个不起眼但很重要的小操作configureHeadlessProperty。它会设置java.awt.headlesstrue确保应用在没有键盘鼠标显示器的服务器环境下也能正常运行某些AWT类。这一步绝大多数人感知不到但它解释了为什么Headless模式是SpringBoot的默认行为。再往下是createApplicationContext。这个方法根据WebApplicationType类型创建对应的ApplicationContext实现如果是SERVLET类型创建AnnotationConfigServletWebServerApplicationContext如果是REACTIVE类型创建AnnotationConfigReactiveWebServerApplicationContext如果是NONE类型创建AnnotationConfigApplicationContext这些类都是GenericApplicationContext的子类其中Servlet版本就继承了内嵌Web服务器的能力。SpringBoot语境里的启动流程在创建ApplicationContext这一步开始与SpringFramework重叠了。4. refresh()整个流程的实权部门——SpringBoot在这里动了什么手脚ApplicationContext创建好以后启动流程进入最关键的一环refreshContext(context)。这个方法内部调用的其实就是SpringFramework中AbstractApplicationContext的refresh()模板方法。如果你读过Spring源码就会知道refresh()方法是SpringIoC容器启动的核心流程一共12个步骤。SpringBoot的讨巧之处在于它不重写这个方法而是通过子类覆写其中特定的模板方法把自己的逻辑塞进SpringFramework既有的流程里。这种在不改变骨架的前提下替换零件的设计思路是理解SpringBoot与Spring关系的关键。4.1 12步refresh流程里SpringBoot重点插手的位置refresh()的主要步骤和SpringBoot在其中的角色对应关系如下表步骤方法SpringFramework做的事SpringBoot插手的事1prepareRefresh准备刷新设置启动时间、活跃状态初始化属性源在这里准备WebApplicationContext相关的早期属性2obtainFreshBeanFactory获取/创建BeanFactory使用DefaultListableBeanFactory3prepareBeanFactory配置BeanFactory的标准特性比如ClassLoader、表达式解析器注册WebApplicationContext相关的后置处理器4postProcessBeanFactory空实现模板方法注册ServletContextAwareProcessor等5invokeBeanFactoryPostProcessors执行BeanFactoryPostProcessorConfigurationClassPostProcessor在这里完成配置类解析和自动配置类导入6registerBeanPostProcessors注册BeanPostProcessor无特殊7initMessageSource初始化MessageSource无特殊8initApplicationEventMulticaster初始化事件广播器无特殊9onRefresh空实现模板方法创建并启动内嵌Web服务器10registerListeners注册监听器无特殊11finishBeanFactoryInitialization实例化所有非懒加载的单例Bean完成所有Controller、Service的创建内嵌Tomcat开始接收请求的准备工作12finishRefresh完成刷新发布ContextRefreshedEvent启动WebServer调用生命周期处理器第5步和第9步是SpringBoot影响最大的两个位置。第5步里ConfigurationClassPostProcessor会解析所有配置类把spr自动配置类通过Import导入进容器第9步里Servlet容器被创建并启动端口开始监听。4.2 第5步为什么是自动装配的关键战场很多讲SpringBoot启动流程的文章讲到invokeBeanFactoryPostProcessors就是一句执行BeanFactory后置处理器带过但这里是理解为什么SpringBoot能自动配置的重中之重。BeanFactoryPostProcessor是SpringFramework提供的扩展点允许在Bean定义加载完成之后、Bean实例化之前对BeanDefinition进行修改。ConfigurationClassPostProcessor实现了这个接口更准确地说是BeanDefinitionRegistryPostProcessor它负责解析配置类上的Configuration、ComponentScan、Import、Bean等注解把这些注解展开成具体的BeanDefinition注册进容器。SpringBoot的自动配置类也是在这个阶段被处理的。SpringBootApplication上的EnableAutoConfiguration注解最终会通过AutoConfigurationImportSelector选择性地加载一批自动配置类——这个过程要走Import机制而Import的解析执行者就是ConfigurationClassPostProcessor。所以你看到的效果是SpringBoot应用启动后容器里自动多出了数据源配置、MyBatis的SqlSessionFactory、RedisTemplate等Bean定义。实际上这些都是第5步里后置处理器把EnableAutoConfiguration展开后看到的一个个自动配置类再把它们内部声明的Bean方法转为BeanDefinition实现的。4.3 第9步的onRefresh内嵌Web服务器在这里被创建对于Web应用来说onRefresh是SpringBoot启动流程里最让人兴奋的环节之一。以Tomcat为例ServletWebServerApplicationContext重写了onRefresh方法并在其中调用createWebServer()方法。它要做的决策是从容器里找到一个ServletWebServerFactory类型的Bean——SpringBoot自动装配阶段如果没有人为干预会默认注册一个TomcatServletWebServerFactory——然后调用工厂的getWebServer(ServletContextInitializer...)方法。在这个方法内部Tomcat实例被创建、配置端口、连接器、线程池参数Context和Servlet容器初始化最后tomcat.start()让端口真正开始监听。这里有个非常实用的排查经验如果项目里同时存在Tomcat的JAR包和Jetty的JAR包SpringBoot不会自己分辨用哪个而是抛出BeanDefinitionStoreException之类的冲突异常。解决方式是把不用的那个从依赖里exclude掉或者显式指定spring.main.web-application-type和对应的Factory类型。很多人遇到多个ServletWebServerFactory Bean的报错就是因为这个。5. 自动装配不是魔法Condition与ConfigurationClassPostProcessor的舞蹈既然提到了自动装配是启动流程中最关键的环节这一节专门展开讲清楚。热词里的springboot自动装配原理是每次面试都绕不开的点而它真正起作用的时机就在refresh的第5步。5.1 AutoConfiguration.imports文件的加载链路SpringBoot 2.7版本之后自动配置类的索引文件从META-INF/spring.factories迁移到了META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports。这个文件内容的格式很简单每行一个自动配置类的全限定类名com.example.starter.autoconfigure.DemoAutoConfiguration com.example.starter.autoconfigure.Demo2AutoConfigurationEnableAutoConfiguration通过AutoConfigurationImportSelector读取这个文件然后对列表里的每个候选配置类做条件化处理。为什么用独立文件而不是spring.factories官方给的理由是单独文件更轻量、更容易解析而且避免了spring.factories里条目过多、不同模块之间的干扰。我自己的理解是这也是一种性能优化——启动阶段解析文件的开销虽然不大但能少读一点是一点。5.2 Conditional注解的评估时机与顺序每个自动配置类上都有一堆Conditional相关注解比如AutoConfiguration ConditionalOnClass(RedisOperations.class) ConditionalOnMissingBean(name redisTemplate) public class RedisAutoConfiguration { Bean ConditionalOnMissingBean(name redisTemplate) public RedisTemplateObject, Object redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplateObject, Object template new RedisTemplate(); template.setConnectionFactory(redisConnectionFactory); return template; } }这些条件注解的评估不是随手就做的而是由ConditionEvaluator在配置类解析期间执行。评估的核心逻辑是检查类的条件ConditionalOnClassclasspath下有没有指定类检查Bean的条件ConditionalOnMissingBean/ConditionalOnBean容器中是否已经存在某个BeanDefinition或Bean检查属性的条件ConditionalOnProperty指定的配置项是否为预期值这里有个容易踩坑的细节ConditionalOnBean和ConditionalOnMissingBean的判断时机在配置类解析阶段而当时的容器里可能还没注册完所有BeanDefinition。所以如果同一个自动配置类里的两个Bean方法互相依赖条件可能出现判断不准的情况。解决办法是在Bean方法参数上声明依赖让Spring自动处理顺序而不是靠条件的先后顺序。5.3 自定义自动配置类的最小可运行示例理解了原理后可以做一个最小的自定义自动配置类来验证整个链路。假设我要做一个DemoService的自动配置包第一步编写自动配置类AutoConfiguration ConditionalOnClass(DemoService.class) ConditionalOnProperty(prefix demo, name enabled, havingValue true, matchIfMissing true) public class DemoAutoConfiguration { Bean ConditionalOnMissingBean public DemoService demoService() { return new DemoService(auto-configured-demo); } }第二步创建META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件内容为com.example.demo.autoconfigure.DemoAutoConfiguration第三步在一个普通SpringBoot项目里引入这个包启动后就能看到DemoService被自动注册了。这就是SpringBoot启动流程中第5步解析配置类时发生的事——你的自定义自动配置类和SpringBoot内置的自动配置类走的是完全相同的通道。6. 从Bean实例化到Web服务器就绪——启动尾段的重要细节refresh的第11步finishBeanFactoryInitialization是量变引发质变的节点。在这之前所有的BeanDefinition已经注册所有的BeanFactoryPostProcessor已经运行完BeanPostProcessor也准备好了。从这一步开始容器会遍历所有非懒加载的单例BeanDefinition逐个实例化。6.1 单例Bean的实例化顺序由什么决定Spring在实例化单例Bean时并不是按照BeanDefinition的注册顺序盲目new。它会先把满足条件的BeanDefinition放入一个List通过AnnotationAwareOrderComparator排序——也就是说实现了Ordered接口或标注了Order注解的Bean可以控制实例化顺序。但大多数Bean之间其实没有显式顺序依赖Spring会按照依赖关系自动构建A依赖B那就先实例化B再实例化A。这个阶段最常见的报错就是循环依赖。比如A和B互相注入Spring在默认情况下单例、非构造器注入能通过三级缓存机制解决——这是Spring闭环能力的体现。但如果你把其中一方改成了构造器注入或者把spring.main.allow-circular-references设置为false循环依赖就会直接抛出BeanCurrentlyInCreationException。我处理的几个生产故障里有一起就是重构时把字段注入改成了构造器注入结果原本正常的循环依赖立刻爆雷。实际经验是比起想尽办法绕过循环依赖不如从设计上消除它——该拆的模块拆开该用事件驱动用的用事件驱动别指望Spring帮你兜底。6.2 onRefresh与finishBeanFactoryInitialization谁先谁后这里有个顺序关系值得单独强调Web服务器在onRefresh里先被创建并启动端口监听但此时DispatcherServlet等Web相关的Bean可能还没完成实例化。SpringBoot的巧妙处理是Tomcat启动时不会马上对外提供服务——实际上这时候请求进来也是能监听到TCP连接的只是业务链路还没完全就绪。真正可以接受请求的状态要到refresh完成之后、ApplicationReadyEvent发布之前的那一瞬间才成立。这也是为什么你通过健康检查脚本探测端口时端口虽然已经通了但应用还没完全ready需要再等一小会儿才返回UP。很多部署脚本踩过这个坑端口一监听就立刻发流量结果应用还在执行ApplicationRunner或初始化缓存导致首批请求超时。6.3 ApplicationRunner与CommandLineRunner启动收尾的钩子refresh方法执行完后run方法会发布ApplicationStartedEvent紧接着执行callRunners。这一步SpringBoot会把容器里所有的ApplicationRunner和CommandLineRunner找出来依次执行它们的run方法。两者的区别很简单ApplicationRunner的run方法接收一个封装好的ApplicationArguments对象可以方便地获取参数名和参数值CommandLineRunner的run方法接收原始String数组稍显原始两者的执行顺序同样可以通过Order注解控制。在实际项目中我习惯把数据初始化、缓存预热、敏感信息加载等操作放进ApplicationRunner里做因为它的参数处理更友好。但要记住这些Runner的执行是同步的如果它内部长时间阻塞应用虽然已经启动完成但一直等不到ApplicationReadyEvent健康检查会一直处于DOWN状态。7. 启动阶段高频故障排查实录从现象逆推流程中的断点讲完整个启动链路我带大家看几个真实场景用从现象反推流程断点的思路来复盘。这也是我写这篇文章的初衷——启动流程不是背出来的八股文而是排查问题时的导航地图。7.1 端口能通但404——问题多半出在ComponentScan现象应用日志显示启动成功Tomcat端口正常监听但访问任何接口都返回404。排查链路先确认DispatcherServlet有没有被创建。如果在日志里看不到RequestMappingHandlerMapping注册的接口信息说明Spring MVC的配置没有生效。检查SpringBootApplication所在类的位置。如果主启动类放在com.example.admin而Controller在com.example.web默认的组件扫描就扫不到Controller。再看是不是有多个ApplicationContext。如果引入了某些老框架它可能自己创建了一个独立的WebApplicationContext接口注册到了另一个容器里。最后检查spring.mvc.servlet.path或server.servlet.context-path配置。这个配置如果被误设接口路径整体加前缀也表现为404。这个问题的根源本质上就是启动流程第5步的组件扫描边界出了问题。主启动类位置决定了扫描根这一点我再强调一次。7.2 启动卡在Root WebApplicationContext: initialization completed现象日志停在这一行之后长时间没有进展进程不退出也不报错。排查链路这一个卡住的阶段对应的是refresh第9步之后、第11步之前——也就是onRefresh创建Web服务器之后的Bean实例化阶段。最可能的原因是某个Bean的构造器或PostConstruct方法里有阻塞操作比如等待数据库连接、调用外部RPC接口超时、递归初始化死循环。用jstack导出线程栈看阻塞点落在哪个类的哪个方法一击命中。我遇到过的典型案例是一个连接池预热的PostConstruct方法它在启动时尝试建立100个数据库连接但数据库连接池的最大连接数配置只有50结果互相等待直到连接超时。解决方式是调整预热逻辑把同步预热改成异步或者先设一个较大的连接池上限。7.3 多个ServletWebServerFactory的Bean冲突现象启动直接报错提示有一个以上ServletWebServerFactory类型的Bean。排查链路检查classpath里是不是同时引入了spring-boot-starter-tomcat和spring-boot-starter-jetty。保留一个排除另一个dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId exclusions exclusion groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-tomcat/artifactId /exclusion /exclusions /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jetty/artifactId /dependency如果你需要两种服务器共存比如一个负责REST、一个负责内部管理接口可以手动配置两个ServletWebServerFactory并分别指定端口但这是高级玩法不建议新手一上来就尝试。这个冲突点发生在自动装配阶段两个Factory都被注册成Bean了第9步onRefresh要找一个时发现有两个候选只能抛异常。理解了流程断点你看到这种报错根本不需要翻日志直接去依赖里查重即可。7.4 版本太高类问题与启动日志中的密文配置解密热词里有一条springboot版本太高这其实是真实存在的问题SpringBoot 3.x要求JDK 17如果项目还在用JDK 8强行升级必然启动失败另外某些第三方starter的版本跟不上SpringBoot大版本升级也会出现自动配置类的方法签名不兼容启动时抛NoSuchMethodError、ClassNotFoundException。还有一种情况SpringBoot 3.x里javax.*包换成了jakarta.*包如果你的老代码里还有import javax.servlet.*启动时根本过不了编译。这些问题都能追溯到启动流程第1步之前的依赖准备阶段——main方法都还没进呢类加载就失败了。至于springboot yml密文常见做法是在配置准备阶段用EnvironmentPostProcessor解密带特定前缀的配置值。解密动作发生在PropertySource加载完成之后、各Bean真正读取配置之前这样保证所有地方拿到的都是明文。如果你打算自己实现记住EnvironmentPostProcessor要注册在spring.factories里而且不要在实现里依赖任何Spring容器的Bean——因为它本身执行得很早容器还没完成装配。7.5 启动慢的排查要点再补充一个所有项目都可能遇到的启动速度慢。从启动流程的角度看慢的地方通常是这几块ComponentScan扫描的包太多这个可以用spring-context-indexer生成候选组件索引来缓解finishBeanFactoryInitialization阶段实例化的单例Bean太多可以考虑把不急着用的Bean改为懒加载或者用spring.main.lazy-initializationtrue全局开启但要注意副作用懒加载Bean在首次使用时才创建可能把启动期故障延迟到运行期才暴露自动配置类评估了大量ConditionalOnClass虽然每个检查很快但量多了也有开销。可以通过排除用不上的自动配置类来提速比如SpringBootApplication(exclude {DataSourceAutoConfiguration.class})8. 最后再说两点实际体会启动流程我读了不下五遍每一遍都有新的收获。第一次读是背面试题知道run方法会调refresh、refresh里有12个步骤第二次是排查线上启动失败开始去理解每一步之间谁先谁后、某个异常出现在哪两个断点之间第三次是写自己的starter、做公司基础框架的时候猛然意识到自动装配的本质就是在特定时机把BeanDefinition批量注册进去。这个境界的提升和具体的技术细节无关纯粹是看问题视角的转变——从用它到驾驭它。如果你也想彻底掌握启动流程我的建议是不要只看SpringBoot的代码一定要配合SpringFramework的refresh方法一起读。SpringBoot本身并没有发明一套新的容器启动机制它只是在一个成熟的容器启动框架上利用模板方法模式在特定位置插入了自己的逻辑。理清哪些是SpringFramework的、哪些是SpringBoot的你就拥有了一张完整的启动地图。另外一个技巧是善用Actuator的/actuator/beans端点如果配置了启动完成后可以看看哪些Bean被创建了、哪些条件没满足被跳过了。这个端点对理解自动装配的生效范围非常有帮助。最终启动流程不是一个需要死记硬背的知识点而是一张排查地图。下次启动出问题时对照流程找断点定位速度会比胡乱试快得多。
返回列表