ApplicationContext 是 Spring 的核心接口或容器。它的功能很多,通过它,我们可以创建、获取、管理 bean;可以发布事件;可以加载资源文件;可以获取容器当前运行的环境。Spring 为了更灵活的配置 ApplicationContext,在容器初始化的过程中,Spring 允许用户修改这个对象,具体的方法就是扩展 ApplicationContextInitializer。
Spring Boot 内置的一些 ApplicationContextInitializer,用于实现 Web 配置,日志配置等功能,本文以 Spring Boot 为例,聊聊 ApplicationContextInitializer 的用法。

简介

ApplicationContextInitializer 是个接口,这个接口中定义了一个 initialize 方法,该方法会在 ApplicationContext 初始化的时候执行。我们可以将其理解为 ApplicationContext 的钩子函数。
@FunctionalInterface 是 JDK1.8 加入的注解,标志这个接口拥有单一的方法。

  1. FunctionalInterface
  2. public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {
  3. /**
  4. * Initialize the given application context.
  5. * @param applicationContext the application to configure
  6. */
  7. void initialize(C applicationContext);
  8. }

我们在使用的时候,只需要继承 ApplicationContextInitializer,实现 initialize 方法就可以了。initialize 方法的入参是当前的 ApplicationContext,通过 ApplicationContext,我们可以获得当前的 Environment,添加或修改一些值;我们可以调用 addApplicationListener 方法添加监听器。总之,很多初始化的工作可以在这里完成。

  1. @Order(1)
  2. public class UserInitializer implements ApplicationContextInitializer {
  3. @Override
  4. public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
  5. System.out.println("UserInitializer");
  6. }
  7. }

ApplicationContextInitializer 可以有多个,支持 @Order 注解,表示执行顺序,越小越早。

SpringFactoriesLoader 是如何加载 ApplicationContextInitializer

ApplicationContextInitializer 的子类想要生效,需要注册到 ApplicationContext 中,Spring Boot 项目启动流程的第一步是创建 SpringApplication 对象,在该对象的构造函数中,程序加载了 ApplicationContextInitializer 的实现类。我们详细了解下这个方法。

  1. public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
  2. // 将 ApplicationContextInitializer 的实现类的实例加入 this.initializers 中
  3. ...
  4. setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
  5. ...
  6. }

该方法中,SpringFactoriesLoader 根据接口类型获得实现类的名称,通过反射创建实例,根据 sort 注解的值对实例排序。通过反射,我们可以很容易的获得某个类的构造函数以及注解,所以创建实例和排序是比较简单的。有意思的点在于,SpringFactoriesLoader 是如何找到指定接口的实现类的?

  1. private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
  2. ClassLoader classLoader = getClassLoader();
  3. // SpringFactoriesLoader 根据接口类型获得实现类的名称
  4. Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
  5. // 反射创建实例
  6. List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
  7. // 根据 sort 注解的值排序
  8. AnnotationAwareOrderComparator.sort(instances);
  9. return instances;
  10. }

SpringFactoriesLoader 是 Spring 提供的一种加载方式。说白了也很简单,就是 SpringFactoriesLoader 会固定加载 classpath 路径下的 META-INF/spring.factories 文件,约定该文件中按照 Properties 格式填写好接口和实现类的全名,如果有多个实现类,用逗号隔开。SpringFactoriesLoader 在 Spring Boot 中的作用非常重要,它不仅是加载初始化器的,后续的加载监听器,分析器,前置处理或后置处理器,使用的都是 SpringFactoriesLoader。

  1. public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
  2. private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
  3. ...
  4. try {
  5. // 本质就是调用 classLoader.getResources 方法
  6. Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
  7. while (urls.hasMoreElements()) {
  8. ...
  9. }
  10. }
  11. ...
  12. }

如下就是我们将上面写的 UserInitializer 注册在了 ApplicationContext 中,Spring Boot 启动的时候会执行 UserInitializer 的 initialize 方法。

  1. org.springframework.context.ApplicationContextInitializer=xxx.xxx.UserInitializer

