【SpringBoot】深入分析 SpringApplication 源码:彻底理解 SpringBoot 启动流程

在黄昏的余晖里,梦境渐浓,如烟如雾。心随星辰,徜徉远方,岁月静好,愿如此刻般绵长。

文章目录

  • 前言
  • 一、SpringBoot 应用
  • 二、SpringApplication
    • 2.1 SpringApplication 中的属性
    • 2.2 SpringApplication 的构造器
    • 2.3 SpringApplication 中的方法
  • 三、SpringBoot 应用的启动流程
  • 四、深入 SpringBoot 启动
    • 4.1 引导阶段
    • 4.2 启动阶段
    • 4.3 运行阶段
  • 五、FAQ
    • 5.1 Spring 容器的生命周期
    • 5.2 Spring 容器的扩展点
  • 六、小结
  • 推荐阅读

前言

SpringApplication 是 Spring Boot 框架中的一个类,它被用于引导和运行 Spring Boot 应用程序。它提供了一种简化的方式来配置和启动应用程序,减少了开发者的工作量。本文将对 SpringApplication 源码进行分析,深入理解其核心。

一、SpringBoot 应用

下面是一个 SpringBoot 应用的常规写法。

@SpringBootApplication
public class App {public static void main(String[] args) {SpringApplication.run(App.class, args);}
}

上述代码,有两个重点:

  1. @SpringBootApplication: 作用是简化配置,自动扫描组件
  2. SpringApplication: SpringBoot 应用的启动入口

二、SpringApplication

SpringApplication 是 SpringBoot 的一个入口类。这个类里的内容较多,我们将逐一进行分析。

2.1 SpringApplication 中的属性

SpringApplication 中定义了大量的属性,这些属性基本上都设置了默认值,可供之后的方法使用。

// 默认的横幅(Banner)位置
private static final String DEFAULT_BANNER_LOCATION = "banner.txt";// 横幅(Banner)位置的属性键
private static final String BANNER_LOCATION_PROPERTY = "spring.banner.location";// Java AWT 无头模式的系统属性键
private static final String SYSTEM_PROPERTY_JAVA_AWT_HEADLESS = "java.awt.headless";// 日志记录器
private static final Log logger = LogFactory.getLog(SpringApplication.class);// 处理应用程序关闭时的清理工作
private Set<ApplicationContext> runningAppContexts;// 主要来源的集合,表示主类或主配置类
private Set<Class<?>> primarySources;// 资源集合,表示应用程序的来源
private Set<String> sources;// 主应用程序类
private Class<?> mainApplicationClass;// 横幅模式,默认为 Banner.Mode.CONSOLE,表示将横幅输出到控制台
private Banner.Mode bannerMode = Banner.Mode.CONSOLE;// 是否记录启动信息,默认为 true
private boolean logStartupInfo = true;// 是否添加命令行属性,默认为 true
private boolean addCommandLineProperties = true;// 是否添加转换服务,默认为 true
private boolean addConversionService = true;// 横幅实例
private Banner banner;// 资源加载器,用于加载应用程序的资源
private ResourceLoader resourceLoader = new DefaultResourceLoader();// Bean 名称生成器,用于生成 Bean 的名称
private BeanNameGenerator beanNameGenerator;// 配置环境,用于获取应用程序的配置属性
private ConfigurableEnvironment environment;// Web 应用程序类型,表示应用程序是一个 Servlet 应用程序还是一个反应式 Web 应用程序
private WebApplicationType webApplicationType;// 是否为无头模式,默认为 true
private boolean headless = true;// 是否注册关闭钩子,默认为 true
private boolean registerShutdownHook = true;// 应用程序上下文初始化器的列表,用于初始化应用程序上下文
private List<ApplicationContextInitializer<?>> initializers;// 应用程序监听器的列表,用于监听应用程序事件
private List<ApplicationListener<?>> listeners;// 默认属性的映射,用于配置应用程序的默认属性
private Map<String, Object> defaultProperties;// 引导注册表初始化器的列表,用于初始化引导注册表
private List<BootstrapRegistryInitializer> bootstrapRegistryInitializers;// 附加配置文件的集合,用于指定额外的配置文件
private Set<String> additionalProfiles;// 是否允许覆盖 Bean 定义,默认为 false
private boolean allowBeanDefinitionOverriding;// 是否允许循环引用,默认为 true
private boolean allowCircularReferences = true;// 是否使用自定义环境,默认为 false
private boolean isCustomEnvironment = false;// 是否延迟初始化,默认为 false
private boolean lazyInitialization = false;// 环境前缀
private String environmentPrefix;// 应用程序上下文工厂
private ApplicationContextFactory applicationContextFactory = ApplicationContextFactory.DEFAULT;// 应用程序启动器
private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT;

2.2 SpringApplication 的构造器

SpringApplication 中表面上有两个有参构造器。但是第一个构造器是通过调用第二个构造器实现对象的初始化,所以,SpringApplication 的初始化我们只需要了解第二个构造器就可以了。

