拓冰建站拓冰建站
首页 / 资讯中心 / 正文

SpringBoot启动流程与自动装配原理深度解析:从main方法到条件注解

1. 从“启动流程”到“自动装配”理解SpringBoot的骨架与灵魂如果你已经跟着前几天的内容一步步搭建了项目配置了依赖也写了一些简单的接口那么今天我们得往深处挖一挖了。很多朋友用SpringBoot感觉就是“开箱即用”写个SpringBootApplication项目就能跑起来非常省心。但省心的背后其实是SpringBoot框架做了大量的“约定大于配置”的工作。今天这第七天我们不写新功能而是来彻底搞懂两件事SpringBoot项目到底是怎么启动的以及那个被说烂了的“自动装配”究竟是怎么一回事。理解了这些你才能从“会用”进阶到“懂原理”遇到启动报错、Bean加载失败、配置不生效这些问题时才能快速定位而不是盲目搜索。很多人觉得看源码枯燥但我的经验是带着问题去看把它当成一个侦探游戏会很有意思。比如你有没有想过执行main方法后第一行代码到底做了什么SpringBootApplication这个注解背后藏了多少东西为什么我们在pom.xml里加个spring-boot-starter-webTomcat就自动内嵌好了我们自己写的Configuration配置类是怎么被Spring容器发现的今天我们就围绕“启动流程”和“自动装配原理”这两个核心结合最新的SpringBoot 3.x也会对比2.x把这条线彻底捋清楚。你会发现之前很多零散的知识点比如事件监听、条件注解都会在这里串联起来。2. SpringBoot启动流程全景拆解一行main方法背后的世界让我们从一个最简单的SpringBoot应用入口开始SpringBootApplication public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } }就是这一行SpringApplication.run(...)启动了一个完整的Spring应用上下文。我们来一步步拆解它背后发生的故事。2.1 SpringApplication的初始化收集元信息与推断应用类型当SpringApplication.run(MyApplication.class, args)被调用时首先会创建一个SpringApplication实例。这个构造过程非常关键它做了以下几件准备工作1. 推断Web应用类型SpringBoot会检查当前类路径下是否存在特定的类来判断你将要运行的是一个什么类型的应用。这是通过WebApplicationType.deduceFromClasspath()方法完成的。如果存在org.springframework.web.reactive.DispatcherHandler且不存在Spring MVC的DispatcherServlet相关类则推断为REACTIVE响应式Web应用如WebFlux。如果存在javax.servlet.Servlet或org.springframework.web.context.ConfigurableWebApplicationContext则推断为SERVLET传统的Servlet Web应用这也是最常见的情况。如果以上都不存在则推断为NONE非Web应用比如一个后台批处理任务。 这个推断结果决定了后续要创建的ApplicationContext应用上下文的类型比如对于SERVLET类型默认会创建AnnotationConfigServletWebServerApplicationContext。2. 加载“引导注册源”这里会加载META-INF/spring.factories文件中org.springframework.boot.Bootstrapper和org.springframework.boot.ApplicationContextInitializer等键下的配置类。在SpringBoot 2.7之后更推荐使用META-INF/spring/目录下的org.springframework.boot.Bootstrapper.imports等新方式。这些初始化器会在应用上下文刷新之前执行用于进行一些早期的初始化工作。3. 推断主配置类也就是我们传入的MyApplication.class。框架会确保这个类被注册到Bean定义中。4. 添加“应用监听器”同样从META-INF/spring.factories或新的/spring/目录中加载ApplicationListener接口的实现类。这些监听器用于监听Spring应用生命周期中的各种事件比如ApplicationStartingEvent应用开始启动、ApplicationPreparedEvent上下文已准备但未刷新、ApplicationStartedEvent上下文已刷新等。我们后面会讲到如何利用这些事件。初始化完成后SpringApplication实例就准备好了它知道要创建什么类型的上下文、有哪些初始化和监听逻辑需要执行。2.2 run方法的执行一个标准化的生命周期接下来run()方法被调用这是一个标准化的流程可以概括为以下几个核心阶段阶段一准备阶段发布ApplicationStartingEvent事件这是最早发布的事件此时ApplicationContext还未创建但SpringApplication和命令行参数args已经可用。一些需要在最早期执行的监听器如日志系统初始化会响应此事件。准备环境ConfigurableEnvironment创建并配置应用运行环境。它会读取所有配置源优先级顺序通常是命令行参数 JNDI属性 Java系统属性-D 操作系统环境变量 随机属性random.* 应用外部的配置文件如application-{profile}.properties/yaml 应用内部的配置文件 PropertySource注解 默认属性。这个过程解决了“配置从哪里来”的问题。发布ApplicationEnvironmentPreparedEvent事件环境准备就绪后发布。像ConfigFileApplicationListener这样的监听器会响应此事件去加载application.properties/yml文件。阶段二上下文创建与准备4.创建ApplicationContext根据之前推断的WebApplicationType实例化对应的应用上下文对象。 5.准备上下文这是一个关键步骤主要包括 *设置环境将准备好的Environment设置到上下文中。 *后置处理上下文调用所有ApplicationContextInitializer的initialize方法允许我们在上下文刷新前对其进行定制。 *发布ApplicationContextInitializedEvent事件。 *向上下文注册“主源”将我们的主类MyApplication.class注册为一个Bean定义源。这是自动装配的起点因为SpringBootApplication注解就在这个类上Spring会处理这个注解。 *加载其他源比如通过Import导入的配置类。阶段三刷新上下文核心中的核心6.刷新ApplicationContext调用上下文的refresh()方法。这是Spring框架最核心的方法SpringBoot在此方法执行前后插入了自己的逻辑。 *发布ApplicationPreparedEvent事件在refresh()方法开始前发布。此时Bean定义已加载但Bean还未实例化。 *执行refresh()这是Spring IOC容器启动的标准流程包括准备Bean工厂、调用Bean工厂后置处理器、注册Bean后置处理器、初始化消息源、初始化事件广播器、注册监听器、实例化所有非懒加载的单例Bean等。 *在refresh()过程中自动装配发生SpringBootApplication注解中EnableAutoConfiguration的关键处理器AutoConfigurationImportSelector会在此阶段被调用它负责加载所有自动配置类。这个过程我们会在下一章详细展开。 *内嵌Web服务器启动对于Web应用在refresh()的后期会触发ServletWebServerApplicationContext的onRefresh()方法这里会创建并启动内嵌的Tomcat、Jetty或Undertow服务器。阶段四启动后回调7.发布ApplicationStartedEvent事件refresh()方法成功完成后发布此时上下文已刷新内嵌服务器如有已启动但CommandLineRunner和ApplicationRunner还未执行。 8.调用CommandLineRunner和ApplicationRunner执行所有实现了这两个接口的Bean的run方法。这是执行一些应用启动后需要立即执行的任务的标准位置比如初始化缓存、加载基础数据等。 9.发布ApplicationReadyEvent事件在所有Runner执行完毕后发布标志着应用已完全准备就绪可以开始接收请求了。 10.返回ApplicationContextrun()方法返回创建好的上下文。整个流程就像一个精密的仪器的启动自检程序每一步都环环相扣。理解了这个流程当你的应用卡在启动的某一步时你就能通过日志事件或者调试快速定位到问题发生的阶段。注意在SpringBoot 2.4之后为了支持新的事件模型和更清晰的职责分离部分事件的发布时机和监听器的加载方式有细微调整但主体流程保持不变。关注SpringApplicationRunListener这个API它是驱动整个事件流的核心。3. 自动装配原理深度剖析约定如何成为配置自动装配是SpringBoot“开箱即用”体验的基石。它的核心思想是根据你引入的依赖classpath下存在的jar包自动推断你可能需要的Bean并将它们装配到Spring容器中。3.1 SpringBootApplication一个复合注解的魔法一切的起点是我们主类上的SpringBootApplication。点开它的源码你会发现它本身是一个“复合注解”主要由三个核心注解组成SpringBootConfiguration EnableAutoConfiguration ComponentScan public interface SpringBootApplication { // ... 排除等属性 }SpringBootConfiguration本质上就是一个Configuration表明这个类是一个Spring的配置类。ComponentScan启用组件扫描默认扫描当前包及其子包下所有带有Component,Service,Repository,Controller等注解的类并将它们注册为Bean。EnableAutoConfiguration这才是开启自动装配大门的钥匙。3.2 EnableAutoConfiguration与AutoConfigurationImportSelectorEnableAutoConfiguration注解的定义如下AutoConfigurationPackage Import(AutoConfigurationImportSelector.class) public interface EnableAutoConfiguration { // ... }关键点在于它使用Import导入了AutoConfigurationImportSelector这个类。这个类实现了DeferredImportSelector接口在Spring处理Import的特定阶段ConfigurationClassParser解析配置类时被调用。AutoConfigurationImportSelector的核心工作是加载所有符合条件的自动配置类。它通过以下步骤完成获取候选配置类列表调用getCandidateConfigurations方法。这个方法会去META-INF/spring.factories文件SpringBoot 2.7也支持META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports中查找org.springframework.boot.autoconfigure.EnableAutoConfiguration键对应的值。这些值就是一个个全限定类名例如org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration。在SpringBoot自带的spring-boot-autoconfigurejar包中这个文件列出了上百个自动配置类。去重与排除移除重复的配置类并处理SpringBootApplication或EnableAutoConfiguration注解上通过exclude/excludeName属性指定的需要排除的类。过滤这是最精妙的一步。并不是spring.factories里列出的所有配置类都会生效。AutoConfigurationImportSelector会使用一系列AutoConfigurationImportFilter来过滤。最重要的过滤器是基于Conditional系列注解的条件判断。3.3 条件注解自动装配的决策大脑条件注解是SpringBoot实现“智能”装配的核心。它们决定了某个配置类或Bean定义是否应该被注册。常见的条件注解有ConditionalOnClass当classpath下存在指定的类时配置才生效。例子DataSourceAutoConfiguration数据源自动配置上可能有ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class})只有当你引入了数据库相关的jar包如HikariCP、MySQL驱动这些类存在时数据源的自动配置才会开启。ConditionalOnMissingBean当Spring容器中不存在指定类型或名称的Bean时配置才生效。这实现了“默认配置”与“用户自定义配置”的完美协作。例子JacksonAutoConfiguration中定义ObjectMapperBean时会加上ConditionalOnMissingBean。这意味着如果你自己在配置类里定义了一个ObjectMapperBeanSpringBoot提供的这个默认Bean就不会被创建你的自定义Bean优先。ConditionalOnProperty当指定的配置属性拥有特定的值时配置才生效。例子CacheAutoConfiguration可能通过ConditionalOnProperty(prefix spring.cache, name type, havingValue redis)来控制Redis缓存配置是否激活。ConditionalOnWebApplication/ConditionalOnNotWebApplication根据是否是Web应用来决定。ConditionalOnResource当类路径下存在指定的资源文件时生效。ConditionalOnJava根据JVM版本决定。一个完整的自动配置类示例我们来看一个简化版的HttpEncodingAutoConfigurationHTTP编码自动配置AutoConfiguration // SpringBoot 2.7 推荐使用此注解替代 Configuration EnableConfigurationProperties(ServerProperties.class) // 让ServerProperties配置类生效并绑定到server.*属性 ConditionalOnWebApplication(type ConditionalOnWebApplication.Type.SERVLET) // 必须是Servlet Web应用 ConditionalOnClass(CharacterEncodingFilter.class) // classpath下必须有CharacterEncodingFilter类 ConditionalOnProperty(prefix server.servlet.encoding, value enabled, matchIfMissing true) // 配置属性控制默认开启 public class HttpEncodingAutoConfiguration { private final Encoding properties; // 注入已绑定的配置属性 public HttpEncodingAutoConfiguration(ServerProperties properties) { this.properties properties.getServlet().getEncoding(); } Bean // 定义CharacterEncodingFilter这个Bean ConditionalOnMissingBean // 如果用户自己没定义我才提供 public CharacterEncodingFilter characterEncodingFilter() { CharacterEncodingFilter filter new OrderedCharacterEncodingFilter(); filter.setEncoding(this.properties.getCharset().name()); filter.setForceRequestEncoding(this.properties.shouldForce(Encoding.Type.REQUEST)); filter.setForceResponseEncoding(this.properties.shouldForce(Encoding.Type.RESPONSE)); return filter; } }这个配置类完美诠释了自动装配它只在特定条件是Web应用、有相关类、配置开启下生效并且只在用户没有自定义Filter时才提供默认的Bean。所有配置值都来自application.properties中的server.servlet.encoding.*。3.4 如何调试与理解自动装配在实际开发中我们经常需要知道为什么我引入了一个starter某个功能没生效或者为什么我自定义的Bean没有覆盖默认的开启调试日志在application.properties中添加debugtrue。启动时控制台会打印两份报告Positive matches哪些自动配置类生效了及原因。Negative matches哪些自动配置类未生效及原因。 这是最直观的调试工具。使用spring-boot-autoconfigure依赖在IDE中直接查看spring-boot-autoconfigurejar包下的META-INF/spring.factories或/spring/目录可以看到所有自动配置类的全貌。理解“默认-覆盖”机制牢记ConditionalOnMissingBean。如果你想定制某个Bean最优雅的方式不是排除整个自动配置类而是直接在Configuration类中定义你自己的Bean。只要你的Bean定义在自动配置类之后被扫描到或通过Order指定顺序并且类型匹配就会覆盖默认的Bean。4. 核心机制延伸事件监听、Runner与配置绑定理解了主干我们再看看启动流程中几个非常有用的扩展点。4.1 应用事件与监听器在生命周期的关键时刻插入逻辑SpringBoot在启动过程中发布了一系列事件。我们可以实现ApplicationListener接口或使用EventListener注解来监听这些事件执行一些初始化或清理工作。常用的事件监听场景ApplicationStartingEvent最早的事件此时环境还未准备适合做一些非常早期的初始化但能做的事情有限。ApplicationEnvironmentPreparedEvent环境已准备好配置文件已加载。可以在这里动态修改Environment中的属性。ApplicationPreparedEventApplicationContext已创建Bean定义已加载但Bean还未实例化。适合进行一些Bean定义的后置处理。ApplicationStartedEvent上下文已刷新应用已启动但Runner还未执行。可以在这里检查一些外部依赖如数据库、Redis是否连通。ApplicationReadyEvent所有Runner已执行完毕应用完全就绪。这是执行一些最终检查或启动成功通知的理想位置。ApplicationFailedEvent启动过程中发生失败。可以在这里进行错误日志上报或资源清理。示例监听应用就绪事件Component public class MyReadyEventListener { EventListener(ApplicationReadyEvent.class) public void onApplicationReady(ApplicationReadyEvent event) { // 应用已完全启动可以安全地调用其他Bean了 SomeService service event.getApplicationContext().getBean(SomeService.class); service.doSomethingAfterStartup(); System.out.println(应用启动完毕开始执行就绪后逻辑...); } }4.2 CommandLineRunner与ApplicationRunner启动后执行任务这两个接口用于在应用完全启动后、开始接收流量前执行一些一次性任务。它们的执行时机在ApplicationReadyEvent之前。CommandLineRunner接收原始的字符串数组命令行参数。Component Order(1) // 可以指定执行顺序 public class MyCommandLineRunner implements CommandLineRunner { Override public void run(String... args) throws Exception { System.out.println(CommandLineRunner执行参数: Arrays.toString(args)); // 初始化任务如加载字典到内存 } }ApplicationRunner接收封装好的ApplicationArguments对象可以更方便地解析--keyvalue格式的参数。Component public class MyApplicationRunner implements ApplicationRunner { Override public void run(ApplicationArguments args) throws Exception { System.out.println(非选项参数: args.getNonOptionArgs()); System.out.println(选项参数foo的值: args.getOptionValues(foo)); // 执行启动任务 } }4.3 配置绑定ConfigurationProperties与宽松绑定自动配置类中大量使用了EnableConfigurationProperties和ConfigurationProperties。这实现了外部配置application.yml到Java Bean的自动绑定。宽松绑定规则SpringBoot支持属性名的宽松绑定这意味着configurationProperties中的属性myFieldName可以对应配置文件中的my-field-name、my_field_name、MY_FIELD_NAME等多种形式这提高了配置的容错性和书写灵活性。最佳实践当你需要定义一组相关的配置属性时应该创建一个用ConfigurationProperties注解的类而不是在代码中到处写Value。这样更类型安全且有IDE的元数据支持需要额外引入spring-boot-configuration-processor依赖来生成元数据文件。ConfigurationProperties(prefix myapp.mail) Data // Lombok注解生成getter/setter public class MailProperties { private String host smtp.default.com; private int port 25; private String username; private String password; // ... 其他属性 } // 在配置类中启用 Configuration EnableConfigurationProperties(MailProperties.class) public class MyAppConfig { // ... }然后在application.yml中配置myapp: mail: host: smtp.qq.com port: 587 username: adminexample.com password: yourpassword5. 实战中的常见问题与深度排查技巧理论懂了但在实际开发中还是会遇到各种妖魔鬼怪。下面分享几个我踩过的坑和对应的排查思路。5.1 Bean冲突与覆盖为什么我的自定义配置不生效这是最常见的问题。症状是你定义了一个DataSourceBean但应用似乎还在使用默认的H2内存数据库。排查步骤检查自动配置报告设置debugtrue查看Positive matches中关于数据源的配置类如DataSourceAutoConfiguration是否生效。重点看它定义的Bean上是否有ConditionalOnMissingBean。确认Bean定义顺序确保你的Configuration类所在的包被组件扫描到并且其Bean定义在自动配置类之后被处理。通常将主类放在根包你的配置类放在子包是安全的。如有必要可以使用AutoConfigureAfter或AutoConfigureBefore来显式指定配置类之间的顺序。使用Primary注解如果你确实需要定义多个同类型Bean并希望其中一个成为主要注入候选可以在你的Bean定义上加上Primary。排除自动配置如果问题复杂可以在SpringBootApplication注解上使用exclude属性暂时排除有问题的自动配置类但这通常是最后的手段因为它可能影响其他依赖此配置的功能。5.2 启动缓慢分析与优化SpringBoot应用启动慢尤其是在本地开发时很影响效率。可能的原因和优化点类路径太长Classpath Hell这是元凶之一。使用spring-boot-maven-plugin的exclude功能移除不必要的依赖。或者使用mvn dependency:tree分析依赖排除传递性依赖中无用的jar包。组件扫描路径过广ComponentScan默认扫描主类所在包及其所有子包。如果项目结构庞大扫描会耗时。可以明确指定扫描的基包ComponentScan(basePackages com.yourcompany.core)。过多的自动配置类不是所有spring-boot-starter都是必需的。检查pom.xml移除开发阶段不需要的starter如spring-boot-starter-data-jpa如果只用MyBatis。懒加载Lazy InitializationSpringBoot 2.2支持全局懒加载。在application.properties中设置spring.main.lazy-initializationtrue。这会让Bean在第一次被请求时才创建而不是启动时全部创建能显著加快启动速度。但要注意这可能会将启动时的问题如配置错误延迟到运行时才发现。使用Spring Boot DevTools它提供了快速重启功能虽然第一次启动不变但后续修改代码后的重启速度会快很多因为它使用了两个类加载器基础类只加载一次。5.3 特定环境配置不加载理解Profile与激活机制application-{profile}.yml文件有时不生效通常是对Profile的激活机制理解有误。激活Profile的几种方式优先级从高到低命令行参数java -jar app.jar --spring.profiles.activeprod,cloudJVM系统参数-Dspring.profiles.activeprod环境变量SPRING_PROFILES_ACTIVEprod应用配置文件在application.yml中写spring.profiles.active: prod不推荐因为失去了环境隔离的意义一个常见的坑在IDE中运行和在服务器上用java -jar运行激活的Profile可能不同。务必在部署脚本或容器启动命令中明确指定--spring.profiles.active。多文档块与Profile在单个application.yml中可以用---分隔符定义多个文档块每个块用spring.config.activate.on-profile指定其生效的Profile。这种方式便于管理但要小心缩进。# 公共配置 server: port: 8080 --- # 开发环境配置 spring: config: activate: on-profile: dev logging: level: root: DEBUG --- # 生产环境配置 spring: config: activate: on-profile: prod logging: level: root: WARN5.4 自定义Starter封装自己的自动配置当你有一套通用的配置或工具想在多个项目间复用时可以封装成自己的SpringBoot Starter。这能让你团队的开发体验和引入官方starter一样流畅。一个简易Starter的构成autoconfigure模块核心包含你的自动配置类AutoConfiguration。在src/main/resources/META-INF/spring/目录下创建org.springframework.boot.autoconfigure.AutoConfiguration.imports文件里面写上你的自动配置类的全限定名。使用Conditional系列注解控制配置生效条件。定义ConfigurationProperties类来支持外部配置。starter模块可选便利性模块通常只有一个pom.xml依赖autoconfigure模块和其他必要的第三方库。其他项目只需要引入这个starter模块的依赖就能获得所有功能。关键点你的自动配置类应该只提供“默认”配置并且大量使用ConditionalOnMissingBean允许使用者轻松覆盖。同时为你的配置属性提供元数据通过spring-boot-configuration-processor这样使用者在application.yml里配置时能有代码提示。理解SpringBoot的启动和自动装配就像拿到了框架的“地图”。之后无论是集成第三方组件如MyBatis, Redis还是排查诡异的问题你都能清楚地知道代码执行到了哪个环节配置是从哪里来的Bean为什么没生效。这远比死记硬背几个注解或配置项重要得多。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门