【SpringBoot】深入分析 SpringApplication 源码:彻底理解 SpringBoot 启动流程
在黄昏的余晖里,梦境渐浓,如烟如雾。心随星辰,徜徉远方,岁月静好,愿如此刻般绵长。
文章目录
前言
SpringApplication
是 Spring Boot 框架中的一个类,它被用于引导和运行 Spring Boot 应用程序。它提供了一种简化的方式来配置和启动应用程序,减少了开发者的工作量。本文将对 SpringApplication 源码进行分析,深入理解其核心。
一、SpringBoot 应用
下面是一个 SpringBoot 应用的常规写法。
@SpringBootApplication
public class App {public static void main(String[] args) {SpringApplication.run(App.class, args);}
}
上述代码,有两个重点:
- @SpringBootApplication: 作用是简化配置,自动扫描组件
- 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 的构造器中,这一部分没有给定初始值的属性会在构造器中计算出初始值。例如:resourceLoader
、primarySources
、webApplicationType
、bootstrapRegistryInitializers
、mainApplicationClass
等。
由此,我们可以看出 SpringApplication 构造器的作用是完成一些属性的初始化工作。
2.3 SpringApplication 中的方法
SpringApplication 中的方法有许多,我们可以将这些方法分类进行讨论。首先,我们将 SpringApplication 中的方法分为 private
和非 private
方法。我们知道 private 修饰的方法只能给 SpringApplication 内部使用,我们并不会直接使用到。所以,其实 private 修饰的方法我们并不需要去了解。
现在剩下的方法,我们再进行一次划分:我们将剩下的方法分为setter、getter
方法和非 setter、getter
方法。我们知道,setter 和 getter 方法是用来控制属性的,不用过多的讨论。
至此,SpringApplication 中剩下值得讨论的方法就只有下面这些了。
而在这些方法中,addBootstrapRegistryInitializer
、addInitializers
、addListeners
、addPrimarySources
这几个方法的功能是和 setter 方法类似的,因为相应的属性是集合,使用 add 方法向集合中添加值。
最终,我们分析出:SpringApplication 只有三个可供开发者直接使用的核心方法。
main()
方法exit()
方法run()
方法
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:当我们打开电源时,从非易失性存储器中加载 BIOS(基本输入/输出系统)或 UEFI(统一可扩展固件接口)固件,并执行 POST(开机自检)。
- 步骤 2:BIOS/UEFI 检测连接到系统的设备,包括 CPU、RAM 和存储设备。
- 步骤 3:选择一个启动设备来引导操作系统。这可以是硬盘、网络服务器或 CD-ROM。
- 步骤 4:BIOS/UEFI 运行引导加载程序(如 GRUB),该程序提供一个菜单供选择操作系统或内核功能。
- 步骤 5:内核准备就绪后,系统切换到用户空间。内核启动 systemd 作为第一个用户空间进程,systemd 管理各个进程和服务,探测所有剩余的硬件,挂载文件系统,并运行桌面环境。
- 步骤 6:systemd 默认激活 default.target 单元,当系统启动时,其他分析单元也会被执行。
- 步骤 7:系统运行一组启动脚本并配置环境。
- 步骤 8:用户将看到登录窗口。系统现在已经准备就绪。
综上,上面的计算机启动过程太多,我们可以进一步总结为以下三个主要阶段:引导、启动和运行。
- 引导阶段:在此阶段,计算机会执行基本输入/输出系统(BIOS)或统一可扩展固件接口(UEFI)等引导加载程序,以加载操作系统的引导记录并将操作系统
内核加载到内存中
。 - 启动阶段:一旦操作系统内核被加载到内存中,计算机将开始执行
操作系统的初始化
过程,包括建立内存管理、初始化进程、加载驱动程序等操作。 - 运行阶段:在这个阶段,操作系统已经完全加载并且用户界面准备就绪,用户可以登录系统并开始使用计算机进行各种任务。
SpringApplication 中的 run() 方法功能和计算机的启动流程是类似的。
run() 方法也可以分成三个阶段:
- 引导阶段:创建
引导容器 DefaultBootstrapContext
、启动监听器 SpringApplicationRunListeners、准备环境 prepareEnvironment、打印 Banner - 启动阶段:创建
应用容器 ConfigurableApplicationContext
,并完成应用容器的初始化工作 - 运行阶段:应用容器准备就绪,可以使用
引导容器 DefaultBootstrapContext
就如同计算机启动过程中的内核,首先完成内核的加载。之后,通过内核初始化应用容器 ConfigurableApplicationContext
(类似操作系统)。当操作系统初始化成功,最终容器达可运行阶段,可供使用。
四、深入 SpringBoot 启动
4.1 引导阶段
SpringApplication 的引导阶段并没有计算机的引导阶段那么复杂。这一阶段,主要是创建引导容器 DefaultBootstrapContext
用于下一阶段引导应用容器 ConfigurableApplicationContext
。其次,在这一阶段也会启动 SpringBoot 的监听器功能 SpringApplicationRunListeners
,以及准备环境 prepareEnvironment()
。
4.2 启动阶段
SpringApplication 的启动阶段内容比较多。这一阶段,核心内容是做了以下几件事情:
- 创建应用容器
ConfigurableApplicationContext
- 准备应用程序上下文 prepareContext()
- 刷新应用程序上下文 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);
}
-
设置环境:将传入的 ConfigurableEnvironment 对象设置到应用程序上下文中,以便上下文可以使用这个环境中的配置和属性。
context.setEnvironment(environment);
-
后处理应用程序上下文:调用自定义方法对应用程序上下文进行额外的处理。具体实现取决于开发者的需求,这一步一般用于添加额外的配置或修改上下文的某些属性。
postProcessApplicationContext(context);
-
应用初始化器:调用所有注册的 ApplicationContextInitializer 初始化器,对应用程序上下文进行进一步的配置。
applyInitializers(context);
-
通知监听器上下文已准备:通知所有 SpringApplicationRunListener,应用程序上下文已准备好。这通常用于在上下文被刷新之前执行一些操作。
listeners.contextPrepared(context);
-
关闭引导上下文:关闭引导上下文,释放资源。
bootstrapContext.close(context);
-
记录启动信息:如果启用了启动信息记录,记录启动信息和配置文件信息。这对于调试和诊断问题很有帮助。
if (this.logStartupInfo) {logStartupInfo(context.getParent() == null);logStartupProfileInfo(context); }
-
注册单例 Bean:将 applicationArguments 和 printedBanner(如果有)注册为单例 Bean,以便它们可以在应用程序的其他部分使用。
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); beanFactory.registerSingleton("springApplicationArguments", applicationArguments);if (printedBanner != null) {beanFactory.registerSingleton("springBootBanner", printedBanner); }
-
设置 Bean 工厂属性:根据配置设置 Bean 工厂的属性,例如是否允许循环引用和 Bean 定义覆盖。
if (beanFactory instanceof AbstractAutowireCapableBeanFactory) {((AbstractAutowireCapableBeanFactory) beanFactory).setAllowCircularReferences(this.allowCircularReferences);if (beanFactory instanceof DefaultListableBeanFactory) {((DefaultListableBeanFactory) beanFactory).setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);} }
-
延迟初始化:如果启用了延迟初始化,向应用程序上下文添加一个 LazyInitializationBeanFactoryPostProcessor,以推迟 Bean 的初始化。
if (this.lazyInitialization) {context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor()); }
-
添加属性源排序后处理器:添加一个 PropertySourceOrderingBeanFactoryPostProcessor,确保属性源按照预期顺序被处理。
context.addBeanFactoryPostProcessor(new PropertySourceOrderingBeanFactoryPostProcessor(context));
-
加载应用程序源:获取所有应用程序源并加载它们。确保源不为空,否则抛出异常。
Set<Object> sources = getAllSources(); Assert.notEmpty(sources, "Sources must not be empty"); load(context, sources.toArray(new Object[0]));
-
通知监听器上下文已加载:通知所有 SpringApplicationRunListener,应用程序上下文已加载完成。这是最后一步,表示上下文已经完全准备好,可以启动应用程序了。
listeners.contextLoaded(context);
此阶段我们需要了解的是:这一阶段是在处理容器准备容器,还未开始处理 Bean
,因为这一阶段并没有将真正的 Bean 注册到容器。上面虽有一个注册单例 Bean,但是注册的是 springApplicationArguments
和 springBootBanner
,这两个 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(); // 结束启动步骤记录}}
}
-
记录启动步骤:记录上下文刷新过程的启动步骤,用于性能监控和排错。
StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
-
准备刷新:这个方法用于准备上下文刷新,包括设置环境、属性源和早期事件发布等。
prepareRefresh();
-
获取新的 Bean 工厂:刷新 Bean 工厂,通常是销毁旧的 Bean 工厂并创建一个新的。
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
-
准备 Bean 工厂:设置 Bean 工厂的一些标准配置,如类加载器、表达式解析器和一些默认的 Bean 后处理器。
prepareBeanFactory(beanFactory);
-
后处理 Bean 工厂:子类可以重写这个方法以便在 Bean 工厂标准初始化之后做一些自定义的修改。
postProcessBeanFactory(beanFactory);
-
调用 Bean 工厂后处理器:这一步调用所有注册的 BeanFactoryPostProcessor,用于修改应用上下文的内部 Bean 定义。
invokeBeanFactoryPostProcessors(beanFactory);
-
注册 Bean 后处理器:注册 BeanPostProcessor,这些处理器会在 Bean 实例化前后进行一些自定义操作。
registerBeanPostProcessors(beanFactory);
-
初始化消息源:初始化应用上下文中的消息源,用于国际化消息处理。
initMessageSource();
-
初始化事件广播器:初始化事件广播器,用于发布应用上下文中的事件。
initApplicationEventMulticaster();
-
特定子类的刷新操作:留给子类实现的钩子方法,允许在刷新上下文时添加一些特定的逻辑。
onRefresh();
-
注册监听器:查找并注册所有的事件监听器。
registerListeners();
-
实例化所有剩余的单例:实例化所有非延迟初始化的单例 Bean,确保它们都已准备好使用。
finishBeanFactoryInitialization(beanFactory);
-
完成刷新:最后一步,主要是清理缓存和发布事件,标志着上下文刷新完成。
finishRefresh();
刷新应用程序上下文的逻辑是比较复杂的。我们可以看到,在 refresh()
方法中,只有刷新的流程,但是没有具体的操作,具体的操作都在方法里面。
这些方法中,有几个我们需要特别讨论一下:
- obtainFreshBeanFactory()
- postProcessBeanFactory()
- invokeBeanFactoryPostProcessors()
- registerBeanPostProcessors()
obtainFreshBeanFactory() 方法底层调用了 loadBeanDefinitions()
方法。loadBeanDefinitions() 见名知意,它的作用是将加载所有 BeanDefinition
。
postProcessBeanFactory() 方法底层调用了 doRegisterBean()
方法。同样地,doRegisterBean() 方法也可见名知意,它的作用是注册 Bean
。
invokeBeanFactoryPostProcessors() 方法的核心作用是在 Spring 容器初始化时,正确地调用所有 BeanFactoryPostProcessor
和 BeanDefinitionRegistryPostProcessor
,以便开发者可以在 Bean 实例化之前修改 Bean 定义和配置,从而影响整个应用程序上下文中的 Bean 行为。这种机制提供了很大的灵活性,使得 Spring 应用程序可以根据需要动态调整 Bean 的配置和定义。
registerBeanPostProcessors() 方法的核心是处理和注册 BeanPostProcessor
实例。在 Spring 容器启动时,会调用该方法,将所有的 BeanPostProcessor 注册到容器中,以便在 Bean 的生命周期中进行处理。
由此,我们可以知道 SpringApplication 刷新的核心
是什么:
这也解释了为什么 Spring 容器会有两大扩展点:
- 容器的扩展点:通过实现
BeanFactoryPostProcessor
或BeanDefinitionRegistryPostProcessor
- Bean 的扩展点:通过实现
BeanPostProcessor
4.3 运行阶段
运行阶段,其实没什么特别需要讨论的。这一阶段容器已准备就绪,Bean 已加载完成。剩下的只是计算一下整个容器启动所花费的时间,然后通知监听器,容器已处于就绪状态。
五、FAQ
5.1 Spring 容器的生命周期
其实,Spring 的官方文档中并没有说明 Spring 容器的生命周期,可是我们经常听到 Spring 容器生命周期这一说法。这一说法其实有两个来源:
- 通过对上述 Spring 容器的启动流程进行总结出来的
- 通过分析
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 官方其实对容器生命周期是有自己的定义的:
源码中有 failed
,但这个是容器异常的情况,可能会在任意阶段发生,所以不应该属于生命周期的某个指定阶段。
我们进一步分析 SpringApplicationRunListener
中的源码,会发现一些方法被废弃了。废弃的方法有两种原因:
-
有更好的替代方案:比如 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) {}
-
阶段差异不大:
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 容器的生命周期。这样做有几个好处:
- 源码更能使人信服,统一大家对 Spring 容器生命周期的理解,最大限度拉齐认知
- 帮助我们深入理解 Spring 官方团队的处理思路,可以更好的理解源码
综上,通常我们认为 Spring 容器的生命周期为:
- starting:
- 时间点:在 Spring Boot 应用启动之初,run 方法被调用时。
- 作用:这个阶段最早发生,可以用于执行一些初始化操作,比如设置启动参数、记录日志等。
- environmentPrepared:
- 时间点:在 Spring Boot 应用的 Environment(包括系统属性、环境变量、配置文件等)准备好之后。
- 作用:此阶段可以用来修改或添加环境属性,或者做一些依赖于环境配置的初始化操作。
- contextPrepared:
- 时间点:在 Spring 应用上下文创建之后但还未加载
BeanDefinition
之前。 - 作用:可以对应用上下文进行进一步配置,设置一些全局属性或注册额外的组件。
- 时间点:在 Spring 应用上下文创建之后但还未加载
- contextLoaded:
- 时间点:在所有的
BeanDefinition
被加载但尚未刷新上下文之前。 - 作用:此阶段可以操作 BeanDefinition,比如动态注册 Bean,修改某些 Bean 的属性等。
- 时间点:在所有的
- started:
- 时间点:在应用上下文刷新并完全启动后。
- 作用:此时,所有的单例
Bean
已经初始化完毕,可以执行一些需要在 Bean 完全初始化后的操作,如启动后台任务、初始化连接池等。
- ready:
- 时间点:在应用完全准备好并能够响应请求时。
- 作用:此阶段标志着应用已经完全启动并准备就绪,可以处理实际的业务请求。
5.2 Spring 容器的扩展点
之前,我们在分析 SpringApplication 源码时,已经发现了两个扩展点:
- 容器的扩展点:通过实现
BeanFactoryPostProcessor
或BeanDefinitionRegistryPostProcessor
- Bean 的扩展点:通过实现
BeanPostProcessor
Spring 是非常灵活的框架,除了上述的扩展点,Spring 也为生命周期的各个阶段提供了扩展点。Spring 容器生命周期的扩展点是通过 SpringApplicationRunListener
实现的监听器机制。
开发者只需要发布生命周期对应的事件,就可以实现在生命周期的指定阶段实现特定功能。
- starting 阶段对应 ApplicationStartingEvent 事件
- environmentPrepared 阶段对应 ApplicationEnvironmentPreparedEvent 事件
- contextPrepared 阶段对应 ApplicationContextInitializedEvent 事件
- contextLoaded 阶段对应 ApplicationPreparedEvent 事件
- started 阶段对应 ApplicationStartedEvent 事件
- ready 阶段对应 ApplicationReadyEvent 事件
六、小结
SpringApplication 源码中包含许多 Spring 容器的底层原理,仔细阅读其源码可以帮助我们深入理解 SpringBoot。
推荐阅读
- Spring 三级缓存
- 深入了解 MyBatis 插件:定制化你的持久层框架
- 深入探究 Spring Boot Starter:从概念到实践
- Zookeeper 注册中心:单机部署
- 【JavaScript】探索 JavaScript 中的解构赋值
相关文章:

