ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Boot启动链路全解析:从main方法到Tomcat与自动配置

Spring Boot启动链路全解析:从main方法到Tomcat与自动配置 你有没有想过当你在IDE里轻轻点下那个绿色的运行按钮一行“Started Application in 1.2 seconds”打印出来Spring Boot到底做了什么很多同学用Spring Boot两三年Controller、Service、Mapper写得飞起面试被问“启动原理”直接卡壳。这篇文章我用实际代码和调试过程把Spring Boot的启动链路掰开揉碎讲清楚——从main()方法到内嵌Tomcat起服务再到自动配置的魔法生效中间到底走了哪些关键节点、涉及哪些核心组件、哪些地方容易踩坑。适合想搞清楚框架本质、准备面试或者排查启动异常的纯Java开发者。1. 启动入口Main方法之后的第一个动作是什么1.1 先分清SpringApplication和应用本身很多人以为Spring Boot启动是“从main方法开始执行Spring代码”这没问题但太笼统。准确地说main方法只是JVM的入口真正负责启动Spring应用的是SpringApplication这个类。我们写的启动类长这样SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }SpringApplication.run()是静态方法它内部会做两件大事先new一个SpringApplication实例再调用实例的run(args)方法。这个过程网上很多人一笔带过但拆分清楚信息量很大。SpringApplication构造过程主要做了这么几件事通过SpringFactoriesLoader从META-INF/spring.factories里加载ApplicationContextInitializer和ApplicationListener列表。根据WebApplicationType推断当前应用类型NONE普通应用、SERVLETServlet容器应用、REACTIVEWebFlux响应式应用。找到包含main方法的类也就是主配置类保存起来后面做配置源。我第一次跟踪这段代码时印象最深的是启动监听器和初始化器在”启动“之前就已经加载好了。这意味着即使run()还没执行Spring已经通过机制拿到了所有广播器和回调组件。这个设计让后续run过程中能随时广播状态事件比如“环境准备好了”“上下文创建了”“刷新完成了”。1.2 run()方法的执行阶段run()方法执行链路大致如下1. 记录启动时间创建SpringApplicationRunListeners并启动 2. 准备Environment系统环境变量、JVM参数、配置文件 3. 创建ApplicationContext根据WebApplicationType选择实现类 4. 准备ApplicationContext设置Environment、执行Initializer、扫描主配置类 5. 刷新ApplicationContext执行bean工厂post处理器、注册bean、处理自动配置 6. 调用runner回调广播ApplicationReadyEvent这里最值得关注的是第5步“刷新上下文”它调用的是AbstractApplicationContext.refresh()这个方法是整个Spring框架IoC容器启动的核心。Spring Boot的很多魔法包括自动配置、条件注解、内嵌服务器启动全部都在refresh过程中被触发。我用一个生活化类比帮助理解main()方法就像你去餐厅的大门SpringApplication是服务员带路的流程Environment相当于菜单和座位安排ApplicationContext是厨房和餐厅本身refresh()是后厨备菜、灶台起火的完整流程。所有菜Bean在refresh阶段被洗好、切好、下锅。1.3 启动监听器与事件广播机制SpringApplicationRunListener是Spring Boot新增的一层事件机制和Spring原生的ApplicationListener不一样。SpringApplicationRunListener专门监听Spring Boot启动过程的多个阶段阶段对应的监听器方法典型用途启动开始starting()打印启动日志、注册JMX环境准备environmentPrepared()附加配置源、激活profile上下文创建contextPrepared()空上下文创建后回调上下文加载contextLoaded()主配置类加载完成后启动完成started()广播ApplicationStartedEvent运行中running()广播ApplicationReadyEvent启动失败failed()记录异常、处理错误页实际开发中很多人会在启动时打印Banner或者做一些环境检查与其硬编码一个PostConstruct不如实现ApplicationRunner或CommandLineRunner它们在run()跑完、容器刷新完毕之后执行比PostConstruct更可靠——因为此时所有Bean已经初始化完成。注意ApplicationRunner和CommandLineRunner的区别只在于参数封装前者拿到的是解析后的ApplicationArguments后者拿到的是原始字符串数组。真正跑业务还是推荐ApplicationRunner因为它可以拿到--server.port8081这种拆好的键值对。2. 自动配置的魔法spring.factories与EnableAutoConfiguration2.1SpringBootApplication其实是个组合注解这个注解大家天天见但拆开看它是三个注解的组合体SpringBootConfiguration EnableAutoConfiguration ComponentScanSpringBootConfiguration就是Configuration的别名标记当前类是配置类。ComponentScan让Spring自动扫描主类所在包及其子包。EnableAutoConfiguration是自动配置的总开关它通过Import(AutoConfigurationImportSelector.class)引入了一个导入器。真正让“自动配置”生效的是AutoConfigurationImportSelector。它做的事情是通过SpringFactoriesLoader.loadFactoryNames()去读取固定的配置文件——老版本Spring Boot 2.4之前是META-INF/spring.factoriesSpring Boot 2.4之后改成了META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports。我当初从2.3升级到2.7时自定义starter里面的自动配置类突然不生效了排查半天最后发现就是因为新版本不再读取spring.factories里的EnableAutoConfiguration键必须把配置类写进AutoConfiguration.imports文件。这个坑非常典型团队里至少三个人遇到过。2.2 条件注解决定自动配置是否启用的关键自动配置类加载进来了不代表里面的Bean一定会注册。每个自动配置类上面都挂着一堆条件注解只有条件满足时才会创建Bean。这些条件注解是Spring Boot从Spring 4引入条件化Bean之后扩展出来的我能想到的最典型例子是RedisAutoConfigurationAutoConfiguration ConditionalOnClass(RedisOperations.class) EnableConfigurationProperties(RedisProperties.class) public class RedisAutoConfiguration { Bean ConditionalOnMissingBean(name redisTemplate) public RedisTemplateObject, Object redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplateObject, Object template new RedisTemplate(); template.setConnectionFactory(connectionFactory); return template; } }这个自动配置只在RedisOperations类存在也就是你引入了Spring Data Redis依赖时才生效同时如果你已经手动声明过一个名为redisTemplate的Bean它就用你的不会重复创建。这就是ConditionalOnMissingBean的威力。下面这几个条件注解是使用频率最高的我把它们整理成表格方便对照条件注解判断依据应用场景ConditionalOnClassclasspath中是否存在指定类按依赖决定是否加载配置ConditionalOnMissingBean容器中是否已存在指定Bean给用户覆盖自动配置的机会ConditionalOnProperty配置文件中的属性值通过配置开关模块功能ConditionalOnWebApplication是否是Web环境Servlet与响应式配置分流ConditionalOnExpressionSpEL表达式复杂条件组合ConditionalOnThreading线程池类型虚拟线程配置场景实际工作中如果你想“改掉Spring Boot的默认配置”最高频的做法就是利用ConditionalOnMissingBean——在自己的配置类里声明一个相同的Bean类型自动配置看到已有Bean就不去创建。2.3 自动配置的覆盖顺序与排除方式自动配置类不是乱序执行的AutoConfigurationImportSelector通过AutoConfigureBefore和AutoConfigureAfter注解显式排定先后顺序。比如DataSourceTransactionManagerAutoConfiguration会晚于DataSourceAutoConfiguration执行这样才能拿到数据源Bean。如果某个自动配置类坑了你比如引入某个依赖后自动创建了你不想要的Bean有两种办法干掉它spring: autoconfigure: exclude: - org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration或者在启动类上写明SpringBootApplication(exclude RedisAutoConfiguration.class)我个人的经验是优先用exclude而不是去改Spring的源码逻辑。排除之后需要的基础Bean比如RedisTemplate自己手动建一个就行非常简单。3. 内嵌容器与ApplicationContext初始化3.1 ServletWebServerApplicationContext的逻辑很多初学者问Spring Boot的项目没有单独的web.xml也没有外部Tomcat那Servlet容器是怎么来的答案藏在ApplicationContext的类型选择上。在SpringApplication.run()创建上下文时如果检测到当前是SERVLET类型就会创建一个ServletWebServerApplicationContext。这个类继承自GenericApplicationContext但它额外实现了WebServerApplicationContext接口—它不仅仅是一个IoC容器还是一个持有WebServer也就是Tomcat、Jetty等的容器。ServletWebServerApplicationContext在onRefresh()阶段会调用createWebServer()方法这个方法的逻辑是从容器中查找ServletWebServerFactory的Bean。默认情况下Spring Boot自动配置了TomcatServletWebServerFactory因为你引入了spring-boot-starter-web默认携带Tomcat依赖。调用factory.getWebServer(servletContextInitializerBeans)创建WebServer传入初始化器列表。实际过程中getWebServer会实例化一个Tomcat对象、设置端口、添加Connector、启动线程最终返回可用的WebServer实例。一句话内嵌容器的生命周期被托管在了Spring的应用上下文里。3.2 端口配置与多实例启动的底层处理配置server.port表面看只是改一个配置文件的值底层其实涉及ServerProperties这个ConfigurationProperties类。ServerProperties被绑定到Spring Environment的server.*前缀上而TomcatServletWebServerFactory在构建WebServer时会读取这些配置。有个实用小技巧分享给大家如果你希望同一份代码可以同时启动多个实例做调试命令行不写固定端口而是写java -jar app.jar --server.port00代表随机端口Tomcat会自己找可用端口。启动日志里会打印实际端口。这在本地联调多个微服务实例、模拟负载均衡时非常有用比手动改配置文件高效得多。3.3 内嵌容器的优雅关闭与停机钩子这是很多人忽略的点。正常通过kill -9杀掉Java进程容器会非正常关闭子线程可能没来得及释放连接池。Spring Boot 2.3之后支持优雅停机server: shutdown: graceful spring: lifecycle: timeout-per-shutdown-phase: 30s此时kill -15触发停机后Tomcat先停止接收新请求给已有请求最多30秒处理时间处理完再销毁Bean、关闭连接池。这一点在微服务场景特别重要——如果RPC框架没有优雅停机服务提供方被kill的瞬间调用方会因为连接断开而拿到一堆超时错误。4. 相关组件协作与实战排查技巧4.1 从启动日志定位问题我调试启动问题几乎不靠猜全靠日志定位。启动过程的关键日志节点有Creating shared instance of singleton bean xxxController Tomcat started on port(s): 8080 (http) with context path Started DemoApplication in 1.2 seconds (process running for 1.5)如果哪天“Tomcat started on port”迟迟不出现说明问题出在ServletWebServerApplicationContext创建容器的环节——通常是端口被占用、Tomcat初始化失败或者ServletContextInitializer抛异常。如果“Started Application”没打印说明refresh()阶段抛错了基本就是Bean初始化有问题。最离谱的一次排查经历有个项目启动要20多秒翻日志发现卡在“Connector initialization”。最后定位到是Tomcat为了解析Session Cookie每次启动要对/dev/random读取熵而服务器熵池不够导致随机数生成阻塞。最后通过在启动参数加-Djava.security.egdfile:/dev/urandom解决。这种问题不深入了解容器启动细节根本联想不到。4.2 自动配置不生效的典型场景我这边整理了一份自动配置不生效的排查清单非常实用症状排查方向引入依赖后配置没生效检查依赖是否在classpath条件注解是否满足自定义Bean没覆盖默认检查方法名、Bean名是否和自动配置一致配置类的Configuration没被检查确认ComponentScan扫描范围是否覆盖排除后仍生效检查排查路径是否准确RESTful包的类名是否正确条件注解引用了错误属性检查ConditionalOnProperty的prefix和name实操时最有效的一个招数是启动时开启自动配置报告。SpringApplication app new SpringApplication(DemoApplication.class); app.setAdditionalProfiles(debug); app.run(args);或者直接在配置文件里写debugtrue启动后控制台会打印一个CONDITIONS EVALUATION REPORT里面明确列出每个自动配置类匹配成功还是失败以及为什么不匹配。这也是网上大家常说debugtrue看条件报告的原因排查条件不生效真的又快又准。4.3 结合版本迁移聊相关组件的影响近两年Spring Boot 3.x的升级带来了不少启动相关的变化。最突出的一点是Spring Boot 3基于Jakarta EE 9javax.servlet.*全部换成了jakarta.servlet.*。从Spring Boot 2项目升级到3时如果代码里直接出现了javax.servlet包的类启动时就会出现ClassNotFound异常。另外一个相关组件变化是Spring Security迁移。Boot 2.7到3.x之间SecurityFilterChain配置方式变化比较大原来的WebSecurityConfigurerAdapter被废弃替换成了SecurityFilterChain的Bean方法。升级之后如果不改配置启动阶段会直接因为依赖过期报错实际工作中升级前一定要看看SecurityConfig是否需要重写。提到最近的热门话题Java 21虚拟线程配合Spring Boot 3.5这属于启动层面影响全局配置的场景。开启虚拟线程的方式spring: threads: virtual: enabled: true配置开启后Tomcat接收请求的Executor会被换成虚拟线程执行器。直观感受是默认最大线程数200的Tomcat不再成为瓶颈虚拟线程能撑起远高于传统线程的高并发场景。不过我有两点提醒第一需要Spring Boot 3.2及以上的版本旧版本配置这个属性直接无效第二虚拟线程更适合IO密集型任务如果你在代码里大量使用synchronized或者CPU密集计算收益会大打折扣。4.4 实际项目中排查启动慢的三个思路如果你的Spring Boot项目启动要10秒以上除了检查基建配置还有几个常见原因值得关注自动配置的Jar包扫描范围过大SpringBootApplication默认扫描主类所在包及其子包。如果主类放在最顶层包扫描范围覆盖整个项目的全部子包长时间扫描也就很正常。保持包结构整洁主类放在com.company.project下而不是com.company可以避免无谓扫描。应用上下文的初始化器太多引入了太多ApplicationContextInitializer每个初始化器里有数据库或远程调用会拖慢启动。如果组件不再需要尽早移除依赖。Bean初始化顺序错误导致循环依赖Spring Boot 2.6之后默认禁止循环依赖启动时直接报错。如果项目里以前有A依赖B、B依赖A的写法升级后必挂。推荐用构造器注入或者Lazy解决而不是靠修改启动参数绕过去。有一次排查一个启动慢到30秒的项目最后发现是自动配置报告里有个ElasticsearchRestClientAutoConfiguration一直匹配成功而项目本身根本没用ES只是传递依赖里带了一个高层客户端包。直接把对应自动配置排除掉启动时间从30秒降到8秒。这也是为什么我一直强烈建议排查启动慢一定要先看条件评估报告哪些自动配置被加载了、哪些没加载一眼就能看清。5. 结合日常开发看懂启动原理带来的实际收益5.1 自定义Starter的完整套路理解了自动配置原理之后写一个自定义Starter变得很简单。核心就三步第一步创建一个自动配置类AutoConfiguration ConditionalOnClass(MyService.class) EnableConfigurationProperties(MyProperties.class) public class MyServiceAutoConfiguration { Bean ConditionalOnMissingBean public MyService myService(MyProperties properties) { return new MyService(properties.getPrefix(), properties.getSuffix()); } }第二步在resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件里写一行com.example.MyServiceAutoConfiguration第三步定义ConfigurationProperties类ConfigurationProperties(prefix my.service) public class MyProperties { private String prefix; private String suffix; // getter/setter }这样其他项目引入这个starter依赖之后只要在配置文件里写上my.service.prefixhelloMyService就会被自动创建出来。整个过程没有一行代码侵入业务项目组件全部由Spring Boot自动装配完成。这个模式也是我们在中大型团队里做组件复用的标准方式把公共能力日志记录、鉴权过滤、接口签名校验封装成starter业务方只需引入依赖并做少量配置。5.2 启动生命周期中可扩展的钩子Spring Boot给了开发者多个“往启动流程里插入逻辑”的时机合理运用可以让业务代码更整洁扩展点执行时机典型场景ApplicationRunner容器刷新完成run()结束前数据初始化、路由预热CommandLineRunner容器刷新完成run()结束前参数解析ApplicationContextInitializer上下文创建后、刷新前注入自定义EnvironmentApplicationListener对应事件触发时处理特定事件PostConstruct每个Bean初始化阶段Bean自身初始化BeanFactoryPostProcessorBean定义加载完成、实例化之前修改Bean定义属性我自己最常用的就是ApplicationRunner做“启动预热”——比如把热点数据加载到Redis缓存、预编译某些动态代理类。但这里也要注意一个坑ApplicationRunner抛异常会让启动失败如果预热逻辑本来就不影响核心功能记得用try-catch包一下。5.3 性能优化视角启动加速理解启动链路后加速启动的思路就很清晰了。优化的维度基本围绕三点减少被扫描的类ComponentScan的basePackages精确到业务包避免扫描到无关的包。减少自动配置的加载用打印条件评估报告的方式找到用不到的自动配置加上exclude或调整依赖。使用懒加载spring.main.lazy-initializationtrue可以在启动时跳过非必要Bean的初始化但生产环境慎用——懒加载会拖慢第一次请求并且掩盖一部分Bean初始化错误让问题更难定位。还有一点经常被忽视使用分层打包和自定义JVM参数可以缩短启动时间但这属于容器编排层面的优化和Boot本身的关系反而是次要的。对于普通应用先保证Bean定义精简、自动配置干净启动时间基本都能控制在3秒以内。我个人在实际操作中的体会是不要为了优化而优化启动快慢要放在整套基础设施环境里看。开发环境和生产环境配置不同、依赖不同启动耗时差5倍是常有的事。条件评估报告是排查启动问题的X光片比任何猜测都管用。最后再分享一个小技巧如果你真的需要把一个Spring Boot应用启动时间压到极限可以试试Spring Boot 3.4之后对CDSClass Data Sharing和AOT编译支持得更好配合GraalVM Native Image启动时间能压到几十毫秒。当然那又是另外一条技术路线了普通场景下先理解好启动链路、熟练使用条件报告和启动事件才是性价比最高的基本功。
返回列表