/*** 创建一个新的 SpringApplication 实例。** @param primarySources 主要来源的类,表示主类或主配置类*/
public SpringApplication(Class<?>... primarySources) {// 调用另一个构造函数,并传入 null 作为 ResourceLoaderthis(null, primarySources);
}
/*** 使用指定的资源加载器创建一个新的 SpringApplication 实例。** @param resourceLoader 资源加载器,用于加载应用程序的资源,可以为 null* @param primarySources 主要来源的类,表示主类或主配置类,不能为空*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {// 设置资源加载器this.resourceLoader = resourceLoader;// 确保 primarySources 不为空,否则抛出异常Assert.notNull(primarySources, "PrimarySources must not be null");// 将 primarySources 转换为 LinkedHashSet 并赋值给 this.primarySourcesthis.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));// 根据类路径推断 Web 应用程序类型(如 Servlet 或 Reactive)this.webApplicationType = WebApplicationType.deduceFromClasspath();// 从 Spring 工厂加载 BootstrapRegistryInitializer 实例并添加到 bootstrapRegistryInitializers 列表中this.bootstrapRegistryInitializers = new ArrayList<>(getSpringFactoriesInstances(BootstrapRegistryInitializer.class));// 从 Spring 工厂加载 ApplicationContextInitializer 实例并设置为初始器setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));// 从 Spring 工厂加载 ApplicationListener 实例并设置为监听器setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));// 推断主应用程序类并赋值给 this.mainApplicationClassthis.mainApplicationClass = deduceMainApplicationClass();
}

之前,我们已经了解 SpringApplication 中的属性大部分会有默认值,但是还有一部分属性并没有给初始值。在 SpringApplication 的构造器中,这一部分没有给定初始值的属性会在构造器中计算出初始值。例如:resourceLoaderprimarySourceswebApplicationTypebootstrapRegistryInitializersmainApplicationClass 等。

由此,我们可以看出 SpringApplication 构造器的作用是完成一些属性的初始化工作。

2.3 SpringApplication 中的方法

SpringApplication 中的方法有许多,我们可以将这些方法分类进行讨论。首先,我们将 SpringApplication 中的方法分为 private非 private 方法。我们知道 private 修饰的方法只能给 SpringApplication 内部使用,我们并不会直接使用到。所以,其实 private 修饰的方法我们并不需要去了解。

现在剩下的方法,我们再进行一次划分:我们将剩下的方法分为setter、getter 方法和非 setter、getter 方法。我们知道,setter 和 getter 方法是用来控制属性的,不用过多的讨论。

image.png

至此,SpringApplication 中剩下值得讨论的方法就只有下面这些了。

image.png

而在这些方法中,addBootstrapRegistryInitializeraddInitializersaddListenersaddPrimarySources 这几个方法的功能是和 setter 方法类似的,因为相应的属性是集合,使用 add 方法向集合中添加值。

image.png

最终,我们分析出:SpringApplication 只有三个可供开发者直接使用的核心方法。

  1. main() 方法
  2. exit() 方法
  3. run() 方法

image.png

main() 方法是命令行场景下使用的。

/*** 一个用于启动应用程序的基本主方法。当应用程序源代码通过* {@literal --spring.main.sources} 命令行参数定义时,此方法非常有用。* <p>* 大多数开发人员可能希望自定义自己的主方法,并调用* {@link #run(Class, String...) run} 方法来启动应用程序。* @param args 命令行参数* @throws Exception 如果应用程序无法启动* @see SpringApplication#run(Class[], String[])* @see SpringApplication#run(Class, String...)*/
public static void main(String[] args) throws Exception {SpringApplication.run(new Class<?>[0], args);
}

exit() 方法的作用是退出 SpringBoot 应用。

/*** 退出应用程序并返回退出码。接收一个应用程序上下文和一个或多个退出码生成器作为参数。* @param context 应用程序上下文* @param exitCodeGenerators 退出码生成器* @return 退出码*/
public static int exit(ApplicationContext context, ExitCodeGenerator... exitCodeGenerators) {// 检查上下文是否为空Assert.notNull(context, "Context must not be null");int exitCode = 0;try {try {// 创建所有的退出码生成器ExitCodeGenerators generators = new ExitCodeGenerators();// 获取应用程序上下文中所有的退出码生成器Collection<ExitCodeGenerator> beans = context.getBeansOfType(ExitCodeGenerator.class).values();// 添加传入的退出码生成器和上下文中的退出码生成器到生成器集合中generators.addAll(exitCodeGenerators);generators.addAll(beans);// 获取最终的退出码exitCode = generators.getExitCode();// 如果退出码不为0,则发布退出码事件if (exitCode != 0) {context.publishEvent(new ExitCodeEvent(context, exitCode));}} finally {// 关闭应用程序上下文close(context);}} catch (Exception ex) {// 打印异常堆栈信息ex.printStackTrace();// 如果之前的退出码为0,则将退出码设置为1exitCode = (exitCode != 0) ? exitCode : 1;}return exitCode;
}

这个方法提供了一种优雅的方式来关闭 Spring 应用程序,并根据上下文中存在的退出码生成器确定合适的退出码。如果在关闭过程中发生异常,也会适当地处理并返回一个退出代码。

run() 方法的作用是运行 SpringBoot 应用,这个方法被重载了 3 3 3 次,前 2 2 2 次重载并无实际的处理逻辑。