【SpringBoot】深入分析 SpringApplication 源码:彻底理解 SpringBoot 启动流程
在黄昏的余晖里,梦境渐浓,如烟如雾。心随星辰,徜徉远方,岁月静好,愿如此刻般绵长。 文章目录 前言一、SpringBoot 应用二、SpringApplication2.1 SpringApplication 中的属性2.2 SpringApplication 的构造器2.3 Sprin…...

边界内聚和耦合
内聚 功能内聚 功能内聚是软件工程中一个重要的概念,它描述了一个模块内部各个元素之间的紧密程度。一个具有高功能内聚的模块意味着其内部的各个组件都共同完成一个具体的、明确的功能,并且这些组件之间的联系不是偶然的,而是因为它们共同服…...

单调栈——AcWing.830单调栈
单调栈 定义 单调栈是一种特殊的数据结构,栈内元素保持某种单调性(通常是单调递增或单调递减)。 运用情况 求解下一个更大元素或下一个更小元素。计算每个元素左边或右边第一个比它大或小的元素。 注意事项 要明确单调栈是递增还是递减…...

手机上安装AI模型是一种什么体验?
昨天参加微软的AI DAY活动,看到微软的技术大佬分享了一个场景,就是坐飞机从上海到北京,机长广播因为天气原因,飞机需要盲降,他说当时听到盲降第一反应感觉有点恐慌,但是因为飞机上受限于网络环境࿰…...