有没有发现这种通过接口获得类实例的方式和 JDBC 有点像。这其实都属于 java 的 SPI 机制(Service Provider Interface),其实就是一种将服务接口与服务实现分离以达到解耦、提高可扩展性的机制。SPI 中接口和接口的实现并不在一个项目中,可以说,SPI 机制是项目级别的隔离,这种方式在框架的设计中很常见。Spring 中 SPI 是通过扩展 META-INF/spring.factories 实现的,Dubbo 中也用了类似的方法,扩展了 META-INF/dubbo 等文件。
ApplicationContextInitializer 除了通过 SPI 进行注册外,其实还可以通过硬编码的方式进行注册。那就是修改 Spring Boot 的启动方法,手动添加一个 Initializer。

  1. @SpringBootApplication
  2. public class DemoApplication {
  3. public static void main(String[] args) {
  4. SpringApplication springApplication = new SpringApplication(DemoApplication.class);
  5. springApplication.addInitializers(new DemoInitializer());
  6. springApplication.run(args);
  7. }
  8. }

这种方式实现的效果和第一种是一样的,本质都是在 SpringApplication 的 initializers 对象上增加一个 Initializer 实例。但是,第一种方式是优于第二种方式的,使用 SPI 的扩展方式,不需要改动原先的代码就可以实现扩展,符合开闭原则。
还有一种方式需要借助 Spring Boot 的配置文件 application.properties。这种方式的原理我们在后面分析 Spring Boot 内置的初始化器的时候会谈到。

  1. context.initializer.classes=xxx.xxx.DemoInitializer

ApplicationContextInitializer 执行阶段

Spring Boot 执行 main 方法,其实就是执行 SpringApplication 的 run 方法。
run 方法是 SpringApplication 的静态方法,其中会生成 SpringApplication 实例对象,真正执行的是实例对象的 run 方法。SpringFactoriesLoader 加载 ApplicationContextInitializer 的过程就发生在生成 SpringApplication 实例的过程中。 类加载完毕,且生成了实例,那这些初始化器什么时候生效呢?如下是 run 方法执行流程。
Spring Boot 扩展ApplicationContextInitializer 源码解析 - 图1
ApplicationContextInitializer 是在准备 Application 的上下文阶段被执行的。我们知道,spring 是在刷新上下文的时候开始通过 BeanFactory 加载 Bean,所以,ApplicationContextInitializer 的执行发生在 Bean 加载之前,但是此时的 Environment 已经初始化完毕,我们可以在该阶段获得 Environment 的实例,方便增加或修改一些值;此时 ApplicationContext 实例也创建好了,可以预先在上下文中加入一些监听器,处理器等。

  1. private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
  2. ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
  3. ApplicationArguments applicationArguments, Banner printedBanner) {
  4. context.setEnvironment(environment);
  5. postProcessApplicationContext(context);
  6. // 执行 ApplicationContextInitializer
  7. applyInitializers(context);
  8. listeners.contextPrepared(context);
  9. bootstrapContext.close(context);
  10. ...
  11. }

applyInitializers 方法中,会遍历之前注册的 initializers,依次调用 initialize 方法。

  1. protected void applyInitializers(ConfigurableApplicationContext context) {
  2. for (ApplicationContextInitializer initializer : getInitializers()) {
  3. ...
  4. // 执行 initialize 方法
  5. initializer.initialize(context);
  6. }
  7. }

Spring Boot 内置的初始化器

Spring 提供了扩展 ApplicationContextInitializer 的方法,Spring Boot 将其发扬光大了。我们可以在 spring-boot 的 jar 包下的 META-INF 中找到 spring.factories,如下是其中的 ApplicationContextInitializer 的配置。

  1. # Application Context Initializers
  2. org.springframework.context.ApplicationContextInitializer=
  3. org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,
  4. org.springframework.boot.context.ContextIdApplicationContextInitializer,
  5. org.springframework.boot.context.config.DelegatingApplicationContextInitializer,
  6. org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer,
  7. org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer

ConfigurationWarningsApplicationContextInitializer

ConfigurationWarningsApplicationContextInitializer 用于报告 Spring 容器的一些常见的错误配置,可以看出,该初始化器为 context 增加了一个 Bean 的后置处理器。这个处理器是在注册 BeanDefinition 实例之后生效的,用于处理注册实例过程中产生的告警信息,其实就是通过日志打印出告警信息。

  1. public class ConfigurationWarningsApplicationContextInitializer
  2. implements ApplicationContextInitializer<ConfigurableApplicationContext> {
  3. ...
  4. @Override
  5. public void initialize(ConfigurableApplicationContext context) {
  6. context.addBeanFactoryPostProcessor(new ConfigurationWarningsPostProcessor(getChecks()));
  7. }
  8. ...
  9. }

