Spring Boot 源码研读之 SpringApplication 对象的创建

Spring Boot 源码研读之 SpringApplication 对象的创建

SpringApplication 对象创建

一般 Spring Boot 项目都是通过在main函数中执行SpringApplication.run(XXXX.class,args);来启动的,而run方法的内部执行首先会进行SpringApplication对象的创建,让我们来看看SpringApplication对象创建做了哪些事。

通过代码跟踪最终 SpringApplication 对象创建调用的是如下构造函数:

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) { this.resourceLoader = resourceLoader; Assert.notNull(primarySources, "PrimarySources must not be null"); this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources)); this.webApplicationType = WebApplicationType.deduceFromClasspath(); this.bootstrapRegistryInitializers = new ArrayList<>( getSpringFactoriesInstances(BootstrapRegistryInitializer.class)); setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); this.mainApplicationClass = deduceMainApplicationClass(); }

通过代码分析,我可以看到 SpringApplication 对象的创建主要有如下操作:

  • resourceLoader:默认赋值null
  • primarySources:Set<Class<?>> 存放当前启动类 XXXX.class 集合,注意:启动类可能在其他位置
  • webApplicationType:通过判断相关类是否加载,确定web服务类型是 REACTIVE,还是 SERVLET。
  • bootstrapRegistryInitializers通过加载并读取META-INF/spring.factories文件中的配置并结合反射的方式来实例化所有BootstrapRegistryInitializer接口的实现类。
  • setInitializers:通过加载并读取META-INF/spring.factories文件中的配置并结合反射的方式来实例化所有ApplicationContextInitializer接口的实现类。
  • setListeners:通过加载并读取META-INF/spring.factories文件中的配置并结合反射的方式来实例化所有ApplicationListener接口的实现类。
  • mainApplicationClass:通过当前线程的堆栈来找到main()函数所在的类,即启动类

getSpringFactoriesInstances(Clazz.class)实现

getSpringFactoriesInstances方法的实现如下图所示,核心逻辑就是读取META-INF/spring.factories配置文件,并通过反射的方式创建指定接口类型的实现类实例。具体使用也可以参考另外一篇文章 《Spring 源码之 SpringFactoriesLoader 类简介-CSDN博客》

通过这一步我们就获取到了所有BootstrapRegistryInitializerApplicationContextInitializerApplicationListener接口的实现类。

补充

spring.factories加载机制

  • 核心机制SpringFactoriesLoader.loadSpringFactories()classpath*:/META-INF/spring.factories,解析key=全限定接口名,value=实现类全限定名,用反射newInstance()批量实例化。
  • 那为什么SpringApplication 在容器还没创建时就能拿到这些扩展点?
    • 因为它走的是classpath 静态扫描 + 反射不依赖 BeanFactory。这是 Spring Boot启动期扩展点机制,和运行期的BeanPostProcessor是两套体系。
  • 演进史
    • Spring Boot 2.7+:引入META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(一行一个自动配置类),逐步替代 spring.factories 里的 auto-configuration 部分
    • Spring Boot 3.0+:spring.factories 里的 auto-configuration 段正式废弃,仅保留少量非 auto-configuration 段
    • Spring Boot 3.2+:spring.factories 文件完全废弃(实际并没有完全废弃
  • 原因:spring.factories 是一个 key-value 巨型文件,所有扩展点都堆在一起,加载时全量解析;改成*.imports按需加载,启动更快、更易维护
  • 总结一句话:"SpringApplication 构造函数用 classpath 扫描 + 反射加载扩展点,绕开 Spring 容器,让扩展点在容器起来之前就能用。2.7 之后 Spring 把这条路从 spring.factories 拆成 .imports,3.2 完全废弃,本质是从'全量加载'变'按需加载'。"

WebApplicationType补充

  • 三种类型REACTIVE(WebFlux)/SERVLET(Spring MVC)/NONE(非 Web 应用)
  • 判断逻辑deduceFromClasspath()ClassUtils.isPresent依次探测:
    • 存在DispatcherServletSERVLET(但这个判断有问题,见下)
    • 存在WebFlux相关类且没有DispatcherServletREACTIVE
    • 都不存在 →NONE
  • ⚠️ 误判场景:项目引了spring-web(因为某个工具包传递依赖),没真用 Spring MVC,可能被误判为 SERVLET,启动报"找不到 DispatcherServlet"或加载多余 MVC Bean
  • 解法:手动setWebApplicationType(WebApplicationType.NONE),或在 SpringApplication 启动前排除多余依赖

deduceMainApplicationClass 链式启动支持

  • 实现new RuntimeException().getStackTrace()拿堆栈,遍历找到main方法所在类
  • 为什么用堆栈而不是参数传入?为了支持SpringApplicationBuilder链式/嵌套启动——用户可能从SpringBootServletInitializer或别的启动类拉起,主类在运行期才确定
  • Trade-off实现简单(不传参) vs 启动类可动态决定——选后者

注:spring boot 版本为3.2.3