【MySQL】主从复制
https://www.bilibili.com/video/BV1Kr4y1i7ru/?p161 https://blog.csdn.net/qq_47959003/article/details/126058710 主从复制是指将数据库的DDL和DML操作通过二进制日志传到从库服务器中,然后在从库上对这些日志重新执行(也叫重做)&…...

vscode插件开发之 - menu配置
上一遍博客介绍了如何从0到1搭建vscode插件开发的base code,这遍博客将重点介绍如何配置menu。通常,开发一款插件,会将插件显示在VSCode 左侧的活动栏(Activity Bar),那么如何配置让插件显示在Activity Bar…...

自学C语言-9
** 第9章 函数 ** 大型程序一般会被分为若干个程序模块,每个模块实现一个特定功能 。C语言中,由函数实现子程序,由子程序实现模块功能。本章致力于使读者了解函数的概念,掌握函数的定义及调用方式;了解内部函数和外部…...

NVIDIA Triton系列01-应用概论
NVIDIA Triton系列01-应用概论 推理识别是人工智能最重要的落地应用,其他与深度学习相关的数据收集、标注、模型训练等工作,都是为了得到更好的最终推理性能与效果。 几乎每一种深度学习框架都能执行个别的推理工作,包括 Tensorflow、Pytorc…...

