微信公众号:掘金专栏:glmapper
微 博:疯狂的石头_henu
欢迎关注,一起学习、一起分享
SpringBoot 作为目前非常流行的微服务框架,它使得构建独立的 Spring 生产级应用变得非常简单,因此受到很多互联网企业的青睐。
最近在写 SOFATracer 集成 Spring Cloud Stream RocketMQ 的过程中,遇到了一些问题,比如:BeanPostProcessor 不生效,如何在 BeanPostProcessor 不生效的情况下去修改一个 Bean 等,这些问题其实都是和 Bean 的生命周期有关系的,当然也和容器启动的过程有关系。SpringBoot 的启动过程对于我来说其实不算陌生,也可以说是比较熟悉,但是之前没有完整的梳理过这一款的东西,再实际的应用过程成难免再去踩一些坑。另外想到之前也写过一篇 SpringBoot系列- FatJar 启动原理 ,刚好承接上篇,继续来探索 SpringBoot 中的一些知识点。
注:本篇基于 SpringBoot 2.1.0.RELEASE 版本,SpringBoot 各个版本之间可能存在差异,不过大体流程基本差不多,所以各位看官在实际的工作过程中也
在这篇 SpringBoot系列- FatJar 启动原理 文章中介绍得到,JarLaunch 最后是构建了一个 MainMethodRunner 实例对象,然后通过反射的方式调用了 BootStrap 类中的 main 方法,这里的 ’BootStrap 类中的 main 方法‘ 实际上就是 SpringBoot 的业务入口,也就是常见的下面的代码片段:
@SpringBootApplication public class GlmapperApplication { public static void main(String[] args) { SpringApplication.run(GlmapperApplication.class, args); } } 复制代码
从代码可以非常直观的了解到,启动是通过调用 SpringApplication 的静态方法 run;这个 run 方法内部其实是会构造一个 SpringApplication 的实例,然后再调用这里实例的 run 方法来启动 SpringBoot的。
/** * Static helper that can be used to run a {@link SpringApplication} from the * specified sources using default settings and user supplied arguments. * @param primarySources the primary sources to load * @param args the application arguments (usually passed from a Java main method) * @return the running {@link ApplicationContext} */ public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) { return new SpringApplication(primarySources).run(args); } 复制代码
因此,如果要分析 SpringBoot 的启动过程,我们需要熟悉 SpringApplication 的构造过程以及 SpringApplication 的 run 方法执行过程即可。
篇幅原因,我们只分析核心的构建流程。
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) { // 资源加载器,默认是 null this.resourceLoader = resourceLoader; // 启动类 bean Assert.notNull(primarySources, "PrimarySources must not be null"); this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources)); // 是否是 web 应用 this.webApplicationType = WebApplicationType.deduceFromClasspath(); // 设置了 ApplicationContextInitializer setInitializers((Collection) getSpringFactoriesInstances( ApplicationContextInitializer.class)); // 设置 ApplicationListener setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); // 启动类 this.mainApplicationClass = deduceMainApplicationClass(); } 复制代码
上面代码段中,需要关注两个点:
要注意的是这里的实例化,并非是通过注解和扫包完成,而是通过一种不依赖 Spring 上下文的加载方法;这种做法是为了能够使得在 Spring 完成启动前做各种配置。Spring 的解决方法是以接口的全限定名作为 key,实现类的全限定名作为 value 记录在项目的 META-INF/spring.factories 文件中,然后通过SpringFactoriesLoader 工具类提供静态方法进行类加载并缓存下来,spring.factories 是Spring Boot 的核心配置文件。SpringFactoriesLoader 可以理解为 Spring 自己提供的一种 spi 扩展实现。SpringBoot 中提供的默认的 spring.factories 配置如下:
# PropertySource Loaders org.springframework.boot.env.PropertySourceLoader=/ // ..省略 # Run Listeners org.springframework.boot.SpringApplicationRunListener=/ // ..省略 # Error Reporters org.springframework.boot.SpringBootExceptionReporter=/ // ..省略 # Application Context Initializers org.springframework.context.ApplicationContextInitializer=// // ..省略 # Application Listeners org.springframework.context.ApplicationListener=/ // ..省略 # Environment Post Processors org.springframework.boot.env.EnvironmentPostProcessor=/ // ..省略 # Failure Analyzers org.springframework.boot.diagnostics.FailureAnalyzer=/ // ..省略 # FailureAnalysisReporters org.springframework.boot.diagnostics.FailureAnalysisReporter=/ // ..省略 复制代码
关于 SpringFactoriesLoader 如何加载这些资源这里就不过多分析,有兴趣的读者可以自行查看相关源码。 org.springframework.core.io.support.SpringFactoriesLoader#loadSpringFactories
这里先直观的看下代码,然后再逐个分析:
public ConfigurableApplicationContext run(String... args) { // 开启容器启动计时 StopWatch stopWatch = new StopWatch(); stopWatch.start(); ConfigurableApplicationContext context = null; // SpringBootExceptionReporter 列表,SpringBoot 允许自定义 Reporter Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>(); // 设置java.awt.headless属性为true还是false // 可详见解释:https://blog.csdn.net/michaelgo/article/details/81634017 configureHeadlessProperty(); // 获取所有 SpringApplicationRunListener ,也是通过 SpringFactoriesLoader 来获取的 SpringApplicationRunListeners listeners = getRunListeners(args); // 发布 starting 事件,在首次启动 run方法时立即调用,可用于非常早的初始化,注意此时容器上下文还没有刷新 listeners.starting(); try { // 构建 ApplicationArguments 对象 ApplicationArguments applicationArguments = new DefaultApplicationArguments( args); // 准备上下文刷新需要的环境属性 -- 详见 prepareEnvironment 过程分析 ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); // spring.beaninfo.ignore,如果为空设置为true configureIgnoreBeanInfo(environment); // 打印 SpringBoot 启动 Banner Banner printedBanner = printBanner(environment); // 创建上下文,这里会根据 webApplicationType 类型来创建不同的 ApplicationContext context = createApplicationContext(); // 加载获取 exceptionReporters exceptionReporters = getSpringFactoriesInstances( SpringBootExceptionReporter.class, new Class[] { ConfigurableApplicationContext.class }, context); // 上下文刷新之前的准备工作 -- 详见 prepareContext 过程分析 prepareContext(context, environment, listeners, applicationArguments, printedBanner); // 刷新上下文 -- 详见 refreshContext 过程分析 refreshContext(context); // 刷新之后回调,SpringBoot 中这个方法是空实现,可以自行扩展 afterRefresh(context, applicationArguments); // 停止计时 stopWatch.stop(); if (this.logStartupInfo) { new StartupInfoLogger(this.mainApplicationClass) .logStarted(getApplicationLog(), stopWatch); } // 发布 started 事件 listeners.started(context); // ApplicationRunner 和 CommandLineRunner 调用 callRunners(context, applicationArguments); } catch (Throwable ex) { // 异常处理 handleRunFailure(context, ex, exceptionReporters, listeners); throw new IllegalStateException(ex); } try { // 发布 running 事件 listeners.running(context); } catch (Throwable ex) { // 异常处理 handleRunFailure(context, ex, exceptionReporters, null); throw new IllegalStateException(ex); } return context; } 复制代码
上面对代码基本都做了一些简单的注释,有几个需要关注的点:
关于 Listeners 执行时机及顺序在之前的文章中有做过非常详细的分析,详见: SpringBoot 系列-事件机制详解 。下面就对其他的 4 个点做下详细的分析。
分析启动过程,本质上是对其整个容器生命周期有个了解,包括 listeners 执行各个事件的时机、PostProcessor 执行的时机,Enviroment Ready 的时机等等。掌握这些扩展和时机,可以在实际的业务开发中来做很多事情。
prepareEnvironment 过程相对来说是比较早的,这里主要就是为上下文刷新提供 Environment。
private ConfigurableEnvironment prepareEnvironment( SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) { // Create and configure the environment ConfigurableEnvironment environment = getOrCreateEnvironment(); // 配置 PropertySources 和 Profiles // 1、将参数和一些默认的属性配置到 environment // 2、激活 profiles configureEnvironment(environment, applicationArguments.getSourceArgs()); // 发布 ApplicationEnvironmentPreparedEvent 事件 listeners.environmentPrepared(environment); // 绑定 SpringApplication 环境 bindToSpringApplication(environment); if (!this.isCustomEnvironment) { environment = new EnvironmentConverter(getClassLoader()) .convertEnvironmentIfNecessary(environment, deduceEnvironmentClass()); } // 附加的解析器将动态跟踪底层 Environment 属性源的任何添加或删除 ConfigurationPropertySources.attach(environment); return environment; } 复制代码
这里面做的事情就是将我们的配置,包括系统配置、application.properties、-D 参数等等统统打包给 environment。在 Spring 中,我们最常见的 xml 中使用的 ${xxx}
或者代码中使用的 @Value("${xxxx}")
等,最后都是从 environment 中拿值的。
这里需要关注的一个比较重要的点是 发布 ApplicationEnvironmentPreparedEvent 事件 ,我们可以通过监听这个事件来 修改 environment 。这里可以参考下 SOFATracer 中 SofaTracerConfigurationListener 是如何利用这个事件来做环境配置处理的。
prepareContext 的处理过程中可以利用的点是非常多的,比如 ApplicationContextInitializer 的执行、ApplicationContextInitializedEvent 和 ApplicationPreparedEvent 事件发布。
private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) { // 设置 environment 给 context,所以需要注意的是,在此之前拿到的 context 中,environment 是没有的。 context.setEnvironment(environment); // 对 ApplicationContext 的后置处理,比如注册 BeanNameGenerator 和 ResourceLoader postProcessApplicationContext(context); // 这里开始执行所有的 ApplicationContextInitializer applyInitializers(context); // 发布 ApplicationContextInitializedEvent 事件 listeners.contextPrepared(context); if (this.logStartupInfo) { logStartupInfo(context.getParent() == null); logStartupProfileInfo(context); } // Add boot specific singleton beans ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); beanFactory.registerSingleton("springApplicationArguments", applicationArguments); if (printedBanner != null) { beanFactory.registerSingleton("springBootBanner", printedBanner); } if (beanFactory instanceof DefaultListableBeanFactory) { // 是否允许 bean 覆盖,这里如果是 false ,则可能会导致 BeanDefinitionOverrideException 异常 ((DefaultListableBeanFactory) beanFactory) .setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding); } // Load the sources Set<Object> sources = getAllSources(); Assert.notEmpty(sources, "Sources must not be empty"); load(context, sources.toArray(new Object[0])); // 发布 ApplicationPreparedEvent 事件 listeners.contextLoaded(context); } 复制代码
ApplicationContextInitializer 是 spring 容器刷新之前初始化 Spring ConfigurableApplicationContext 的回调接口,ApplicationContextInitializer 的 initialize 方法执行之前,context 是还没有刷新的。可以看到在 applyInitializers 之后紧接着发布了 ApplicationContextInitializedEvent 事件。其实这两个点都可以对 context 搞一些事情,ApplicationContextInitializer 更纯粹些,它只关注 context;而 ApplicationContextInitializedEvent 事件源中除了 context 之外,还有 springApplication 对象和参数 args。
prepareContext 最后阶段是发布了 ApplicationPreparedEvent 事件,表示上下文已经准备好了,可以随时执行 refresh 了。
refreshContext 是 Spring 上下文刷新的过程,这里实际调用的是 AbstractApplicationContext 的 refresh 方法;所以 SpringBoot 也是复用了 Spring 上下文刷新的过程。
@Override public void refresh() throws BeansException, IllegalStateException { // 加锁处理 synchronized (this.startupShutdownMonitor) { // 准备刷新此上下文。主要包括占位符的替换及验证所有的 properties prepareRefresh(); // 这里做了很多事情: // 1、让子类刷新内部beanFactory ,创建IoC容器(DefaultListableBeanFactory--ConfigurableListableBeanFactory 的实现类) // 2、加载解析XML文件(最终存储到Document对象中) // 3、读取Document对象,并完成BeanDefinition的加载和注册工作 ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); // 对 beanFactory 进行一些预处理(设置一些公共属性) prepareBeanFactory(beanFactory); try { // 允许在 AbstractApplicationContext的子类中对 BeanFactory 进行后置处理,postProcessBeanFactory()这个方法是个空实现。 postProcessBeanFactory(beanFactory); // 调用 BeanFactoryPostProcessor 后置处理器处理 BeanFactory 实例(BeanDefinition) invokeBeanFactoryPostProcessors(beanFactory); // 注册BeanPostProcessor后置处理器,BeanPostProcessors后置处理器用于拦截bean的创建 // 用于对创建后的bean实例进行处理 registerBeanPostProcessors(beanFactory); // 初始化消息资源 initMessageSource(); // 初始化应用事件广播器 initApplicationEventMulticaster(); // 初始化特殊的bean,这个方法是空实现,让AbstractApplicationContext的子类重写 onRefresh(); // 注册监听器(ApplicationListener) registerListeners(); // 实例化剩余的单例bean(非懒加载方式), Bean的 IoC、DI 和 AOP 都是发生在此步骤 finishBeanFactoryInitialization(beanFactory); // 完成刷新 // 1、发布 ContextRefreshedEvent 事件 // 2、处理 LifecycleProcessor finishRefresh(); } catch (BeansException ex) { if (logger.isWarnEnabled()) { logger.warn("Exception encountered during context initialization - " + "cancelling refresh attempt: " + ex); } // 销毁已经创建的单例以避免资源悬空。 destroyBeans(); // 重置 ”active“ 标记 cancelRefresh(ex); throw ex; } finally { // 重置Spring内核中的常用自检缓存,清空单例bean内缓存 resetCommonCaches(); } } } 复制代码
这个过程涉及到的东西非常多,可扩展的点也非常多,包括 BeanFactoryPostProcessor 处理、BeanPostProcessor 处理、LifecycleProcessor 处理已经 发布 ContextRefreshedEvent 事件等。到这里容器刷新已经完成,容器已经 ready,DI 和 AOP 也已经完成。
BeanFactoryPostProcessor 处理
BeanFactoryPostProcessor 可以对我们的 beanFactory 内所有的 beandefinition(未实例化)数据进行修改,这个过程是在 bean 还没有实例化之前做的。所以在这,我们通过自己去注册一些 beandefinition ,也可以对 beandefinition 做一些修改。关于 BeanFactoryPostProcessor 的用法在很多框架中都有体现,这里以 SOFATracer 中修改 Datasource 为例来说明下。
SOFATracer 中为了对有所基于 jdbc 规范的数据源进行埋点,提供了一个 DataSourceBeanFactoryPostProcessor,用于修改原生 DataSource 来实现一层代理。代码详见: com.alipay.sofa.tracer.boot.datasource.processor.DataSourceBeanFactoryPostProcessor
这里只看核心代码部分,在 postProcessBeanFactory 方法中会根据 Datasource 的类型来创建不同的 DataSourceProxy;创建 DataSourceProxy 的过程就是修改原生 Datasource 的过程。
private void createDataSourceProxy(ConfigurableListableBeanFactory beanFactory, String beanName, BeanDefinition originDataSource, String jdbcUrl) { // re-register origin datasource bean BeanDefinitionRegistry beanDefinitionRegistry = (BeanDefinitionRegistry) beanFactory; // 先把之前已经存在的 Datasource 的 BeanDefinition 移除 beanDefinitionRegistry.removeBeanDefinition(beanName); boolean isPrimary = originDataSource.isPrimary(); originDataSource.setPrimary(false); // 换个 beanName ,重新注册到容器中 beanDefinitionRegistry.registerBeanDefinition(transformDatasourceBeanName(beanName), originDataSource); // 构建代理的 datasource BeanDefinition,类型为 SmartDataSource RootBeanDefinition proxiedBeanDefinition = new RootBeanDefinition(SmartDataSource.class); // 设置 BeanDefinition 相关属性 proxiedBeanDefinition.setRole(BeanDefinition.ROLE_APPLICATION); proxiedBeanDefinition.setPrimary(isPrimary); proxiedBeanDefinition.setInitMethodName("init"); proxiedBeanDefinition.setDependsOn(transformDatasourceBeanName(beanName)); // 获取原生 datasource 的属性值 MutablePropertyValues originValues = originDataSource.getPropertyValues(); MutablePropertyValues values = new MutablePropertyValues(); String appName = environment.getProperty(TRACER_APPNAME_KEY); // 修改和新增属性 Assert.isTrue(!StringUtils.isBlank(appName), TRACER_APPNAME_KEY + " must be configured!"); values.add("appName", appName); values.add("delegate", new RuntimeBeanReference(transformDatasourceBeanName(beanName))); values.add("dbType", DataSourceUtils.resolveDbTypeFromUrl(unwrapPropertyValue(originValues.get(jdbcUrl)))); values.add("database", DataSourceUtils.resolveDatabaseFromUrl(unwrapPropertyValue(originValues.get(jdbcUrl)))); // 将新的 values 设置给代理 BeanDefinition proxiedBeanDefinition.setPropertyValues(values); // 将代理的 datasource BeanDefinition 注册到容器中 beanDefinitionRegistry.registerBeanDefinition(beanName, proxiedBeanDefinition); } 复制代码
上面这段代码就是 BeanFactoryPostProcessor 一种典型的应用场景,就是修改 BeanDefinition。
BeanFactoryPostProcessor 处理过程代码比较长,这里就不在具体分析处理的流程。需要关注的点是:1、BeanFactoryPostProcessor 的作用,它能做哪些事情;2、它是在容器启动的哪个阶段执行的。
registerBeanPostProcessors 的处理过程
registerBeanPostProcessors 是用于注册 BeanPostProcessor 的。BeanPostProcessor 的作用时机相对于 BeanFactoryPostProcessor 来说要晚一些,BeanFactoryPostProcessor 处理的是 BeanDefinition,Bean 还没有实例化;BeanPostProcessor 处理的是 Bean,BeanPostProcessor 包括两个方法,分别用于在 Bean 实例化之前和实例化之后回调。
开篇有提到,在某些场景下会出现 BeanPostProcessor 不生效。对于 Spring 来说,BeanPostProcessor 本身也会被注册成一个 Bean,那么自然就可能会出现,BeanPostProcessor 处理的 bean 在 BeanPostProcessor 本身初始化之前就已经完成了的情况。
registerBeanPostProcessors 大体分为以下几个部分:
这里还是以扩展时机为主线,Bean 的 IoC、DI 和 AOP 初始化过程不细究。
LifecycleProcessor 的处理过程
LifecycleProcessor 的处理过程是在 finishRefresh 方法中执行,下面先看下 finishRefresh 方法:
protected void finishRefresh() { // 清除上下文级的资源缓存(比如扫描的ASM元数据)。 clearResourceCaches(); // 为此上下文初始化 LifecycleProcessor。 initLifecycleProcessor(); // 首先将 refresh 传播到 LifecycleProcessor。 getLifecycleProcessor().onRefresh(); // 发布 ContextRefreshedEvent 事件 publishEvent(new ContextRefreshedEvent(this)); // Participate in LiveBeansView MBean, if active. LiveBeansView.registerApplicationContext(this); } 复制代码
初始化 initLifecycleProcessor 是从容器中拿到所有的 LifecycleProcessor ,如果业务代码中没有实现 LifecycleProcessor 接口的 bean ,则使用默认的 DefaultLifecycleProcessor。
onRefresh 过程是 最后会调用到 Lifecycle 接口的 start 方法。LifeCycle 定义 Spring 容器对象的生命周期,任何 spring 管理对象都可以实现该接口。然后,当 ApplicationContext 本身接收启动和停止信号(例如在运行时停止/重启场景)时,spring 容器将在容器上下文中找出所有实现了 LifeCycle 及其子类接口的类,并一一调用它们实现的类。spring 是通过委托给生命周期处理器 LifecycleProcessor 来实现这一点的。Lifecycle 接口定义如下:
public interface Lifecycle { /** * 启动当前组件 * 1、如果组件已经在运行,不应该抛出异常 * 2、对于容器,这将把开始信号传播到应用的所有组件 */ void start(); /** * 通常以同步方式停止该组件,当该方法执行完成后,该组件会被完全停止。当需要异步停止行为时,考虑实现 SmartLifecycle 和它的 stop * (Runnable) 方法变体。注意,此停止通知在销毁前不能保证到达:在常规关闭时,{@code Lifecycle} bean将首先收到一个停止通知,然后才传播 * 常规销毁回调;然而,在上下文的生命周期内的热刷新或中止的刷新尝试上,只调用销毁方法。对于容器,这将把停止信号传播到应用的所有组件 */ void stop(); /** * 检查此组件是否正在运行。 * 1. 只有该方法返回 false 时,start方法才会被执行。 * 2. 只有该方法返回 true 时,stop(Runnable callback) 或 stop() 方法才会被执行。 */ boolean isRunning(); } 复制代码
至此,容器刷新其实已经就完成了。可以看到 Spring 或者 SpringBoot 在整个启动过程中,有非常多的口子暴露出来,供用户使用,非常灵活。
与正常流程类似,异常处理流程同样作为 SpringBoot 生命周期的一个环节,在异常发生时,会通过一些机制来处理收尾过程。异常处理部分 SpringBoot 1.x 版本和 SpringBoot 2.x 版本差异还是比较大的。这里只分析 SpringBoot 2.x 的处理过程。这里直接贴一段代码:
private void handleRunFailure(ConfigurableApplicationContext context, Throwable exception, Collection<SpringBootExceptionReporter> exceptionReporters, SpringApplicationRunListeners listeners) { try { try { // exitCode handleExitCode(context, exception); if (listeners != null) { // failed listeners.failed(context, exception); } } finally { // 这里也是扩展的口子 reportFailure(exceptionReporters, exception); if (context != null) { context.close(); } } } catch (Exception ex) { logger.warn("Unable to close ApplicationContext", ex); } ReflectionUtils.rethrowRuntimeException(exception); } 复制代码
上述代码片段主要做了以下几件事:
在 SpringApplicationRunListeners#failed 中,业务产生的异常将直接被抛出,而不会影响异常处理的主流程。
至此,SpringBoot 启动的主流程已经全部分析完成了。从扩展和扩展时机的角度来看,整个过程中,SpringBoot 提供了非常多的扩展口子,让用户可以在容器启动的各个阶段(无论是启动,环境准备,容器刷新等等)做一些定制化的操作。用户可以利用这些扩展接口来修改 bean 、修改环境变量,给用户极大的空间。