【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 作为 ResourceLoader
    this(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.primarySources
    this.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.mainApplicationClass
    this.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,则将退出码设置为1
        exitCode = (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);
        // 配置忽略 BeanInfo
        configureIgnoreBeanInfo(environment);
        // 打印 Banner
        Banner 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,如 springApplicationArguments
    beanFactory.registerSingleton("springApplicationArguments", applicationArguments);

    // 如果存在打印的横幅,则注册 springBootBanner 单例 bean
    if (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 应用上下文。
     */
    @Deprecated
    default void started(ConfigurableApplicationContext context) {
    }

    /**
     * 当应用完全就绪并可以接受请求时被调用。这个方法包含了一个 `Duration` 参数,表示从启动到就绪所花费的时间。
     *
     * @param context  Spring 应用上下文。
     * @param timeTaken 从启动到就绪所花费的时间。
     */
    default void ready(ConfigurableApplicationContext context, Duration timeTaken) {
        running(context);
    }

    /**
     * 这个方法已废弃,不推荐继续使用。建议使用带 `Duration` 参数的 `ready` 方法。
     *
     * @param context Spring 应用上下文。
     */
    @Deprecated
    default 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 中的解构赋值

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

第一个SpringBoot程序

第一个SpringBoot程序 目录介绍 当我们创建了一个SpringBoot项目之后&#xff0c;会出现如下的目录结构 SpringBoot项⽬有两个主要的⽬录&#xff1a; src/main/java: Java源代码 src/main/resources:为静态资源或配置⽂件&#xff1a; /static&#xff1a;静态资源⽂件夹,⽐…

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

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

【MySQL】InnoDB引擎(MVCC)

https://www.bilibili.com/video/BV1Kr4y1i7ru/?p141 https://blog.csdn.net/weixin_52574640/article/details/129961415 MVCC&#xff0c;全称Multo-Version Concurrentcy Control,多版本并发控制。指维护一个数据的多个版本&#xff0c;使得读写操作没有冲突&#xff0c;快…

Linux电话本的编写-shell脚本编写

该电话本可以实现以下功能 1.添加用户 2.查询用户 3.删除用户 4.展示用户 5.退出 代码展示&#xff1a; #!/bin/bash PHONEBOOKphonebook.txt function add_contact() { echo "Adding new contact..." read -p "Enter name: " name …

Dubbo3 服务原生支持 http 访问,兼具高性能与易用性

作者&#xff1a;刘军 作为一款 rpc 框架&#xff0c;Dubbo 的优势是后端服务的高性能的通信、面向接口的易用性&#xff0c;而它带来的弊端则是 rpc 接口的测试与前端流量接入成本较高&#xff0c;我们需要专门的工具或协议转换才能实现后端服务调用。这个现状在 Dubbo3 中得…

【设计模式】结构型设计模式之 从IO流设计思想来看装饰器模式

介绍 装饰器模式也称为包装模式(Wrapper Pattern) 是指在不改变原有对象的基础之上&#xff0c;将功能附加到对象上&#xff0c;提供了比继承更有弹性的替代方案(扩展原有对象的功能)&#xff0c;属于结构型模式。 装饰器模式的核心是功能扩展&#xff0c;使用装饰器模式可以透…

内网不能访问网站怎么办?

内网不能访问网站是在网络使用过程中常见的问题之一。当我们使用局域网连接时&#xff0c;有时候会遇到无法访问特定网站的情况。这可能是因为网络环境复杂&#xff0c;或者受到了某些限制。本篇文章将介绍一种解决内网不能访问网站问题的产品——天联组网。 天联组网是一款由…

C#开发-集合使用和技巧(二)Lambda 表达式介绍和应用

C#开发-集合使用和技巧 Lambda 表达式介绍和应用 C#开发-集合使用和技巧介绍简单的示例&#xff1a;集合查询示例&#xff1a; 1. 基本语法从主体语句上区分&#xff1a;1. 主体为单一表达式2. 主体是代码块&#xff08;多个表达式语句&#xff09; 从参数上区分1. 带输入参数的…

【odoo | XML-RPC】odoo外部API解读,实现跨系统间的通讯!

概要 文章注意对官方的XML-RPC进行解读实操&#xff0c;以python为例&#xff0c;给大家介绍其使用方式和调用方法。 内容 什么是odoo的外部API? Odoo 的外部 API 是一种允许外部应用程序与 Odoo 实例进行交互的接口。通过 API&#xff0c;可以执行各种操作&#xff0c;例如…

移动端超超超详细知识点总结(Part3)

flex布局体验 1. 传统布局与flex布局 传统布局&#xff1a; 兼容性好布局繁琐局限性&#xff0c;不能再移动端很好的布局flex 弹性布局&#xff1a; 操作方便&#xff0c;布局极为简单&#xff0c;移动端应用很广泛PC 端浏览器支持情况较差IE 11或更低版本&#xff0c;不支持…

宝藏速成秘籍(7)堆排序法

一、前言 1.1、概念 堆排序&#xff08;Heapsort&#xff09;是指利用堆这种数据结构所设计的一种排序算法 。堆是一个近似 完全二叉树 的结构&#xff0c;并同时满足堆积的性质&#xff1a;即子结点的键值或索引总是小于&#xff08;或者大于&#xff09;它的父节点。 1.2、排…

Uni-App中的u-datetime-picker时间选择器Demo

目录 前言Demo 前言 对于网页端的推荐阅读&#xff1a;【ElementUI】详细分析DatePicker 日期选择器 事情起因是两个时间选择器同步了&#xff0c;本身是从后端慢慢步入全栈&#xff0c;对此将这个知识点从实战进行提炼 通过Demo进行总结 Demo 用于选择日期和时间的组件&a…

zookeeper介绍 和 编译踩坑

zookeeper 分布式协调服务 ZooKeeper原理及介绍 - 鹿泉 - 博客园 Zookeeper是在分布式环境中应用非常广泛&#xff0c;它的优秀功能很多&#xff0c;比如分布式环境中全局命名服务&#xff0c;服务注册中心&#xff0c;全局分布式锁等等。 本项目使用其分布式服务配置中心&am…

Java--数组的使用

1.普通For循环&#xff08;用的最多&#xff0c;需从中取出数据以及下标&#xff09; eg&#xff1a;图中三类问题都可 2.For-each循环&#xff08;一般用来打印一些结果&#xff09; eg&#xff1a;打印数组的具体元素 3.数组作方法入参&#xff08;对数组进行一些操作&#x…

【实例分享】银河麒麟高级服务器操作系统环境资源占用异常-情况分析及处理方法

1.情况描述 使用vsftp进行文件传输&#xff0c;发现sshd进程cpu占用异常&#xff0c;并且su和ssh登录相比正常机器会慢2秒左右。 图&#xff11; 2.问题分析 通过strace跟踪su和sshd进程&#xff0c;有大量ssh:notty信息。 图2 配置ssh绕过pam模块认证后&#xff0c;ssh连接速…

外观模式(大话设计模式)C/C++版本

外观模式 C #include <iostream> using namespace std;class stock1 { public:void Sell(){cout << "股票1卖出" << endl;}void Buy(){cout << "股票1买入" << endl;} };class stock2 { public:void Sell(){cout <<…

C++设计模式——Decorator装饰器模式

一&#xff0c;装饰器模式简介 装饰器模式是一种结构型设计模式&#xff0c; 它允许在不改变现有对象的情况下&#xff0c;动态地将功能添加到对象中。 装饰器模式是通过创建具有新行为的对象来实现的&#xff0c;这些对象将原始对象进行了包装。 装饰器模式遵循开放/关闭原…

AI办公自动化:批量在多个Word文档中插入对应图片

工作任务&#xff1a;文件夹中有多个word文档和word文档名称一致的图片&#xff0c;要把这些图片都插入到word文档中 在chatpgt中输入提示词&#xff1a; 你是一个Python编程专家&#xff0c;写一个Python脚本&#xff0c;具体步骤如下&#xff1a; 打开文件夹&#xff1a;F:…

c++/c输出double问题

这个我大抵能理解&#xff0c;%d是int嘛。 这是为啥&#xff1f; 这样又好了&#xff1f; 这我也能理解 这也可以 这也对&#xff1f; &#xff08;我知道我呢个函数为什么不对了&#xff0c;我的函数写的是int(&#xff09;) 附&#xff1a;保留几位小数&#xff1a; %.2f

手把手教你入门vue+springboot开发(三)--登录功能后端

文章目录 前言一、redis安装二、后端代码1.修改application.yml文件2.增加utils文件3.增加Result类4.修改UserController类5.修改UserMapper类6.修改UserService和UserServiceImpl类7.增加LoginInterceptor类8.增加WebConfig类9.修改pom.xml文件 前言 前两篇我们用vuespringbo…