LIMS(实验室)信息管理系统源码、有哪些应用领域?采用C# ASP.NET dotnet 3.5 开发的一套实验室信息系统源码
LIMS(实验室)信息管理系统源码、有哪些应用领域?采用C# ASP.NET dotnet 3.5 开发的一套实验室信息系统源码 LIMS实验室信息管理系统,是一种基于计算机硬件和数据库技术,集多个功能模块为一体的信息管理系统。该系统主…...

Web前端进国企:挑战与机遇并存
Web前端进国企:挑战与机遇并存 随着互联网的飞速发展,Web前端技术已经成为企业信息化建设的重要组成部分。对于许多热衷于前端技术的年轻人来说,进入国企工作既是一种挑战,也是一种机遇。本文将从四个方面、五个方面、六个方面和…...

快速上手SpringBoot
黑马程序员Spring Boot2 文章目录 1、SpringBoot 入门程序开发1.1 创建一个新的项目 2、浅谈入门程序工作原理2.1 parent2.2 starter2.3 引导类2.4 内嵌tomcat 1、SpringBoot 入门程序开发 1.1 创建一个新的项目 file > new > project > empty Project 创建新模块&a…...

SQL 快速参考
SQL 快速参考 SQL(Structured Query Language)是一种用于管理关系数据库管理系统(RDBMS)的标准编程语言。它用于执行各种操作,如查询、更新、插入和删除数据库中的数据。本快速参考将提供SQL的基本语法和常用命令&…...