/*** 这个方法接受一个主要源类和一组命令行参数来运行一个 Spring 应用程序。* 通常,主要源是一个带有 @SpringBootApplication 注解的类。* 它委托给另一个 run 方法来执行。** @param primarySource 应用程序的主要源类* @param args          命令行参数* @return 可配置的应用程序上下文*/
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {// 委托给另一个 run 方法,传递一个包含主要源类的数组和命令行参数return run(new Class<?>[] { primarySource }, args);
}
/*** 创建一个新的 SpringApplication 对象,并使用给定的主要源类数组初始化它,* 然后调用 SpringApplication 的 run 方法来启动应用程序。*/
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {// 创建一个新的 SpringApplication 对象,使用给定的主要源类数组初始化它,并调用 run 方法启动应用程序return new SpringApplication(primarySources).run(args);
}
/*** 运行 Spring 应用程序,并返回配置好的应用程序上下文。** @param args 命令行参数* @return 配置好的 Spring 应用程序上下文*/
public ConfigurableApplicationContext run(String... args) {// 记录启动时间long startTime = System.nanoTime();// 创建引导上下文DefaultBootstrapContext bootstrapContext = createBootstrapContext();ConfigurableApplicationContext context = null;// 配置 Headless 属性configureHeadlessProperty();// 获取运行监听器SpringApplicationRunListeners listeners = getRunListeners(args);// 通知监听器应用程序即将启动listeners.starting(bootstrapContext, this.mainApplicationClass);try {// 创建应用程序参数对象ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);// 准备环境ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);// 配置忽略 BeanInfoconfigureIgnoreBeanInfo(environment);// 打印 BannerBanner printedBanner = printBanner(environment);// 创建应用程序上下文context = createApplicationContext();// 设置应用程序启动信息context.setApplicationStartup(this.applicationStartup);// 准备应用程序上下文prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);// 刷新应用程序上下文refreshContext(context);// 在刷新后执行其他操作afterRefresh(context, applicationArguments);// 计算启动所花费的时间Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);// 如果需要,记录启动信息if (this.logStartupInfo) {new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);}// 通知监听器应用程序已启动listeners.started(context, timeTakenToStartup);// 调用运行器callRunners(context, applicationArguments);}catch (Throwable ex) {// 处理运行失败情况handleRunFailure(context, ex, listeners);throw new IllegalStateException(ex);}try {// 计算应用程序准备所花费的时间Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);// 通知监听器应用程序已准备就绪listeners.ready(context, timeTakenToReady);}catch (Throwable ex) {// 处理运行失败情况handleRunFailure(context, ex, null);throw new IllegalStateException(ex);}// 返回应用程序上下文return context;
}

三、SpringBoot 应用的启动流程

计算机的启动过程通常按以下步骤进行:

  1. 步骤 1:当我们打开电源时,从非易失性存储器中加载 BIOS(基本输入/输出系统)或 UEFI(统一可扩展固件接口)固件,并执行 POST(开机自检)。
  2. 步骤 2:BIOS/UEFI 检测连接到系统的设备,包括 CPU、RAM 和存储设备。
  3. 步骤 3:选择一个启动设备来引导操作系统。这可以是硬盘、网络服务器或 CD-ROM。
  4. 步骤 4:BIOS/UEFI 运行引导加载程序(如 GRUB),该程序提供一个菜单供选择操作系统或内核功能。
  5. 步骤 5:内核准备就绪后,系统切换到用户空间。内核启动 systemd 作为第一个用户空间进程,systemd 管理各个进程和服务,探测所有剩余的硬件,挂载文件系统,并运行桌面环境。
  6. 步骤 6:systemd 默认激活 default.target 单元,当系统启动时,其他分析单元也会被执行。
  7. 步骤 7:系统运行一组启动脚本并配置环境。
  8. 步骤 8:用户将看到登录窗口。系统现在已经准备就绪。

01911933-5a25-4dba-a57c-d9bd65680d84_1280x1664.gif

综上,上面的计算机启动过程太多,我们可以进一步总结为以下三个主要阶段:引导、启动和运行。

  1. 引导阶段:在此阶段,计算机会执行基本输入/输出系统(BIOS)或统一可扩展固件接口(UEFI)等引导加载程序,以加载操作系统的引导记录并将操作系统内核加载到内存中
  2. 启动阶段:一旦操作系统内核被加载到内存中,计算机将开始执行操作系统的初始化过程,包括建立内存管理、初始化进程、加载驱动程序等操作。
  3. 运行阶段:在这个阶段,操作系统已经完全加载并且用户界面准备就绪,用户可以登录系统并开始使用计算机进行各种任务。

SpringApplication 中的 run() 方法功能和计算机的启动流程是类似的。

run() 方法也可以分成三个阶段:

  1. 引导阶段:创建引导容器 DefaultBootstrapContext、启动监听器 SpringApplicationRunListeners、准备环境 prepareEnvironment、打印 Banner
  2. 启动阶段:创建应用容器 ConfigurableApplicationContext,并完成应用容器的初始化工作
  3. 运行阶段:应用容器准备就绪,可以使用

在这里插入图片描述

引导容器 DefaultBootstrapContext 就如同计算机启动过程中的内核,首先完成内核的加载。之后,通过内核初始化应用容器 ConfigurableApplicationContext(类似操作系统)。当操作系统初始化成功,最终容器达可运行阶段,可供使用。

四、深入 SpringBoot 启动

4.1 引导阶段

在这里插入图片描述

SpringApplication 的引导阶段并没有计算机的引导阶段那么复杂。这一阶段,主要是创建引导容器 DefaultBootstrapContext 用于下一阶段引导应用容器 ConfigurableApplicationContext。其次,在这一阶段也会启动 SpringBoot 的监听器功能 SpringApplicationRunListeners,以及准备环境 prepareEnvironment()

4.2 启动阶段

在这里插入图片描述

SpringApplication 的启动阶段内容比较多。这一阶段,核心内容是做了以下几件事情:

  1. 创建应用容器 ConfigurableApplicationContext
  2. 准备应用程序上下文 prepareContext()
  3. 刷新应用程序上下文 refreshContext()

第一个核心内容,创建应用容器,其实很好理解。因为这就是我们真正使用的容器。我们唯一需要知道的就是应用容器的创建时机是在此处。