ContextIdApplicationContextInitializer

ContextIdApplicationContextInitializer 用于设置 Spring 应用上下文 ID,这个 ID 可以通过 ApplicationContext#getId() 的方式获得。

  1. public class ContextIdApplicationContextInitializer
  2. implements ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
  3. ...
  4. @Override
  5. public void initialize(ConfigurableApplicationContext context) {
  6. ContextId contextId = getContextId(applicationContext);
  7. applicationContext.setId(contextId.getId());
  8. applicationContext.getBeanFactory().registerSingleton(ContextId.class.getName(), contextId);
  9. }
  10. ...
  11. }

DelegatingApplicationContextInitializer

DelegatingApplicationContextInitializer 看到 Delegating 就知道了,这个初始化器是为了别人服务的。这个初始化器会获得 application.properties 下的配置为 context.initializer.classes 的值,这个值是初始化器的全路径名,多个之间用逗号隔开。获得名称后,使用反射将其实例化,并依次触发初始化器的 initialize 方法。DelegatingApplicationContextInitializer 使得 Spring Boot 的用户可以在 application.properties 中配置初始化器。需要注意的是,DelegatingApplicationContextInitializer 的优先级是 0 ,所以不论 context.initializer.classes 配置的初始化器的 order 是多少,都会按照 0 的优先级执行。

  1. public class DelegatingApplicationContextInitializer
  2. implements ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
  3. private int order = 0;
  4. ...
  5. @Override
  6. public void initialize(ConfigurableApplicationContext context) {
  7. // environment 中存放了 Spring Boot 的配合,包括 application.properties 下的配置
  8. ConfigurableEnvironment environment = context.getEnvironment();
  9. // 从 application.properties 中找出配置为 context.initializer.classes 的值
  10. // context.initializer.classes 的值是初始化器的全路径名。可以有多个。
  11. List<Class<?>> initializerClasses = getInitializerClasses(environment);
  12. // 依次触发初始化器
  13. if (!initializerClasses.isEmpty()) {
  14. applyInitializerClasses(context, initializerClasses);
  15. }
  16. }
  17. ...
  18. }

RSocketPortInfoApplicationContextInitializer

RSocketPortInfoApplicationContextInitializer 和 ServerPortInfoApplicationContextInitializer 都是给 ApplicationContext 增加了个监听器,二者都是监听 RSocketServerInitializedEvent 事件,为环境 Environment 中添加一个属性源,不同之处在于一个是增加 SocketPort,一个是增加 ServerPort。代码就不贴了。
除了 spring-boot 下的 META-INF/spring.factories 存在初始化器外,spring-boot-autoconfigure 下也存在 META-INF/spring.factories。这里也定义了两个初始化器。从这里也可以看出,使用 SPI 的方式确实降低了项目间的耦合,每个项目都能定义自己的实现。

  1. # Initializers
  2. org.springframework.context.ApplicationContextInitializer=
  3. org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,
  4. org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

总结

  1. ApplicationContextInitializer 是 Spring 对外提供的扩展点之一,用于在 ApplicationContext 容器加载 Bean 之前对当前的上下文进行配置。
  2. ApplicationContextInitializer 的实现有三种,第一种是在 classpath 路径下的 META-INF/spring.factories 文件中填写接口和实现类的全名,多个实现的话用逗号分隔。第二种是在 Spring Boot 启动代码中手动添加初始化器,第三种是在 application.properties 中配置 context.initializer.classes。
  3. SpringFactoriesLoader 是 spring 提供的,用于加载外部项目配置的加载器。他会固定的读取 META-INF/spring.factories 文件,解析该文件,获得指定接口的实现类。SpringFactoriesLoader 这种加载配置的方式是典型的 SPI 方式,在 Spring Boot 中大量使用,这种方式将服务接口与服务实现分离,达到解耦、提高可扩展性的目的。
  4. Spring Boot 内置了一些初始化器,大部分功能是配置环境变量,比如 ServerPortInfoApplicationContextInitializer,实现手段是为 ApplicationContext 增加监听器。还用于配置日志,比如 ConfigurationWarningsApplicationContextInitializer 实现手段是增加 Bean 后处理器做校验。比较特殊的是 DelegatingApplicationContextInitializer,它会获得 application.properties 中配置的 context.initializer.classes,将其作为初始化器进行加载和执行。

    参考

    https://juejin.cn/post/6918692403989708813