Cask ‘oraclexxx‘ is unavailable: No Cask with this name exists.
brew search oracle-jdk或brew search --cask oracle-jdk 原因:Homebrew官方仓库不再维护多个旧版本的OracleJDK 不推荐使用Homebrew环境安装JDK //指定版本安装 brew install --cask temurin17 //设置 JAVA_HOME 环境变量 //找到安装的JDK 版本的路径 /usr/lib…...

2024年武汉市中级、高级职称水测考试开卷方法分享
2024年武汉市(除开东湖高新区外)职称首次组织全员水测,先考水测后报名,水测报名在5月16号截止。 武汉市水测组织形式: 武汉市2024年专业技术职务水平能力测试分为笔试和面试,面试答辩有关事项另行通知&…...

计算机网络(6) ICMP协议
ICMP(Internet Control Message Protocol,互联网控制消息协议)是一种用于在IP网络中传递控制消息和错误报告的协议。ICMP是IP协议族的一部分,尽管它并不用于传输用户数据,但它在网络诊断和管理中起着关键作用。以下是关…...

FuTalk设计周刊-Vol.036
🔥AI漫谈 热点捕手 1、Stable Zero123:从单张图像生成高质量 3D 对象 Stable Zero123 可以生成物体的新颖视图,展示从各个角度对物体外观的 3D 理解,由于训练数据集和高程条件的改进,其质量比 Zero1-to-3 或 Zero123-XL 显著提高…...