第二个核心内容,准备应用程序上下文这一阶段主要是在启动 Spring 应用程序时,准备应用程序的上下文。它的主要步骤包括设置环境、应用初始化器、记录启动信息、注册单例 Bean、设置 Bean 工厂属性、延迟初始化、添加属性源排序后处理器、加载应用程序源,并通知监听器应用程序上下文的准备和加载完成。这些步骤确保了应用程序上下文的正确配置和准备,为应用程序的顺利启动提供了必要的准备工作。

private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,ApplicationArguments applicationArguments, Banner printedBanner) {// 将环境对象设置到应用程序上下文中context.setEnvironment(environment);// 对应用程序上下文进行后处理postProcessApplicationContext(context);// 应用初始化器到应用程序上下文中applyInitializers(context);// 通知监听器应用程序上下文已准备好listeners.contextPrepared(context);// 关闭引导上下文bootstrapContext.close(context);// 如果需要记录启动信息,记录启动信息和启动配置文件信息if (this.logStartupInfo) {logStartupInfo(context.getParent() == null);logStartupProfileInfo(context);}// 获取应用程序上下文的 Bean 工厂ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();// 注册特定于 Spring Boot 的单例 bean,如 springApplicationArgumentsbeanFactory.registerSingleton("springApplicationArguments", applicationArguments);// 如果存在打印的横幅,则注册 springBootBanner 单例 beanif (printedBanner != null) {beanFactory.registerSingleton("springBootBanner", printedBanner);}// 检查 Bean 工厂实例是否为 AbstractAutowireCapableBeanFactory 类型if (beanFactory instanceof AbstractAutowireCapableBeanFactory) {// 设置是否允许循环引用((AbstractAutowireCapableBeanFactory) beanFactory).setAllowCircularReferences(this.allowCircularReferences);// 检查 Bean 工厂实例是否为 DefaultListableBeanFactory 类型if (beanFactory instanceof DefaultListableBeanFactory) {// 设置是否允许 Bean 定义覆盖((DefaultListableBeanFactory) beanFactory).setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);}}// 如果启用了延迟初始化,则添加延迟初始化 Bean 工厂后处理器if (this.lazyInitialization) {context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());}// 添加属性源排序的 Bean 工厂后处理器context.addBeanFactoryPostProcessor(new PropertySourceOrderingBeanFactoryPostProcessor(context));// 加载应用程序的源Set<Object> sources = getAllSources();Assert.notEmpty(sources, "Sources must not be empty");load(context, sources.toArray(new Object[0]));// 通知监听器应用程序上下文已加载完成listeners.contextLoaded(context);
}
  1. 设置环境:将传入的 ConfigurableEnvironment 对象设置到应用程序上下文中,以便上下文可以使用这个环境中的配置和属性。

    context.setEnvironment(environment);
    
  2. 后处理应用程序上下文:调用自定义方法对应用程序上下文进行额外的处理。具体实现取决于开发者的需求,这一步一般用于添加额外的配置或修改上下文的某些属性。

    postProcessApplicationContext(context);
    
  3. 应用初始化器:调用所有注册的 ApplicationContextInitializer 初始化器,对应用程序上下文进行进一步的配置。

    applyInitializers(context);
    
  4. 通知监听器上下文已准备:通知所有 SpringApplicationRunListener,应用程序上下文已准备好。这通常用于在上下文被刷新之前执行一些操作。

    listeners.contextPrepared(context);
    
  5. 关闭引导上下文:关闭引导上下文,释放资源。

    bootstrapContext.close(context);
    
  6. 记录启动信息:如果启用了启动信息记录,记录启动信息和配置文件信息。这对于调试和诊断问题很有帮助。

    if (this.logStartupInfo) {logStartupInfo(context.getParent() == null);logStartupProfileInfo(context);
    }
    
  7. 注册单例 Bean:将 applicationArguments 和 printedBanner(如果有)注册为单例 Bean,以便它们可以在应用程序的其他部分使用。

    ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
    beanFactory.registerSingleton("springApplicationArguments", applicationArguments);if (printedBanner != null) {beanFactory.registerSingleton("springBootBanner", printedBanner);
    }
    
  8. 设置 Bean 工厂属性:根据配置设置 Bean 工厂的属性,例如是否允许循环引用和 Bean 定义覆盖。

    if (beanFactory instanceof AbstractAutowireCapableBeanFactory) {((AbstractAutowireCapableBeanFactory) beanFactory).setAllowCircularReferences(this.allowCircularReferences);if (beanFactory instanceof DefaultListableBeanFactory) {((DefaultListableBeanFactory) beanFactory).setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);}
    }
    
  9. 延迟初始化:如果启用了延迟初始化,向应用程序上下文添加一个 LazyInitializationBeanFactoryPostProcessor,以推迟 Bean 的初始化。

    if (this.lazyInitialization) {context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
    }
    
  10. 添加属性源排序后处理器:添加一个 PropertySourceOrderingBeanFactoryPostProcessor,确保属性源按照预期顺序被处理。

    context.addBeanFactoryPostProcessor(new PropertySourceOrderingBeanFactoryPostProcessor(context));
    
  11. 加载应用程序源:获取所有应用程序源并加载它们。确保源不为空,否则抛出异常。

    Set<Object> sources = getAllSources();
    Assert.notEmpty(sources, "Sources must not be empty");
    load(context, sources.toArray(new Object[0]));
    
  12. 通知监听器上下文已加载:通知所有 SpringApplicationRunListener,应用程序上下文已加载完成。这是最后一步,表示上下文已经完全准备好,可以启动应用程序了。

    listeners.contextLoaded(context);
    

