Spring Boot自动配置原理:从启动流程到条件注解的源码剖析
2026/9/13 12:53:06 网站建设 项目流程

很多人第一次接触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.Servletorg.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加载所有ApplicationContextInitializerApplicationListener。这里用的就是Spring Framework老牌工具类SpringFactoriesLoader.loadFactories(),它会把classpath下所有jar包里的META-INF/spring.factories文件读出来,按接口类型做一次索引,然后实例化所有配置的实现类。这个机制就是后面自动配置的地基,现在先记住这个概念,第三节我们会深入展开。

第三件事是推断主应用程序类。SpringApplication构造的时候会去找堆栈里包含main方法且不带@SpringBootApplication异常信息的那个类,作为primary source。这个推断结果关系到后续组件扫描的基准包,所以主类位置放错了,后面扫描不到Bean就一点也不奇怪,这个坑在第二节会专门讲。

1.2 run()方法的九段关键流程

构造完之后才是真正执行启动流程的run()方法。为了看清楚它做了什么,最直接的办法是打开SpringApplication.run()的源码,或者直接在IDE里对run()下断点,跟着调用栈走一遍。整个流程核心可以分为九个阶段:

  1. 启动StopWatch并开始计时,这就是启动日志里"Started Application in X.XX seconds"的数据来源。
  2. 创建并启动SpringApplicationRunListeners,这些监听器会向外广播ApplicationStartingEventApplicationEnvironmentPreparedEvent等事件。
  3. 解析ApplicationArguments,把main方法传入的args包装成标准参数对象。
  4. 创建并配置Environment,会读取application.propertiesapplication.yml以及系统环境变量,这里会加载ConfigDataEnvironmentPostProcessor完成配置文件的解析。
  5. 如果配置了spring.main.banner-mode,这里打印Banner。
  6. 创建应用程序上下文ApplicationContext,具体类型就是在构造阶段推断出来的那三种之一。
  7. prepareContext()阶段,把ApplicationContextInitializer逐个apply到容器上,同时向容器注册一些基础Bean,比如SpringApplicationArgumentsSpringApplicationBannerPrinter等。
  8. refreshContext()阶段,这是整个启动最重的一步,内部调用的就是Spring Framework的AbstractApplicationContext.refresh(),后面单开一小节讲。
  9. afterRefresh()阶段,Spring Boot会在这个阶段调用ApplicationRunnerCommandLineRunner的实现类,做启动后的扩展。

我把这几个阶段对应到源码的变量和方法上,方便你自己去对照:

阶段关键方法结果
计时启动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方法得到注册;后者负责实例化所有非懒加载的单例Bean,DataSource、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 主类为什么放在最外层包:扫描基准的推断逻辑

@ComponentScanbasePackages在有显式配置时按配置执行;没配置的时候,会通过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负责的是自动装配的Bean,@ComponentScan负责的是用户业务Bean,两者是并行的两条线。很多新手以为在配置类上加了@Configuration就自动开启自动配置了,实际上@Configuration只负责把当前配置类实例化为容器配置,自动配置功能是由@EnableAutoConfiguration单独开启的。@SpringBootApplication只是因为包含了三合一注解才同时具备这三个能力。

3. 自动配置的基石:sp​​ring.factories和SpringFactoriesLoader链路

3.1 从classpath扫描META-INF/spring.factories

Spring 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.factoriesEnableAutoConfiguration=xxxAutoConfiguration
Spring Boot 2.7 - 2.7.x双兼容:spring.factories+AutoConfiguration.imports两种写法同时支持
Spring Boot 3.xMETA-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports每行一个类名

读取这些文件的核心就是SpringFactoriesLoader,它干的活可以概括为三步:扫描所有jar包里的约定资源文件,解析properties或imports格式的文本,按接口类型建立"类名到实现类集合"的映射。这个机制本质上就是Java标准的SPI思路,只不过Spring自己实现了一套,多了排序、去重、按类型分类的能力。这也是为什么我们自己写starter的时候,一定要把自动配置类的全限定类名写进AutoConfiguration.importsspring.factories,否则Spring根本不知道有这么一个配置类存在。

3.2 AutoConfigurationImportSelector如何把配置类导入容器

@EnableAutoConfiguration注解本身没有多少代码,它真正干活的是一行@Import(AutoConfigurationImportSelector.class)AutoConfigurationImportSelector实现了DeferredImportSelector,这个"Deferred"(延迟)是很关键的设计:普通@Import进来的类会立即被处理,而DeferredImportSelector会等到所有普通配置类都解析完之后再执行。这样做的好处是,用户自定义的@Configuration类和@Bean方法会先注册,后续自动配置在判断@ConditionalOnMissingBean时,就能准确知道用户是否已经自己定义过某个Bean,从而决定要不要创建默认Bean。

