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

SpringApplication 对象创建

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

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

java 复制代码
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