FEATURED · 精选文章

Spring Boot自动配置原理:从启动流程到条件注解的源码剖析

发布时间 / 2026/9/13 12:53:09
来源 / 创域科博编辑部
栏目 / 资讯中心
Spring Boot自动配置原理:从启动流程到条件注解的源码剖析 很多人第一次接触Spring Boot都是从那个神奇的main方法开始的。明明只是点了一下run整个Web应用就起来了——内嵌的Tomcat在跑、DataSource连上了、MyBatis的Mapper都注册好了、RedisTemplate跟自动约定好了一样能直接用。背面试题的时候大家都会说这是因为自动配置但一旦追问下去spring.factories怎么加载、EnableAutoConfiguration到底导入了什么、为什么自定义Bean能覆盖掉自动配置的默认Bean很多人就开始含糊了。这篇文章我想从源码执行的顺序出发把Spring Boot从启动入口到自动配置生效的整条链路完整走一遍结合实际排查经验把关键节点的原理拆开讲透。适合那些已经写过几个Spring Boot项目、但还没系统看过源码的开发者看完之后你再遇到配置不生效启动变慢引入依赖却没用这类问题会有一个清晰的排查方向。1. 从main方法动手SpringApplication.run()内部到底发生了什么1.1 构造阶段类型推断、初始化器和监听器的加载当我们调用SpringApplication.run(Application.class, args)的时候第一步其实是new SpringApplication(primarySources)。这一步很多人直接跳过不看但它是整个启动过程的预加载阶段三件重要的事在这里发生。第一件事是推断应用类型。WebApplicationType.deduceFromClasspath()会检查当前classpath里有没有jakarta.servlet.Servlet和org.springframework.web.context.ConfigurableWebApplicationContext。如果servlet和Spring MVC都在就推断为SERVLET类型如果只有org.springframework.web.reactive.DispatcherHandler就推断为REACTIVE类型如果全都没有就是NONE也就是纯非Web应用。这个推断结果决定了后面创建的是AnnotationConfigServletWebServerApplicationContext还是AnnotationConfigReactiveWebServerApplicationContext还是普通的AnnotationConfigApplicationContext。我曾经遇到过一个项目把WebFlux和Spring MVC的依赖同时引入了启动时应用类型判定直接乱套容器加载方式不符合预期接口行为也变了排查了很久才发现是classpath里两类Web框架并存导致的。第二件事是从META-INF/spring.factories加载所有ApplicationContextInitializer和ApplicationListener。这里用的就是Spring Framework老牌工具类SpringFactoriesLoader.loadFactories()它会把classpath下所有jar包里的META-INF/spring.factories文件读出来按接口类型做一次索引然后实例化所有配置的实现类。这个机制就是后面自动配置的地基现在先记住这个概念第三节我们会深入展开。第三件事是推断主应用程序类。SpringApplication构造的时候会去找堆栈里包含main方法且不带SpringBootApplication异常信息的那个类作为primary source。这个推断结果关系到后续组件扫描的基准包所以主类位置放错了后面扫描不到Bean就一点也不奇怪这个坑在第二节会专门讲。1.2 run()方法的九段关键流程构造完之后才是真正执行启动流程的run()方法。为了看清楚它做了什么最直接的办法是打开SpringApplication.run()的源码或者直接在IDE里对run()下断点跟着调用栈走一遍。整个流程核心可以分为九个阶段启动StopWatch并开始计时这就是启动日志里Started Application in X.XX seconds的数据来源。创建并启动SpringApplicationRunListeners这些监听器会向外广播ApplicationStartingEvent、ApplicationEnvironmentPreparedEvent等事件。解析ApplicationArguments把main方法传入的args包装成标准参数对象。创建并配置Environment会读取application.properties、application.yml以及系统环境变量这里会加载ConfigDataEnvironmentPostProcessor完成配置文件的解析。如果配置了spring.main.banner-mode这里打印Banner。创建应用程序上下文ApplicationContext具体类型就是在构造阶段推断出来的那三种之一。prepareContext()阶段把ApplicationContextInitializer逐个apply到容器上同时向容器注册一些基础Bean比如SpringApplicationArguments、SpringApplicationBannerPrinter等。refreshContext()阶段这是整个启动最重的一步内部调用的就是Spring Framework的AbstractApplicationContext.refresh()后面单开一小节讲。afterRefresh()阶段Spring Boot会在这个阶段调用ApplicationRunner和CommandLineRunner的实现类做启动后的扩展。我把这几个阶段对应到源码的变量和方法上方便你自己去对照阶段关键方法结果计时启动StopWatch.start()得到启动耗时广播事件SpringApplicationRunListeners.starting()触发StartingEvent参数包装ApplicationArguments统一访问args环境准备prepareEnvironment()生成Environment打印BannerprintBanner()控制台Banner或关闭创建容器createApplicationContext()生成ApplicationContext上下文准备prepareContext()应用初始化器并注册基础Bean刷新容器refreshContext()完成所有Bean的创建和初始化启动后回调callRunners()执行Runner接口1.3 refresh()之后Spring容器才算真正活了refreshContext()是理解Spring Boot启动过程的关键也是无数面试题里Spring Boot启动过程和Spring IOC容器初始化过程交汇的地方。它调用的AbstractApplicationContext.refresh()做了12个步骤prepareRefresh()、obtainFreshBeanFactory()、prepareBeanFactory()、postProcessBeanFactory()、invokeBeanFactoryPostProcessors()、registerBeanPostProcessors()、initMessageSource()、initApplicationEventMulticaster()、onRefresh()、registerListeners()、finishBeanFactoryInitialization()、finishRefresh()。其中对启动过程影响最大的是invokeBeanFactoryPostProcessors()和finishBeanFactoryInitialization()。前者处理所有BeanFactoryPostProcessor自动配置类就是在这个阶段被当成配置类解析相关Bean方法得到注册后者负责实例化所有非懒加载的单例BeanDataSource、JdbcTemplate、MyBatis的SqlSessionFactory都是在这个阶段new出来的。而内嵌的Tomcat等Web服务器则是在onRefresh()阶段启动的ServletWebServerApplicationContext重写了这个钩子在这里调用createWebServer()把Tomcat拉起来。理解了这层关系你再看启动日志里Tomcat initialized with port(s): 8080和Root WebApplicationContext: initialization completed的顺序就有一种看透流程的通透感。2. SpringBootApplication不是魔法组合注解拆解与扫描边界2.1 三个注解的分工与职责SpringBootApplication是一个三合一组合注解它把以下三个注解聚合在了一起SpringBootConfiguration本质是一个Configuration告诉Spring这个类是配置类可以定义Bean方法。有一段时间初级开发者会同时写SpringBootApplication又额外加Configuration其实完全重复。EnableAutoConfiguration开启自动配置是本节和第三节的关键。ComponentScan默认扫描当前类所在包及其子包下的所有Component、Service、Repository、Controller等注解组件。我在项目里见过一种迷惑操作主类放在com.example.order包下但业务代码写在com.example.user包下结果Controller怎么都扫不到页面404排查到最后才发现是包路径问题。这其实不怪注解ComponentScan在没有任何参数的时候就是以标注该注解的类所在包为基准进行扫描的。2.2 主类为什么放在最外层包扫描基准的推断逻辑ComponentScan的basePackages在有显式配置时按配置执行没配置的时候会通过ClassPathBeanDefinitionScanner找到主类所在的包名。假如主类在com.example.Application扫描基准就是com.example它会递归扫描com.example.xxx所有子包。所以项目里主类永远放在所有业务包的最外层不是为了好看而是为了给扫描画一个合理的边界。如果因为历史原因主类放错了位置有几种补救方式。第一种在SpringBootApplication上显式声明scanBasePackages com.example第二种额外加一个ComponentScan(basePackages com.example)但要注意两个注解同时存在时ComponentScan不会替代SpringBootApplication里的隐式扫描而是可能造成重复扫描第三种把主类移动到正确的包名层级。三种方式里我推荐优先调整包结构因为显式改扫描路径虽然能解决问题却会增加新成员理解项目的成本每多一个特殊配置就多一个出问题的机会。2.3 调整扫描路径的常识与注意点在实际项目中偶尔会遇到需要扫描外部jar包中组件的场景此时需要把外部包的路径加进scanBasePackages。但我更建议的做法是尽量少依赖跨包扫描把需要被扫描的类打包在应用主包之下或者通过Bean方法显式注册外部组件。跨包扫描太广会拖慢启动速度也会引入一些意外的Bean冲突。另外一个常见的认知误区是EnableAutoConfiguration负责的是自动装配的BeanComponentScan负责的是用户业务Bean两者是并行的两条线。很多新手以为在配置类上加了Configuration就自动开启自动配置了实际上Configuration只负责把当前配置类实例化为容器配置自动配置功能是由EnableAutoConfiguration单独开启的。SpringBootApplication只是因为包含了三合一注解才同时具备这三个能力。3. 自动配置的基石sp​​ring.factories和SpringFactoriesLoader链路3.1 从classpath扫描META-INF/spring.factoriesSpring Boot的自动配置听起来很玄学但拆开看核心就一句话在启动的时候从classpath下所有jar包的约定位置读取配置类列表把这些配置类全部导入容器再通过条件注解决定哪些生效。这个约定位置在Spring Boot 2.7版本之前是META-INF/spring.factories文件内容是properties格式。比如Redis的自动配置就在spring-boot-autoconfigure.jar里的META-INF/spring.factories文件中声明org.springframework.boot.autoconfigure.EnableAutoConfiguration\ org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\ org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration从Spring Boot 2.7开始官方引入了新的注册文件META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports目的是与spring.factories中其他类型的条目做分离让自动配置列表更清晰同时也能改善启动性能。Spring Boot 3.x已经完全使用新机制不再从spring.factories读取自动配置项。版本声明位置示例Spring Boot 1.x - 2.6.xMETA-INF/spring.factoriesEnableAutoConfigurationxxxAutoConfigurationSpring Boot 2.7 - 2.7.x双兼容spring.factoriesAutoConfiguration.imports两种写法同时支持Spring Boot 3.xMETA-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports每行一个类名读取这些文件的核心就是SpringFactoriesLoader它干的活可以概括为三步扫描所有jar包里的约定资源文件解析properties或imports格式的文本按接口类型建立类名到实现类集合的映射。这个机制本质上就是Java标准的SPI思路只不过Spring自己实现了一套多了排序、去重、按类型分类的能力。这也是为什么我们自己写starter的时候一定要把自动配置类的全限定类名写进AutoConfiguration.imports或spring.factories否则Spring根本不知道有这么一个配置类存在。3.2 AutoConfigurationImportSelector如何把配置类导入容器EnableAutoConfiguration注解本身没有多少代码它真正干活的是一行Import(AutoConfigurationImportSelector.class)。AutoConfigurationImportSelector实现了DeferredImportSelector这个Deferred延迟是很关键的设计普通Import进来的类会立即被处理而DeferredImportSelector会等到所有普通配置类都解析完之后再执行。这样做的好处是用户自定义的Configuration类和Bean方法会先注册后续自动配置在判断ConditionalOnMissingBean时就能准确知道用户是否已经自己定义过某个Bean从而决定要不要创建默认Bean。getAutoConfigurationEntry()方法内部的处理链路是先通过getCandidateConfigurations()拿到所有候选自动配置类然后执行去重removeDuplicates再执行用户显式排除的类getExclusions最后交给filter和fireAutoConfigurationImportEvents做条件过滤和事件广播。这里条件过滤就是用ConditionEvaluator去评估每个自动配置类上的条件注解。以RedisAutoConfiguration为例它上面有ConditionalOnClass(RedisOperations.class)classpath里没有Redis依赖这个类就会被过滤掉不进容器。这就是为什么你引入starter之后功能自动生效移除依赖之后也没任何报错的原因。3.3 自动配置的加载顺序与排除方式自动配置类之间的关系并不是平级的很多配置类有依赖关系。比如DataSourceTransactionManagerAutoConfiguration需要在DataSourceAutoConfiguration之后生效MybatisAutoConfiguration通常在DataSourceAutoConfiguration之后。为了控制顺序Spring Boot提供了三个注解AutoConfigureBefore在指定的配置类之前生效。AutoConfigureAfter在指定的配置类之后生效。AutoConfigureOrder数值越小越优先。DeferredImportSelector还有一个重要特性它会自动按照Order排序但用户完全不用关心大多数自动配置之间的顺序因为条件注解会兜底。我自己排错时见过一个场景——自定义了一个DataSource但自动配置仍然抢先创建了默认的DataSource导致容器里出现了两个数据源候选注入时报NoUniqueBeanDefinitionException。这个场景的关键在于自动配置虽然是延迟处理的但如果你用Bean注册数据源的方法所在的配置类本身没有被正确扫描到用户Bean的定义就晚于自动配置ConditionalOnMissingBean判断不出用户已经自定义冲突就这样产生了。这也是我们在处理自动配置覆盖问题时首先要确认自定义配置类是否在扫描范围内。排除自动配置有几种常见方式。一是在SpringBootApplication或EnableAutoConfiguration上配置exclude属性二是在配置文件中写spring.autoconfigure.exclude适合临时调试三是通过AutoConfigurationImportSelector的exclude机制。我遇到过排除配置写错类名导致启动失败的情况所以提醒一下排除项一定要写自动配置类的全限定类名而且这个配置类必须在classpath内排除一个不存在的类会直接抛IllegalStateException。spring.autoconfigure.excludeorg.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration4. 条件装配是真正让自动配置智能的东西4.1 ConditionalOnClass类路径条件如何触发如果自动配置只是把所有配置类无脑导入容器那Spring Boot会创建一堆无用的Bean。为了避免这个问题Spring Boot在自动配置类上大量使用条件注解其中最常用的就是ConditionalOnClass。RedisAutoConfiguration上的ConditionalOnClass(RedisOperations.class)本质上是告诉Spring只有当RedisOperations这个类可以被当前ClassLoader加载时才允许这个配置类生效。它的判断逻辑由OnClassCondition实现底层就是一个按类名进行的Class.forName()探测但是为了性能Spring会先通过org.springframework.util.ClassUtils快速判断再使用ClassLoader加载。这里有个细节容易踩坑在一个自动配置类上ConditionalOnClass的value值对应的只是类可以存在并不要求这个类一定要被使用。也就是说只要classpath里有这个类条件就成立不管你是否真的在业务代码里用它。所以有时你只是想引入一个工具包却意外触发了一堆自动配置导致启动变慢。遇到这种情况可以用--debug启动或配置logging.level.org.springframework.boot.autoconfigureDEBUG查看条件评估报告逐一确认哪些配置被匹配了。4.2 ConditionalOnMissingBean与配置覆盖规则ConditionalOnMissingBean是另一个高频出现的条件注解它的语义是当容器中不存在指定类型的Bean时这个配置才生效。这给用户提供了一种自然的覆盖机制——如果你想用自己的实现替换自动配置的默认实现只需要在某个配置类里定义同类型的Bean方法自动配置看到容器里已经有这个类型的Bean就不再生成了。这个机制听起来很简单实际操作中有几个坑。第一个坑用户自定义Bean必须能被Spring识别到如果配置类没有被扫描自定义Bean不存在自动配置就会启用默认Bean。第二个坑ConditionalOnMissingBean的判断是基于当前处理到的时间点的如果用户配置类在自动配置之后被处理同样无法正确看到用户的Bean。这也就是为什么AutoConfigurationImportSelector要实现DeferredImportSelector——就是为了等用户的配置类先注册完再执行自动配置。实际上Spring Boot还提供AutoConfigureAfter来控制执行顺序如果你在自定义配置里需要强制保证顺序可以使用这个注解。来看一个DataSource覆盖的经典场景Spring Boot的DataSourceAutoConfiguration是一个配置类官方的DataSource是HikariCP自动创建的但如果你想用Druid只需要引入Druid的starter然后定义DataSource的Bean方法即可。因为Druid starter自己的自动配置类会在DataSourceAutoConfiguration之前执行注册等到DataSourceAutoConfiguration判断ConditionalOnMissingBean时发现已经有了数据源Bean就不再创建。如果你的自定义DataSource没有被生效优先检查Druid或Druid starter是否在classpath里以及你的Bean方法所在的配置类是否在主包扫描路径内。4.3 ConditionalOnProperty和属性绑定除了基于类路径和Bean是否存在的条件自动配置还大量使用ConditionalOnProperty通过配置项控制配置类是否生效。比如ServerProperties有server.port一些功能的启用开关也通过这种方式控制。我经常用这个注解来控制生产环境和本地环境不同的加载逻辑。用法很简单比如定义一个自定义配置类Configuration ConditionalOnProperty(prefix my.redis, name enabled, havingValue true, matchIfMissing true) public class MyRedisConfiguration { Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(connectionFactory); return template; } }matchIfMissing true表示配置项不存在时也匹配可以避免升级配置导致功能突然消失。实际排错的时候很多配置没生效的问题就出在这里配置项的key拼写错了、前缀写错了、havingValue类型不对导致条件评估失败。遇到这种问题最快的排查办法是打开条件评估报告查看该配置类的匹配结果里面会明确显示Matched还是Did not match以及不匹配的原因。条件注解的设计是一个典型的职责分离思路ConditionalOnClass管依赖ConditionalOnMissingBean管覆盖ConditionalOnProperty管开关ConditionalOnWebApplication管环境。理解了这四个核心条件自动配置的代码阅读难度会下降一大截。5. 启动提速与排障从启动日志到自定义Starter5.1 怎么看出哪些自动配置在起作用想了解当前项目里具体哪些自动配置生效了最直接的方式是在配置文件里加一行配置然后启动应用debugtrue开启debug模式后Spring Boot会输出一份自动配置报告分成两个部分Positive matches列出了所有匹配成功加载的自动配置类Negative matches列出了所有匹配失败被跳过的自动配置类。每一个条目后面会附带条件注解的匹配信息非常方便排查。如果是Spring Boot 2.4以上版本还可以对启动过程做更细粒度的日志分组把核心启动日志单独打开logging.level.org.springframework.boot.context.loggingDEBUG我在实际项目里观察过一个奇怪的现象应用本身没多少业务代码启动却需要12秒打开自动配置报告后发现MongoAutoConfiguration、ElasticsearchRestClientAutoConfiguration、JpaRepositoriesAutoConfiguration这些项目根本没用到的配置全部匹配成功了原因就是ClassLoader里存在对应依赖而这些依赖来自内部公共SDK。所以启动慢不一定是代码问题先看看自动配置报告往往能发现很多你以为没开启实际早就被依赖带进来了的配置。这种情况下把用不到的自动配置显式排除掉启动时间能缩短不少。另外ComponentScan扫描范围过大是启动慢的一个容易被忽视的原因可以结合spring.main.lazy-initializationtrue做临时测试看是不是Bean初始化耗时过大但这个开关在生产环境不建议直接开会带来隐藏的懒加载时序问题。5.2 排除自动配置的几种方式我前面提到了spring.autoconfigure.exclude这里再把几种排除方式放在一起对比方便你按场景选择方式适用场景注意点SpringBootApplication(exclude XxxAutoConfiguration.class)全局排除明确知道不用类必须存在否则启动报错spring.autoconfigure.exclude...按环境排除改配置即可适合不同环境差异自定义AutoConfigurationImportFilter高级定制统一过滤需要自己实现filter接口如果你排除的是某个具体功能比如不想让Spring Boot自动配置DataSource而是想自己完全控制那么最稳妥的组合是SpringBootApplication(exclude DataSourceAutoConfiguration.class) public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } }同时还要注意很多自动配置之间有依赖关系比如排除了DataSourceAutoConfiguration之后DataSourceTransactionManagerAutoConfiguration也会因为缺少DataSource而不再生效但它在条件评估中会显示Did not match属于正常的级联失效不必恐慌。5.3 手写一个自定义Starter的完整思路理解了自动配置原理之后你可以尝试写一个自己的starter这是把知识变成能力的最好方式也能帮你彻底弄懂Spring Boot的自动配置机制。一个标准的starter通常包含两个模块xxx-spring-boot-autoconfigure模块负责写自动配置逻辑xxx-spring-boot-starter模块只是一个空壳pom里依赖autoconfigure模块和需要的第三方库。这样设计的好处是你的业务项目按需决定依赖starter本体或只依赖autoconfigure模块。以我最近写的一个短信发送starter为例核心步骤大概是这几步。第一步在autoconfigure模块里定义属性配置类ConfigurationProperties(prefix my.sms) public class SmsProperties { private String accessKeyId; private String accessKeySecret; private String signName; // getter/setter省略 }第二步写自动配置类注意条件注解的搭配AutoConfiguration EnableConfigurationProperties(SmsProperties.class) ConditionalOnClass(SmsClient.class) ConditionalOnProperty(prefix my.sms, name enabled, havingValue true, matchIfMissing true) public class SmsAutoConfiguration { Bean ConditionalOnMissingBean public SmsClient smsClient(SmsProperties properties) { return new DefaultSmsClient(properties); } }第三步也是最容易出错的一步注册自动配置类。Spring Boot 3.x在src/main/resources/META-INF/spring/目录下创建org.springframework.boot.autoconfigure.AutoConfiguration.imports文件每行写一个自动配置类的全限定名com.example.sms.autoconfigure.SmsAutoConfigurationSpring Boot 2.7及以下则是在META-INF/spring.factories里写org.springframework.boot.autoconfigure.EnableAutoConfiguration\ com.example.sms.autoconfigure.SmsAutoConfiguration写完starter之后一定要在干净项目里做一次验证确认引入依赖后自动配置生效配置my.sms.enabledfalse后自动配置不生效自己定义了SmsClient的Bean后自动配置的默认Bean被覆盖。这三个验证点覆盖了自动配置的三大条件注解也是我每次写starter时的固定冒烟测试。在实际工作中我还习惯用Autoconfigure的AutoConfiguration(after DataSourceAutoConfiguration.class)或AutoConfigureBefore/AutoConfigureAfter来控制顺序。如果你有多个自动配置类需要按顺序加载这个能力非常重要否则可能出现某个Bean在依赖的Bean创建之后才注册导致NoSuchBeanDefinitionException。读完这段源码再回头看Spring Boot的启动过程你会发现它其实并不神秘先通过SpringFactoriesLoader发现候选的配置类再用DeferredImportSelector推迟导入用条件注解逐个筛选最终把真正需要的Bean组装进容器。我个人在实际操作中还有一个习惯读源码的时候不要从第一行读到最后一行先抓主线——run()、refresh()、onRefresh()、finishBeanFactoryInitialization()遇到不懂的地方再单独展开。这样一遍走下来你就对启动过程和自动配置的每一个环节都有了掌控感而不是停留在背结论的层面。最后再分享一个小技巧每引入一个新依赖启动后先扫一眼自动配置报告看看多了哪些生效的配置类这个习惯能帮你省掉很多未来排查启动慢和Bean冲突的麻烦。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