getAutoConfigurationEntry()方法内部的处理链路是:先通过getCandidateConfigurations()拿到所有候选自动配置类,然后执行去重(removeDuplicates),再执行用户显式排除的类(getExclusions),最后交给filterfireAutoConfigurationImportEvents做条件过滤和事件广播。这里"条件过滤"就是用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,适合临时调试;三是通过AutoConfigurationImportSelectorexclude机制。我遇到过排除配置写错类名导致启动失败的情况,所以提醒一下:排除项一定要写自动配置类的全限定类名,而且这个配置类必须在classpath内,排除一个不存在的类会直接抛IllegalStateException

spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration

4. 条件装配是真正让自动配置"智能"的东西

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.autoconfigure=DEBUG查看条件评估报告,逐一确认哪些配置被匹配了。

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,通过配置项控制配置类是否生效。比如ServerPropertiesserver.port,一些功能的启用开关也通过这种方式控制。我经常用这个注解来控制生产环境和本地环境不同的加载逻辑。

用法很简单,比如定义一个自定义配置类:

@Configuration @ConditionalOnProperty(prefix = "my.redis", name = "enabled", havingValue = "true", matchIfMissing = true) public class MyRedisConfiguration { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); return template; } }

matchIfMissing = true表示配置项不存在时也匹配,可以避免升级配置导致功能突然消失。实际排错的时候,很多"配置没生效"的问题就出在这里:配置项的key拼写错了、前缀写错了、havingValue类型不对,导致条件评估失败。遇到这种问题,最快的排查办法是打开条件评估报告,查看该配置类的匹配结果,里面会明确显示"Matched"还是"Did not match",以及不匹配的原因。

条件注解的设计是一个典型的职责分离思路:@ConditionalOnClass管依赖,@ConditionalOnMissingBean管覆盖,@ConditionalOnProperty管开关,@ConditionalOnWebApplication管环境。理解了这四个核心条件,自动配置的代码阅读难度会下降一大截。

5. 启动提速与排障:从启动日志到自定义Starter

5.1 怎么看出哪些自动配置在起作用

想了解当前项目里具体哪些自动配置生效了,最直接的方式是在配置文件里加一行配置,然后启动应用:

debug=true

开启debug模式后Spring Boot会输出一份自动配置报告,分成两个部分:"Positive matches"列出了所有匹配成功加载的自动配置类,"Negative matches"列出了所有匹配失败被跳过的自动配置类。每一个条目后面会附带条件注解的匹配信息,非常方便排查。

如果是Spring Boot 2.4以上版本,还可以对启动过程做更细粒度的日志分组,把核心启动日志单独打开:

logging.level.org.springframework.boot.context.logging=DEBUG

我在实际项目里观察过一个奇怪的现象:应用本身没多少业务代码,启动却需要12秒,打开自动配置报告后发现MongoAutoConfigurationElasticsearchRestClientAutoConfigurationJpaRepositoriesAutoConfiguration这些项目根本没用到的配置全部匹配成功了,原因就是ClassLoader里存在对应依赖,而这些依赖来自内部公共SDK。所以启动慢不一定是代码问题,先看看自动配置报告,往往能发现很多"你以为没开启,实际早就被依赖带进来了"的配置。这种情况下,把用不到的自动配置显式排除掉,启动时间能缩短不少。另外,@ComponentScan扫描范围过大是启动慢的一个容易被忽视的原因,可以结合spring.main.lazy-initialization=true做临时测试,看是不是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.SmsAutoConfiguration

Spring Boot 2.7及以下则是在META-INF/spring.factories里写:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.example.sms.autoconfigure.SmsAutoConfiguration

写完starter之后,一定要在干净项目里做一次验证,确认:引入依赖后自动配置生效;配置my.sms.enabled=false后自动配置不生效;自己定义了SmsClient@Bean后自动配置的默认Bean被覆盖。这三个验证点覆盖了自动配置的三大条件注解,也是我每次写starter时的固定冒烟测试。

在实际工作中,我还习惯用Autoconfigure@AutoConfiguration(after = DataSourceAutoConfiguration.class)@AutoConfigureBefore/@AutoConfigureAfter来控制顺序。如果你有多个自动配置类需要按顺序加载,这个能力非常重要,否则可能出现某个Bean在依赖的Bean创建之后才注册,导致NoSuchBeanDefinitionException

读完这段源码再回头看Spring Boot的启动过程,你会发现它其实并不神秘:先通过SpringFactoriesLoader发现候选的配置类,再用DeferredImportSelector推迟导入,用条件注解逐个筛选,最终把真正需要的Bean组装进容器。我个人在实际操作中还有一个习惯:读源码的时候不要从第一行读到最后一行,先抓主线——run()refresh()onRefresh()finishBeanFactoryInitialization(),遇到不懂的地方再单独展开。这样一遍走下来,你就对启动过程和自动配置的每一个环节都有了掌控感,而不是停留在背结论的层面。最后再分享一个小技巧:每引入一个新依赖,启动后先扫一眼自动配置报告,看看多了哪些生效的配置类,这个习惯能帮你省掉很多未来排查启动慢和Bean冲突的麻烦。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询