Java——面向对象进阶(三)
前言: 抽象类,接口,内部类 文章目录 一、抽象类1.1 抽象方法1.2 抽象类1.3 抽象类的使用 二、 接口2.1 接口的定义和实现2.2 default 关键字2.3 实现接口时遇到的问题 三、内部类3.1 成员内部类3.2 静态内部类3.3 成员内部类3.4 匿名内部类&a…...

鸿蒙开发电话服务:【@ohos.telephony.observer (observer)】
observer 说明: 本模块首批接口从API version 6开始支持。后续版本的新增接口,采用上角标单独标记接口的起始版本。 导入模块 import observer from ohos.telephony.observerobserver.on(‘networkStateChange’) on(type: ‘networkStateChange’, ca…...

希亦、追觅、云鲸洗地机:究竟有何不同?选择哪款更合适
最近收到很多私信里,要求洗地机测评的呼声特别高,作为宠粉的测评博主,当然是马上安排起来,满足大家对想看洗地机的愿望。这次洗地机测评,我挑选了三款热门的品牌型号,并从多个维度对它们进行使用测评&#…...

代码随想录算法训练营第二十六天
题目:455. 分发饼干 贪心第一题 这里的局部最优就是大饼干喂给胃口大的,充分利用饼干尺寸喂饱一个,全局最优就是喂饱尽可能多的小孩。或者小饼干先喂饱小胃口 首先要对 g 和 s进行排序这样才能知道最大的胃口和最大的饼干然后进行遍历即可…...

[面试题]Java【并发】
[面试题]Java【基础】[面试题]Java【虚拟机】[面试题]Java【并发】[面试题]Java【集合】[面试题]MySQL 因为 Java 并发涉及到的内容会非常多,本面试题可能很难覆盖到所有的知识点,所以推荐 《Java并发编程的艺术》 。 Java 线程 线程 通知 等待 线…...

基于VSCode和MinGW-w64搭建LVGL模拟开发环境
目录 概述 1 运行环境 1.1 版本信息 1.2 软件安装 1.2.1 下载安装VS Code 1.2.1.1 下载软件 1.2.1.1 安装软件 1.2.2 下载安装MinGW-w64 1.2.2.1 下载软件 1.2.2.2 安装软件 1.2.3 下载安装SDL 1.2.3.1 下载软件 1.2.3.2 安装软件 1.2.4 下载安装CMake 1.2.4.…...

H5112B 降压恒流芯片12V24V36V48V60V72V100V 1.2ALED 调光无频闪光滑细腻
H5112B多功能LED恒流驱动器是一款具有良好性能与高度集成度的驱动芯片。以下是该产品的主要优点及应用领域的详细分析: 产品优点: 宽电压输入范围:H5112B支持5V至90V的宽电压输入范围,使其能够适应多种不同的电源环境࿰…...

真心建议大家冲一冲新兴领域,工资高前景好【大模型NLP开发篇】
前言 从ChatGPT到新近的GPT-4,GPT模型的发展表明,AI正在向着“类⼈化”⽅向迅速发展。 GPT-4具备深度阅读和识图能⼒,能够出⾊地通过专业考试并完成复杂指令,向⼈类引以为傲的“创造⼒”发起挑战。 现有的就业结构即将发⽣重⼤变…...

深度剖析淘宝扭蛋机源码:打造趣味性电商活动的秘诀
在当今电商市场中,如何吸引用户的注意力、提升用户的参与度成为了各大电商平台竞相追求的目标。淘宝扭蛋机作为一种新型的电商活动形式,以其趣味性和互动性深受用户喜爱。本文将深度剖析淘宝扭蛋机源码,探讨其如何打造趣味性与互动性并存的电…...

vue3+优化vue-baidu-map中marker点过多导致的页面卡顿问题
场景: 移动端h5中,当我们需要在地图中展示很多marker点坐标的时候,通常会使用 bm-marker ,去循环生成marker点,在数量不多的情况下是没问题的,但是随着数据量的增加,地图就会变得卡顿,以及渲染延…...

PMS助力制造企业高效运营︱PMO大会
全国PMO专业人士年度盛会 北京易贝恩项目管理科技有限公司副总经理朱洪泽女士受邀为PMO评论主办的2024第十三届中国PMO大会演讲嘉宾,演讲议题为“PMS助力制造企业高效运营”。大会将于6月29-30日在北京举办,敬请关注! 议题简要: …...

认识一些分布-关于极值点分布的一些知识
可以参考下面资料: Extreme Value Distribution & the Extreme Value Theory - Statistics How To...

Anaconda环境安装失败的解决方案
链接步骤的补充。 为了运行marlib,需要一个全新的Anaconda环境。但是,不想把文件安装在C盘,会造成空间不足。于是试着在.condarc文件里面改动了路径,具体如图。 上图中,在defaults前面添加了D盘的路径作为安装路径。 …...

mac 本地启动rocketmq
Mac 本地起rocketmq 官网下载:RocketMq官网下载地址 下载后解压 如果电脑配置不高或者不希望rocketmq占用太大内存的,修改配置/bin/runbroker.sh JAVA_OPT"${JAVA_OPT} -server -Xms512m -Xmx512m -Xmn256m"-Xmx4g 初始堆大小 4g -Xms4g 最大…...