SpringBoot扩展ApplicationCont

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

Spring Boot 内置的一些 ApplicationContextInitializer,用于实现 Web 配置,日志配置等功能,本文以 Spring Boot 为例,聊聊 ApplicationContextInitializer 的用法。

ApplicationContextInitializer

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

@FunctionalInterface
public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {

	/**
	 * Initialize the given application context.
	 * @param applicationContext the application to configure
	 */
	void initialize(C applicationContext);

}
复制代码

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

@Order(1)
public class UserInitializer implements ApplicationContextInitializer {
    @Override
    public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
        System.out.println("UserInitializer");
    }
}
复制代码

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

SpringFactoriesLoader 是如何加载 ApplicationContextInitializer 的

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

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {

  // 将 ApplicationContextInitializer 的实现类的实例加入 this.initializers 中
  ...
  setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
  ...
}
复制代码

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

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
  ClassLoader classLoader = getClassLoader();

  // SpringFactoriesLoader 根据接口类型获得实现类的名称
  Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));

  // 反射创建实例
  List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);

  // 根据 sort 注解的值排序
  AnnotationAwareOrderComparator.sort(instances);
  return instances;
}
复制代码

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

public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
  ...
  try {
    // 本质就是调用 classLoader.getResources 方法
    Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
    while (urls.hasMoreElements()) {
      ...
    }
  }
  ...
}
复制代码

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

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。


@SpringBootApplication
public class DemoApplication {

	public static void main(String[] args) {
		SpringApplication springApplication = new SpringApplication(DemoApplication.class);
		springApplication.addInitializers(new DemoInitializer());
		springApplication.run(args);
	}

}
复制代码

这种方式实现的效果和第一种是一样的,本质都是在 SpringApplication 的 initializers 对象上增加一个 Initializer 实例。但是,第一种方式是优于第二种方式的,使用 SPI 的扩展方式,不需要改动原先的代码就可以实现扩展,符合开闭原则。

还有一种方式需要借助 Spring Boot 的配置文件 application.properties。这种方式的原理我们在后面分析 Spring Boot 内置的初始化器的时候会谈到。

context.initializer.classes=xxx.xxx.DemoInitializer
复制代码

ApplicationContextInitializer 执行阶段

Spring Boot 执行 main 方法,其实就是执行 SpringApplication 的 run 方法。

public static void main(String[] args) {
  SpringApplication.run(DemoApplication.class, args);
}
复制代码

run 方法是 SpringApplication 的静态方法,其中会生成 SpringApplication 实例对象,真正执行的是实例对象的 run 方法。SpringFactoriesLoader 加载 ApplicationContextInitializer 的过程就发生在生成 SpringApplication 实例的过程中。 类加载完毕,且生成了实例,那这些初始化器什么时候生效呢?如下是 run 方法执行流程。

ApplicationContextInitializer 是在准备 Application 的上下文阶段被执行的。我们知道,spring 是在刷新上下文的时候开始通过 BeanFactory 加载 Bean,所以,ApplicationContextInitializer 的执行发生在 Bean 加载之前,但是此时的 Environment 已经初始化完毕,我们可以在该阶段获得 Environment 的实例,方便增加或修改一些值;此时 ApplicationContext 实例也创建好了,可以预先在上下文中加入一些监听器,处理器等。

private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
			ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments, Banner printedBanner) {
  context.setEnvironment(environment);
  postProcessApplicationContext(context);
  
  // 执行 ApplicationContextInitializer
  applyInitializers(context);

  listeners.contextPrepared(context);
  bootstrapContext.close(context);
  ...
}
复制代码

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

protected void applyInitializers(ConfigurableApplicationContext context) {
  for (ApplicationContextInitializer initializer : getInitializers()) {
    ...
    // 执行 initialize 方法
    initializer.initialize(context);
  }
}
复制代码

Spring Boot 内置的初始化器

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

# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,
org.springframework.boot.context.ContextIdApplicationContextInitializer,
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,
org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer,
org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer
复制代码

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

public class ConfigurationWarningsApplicationContextInitializer
		implements ApplicationContextInitializer<ConfigurableApplicationContext> {
  ...    
  @Override
	public void initialize(ConfigurableApplicationContext context) {
		context.addBeanFactoryPostProcessor(new ConfigurationWarningsPostProcessor(getChecks()));
	}
  ...
}
复制代码

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

public class ContextIdApplicationContextInitializer
		implements ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
  ...    
  @Override
	public void initialize(ConfigurableApplicationContext context) {
	  ContextId contextId = getContextId(applicationContext);
		applicationContext.setId(contextId.getId());
		applicationContext.getBeanFactory().registerSingleton(ContextId.class.getName(), contextId);
	}
  ...
}
复制代码

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

public class DelegatingApplicationContextInitializer
		implements ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
  private int order = 0;    
  ...
  @Override
	public void initialize(ConfigurableApplicationContext context) {
    // environment 中存放了 Spring Boot 的配合,包括 application.properties 下的配置
		ConfigurableEnvironment environment = context.getEnvironment();
    // 从 application.properties 中找出配置为 context.initializer.classes 的值
    // context.initializer.classes 的值是初始化器的全路径名。可以有多个。
		List<Class<?>> initializerClasses = getInitializerClasses(environment);
    // 依次触发初始化器
		if (!initializerClasses.isEmpty()) {
			applyInitializerClasses(context, initializerClasses);
		}
	}
  ...
}
复制代码

RSocketPortInfoApplicationContextInitializer 和 ServerPortInfoApplicationContextInitializer 都是给 ApplicationContext 增加了个监听器,二者都是监听 RSocketServerInitializedEvent 事件,为环境 Environment 中添加一个属性源,不同之处在于一个是增加 SocketPort,一个是增加 ServerPort。代码就不贴了。

除了 spring-boot 下的 META-INF/spring.factories 存在初始化器外,spring-boot-autoconfigure 下也存在 META-INF/spring.factories。这里也定义了两个初始化器。从这里也可以看出,使用 SPI 的方式确实降低了项目间的耦合,每个项目都能定义自己的实现。

# Initializers
org.springframework.context.ApplicationContextInitializer=
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,
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,将其作为初始化器进行加载和执行。

如果您觉得有所收获,就请点个赞吧!