此阶段我们需要了解的是:这一阶段是在处理容器准备容器,还未开始处理 Bean,因为这一阶段并没有将真正的 Bean 注册到容器。上面虽有一个注册单例 Bean,但是注册的是 springApplicationArgumentsspringBootBanner,这两个 Bean 都属于容器相关的。

第三个核心内容,刷新应用程序上下文

public void refresh() throws BeansException, IllegalStateException {synchronized (this.startupShutdownMonitor) { // 确保线程安全StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh"); // 记录启动步骤// 准备此上下文以进行刷新。prepareRefresh();// 告诉子类刷新内部 bean 工厂。ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();// 为该上下文准备 bean 工厂。prepareBeanFactory(beanFactory);try {// 允许在上下文子类中对 bean 工厂进行后处理。postProcessBeanFactory(beanFactory);StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");// 调用在上下文中注册为 bean 的工厂处理器。invokeBeanFactoryPostProcessors(beanFactory);// 注册拦截 bean 创建的 bean 处理器。registerBeanPostProcessors(beanFactory);beanPostProcess.end();// 初始化此上下文的消息源。initMessageSource();// 初始化此上下文的事件广播器。initApplicationEventMulticaster();// 初始化特定上下文子类中的其他特殊 bean。onRefresh();// 检查监听器 bean 并注册它们。registerListeners();// 实例化所有剩余的(非 lazy-init)单例。finishBeanFactoryInitialization(beanFactory);// 最后一步:发布相应事件。finishRefresh();}catch (BeansException ex) {if (logger.isWarnEnabled()) {logger.warn("在上下文初始化期间遇到异常 - 取消刷新尝试: " + ex);}// 销毁已创建的单例以避免资源悬挂。destroyBeans();// 重置 'active' 标志。cancelRefresh(ex);// 将异常传播给调用者。throw ex;}finally {// 重置 Spring 核心中的常见内省缓存,因为我们可能不再需要单例 bean 的元数据...resetCommonCaches();contextRefresh.end(); // 结束启动步骤记录}}
}
  1. 记录启动步骤:记录上下文刷新过程的启动步骤,用于性能监控和排错。

    StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
    
  2. 准备刷新:这个方法用于准备上下文刷新,包括设置环境、属性源和早期事件发布等。

    prepareRefresh();
    
  3. 获取新的 Bean 工厂:刷新 Bean 工厂,通常是销毁旧的 Bean 工厂并创建一个新的。

    ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
    
  4. 准备 Bean 工厂:设置 Bean 工厂的一些标准配置,如类加载器、表达式解析器和一些默认的 Bean 后处理器。

    prepareBeanFactory(beanFactory);
    
  5. 后处理 Bean 工厂:子类可以重写这个方法以便在 Bean 工厂标准初始化之后做一些自定义的修改。

    postProcessBeanFactory(beanFactory);
    
  6. 调用 Bean 工厂后处理器:这一步调用所有注册的 BeanFactoryPostProcessor,用于修改应用上下文的内部 Bean 定义。

    invokeBeanFactoryPostProcessors(beanFactory);
    
  7. 注册 Bean 后处理器:注册 BeanPostProcessor,这些处理器会在 Bean 实例化前后进行一些自定义操作。

    registerBeanPostProcessors(beanFactory);
    
  8. 初始化消息源:初始化应用上下文中的消息源,用于国际化消息处理。

    initMessageSource();
    
  9. 初始化事件广播器:初始化事件广播器,用于发布应用上下文中的事件。

    initApplicationEventMulticaster();
    
  10. 特定子类的刷新操作:留给子类实现的钩子方法,允许在刷新上下文时添加一些特定的逻辑。

    onRefresh();
    
  11. 注册监听器:查找并注册所有的事件监听器。

    registerListeners();
    
  12. 实例化所有剩余的单例:实例化所有非延迟初始化的单例 Bean,确保它们都已准备好使用。

    finishBeanFactoryInitialization(beanFactory);
    
  13. 完成刷新:最后一步,主要是清理缓存和发布事件,标志着上下文刷新完成。

    finishRefresh();
    

刷新应用程序上下文的逻辑是比较复杂的。我们可以看到,在 refresh() 方法中,只有刷新的流程,但是没有具体的操作,具体的操作都在方法里面。

这些方法中,有几个我们需要特别讨论一下:

  1. obtainFreshBeanFactory()
  2. postProcessBeanFactory()
  3. invokeBeanFactoryPostProcessors()
  4. registerBeanPostProcessors()

obtainFreshBeanFactory() 方法底层调用了 loadBeanDefinitions() 方法。loadBeanDefinitions() 见名知意,它的作用是将加载所有 BeanDefinition

postProcessBeanFactory() 方法底层调用了 doRegisterBean() 方法。同样地,doRegisterBean() 方法也可见名知意,它的作用是注册 Bean

invokeBeanFactoryPostProcessors() 方法的核心作用是在 Spring 容器初始化时,正确地调用所有 BeanFactoryPostProcessorBeanDefinitionRegistryPostProcessor,以便开发者可以在 Bean 实例化之前修改 Bean 定义和配置,从而影响整个应用程序上下文中的 Bean 行为。这种机制提供了很大的灵活性,使得 Spring 应用程序可以根据需要动态调整 Bean 的配置和定义。

registerBeanPostProcessors() 方法的核心是处理和注册 BeanPostProcessor 实例。在 Spring 容器启动时,会调用该方法,将所有的 BeanPostProcessor 注册到容器中,以便在 Bean 的生命周期中进行处理。

由此,我们可以知道 SpringApplication 刷新的核心是什么:

在这里插入图片描述

这也解释了为什么 Spring 容器会有两大扩展点:

  1. 容器的扩展点:通过实现 BeanFactoryPostProcessorBeanDefinitionRegistryPostProcessor
  2. Bean 的扩展点:通过实现 BeanPostProcessor

4.3 运行阶段

运行阶段,其实没什么特别需要讨论的。这一阶段容器已准备就绪,Bean 已加载完成。剩下的只是计算一下整个容器启动所花费的时间,然后通知监听器,容器已处于就绪状态。

五、FAQ

5.1 Spring 容器的生命周期

其实,Spring 的官方文档中并没有说明 Spring 容器的生命周期,可是我们经常听到 Spring 容器生命周期这一说法。这一说法其实有两个来源:

  1. 通过对上述 Spring 容器的启动流程进行总结出来的
  2. 通过分析 SpringApplicationRunListener 接口

许多人都是通过分析 Spring 容器启动流程,然后在此基础上进行总结的。这种方式有个非常大的弊端:由于每个人总结的思路不同会导致结果不一致,从而形成了多种说法。懂的人自然懂,初学者或者想要深入理解 Spring 容器生命周期的人会自然而然的产生疑惑,不知道究竟哪个版本更为准确。

其实,我更加推荐通过分析 SpringApplicationRunListener 接口来讨论 Spring 容器的生命周期。

public interface SpringApplicationRunListener {/*** 在应用启动之初被调用。此时除非必要,不应该执行任何需要配置环境或上下文的操作。** @param bootstrapContext Spring Boot 的引导上下文,用于存储和共享跨多个阶段的数据。*/default void starting(ConfigurableBootstrapContext bootstrapContext) {}/*** 当环境准备完毕时被调用。在这个阶段,可以对环境进行进一步的自定义。** @param bootstrapContext Spring Boot 的引导上下文,用于存储和共享跨多个阶段的数据。* @param environment      配置环境对象,包含了所有的配置信息。*/default void environmentPrepared(ConfigurableBootstrapContext bootstrapContext,ConfigurableEnvironment environment) {}/*** 当应用上下文准备完毕时被调用。此时上下文已经创建,但还没有加载 Bean 定义。** @param context Spring 应用上下文。*/default void contextPrepared(ConfigurableApplicationContext context) {}/*** 当应用上下文加载完毕时被调用。此时所有的 Bean 定义已经加载,但没有刷新上下文。** @param context Spring 应用上下文。*/default void contextLoaded(ConfigurableApplicationContext context) {}/*** 当应用上下文刷新并完全启动后被调用。这个方法包含了一个 `Duration` 参数,表示启动所花费的时间。** @param context  Spring 应用上下文。* @param timeTaken 启动所花费的时间。*/default void started(ConfigurableApplicationContext context, Duration timeTaken) {started(context);}/*** 这个方法已废弃,不推荐继续使用。建议使用带 `Duration` 参数的 `started` 方法。** @param context Spring 应用上下文。*/@Deprecateddefault void started(ConfigurableApplicationContext context) {}/*** 当应用完全就绪并可以接受请求时被调用。这个方法包含了一个 `Duration` 参数,表示从启动到就绪所花费的时间。** @param context  Spring 应用上下文。* @param timeTaken 从启动到就绪所花费的时间。*/default void ready(ConfigurableApplicationContext context, Duration timeTaken) {running(context);}/*** 这个方法已废弃,不推荐继续使用。建议使用带 `Duration` 参数的 `ready` 方法。** @param context Spring 应用上下文。*/@Deprecateddefault void running(ConfigurableApplicationContext context) {}/*** 当应用启动过程中发生错误时被调用。此时可以记录错误信息或进行其他的错误处理操作。** @param context   Spring 应用上下文。* @param exception 启动过程中发生的异常。*/default void failed(ConfigurableApplicationContext context, Throwable exception) {}
}

在之前分析 SpringBoot 容器启动流程时,我们故意忽略了 Spring Listener 的逻辑。我们仔细回顾一下源代码就会发现:SpringApplication 会在完成一个阶段之后,就会调用的 SpringApplicationRunListener 中相应的方法标志当前的处理阶段同时会执行一些该阶段的处理逻辑。

由此可见,Spring 官方其实对容器生命周期是有自己的定义的:

workspace.png

源码中有 failed,但这个是容器异常的情况,可能会在任意阶段发生,所以不应该属于生命周期的某个指定阶段。

我们进一步分析 SpringApplicationRunListener 中的源码,会发现一些方法被废弃了。废弃的方法有两种原因:

  1. 有更好的替代方案:比如 started() 方法更推荐使用有计算启动时间的方法

    /*** 当应用上下文刷新并完全启动后被调用。这个方法包含了一个 `Duration` 参数,表示启动所花费的时间。** @param context  Spring 应用上下文。* @param timeTaken 启动所花费的时间。*/
    default void started(ConfigurableApplicationContext context, Duration timeTaken) {started(context);
    }/*** 这个方法已废弃,不推荐继续使用。建议使用带 `Duration` 参数的 `started` 方法。** @param context Spring 应用上下文。*/
    @Deprecated
    default void started(ConfigurableApplicationContext context) {}
    
  2. 阶段差异不大running 阶段和 ready 阶段差别不大,直接使用 ready 阶段替代

    default void ready(ConfigurableApplicationContext context, Duration timeTaken) {running(context);
    }/*** 这个方法已废弃,不推荐继续使用。建议使用带 `Duration` 参数的 `ready` 方法。** @param context Spring 应用上下文。*/
    @Deprecated
    default void running(ConfigurableApplicationContext context) {}
    

所以,我们知道:对于 Spring 容器的生命周期其实并没有官方的明确定义,官方也可能会对一些细节进行重新定义。但是,我们可以采用官方在 SpringApplicationRunListener 中的定义来描述 Spring 容器的生命周期。这样做有几个好处:

  1. 源码更能使人信服,统一大家对 Spring 容器生命周期的理解,最大限度拉齐认知
  2. 帮助我们深入理解 Spring 官方团队的处理思路,可以更好的理解源码

综上,通常我们认为 Spring 容器的生命周期为:

  1. starting
    • 时间点:在 Spring Boot 应用启动之初,run 方法被调用时。
    • 作用:这个阶段最早发生,可以用于执行一些初始化操作,比如设置启动参数、记录日志等。
  2. environmentPrepared
    • 时间点:在 Spring Boot 应用的 Environment(包括系统属性、环境变量、配置文件等)准备好之后。
    • 作用:此阶段可以用来修改或添加环境属性,或者做一些依赖于环境配置的初始化操作。
  3. contextPrepared
    • 时间点:在 Spring 应用上下文创建之后但还未加载 BeanDefinition 之前。
    • 作用:可以对应用上下文进行进一步配置,设置一些全局属性或注册额外的组件。
  4. contextLoaded
    • 时间点:在所有的 BeanDefinition 被加载但尚未刷新上下文之前。
    • 作用:此阶段可以操作 BeanDefinition,比如动态注册 Bean,修改某些 Bean 的属性等。
  5. started
    • 时间点:在应用上下文刷新并完全启动后。
    • 作用:此时,所有的单例 Bean 已经初始化完毕,可以执行一些需要在 Bean 完全初始化后的操作,如启动后台任务、初始化连接池等。
  6. ready
    • 时间点:在应用完全准备好并能够响应请求时。
    • 作用:此阶段标志着应用已经完全启动并准备就绪,可以处理实际的业务请求。

5.2 Spring 容器的扩展点

之前,我们在分析 SpringApplication 源码时,已经发现了两个扩展点:

  1. 容器的扩展点:通过实现 BeanFactoryPostProcessorBeanDefinitionRegistryPostProcessor
  2. Bean 的扩展点:通过实现 BeanPostProcessor

Spring 是非常灵活的框架,除了上述的扩展点,Spring 也为生命周期的各个阶段提供了扩展点。Spring 容器生命周期的扩展点是通过 SpringApplicationRunListener 实现的监听器机制。

ApplicationEvent.png

开发者只需要发布生命周期对应的事件,就可以实现在生命周期的指定阶段实现特定功能。

  1. starting 阶段对应 ApplicationStartingEvent 事件
  2. environmentPrepared 阶段对应 ApplicationEnvironmentPreparedEvent 事件
  3. contextPrepared 阶段对应 ApplicationContextInitializedEvent 事件
  4. contextLoaded 阶段对应 ApplicationPreparedEvent 事件
  5. started 阶段对应 ApplicationStartedEvent 事件
  6. ready 阶段对应 ApplicationReadyEvent 事件

六、小结

SpringApplication 源码中包含许多 Spring 容器的底层原理,仔细阅读其源码可以帮助我们深入理解 SpringBoot。

推荐阅读

  1. Spring 三级缓存
  2. 深入了解 MyBatis 插件:定制化你的持久层框架
  3. 深入探究 Spring Boot Starter:从概念到实践
  4. Zookeeper 注册中心:单机部署
  5. 【JavaScript】探索 JavaScript 中的解构赋值

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/27536.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Linux】基础IO——文件描述符,重定向,FILE

话接上篇&#xff1a; 1.文件描述符fd 磁盘文件 VS 内存文件&#xff1f; 当文件存储在磁盘当中时&#xff0c;我们将其称之为磁盘文件&#xff0c;而当磁盘文件被加载到内存当中后&#xff0c;我们将加载到内存当中的文件称之为内存文件。磁盘文件和内存文件之间的关系就像程…

JVM 三色标记算法

三色标记算法核心原理 三色标记算法是一种JVM的垃圾标记算法&#xff0c;CMS/G1垃圾回收器就是使用的这种算法&#xff0c;它可以让JVM在不发生或者尽可能短的发生STW&#xff08;Stop The World&#xff09;的情况下进行垃圾的标记和清除。 顾名思义&#xff0c;三色标记算法…

实现JWT认证与授权的Spring Boot项目详解

我们将详细介绍如何使用JWT&#xff08;JSON Web Tokens&#xff09;结合Spring Boot框架实现用户认证和授权系统。此方案将包括用户注册、登录以及通过JWT令牌进行后续请求的身份验证过程。我们将从引入必要的依赖开始&#xff0c;然后逐步构建项目的各个部分&#xff0c;包括…

精品丨PowerBI迁移到SSAS

业务场景&#xff1a; 企业初期在进行 BI 可视化路线的时候&#xff0c;往往不会选择方案较为完整的SSAS&#xff0c;而是会选择轻量的 PowerBI 方案&#xff0c;究其根本还是软件成本的问题。 但是随着模型越来越臃肿&#xff0c;维护成本越来越高&#xff0c;有很多模型需要进…

Java面向对象-抽象类和抽象方法

Java面向对象-抽象类和抽象方法 1、代码案例展示2、抽象类和抽象方法的关系&#xff1a; 1、代码案例展示 1、在一个类中会有一类方法&#xff0c;无需重写&#xff0c;直接使用 2、在一个类中会有一类方法&#xff0c;会对这个方法进行重写 3、一个方法的方法体去掉&#xff…

【文心智能体分享】日记周报助手

引言 在繁忙的实习生活中&#xff0c;你是否曾为如何整理日常的工作日志、周报、月报而烦恼&#xff1f;现在&#xff0c;我们为你带来了一个全新的智能体——“日记周报助手”&#xff0c;它将成为你实习过程中的得力助手&#xff0c;帮你轻松整理实习日志&#xff0c;让你的…

mysql 中的锁

一.锁的介绍 锁是计算机协调多个进程或线程并发访问某一资源的机制&#xff0c;在数据库中&#xff0c;除了传统的计算资源&#xff08;cpu&#xff0c;ram&#xff0c;i/o&#xff09;的争用以外&#xff0c;数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性…

初见 Rollup 的十大常见问题

文章目录 初见 Rollup 的十大常见问题1. 超神奇的 Rollup 英文解释&#xff01;2. 为什么 ESM 要比 CommonJS 要好呢&#xff1f;3. 什么是 tree-shaking ?4. 如何使用 Rollup 处理 CommonJS&#xff1f;5. 为什么 node-resolve 不是一个内置功能&#xff1f;6. 为什么在进行代…

如何警用root用户登录ssh

使用tail指令&#xff0c;可以动态查看日志信息。 &#xff08;tail -f /var/log/secure或messages&#xff09; 使用>符号&#xff0c;可以清空日志内容&#xff0c;不删除文件本身。 禁用root用户为以下步骤&#xff1a; 首先使用useradd创建用户&#xff08;可以修改为其…

STM32HAL-最简单的时间片论法

目录 概述 一、开发环境 二、STM32CubeMx配置 三、编码 四、运行结果 五、总结 概述 本文章使用最简单的写法时间片论法框架,非常适合移植各类型单片机,特别是资源少的芯片上。接下来将在stm32单片机上实现,只需占用1个定时器作为tick即可。(按键框架+时间片论法)…

【数据结构之B树的讲解】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

【乐吾乐2D可视化组态编辑器】开关、阀门、报警状态切换

开关状态 开关的断开与闭合&#xff1a;将电力组件的“开”与“关”2个组件重叠在一起&#xff0c;右键选择“组合为状态”&#xff0c;属性面板中就可以任意切换状态。 视频教程&#xff1a;开关阀门多状态控制 乐吾乐2D可视化组态编辑器地址&#xff1a;https://2d.le5le.co…

【python】python指南(三):使用正则表达式re提取文本中的http链接

一、引言 对于算法工程师来说&#xff0c;语言从来都不是关键&#xff0c;关键是快速学习以及解决问题的能力。大学的时候参加ACM/ICPC一直使用的是C语言&#xff0c;实习的时候做一个算法策略后台用的是php&#xff0c;毕业后做策略算法开发&#xff0c;因为要用spark&#x…

js编程环境配置-vscode

1、安装Node.js 官网下载 选择适合你Windows系统架构&#xff08;32位或64位&#xff09;的安装包。windows系统选择“Windows Installer (.msi)”或“Windows Binary (.exe)”进行下载。 双击下载的.msi或.exe文件进行安装。 在cmd中输入node --version和npm --version&…

2-4 基于matlab的洛伦兹系统分岔图实现

基于matlab的洛伦兹系统分岔图实现。通过2种方法&#xff0c;最大值法&#xff0c;庞加莱截面法进行输出分岔图。可直接运行。 2-4 洛伦兹系统分岔图 最大值法 - 小红书 (xiaohongshu.com)

如何平衡安全访问和办公效率?零信任安全×统一身份才是解决之道

在远程办公、混合办公、跨团队协作日益频繁的今天&#xff0c;企业的业务开展需要支持多种访问接入的需求和场景。如何平衡企业数据的安全访问和办公效率将成为挑战。 在业务的多种接入场景上&#xff0c;企业引入零信任&#xff08;Zero Trust&#xff0c;ZT&#xff09;产品…

ESP-IDF OTA升级过程中遇到的“esp_transport_read returned:-1 and errno:128”问题(4)

接前一篇文章:ESP-IDF OTA升级过程中遇到的“esp_transport_read returned:-1 and errno:128”问题(3) 上一回讲到,笔者准备第二天围绕信号强度展开进一步测试。实际上没等到第二天,笔者在当天下午下班时间(18点)以后就进行了相关测试(不过测试倒并不是完全针对于信号强…

手机是如何实现多个应用程序同时运行的?

想要理解这个问题&#xff0c;我们要先了解一下操作系统以及进程相关的知识&#xff1a; 操作系统的功能有很多&#xff0c; 例如&#xff1a; 进程管理&#xff08;Process Management&#xff09;&#xff1a; 功能&#xff1a;创建和终止进程&#xff0c;进程调度&#xf…

【2024最新华为OD-C/D卷试题汇总】[支持在线评测] 连续区间和(100分) - 三语言AC题解(Python/Java/Cpp)

🍭 大家好这里是清隆学长 ,一枚热爱算法的程序员 ✨ 本系列打算持续跟新华为OD-C/D卷的三语言AC题解 💻 ACM银牌🥈| 多次AK大厂笔试 | 编程一对一辅导 👏 感谢大家的订阅➕ 和 喜欢💗 📎在线评测链接 连续区间和(100分) 🌍 评测功能需要订阅专栏后私信联系清隆…

海豚调度异常处理: 使用 arthas 在内存中删除启动失败的工作流

&#x1f4a1; 本系列文章是 DolphinScheduler 由浅入深的教程&#xff0c;涵盖搭建、二开迭代、核心原理解读、运维和管理等一系列内容。适用于想对 DolphinScheduler了解或想要加深理解的读者。祝开卷有益。大数据学习指南 大家好&#xff0c;我是小陶&#xff0c;DolphinSch…