
写Spring Boot的启动过程这个题我太熟悉了。最近在帮团队排查一个老旧项目的启动超时问题顺手把Spring Boot从main方法到业务就绪的完整链路重新梳理了一遍又翻了翻源码结合自己平时踩坑的经验整理成这篇东西。无论你是准备“springboot面试题”的求职者还是已经在“springboot项目”里折腾过一段时间的开发者这篇文章都能帮你把启动过程这块理解得更透。我尽量不写那种“从入门到放弃”的教科书内容而是从实际工作视角出发讲讲Spring Boot到底是怎么把应用拉起来的每个环节为什么这样设计哪些地方容易出问题以及怎么排查。1. 先建立整体认知Spring Boot启动到底在做什么1.1 一句话版本启动过程是“资源盘点 环境准备 工厂开工”如果你用一句话向别人解释Spring Boot启动过程可以这样说Spring Boot启动本质上就是“把当前项目里所有依赖的组件、配置、类扫描进来然后按照预设规则把Bean工厂初始化好最后把Web服务跑起来”。这句话听起来简单但里面藏了很多细节。很多人在“springboot配置”上遇到问题或者在“springboot版本太高”之后项目起不来根源都是对启动过程缺少整体认知只知道点“Run”按钮却不知道背后发生了什么。所以咱们先建立一张完整的认知地图后面再逐个环节拆开。1.2 为什么Spring Boot能“一键启动”而传统Spring项目不行要理解启动过程必须先回答一个问题为什么Spring Boot一个main方法就能全自动跑起来而传统Spring项目要配一堆XML还要部署到Tomcat里答案在三个核心设计上自动装配AutoConfigurationSpring Boot通过约定大于配置把常用框架的初始化逻辑写成自动配置类你只要引入了对应的starter它就自动帮你把相关Bean创建好。内嵌容器Spring Boot把Tomcat、Jetty、Undertow这些Web服务器内嵌到应用里启动时直接创建容器实例省去外部部署步骤。条件化配置通过Conditional系列注解根据当前classpath、配置项、Bean存在与否来决定是否创建某个Bean这就是“智能”的底层逻辑。这三个设计共同决定了Spring Boot启动过程的特点自动、可预测、可调节。你不需要知道每一项配置具体怎么生效但当你需要排查问题时就必须要知道它会在哪个阶段被加载。1.3 宏观阶段划分启动过程是一根完整的链条从宏观角度来看Spring Boot启动过程可以分为五大阶段启动入口main方法调用SpringApplication.run()。实例化准备创建SpringApplication对象确定应用类型、加载启动类、初始化initializer和listener。环境准备准备Environment加载application.properties/application.yml配置准备命令行参数。容器初始化创建ApplicationContext执行BeanDefinition扫描和注册执行自动装配实例化所有单例Bean触发各种扩展点。完成启动启动内嵌Web服务器发布 ApplicationReadyEvent 事件等待请求。每个阶段之间是有严格先后顺序的而且Spring Boot在关键节点都会发布事件。你监听这些事件就能在特定时机插入自己的逻辑。这不只是面试点更是日常开发做监控、做预热、做动态配置刷新的基础。2. SpringApplication.run()主链路怎么走从实例化到容器刷新2.1 创建SpringApplication示例启动过程的第一步就藏了很多细节很多人的认知是“Spring Boot启动就是从main方法开始执行”这句话没错但不够精确。main方法里调用的SpringApplication.run(Application.class, args)其实是分两步走的第一步new一个SpringApplication实例第二步调用这个实例的run方法。在new的过程中SpringApplication的构造器会做四件关键的事推断应用类型通过classpath中是否存在特定类来判断当前是Servlet Web应用、响应式Web应用还是非Web应用。举个例子如果classpath里有javax.servlet.Servlet和org.springframework.web.context.ConfigurableWebApplicationContext它就判定为Servlet Web应用如果存在org.springframework.web.reactive.DispatcherHandler且不存在Servlet类就是响应式应用。读取配置主类找到标注了SpringBootApplication或其组合注解中包含SpringBootConfiguration的类这就是后面扫描包的起点。加载ApplicationContextInitializer从spring.factories新版是META-INF/spring.factories或org.springframework.boot.autoconfigure.AutoConfiguration.imports里读取所有ApplicationContextInitializer实现类并实例化。加载ApplicationListener同理把启动过程中需要监听的事件监听器全部加载进去。如果你看源码会发现这一步还会设置一些默认属性比如设置spring.beaninfo.ignore的默认值、设置系统退出码等。这些细节平时很少关注但在排查“为什么某个Listener没生效”时就得回到这一步找原因。2.2 run方法的“主循环”从StopWatch到监听器new完实例后进入run方法主体。Spring Boot这里用一个StopWatch计时所以启动日志里才有一句“Started Application in X seconds”。整个run方法可以用下面的思路来理解先加载SpringApplicationRunListeners这是专门监听运行过程的监听器。然后逐个调用监听器的starting()方法表示启动开始。紧接着准备Environment解析命令行参数并写入环境变量。打印Banner。根据应用类型创建ApplicationContext。通过prepareContext准备上下文包括注册启动类、加载BeanDefinition等。调用refreshContext容器刷新的核心操作。最后回调running()发布ApplicationReadyEvent启动结束。这里要注意ApplicationRunListeners和ApplicationListener是两套东西。前者是Spring Boot内部用来回调启动状态的后者是用户在容器层面监听事件的。两者职责不同千万别搞混。2.3 环境准备与配置加载前端配置从这里进入Bean环境准备是整个启动过程中最容易出问题也最值得详细讲的一环因为它直接关联日常开发中的“springboot配置”工作。Environment对象在Spring Boot里是一个大容器里面可以有多个PropertySource各自维护一组键值对。这些PropertySource的加载顺序是有讲究的从前往后优先级递减Devtools全局配置测试环境TestPropertySource注解命令行参数SPRING_APPLICATION_JSON中的属性ServletConfig和ServletContext参数JNDI属性Java系统属性System.getProperties()操作系统环境变量RandomValuePropertySourcejar包外部的application-{profile}.properties/ymljar包内部的application-{profile}.properties/ymljar包外部的application.properties/ymljar包内部的application.properties/ymlPropertySource注解加载的配置这个顺序非常重要实际中最常见的坑就是“我明明改了配置文件为什么没生效”大多数时候就是没搞懂优先级。比如同名key在命令行参数里的值一定会覆盖application.yml里的值这是设计好的规则不是Bug。另外profile的加载发生在Environment准备的早期。你可以在配置文件名上用application-{profile}.properties来区分环境也可以用spring.profiles.active指定当前生效的profile。Spring Boot加载主配置文件后会根据profile再次加载对应文件后加载的会覆盖先加载的同名配置。2.4 Web服务启动时机容器刷新完成后才真正拉起Tomcat有一个很多人没有注意到的细节内嵌Tomcat不是在run方法一开始就启动的而是在ApplicationContext refresh阶段onRefresh方法里创建WebServer然后start方法启动它。也就是说Spring容器的初始化和Web服务器的启动是交织在一起的。具体流程是refreshContext触发AbstractApplicationContext的refresh方法。refresh过程中调用onRefreshServletWebServerApplicationContext重写了这个方法创建ServletWebServerFactory并准备WebServer。容器刷新完成后执行finishRefresh里面会启动WebServer。此时Tomcat端口开始监听但应用还没完全启动完成因为Bean的初始化可能还没结束实际上在refresh最后一步finishBeanFactoryInitialization已经完成了单例Bean的预实例化但一些非单例Bean和后置处理仍在排队。搞懂这个时间线再去分析“端口已被占用”“为什么日志显示Tomcat started on port 8080但没有继续执行”这类问题思路就清晰很多。3. 自动装配原理Spring Boot启动里最硬核的一环3.1 自动装配从哪儿读取配置从spring.factories到AutoConfiguration.imports很多人把“springboot自动装配原理”挂在嘴边但一问细节就卡壳。自动装配的起点是主启动类上的SpringBootApplication注解。这个注解由SpringBootConfiguration、EnableAutoConfiguration和ComponentScan三个注解组合而成。其中EnableAutoConfiguration是核心它通过Import(AutoConfigurationImportSelector.class)引入了一个选择器。这个选择器做的事情简单来说就是“去classpath里找所有自动配置类的全限定名列表然后按条件筛选”。老版本Spring Boot 2.7之前的自动配置类列表配置在META-INF/spring.factories文件里以EnableAutoConfiguration为key后面跟着一堆配置类全限定名。新版本2.7之后为了减少无意义的类加载改成了META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件一行一个类名。这个变化对普通应用开发者来说是无感的因为你引入的starter已经在自己的jar包里放好了对应文件。但如果你自己写starter就必须注意版本差异别用老格式去适配新版本否则会加载不到自动配置类。3.2 Conditional条件装配为什么POM里一加依赖功能就自动生效自动装配类给你看其实是一堆标注了Conditional注解的Bean定义。每个自动配置类在做任何实事之前都会先用条件注解“问自己三个问题”我需要的类在不在classpath中看ConditionalOnClass。用户有没有手动配置过相关Bean看ConditionalOnMissingBean。配置项的值对不对看ConditionalOnProperty。当年我看到这堆条件注解时第一反应是“这不就是把if-else拆到注解上了嘛”。但实际上它的价值比if-else大得多因为条件判断发生在容器扫描和Bean定义注册阶段而不是Bean实例化阶段。这就意味着Spring可以在不实例化任何类的前提下提前决定哪些配置类需要跳过这对启动性能是友好的。举一个实际例子RedisAutoConfiguration上面有ConditionalOnClass(RedisOperations.class)如果项目里没引入spring-boot-starter-data-redis那么classpath里就不存在RedisOperations自动配置整个跳过不会浪费任何初始化开销。一旦你引入starter条件满足RedisTemplate、StringRedisTemplate这些Bean就被自动创建出来了。这就是为什么别人常问“你引入一个starter什么配置都没写为什么能用”答案就在条件装配里。3.3 配置优先级与覆盖规则自动配置类和你的自定义Bean谁说了算自动装配虽然方便但它的设计原则是“让你能覆盖”而不是“替你独断”。Spring Boot在自动配置类上大量使用ConditionalOnMissingBean意思是“当容器里没有某种Bean时我才创建默认的”。这个设计有一个很实用的推论如果你想让某个组件的行为变成自定义的只要在配置类里自己定义一个同名类型的Bean即可。比如默认的ObjectMapper太中庸你可以在配置类里写一个ObjectMapper对象自动配置类看到已经存在ObjectMapper就会放弃创建默认的。但这里的坑在于ConditionalOnMissingBean的判断时机很微妙。如果自动配置类的判断发生在你的配置类注册之前可能你的自定义Bean还没被扫描到自动配置判断“缺少Bean”就自己创建了默认Bean等你的Bean注册后容器里出现两个同类型Bean启动直接报NoUniqueBeanDefinitionException。排查这类问题时我的经验是先确认你的配置类和自动配置类谁先注册。如果没有特殊处理加AutoConfigureBefore或AutoConfigureAfter可以控制自动配置类的执行顺序或者直接把自定义配置类放到扫描路径更靠前的位置用Order调整不行因为Bean定义注册顺序不完全由Order决定。4. 实操过程把启动过程变成可观测的调试现场4.1 启动日志逐行读从“Starting Application”到“Started Application”我平时排查启动问题第一件事不是看代码而是看启动日志。Spring Boot的启动日志信息量很大值得逐行读。一个典型的启动日志长这样2025-01-15 10:00:00.001 INFO 12345 --- [ main] com.example.DemoApplication : Starting DemoApplication using Java 17 on hostname with PID 12345 2025-01-15 10:00:00.101 INFO 12345 --- [ main] com.example.DemoApplication : No active profile set, falling back to 1 default profile: default 2025-01-15 10:00:00.502 INFO 12345 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) 2025-01-15 10:00:00.702 INFO 12345 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2025-01-15 10:00:02.312 INFO 12345 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path 2025-01-15 10:00:02.800 INFO 12345 --- [ main] com.example.DemoApplication : Started DemoApplication in 2.799 seconds (process running for 3.222)注意这些日志的顺序第一条日志是SpringApplication.run刚进入时打的此时还没有Environment准备。第二条日志说明profile加载完成。“Tomcat initialized”说明容器已创建WebServer实例但还没开始监听端口。“Tomcat started on port”说明端口已经监听但容器刷新仍在进行中。最后一条“Started DemoApplication”说明所有Bean初始化完成refresh成功ApplicationReadyEvent已经发布。如果你的应用在第二条日志后卡住了说明卡在配置加载或环境准备阶段如果卡在“Tomcat initialized”和“Tomcat started”之间说明创建WebServer工厂或初始化连接器时出了问题。4.2 定位启动耗时StopWatch从哪来的怎么找到启动慢的Bean启动变慢是Spring Boot项目常见痛点。定位启动慢的Bean我有两个常用方案第一个方案打开Bean初始化耗时可视化。在application.yml里配置logging: level: org.springframework.boot.context.embedded: DEBUG org.springframework.boot.autoconfigure: DEBUG然后把日志级别调到DEBUG再启动时会打印大量自动配置报告和Bean初始化信息。这些信息很长建议重定向到文件里再分析比如mvn spring-boot:run startup.log 21第二个方案用启动事件自制一个耗时记录器。创建一个监听器监听ContextRefreshedEvent和ApplicationReadyEvent在事件里计算差值就能得到容器刷新总耗时。如果想细化到单个Bean可以对BeanFactoryPostProcessor和BeanPostProcessor动手脚但一般用不上日常排查用分阶段日志就够。更高级的做法是用Spring Boot 2.4之后提供的启动阶段指标StartupSteps。在SpringApplication里设置ApplicationStartup为BufferingApplicationStartup启动后从上下文取出来处理就能看到每一个启动步骤的耗时记录适合做性能调优。4.3 自定义Banner版本信息和个性化展示都藏在这里聊聊轻松一点的。网上经常看到“springboot banner生成器”“springboot banner在线生成”这类搜索词这背后对应的就是Spring Boot的Banner功能。Banner的加载顺序是先看classpath下有没有banner.txt或banner.image支持图片。如果指定了spring.banner.location则用指定路径。如果classpath里没有就打印默认的Spring Boot ASCII Logo。Banner绘制是在Environment准备之后、容器创建之前进行的所以它不依赖Spring容器用的是SpringBootBannerPrinter。你可以在banner.txt里用占位符引用启动信息比如“${application.title}”“${application.version}”“${spring-boot.version}”这些环境变量。如果你的做法是把Banner关掉来减少日志输出可以用spring.main.banner-modeoff。但我想提醒一句Banner在排查问题时是有用的它上面那串版本号能直接告诉你当前应用的Spring Boot版本特别是在“springboot版本太高”导致行为变化的时候一眼就能确认版本。4.4 干预启动过程的常用手段监听器、Initializer、CommandLineRunner真实项目里不可能只做“观察启动”一定需要“干预启动”。常见的干预手段有三个第一个是ApplicationRunner和CommandLineRunner。这两个接口让你在容器刷新完成后执行自定义逻辑区别是入参不同ApplicationRunner接收ApplicationArgumentsCommandLineRunner接收原始String数组。加Order控制先后顺序。这个机制常用于初始化缓存、预热连接池、启动时加载数据字典。第二个是ApplicationContextInitializer。它在容器refresh之前执行可以用来在容器刷新前注册一些必要的BeanPostProcessor或者修改Environment。如果你写过一个实现类但发现没生效请检查spring.factories里的配置是否写对以及在Spring Boot 3.x版本中initializer的加载要按新的接口规范。第三个是ApplicationListener。通过监听不同阶段的事件可以在特定时机做事。举例监听ApplicationReadyEvent执行健康检查主动探测可能比等外部探测更及时因为此时Tomcat已经监听端口但外部流量还没打进来。5. 高频问题与排查技巧实录5.1 启动失败端口冲突、Bean重复、版本不兼容启动失败是每个人都会遇到的事我把最常见的三类问题列出来。端口冲突错误日志一般是“Port 8080 was already in use”。排查方法是先找到占用进程netstat -ano | grep 8080 kill -9 8080 # 或 taskkill /F /PID也可以简单改配置端口。生产环境不建议把端口写死在配置文件里用环境变量注入更灵活比如server.port${SERVER_PORT:8080}。Bean重复或Bean找不到这类问题的日志有很强的规律性。NoSuchBeanDefinitionException说明启动时没有创建对应Bean先查自动装配条件有没有满足NoUniqueBeanDefinitionException说明有多个候选先查是不是自己又定义了一个同名类型Bean把自动配置顶了。版本不兼容就是刚才说的“springboot版本太高”的情况。最常见的是Spring Boot 3.x要求JDK 17项目的JDK版本不够直接启动失败报UnsupportedClassVersionError或干脆加载不了ApplicationContext。再有就是Spring Boot 3.x迁移到Jakarta命名空间原来javax.servlet.的类全部变成jakarta.servlet.如果你引用的依赖还是老版本启动阶段经常遇到ClassNotFoundException。遇到这类问题先核对三件事JDK版本、Spring Boot版本、依赖兼容性。5.2 循环依赖启动过程中它会在哪个环节爆发循环依赖这个坑在面试和实战中都是重点所以得讲透。先明确一点循环依赖检测发生在Bean实例化阶段也就是容器refresh时finishBeanFactoryInitialization这一步。比如A依赖BB依赖A单例默认情况下Spring容器通过三级缓存可以解决构造器之外的循环依赖但如果你用的是构造器注入或者开启了禁止循环依赖的配置spring.main.allow-circular-referencesfalse启动就会直接报BeanCurrentlyInCreationException。我的开发经验是在Spring Boot项目中尽量不用字段注入推荐构造器注入。但这会带来一个后果如果你设计不当更容易触发构造器循环依赖。解决方案不是去改allow-circular-references而是重构设计把互相依赖的部分拆开或改成方法级注入。这个点面试时很容易被深挖因为面试官关心的是你到底理解不理解Bean生命周期和三级缓存而不只是“启动会报错、把缓存打开就好”这种表面答案。5.3 事务失效为什么启动成功了但运行时发现注解不生效事务失效虽然发生在运行期但根源往往在启动阶段的扫描和代理创建环节。Spring Boot启用事务很简单在启动类或配置类上加EnableTransactionManagement就行实际上自动配置已默认启用。事务生效需要一个关键条件目标Bean必须被Spring AOP代理而且调用必须通过代理对象。常见失效场景包括同类内部调用this.method()直接绕过代理。启动过程不会报错但事务就是没效果。Bean没有被Spring管理事务自然不生效。方法非publicCGLIB代理也无法拦截。事务管理器没有对应的数据源。Transactional加在接口方法是JDK动态代理时代的方式换成Spring Boot配置后很多坑都集中在内部调用。排查这类问题除了检查代理方式最直接的办法是启动时打印Bean类型看看实际注入的是原始类还是代理类Component public class ProxyCheckRunner implements ApplicationRunner { Override public void run(ApplicationArguments args) { System.out.println(SomeService.class.getName() - someService.getClass().getName()); } }如果类型不是代理类说明代理创建有问题后面的事务自然瘫掉。5.4 常见面试题速查启动过程中最容易被问到的问题面试环节“springboot启动过程”这个话题几乎是必问。我整理了高频问题和推荐回答方向。请描述Spring Boot启动流程。回答思路从SpringApplication.run入手分为创建SpringApplication和调用run两段run里依次说监听器启动、Environment准备、Banner打印、容器创建、prepareContext、refreshContext、启动WebServer、发布事件。Spring Boot自动装配原理是怎么实现的。回答思路EnableAutoConfiguration引入AutoConfigurationImportSelector读取AutoConfiguration.imports按条件注解筛选最终注册Bean定义。Spring Boot应用启动时Bean的实例化顺序是什么。回答思路先加载BeanDefinition再按依赖关系实例化构造器注入决定依赖创建顺序DependsOn可以显式控制。Spring Boot启动过程哪些阶段最容易出问题。回答思路环境准备和refresh阶段最容易出问题包括配置冲突、循环依赖、版本不兼容。了解Spring Boot的Banner的加载流程吗。回答思路Environment准备后由SpringBootBannerPrinter打印可以关闭或自定义。这里我建议不要把面试当背书最好能把源码关键类名说出来比如SpringApplication、ApplicationContextInitializer、AutoConfigurationImportSelector、ConfigurableEnvironment、AbstractApplicationContext这会让“是否真的读过源码”一目了然。6. 版本差异与升级经验不同版本启动过程哪里不一样6.1 Spring Boot 2.x和3.x的启动链有什么变化Spring Boot 3.x是一个大版本跨越不只是版本号变高启动链路底层也有实质变化。第一JDK基线变化。3.x必须JDK 17启动时会有更严格的类文件版本校验这在JDK 8环境上会直接报错。第二Jakarta EE替换。2.x用javax.servlet等包3.x用jakarta.servlet等包启动时类加载路径会变。如果你项目里有老依赖引用了javax包启动阶段很可能因为类找不到而失败。第三自动装配配置文件格式调整。2.7之前用spring.factories2.7之后换成AutoConfiguration.imports但2.7保持兼容3.x已经完全切到新格式。自研starter的代码若是旧格式在3.x下自动装配不会生效。第四Spring Framework 6.0的底层升级比如AOT编译相关的新特性从Spring Framework 6开始支持AOT对应Spring Boot 3.x中的GraalVM Native Image支持。这个影响启动过程的主要体现在如果你用Native Image启动过程不是运行时完成的而是构建期生成加载逻辑更陡峭但启动时间非常快。6.2 版本太高导致的问题典型表现和排查方向网上“springboot 4.0 找不到aop”这种热词说明大家对版本升级后的兼容性是真有痛点。当你说“版本太高”时我遇到过的典型表现有三种。表现一是引入的第三方starter只在2.x上测试过内部依赖了旧版spring-boot-autoconfigure可能触发自动装配类冲突日志里出现多个相同前缀的自动配置报告甚至启动直接失败。表现二是Spring Boot升级后某些默认行为变了。比如2.4之后路径匹配策略从AntPathMatcher改为PathPatternParser很多api地址失效2.6开始循环依赖默认行为变化允许循环依赖的开关从正常变警告Spring Boot 3.2后默认的Http客户端可能也有变化。这类问题启动阶段可能不直接报错但运行时不工作了排查起来相当头疼。表现三是构建工具插件版本不匹配。比如maven或gradle插件版本跟不上打出来的包结构不符合新版规范启动时找不到主类。排查思路只有一个升级不能一把梭。先看Release Notes再逐个模块升级每次升级后跑一遍启动测试和核心链路回归。特别是跨大版本强烈建议逐步升级2.x - 2.7 - 3.x不要从2.1直接跳到3.2。6.3 实际工作中的启动性能优化建议最后聊一聊启动性能优化。这个问题在微服务环境下越来越重要因为服务多、启动频繁能省一秒都是实打实的成本。我的常用优化手段有下面几种懒加载spring.main.lazy-initializationtrue能显著减少启动耗时但代价是第一次请求可能更慢。适合非核心服务或异步处理场景。排除无关自动配置在SpringBootApplication上使用exclude属性或通过spring.autoconfigure.exclude配置把没用的自动配置类全部排除减少扫描和判断。关键是别过度排除否则依赖变成手动装配维护成本飙升。减少ComponentScan扫描范围默认扫描启动类所在包及其子包。如果包结构太深、类太多扫描耗时明显。把启动类尽量放在包根部比移动扫描路径简单有效。使用Spring Boot 3.x的AOT Native Image如果是云原生场景这个方法最彻底启动可以到毫秒级但构建时间长、反射配置麻烦适合对启动有极致要求的服务。自定义Banner关闭或缩小Banner打印也有微小开销生产环境一般关闭或设置成off能省一点点但最重要的是减少日志噪声。优化时务必以真实数据为准先记录优化前启动耗时再逐项优化最后对照结果不要凭感觉。个人实操中的最后体会扯了这么多说点掏心窝的话。Spring Boot启动过程这个东西表面看是一套启动日志和几个注解往里挖其实涉及Spring Framework的IoC容器、Bean生命周期、条件装配、AOP代理以及内嵌Web容器调度每一块都值得单独研究。我在实际排查问题时最大的感受是启动过程就像一条流水线任何一环出了问题后面的环节都会跟着受影响。所以不要只看报错日志最后一行要看整个启动链路卡在哪一步。最后分享一个很实用的小技巧如果你怀疑某个Bean没有按预期创建在启动时打开自动配置报告也就是在配置里设置debugtrueSpring Boot会把“匹配成功的自动配置”和“未匹配的自动配置”全部列出来上面会写清楚为什么没匹配。这个报告在排查启动阶段的问题时比任何断点调试都好用得多。启动过程这个话题纸上谈兵远不如亲手跑一遍、读一遍日志、改一次配置来得透彻希望你也能在自己的项目里把这条链路真正吃透。