Spring Boot 是如何监听启动事件的

979 阅读9分钟

Spring Boot 启动是分阶段的,在不同的阶段,Spring Boot 会发出不同的事件,我们可以监听这些事件,实现自定义的处理方法。很多的框架和 Sprint Boot 间的整合就利用了这个机制。

事件监听模式的核心在于,事件源经过事件的封装传给监听器,当事件源触发事件后,监听器接收到事件并执行事件的回调方法。本文在分析 Spring Boot 的启动事件过程中,重点关注以下几个问题。

  1. Spring Boot 启动事件有哪些。
  2. Spring Boot 事件监听器的加载方式。
  3. Spring Boot 事件监听回调函数是如何执行的。
  4. Spring Boot 如何解耦“阶段”与“事件”。

Spring Boot 中的启动事件

Spring Boot 启动的时候会发出很多事件,我们看下这些事件的继承关系。

由于这些事件其实只是起到标志的作用,所以可以用简单的值对象表示,继承关系也很简单。EventObject 是 JDK 提供的对象,其中只有一个成员变量 source,表示该事件的发生源。

public class EventObject implements java.io.Serializable {
    ...
    protected transient Object source;
    ...
}

Spring 扩展了 EventObject,提供了 ApplicationEvent。增加了 timestamp 字段,记录事件发生的时间。Spring Boot 在此基础上扩展了 SpringApplicationEvent,增加了 args 字段,表示一些额外的传参。由于只是简单的增加了成员变量,就不贴代码了。这里列出 SpringApplicationEvent 的类图,主要是为了和 ApplicationContextEvent 做区分,前者是应用启动生命周期事件,后者是 ApplicationContext 在不同的阶段发出的事件,不能一概而论。

这种事件的继承关系很好的表示了事件发生源的不同,以及事件的从属关系。Spring Boot 在启动的不同阶段发出不同的事件。如下是 Spring Boot 启动时不同阶段事件的触发顺序。starting 阶段,表示 Spring Boot 开始启动; environmentPrepared 表示环境变量准备完成;contextPrepared 表示上下文对象准备完成;contextLoaded 表示上下文对象加载完毕;started 表示 Spring Boot 启动完成;running 表示此时容器运行中。如果在启动中任何阶段发生异常,进入 failed 阶段。

在 starting 阶段,会有一个广播器(Multicaster)负责触发 ApplicationStartingEvent 事件。environmentPrepared 阶段会触发 ApplicationEnvironmentPreparedEvent 事件,其它的就不列举了。在触发事件后,会执行相关监听器的回调函数,完成工作。

事件监听器的注册方式

Spring 中的事件监听器如下所示,只需要实现一个 onApplicationEvent 方法就可以了,这个接口需要指定一个泛型,泛型的类型就是需要处理的事件的类型。后续我们可以看到事件广播器会调用这个 onApplicationEvent 方法。

@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
  /**
   * Handle an application event.
   * @param event the event to respond to
   */
  void onApplicationEvent(E event);
  ...
}

我们如果想要注册一个监听事件,一般的做法是 new 一个事件的实例,然后将其加入到事件广播器的监听对列中。Spring Boot 当然不会使用这样的方式,会导致无法扩展监听器。我们之前在谈到Spring Boot 扩展 ApplicationContextInitializer 的方式时,曾经提到一种 SPI 的设计模式,可以将服务接口与服务实现分离以达到解耦、提高系统的可扩展性。其实,Spring Boot 在监听事件的注册也是使用了相同的方式,设置初始化器和设置事件监听器代码都是大同小异。

// 设置初始化器
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 设置事件监听器
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));

getSpringFactoriesInstances 这个方法会调用 SpringFactoriesLoader 固定加载 classpath 路径下的 META-INF/spring.factories 文件。约定该文件中按照 Properties 格式填写好接口和实现类的全名。ApplicationListener.class 的全路径名是 org.springframework.context.ApplicationListener,找到 key 是 org.springframework.context.ApplicationListener 的一行,读取其 value 值,是该接口的实现类,可以有多个,用逗号隔开。

我们在 spring-boot 的 jar 包下的 META-INF 中找到 spring.factories,如下是其中的 ApplicationListener 的配置。

# Application Listeners
org.springframework.context.ApplicationListener=
org.springframework.boot.ClearCachesApplicationListener,
org.springframework.boot.builder.ParentContextCloserApplicationListener,
org.springframework.boot.context.FileEncodingApplicationListener,
org.springframework.boot.context.config.AnsiOutputApplicationListener,
org.springframework.boot.context.config.DelegatingApplicationListener,
org.springframework.boot.context.logging.LoggingApplicationListener,
org.springframework.boot.env.EnvironmentPostProcessorApplicationListener,
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener

Spring 通过这种方式实现了事件监听器的解耦,其余的项目也可以使用同样的方式注册自己的监听器。和初始化器相同,Spring Boot 中注册事件监听器的方式也有三种,第一种是在 classpath 路径下的 META-INF/spring.factories 文件中填写接口和实现类的全名,多个实现的话用逗号分隔。第二种是在 Spring Boot 启动代码中手动往 ApplicationContext 中添加初始化器,第三种是在 application.properties 中配置 context.listener.classes。

监听器的加载流程结合Spring Boot 扩展 ApplicationContextInitializer 的方式中系统初始化器的加载流程一起看,我们基本可以看出 Spring Boot 中实现项目级别解耦的方法,Spring Boot 中定义好了接口,然后使用 SpringFactoriesLoader 固定读取 META-INF/spring.factories 文件中配置的接口实现,每个项目都可以拥有自己的 spring.factories 文件,自由定义扩展类的实现。一般而言,Spring Boot 中会有自己的 spring.factories 文件,这里面定义了 Spring Boot 自己的实现,其中会有一个叫 DelegatingApplicationXXX 的实现类,用于实现在 application.properties 中也能配置特定接口的实现。

Spring 中的事件广播器

Spring Boot 扩展了 Spring 中的事件,但是 Spring Boot 和 Spring 的事件广播器完全是一致的。都是使用 SimpleApplicationEventMulticaster。如下,SimpleApplicationEventMulticaster 继承了 AbstractApplicationEventMulticaster, AbstractApplicationEventMulticaster 实现了三个接口,分别为 ApplicationEventMulticaster, BeanClassLoaderAware, BeanFactoryAware。 BeanClassLoaderAware 和 BeanFactoryAware 都是初始化类加载器及 Bean 容器的。

public interface ApplicationEventMulticaster {
  ...
  // 新增监听器
  void addApplicationListener(ApplicationListener<?> listener);
  // 移除监听器
  void removeApplicationListener(ApplicationListener<?> listener);
  // 事件广播
  void multicastEvent(ApplicationEvent event);
  ...
}

ApplicationEventMulticaster 是事件广播的接口,定义方法主要有三类,新增事件监听器,移除事件监听器以及事件广播。

可以大概的看下 SimpleApplicationEventMulticaster 的 multicastEvent 方法,获得当前注册在应用上的事件监听器,判断该监听器是否监听了当前事件,然后依次执行 invokeListener 方法,其实就是执行监听器的 onApplicationEvent 方法。

public class SimpleApplicationEventMulticaster extends AbstractApplicationEventMulticaster {
  ...
  @Override
  public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
      ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
      Executor executor = getTaskExecutor();
      for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
          if (executor != null) {
              executor.execute(() -> invokeListener(listener, event));
          }
          ...
      }
  }
  ...
}

“阶段”与“事件”

了解了 Spring Boot 的事件以及事件的广播后,我们可以思考这样一个问题,在 starting 阶段只能触发 ApplicationStartingEvent 事件吗?一般系统执行到某个阶段的时候,发出特定的事件。Spring Boot 在此基础上更进一步,将“事件”与“阶段”解耦了。具体的方式是通过 SpringApplicationRunListener 接口。

SpringApplicationRunListener 接口中定义了 Spring 容器启动的不同阶段。我们之前画的阶段的流转图就是根据这个来的。

public interface SpringApplicationRunListener {
  ...
	default void starting(...) {}

	default void environmentPrepared(...) {}

	default void contextPrepared(...) {}
  ...
}

spring.factories 中定义了这个接口的实现,如下是 Spring Boot 中的实现。

# Run Listeners
org.springframework.boot.SpringApplicationRunListener=
org.springframework.boot.context.event.EventPublishingRunListener

EventPublishingRunListener 是 Spring Boot 指定的事件发布器,它同时实现了 SpringApplicationRunListener 和 Ordered 接口。我们可以看出,在 starting 阶段,SimpleApplicationEventMulticaster 发出了一个 ApplicationStartingEvent 事件。其余阶段发出事件的代码都是类似的。

public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {
	...

  public EventPublishingRunListener(SpringApplication application, String[] args) {
      this.application = application;
      this.args = args;
      this.initialMulticaster = new SimpleApplicationEventMulticaster();
      for (ApplicationListener<?> listener : application.getListeners()) {
          this.initialMulticaster.addApplicationListener(listener);
		}
	}

	@Override
	public int getOrder() {return 0;}

	@Override
	public void starting(ConfigurableBootstrapContext bootstrapContext) {
        	this.initialMulticaster
                	.multicastEvent(new ApplicationStartingEvent(bootstrapContext, this.application, this.args));
	}

  ...
}

我们在执行 run 方法的时候,会获得当前项目中所有的 SpringApplicationRunListener,然后在每个阶段循环发出该阶段对应的事件。getRunListeners 的方法其实就是通过 SpringFactoriesLoader 加载 spring.factories 中 SpringApplicationRunListener 实现的过程。

public ConfigurableApplicationContext run(String... args) {
      ...
      SpringApplicationRunListeners listeners = getRunListeners(args);
      listeners.starting(bootstrapContext, this.mainApplicationClass);
      ...
}

Spring Boot 通过将事件发布器的实现类配置在 spring.factories 文件中,实现“阶段”和“事件”的解耦。

SpringApplication 与 ApplicationContext 中的事件监听

Spring Boot 的启动执行的是 SpringApplication 的 run 方法,我们本文中将的事件监听器也是注册在 SpringApplication 上的。我们在说初始化器的时候提到过,一些初始化器其实是给 ApplicationContext 中添加了监听器。可以看出,目前监听器会存在于两个地方,一个是 SpringApplication,一个是 ApplicationContext。

这也很好理解,程序刚启动的时候,比如执行到 starting 阶段的时候,我们要扩展一些功能,肯定不能在 ApplicationContext 上添加监听器,因为这个阶段,还没有 ApplicationContext 这个对象。注册到 SpringApplication 上是最自然的选择。

两者的触发的机制是有一些不同的,注册到 SpringApplication 上的事件会通过 SpringApplicationRunListener 触发。而注册到 ApplicationContext 中的事件则由 ApplicationContext 中的事件广播器触发。但是并不是说这两类监听是无法转换的,Spring Boot 默认的 SpringApplicationRunListener 是 EventPublishingRunListener。我们可以在 contextLoaded 方法中看到,在 context 加载好之后,会把 SpringApplication 上注册的监听器全部放到 SpringApplication 上一份。也就是说,我们注册的监听事件不仅能监听 Spring Boot 启动阶段的事件,还能监听 ApplicationContext 触发的事件。

@Override
public void contextLoaded(ConfigurableApplicationContext context) {
  for (ApplicationListener<?> listener : this.application.getListeners()) {
    if (listener instanceof ApplicationContextAware) {
      ((ApplicationContextAware) listener).setApplicationContext(context);
    }
    context.addApplicationListener(listener);
  }
  this.initialMulticaster.multicastEvent(new ApplicationPreparedEvent(this.application, this.args, context));
}

默认情况下注册到 ApplicationContext 中的监听器是无法监听 Spring 启动事件的,原因在于 Spring 并没有把 ApplicationContext 中的监听器加到 SpringApplication 中。而 EventPublishingRunListener 也没有触发 ApplicationContext 中的监听器。如果你想要这么做的话,可以自定义一个 SpringApplicationRunListener,自己实现 Spring 启动的不同阶段的逻辑。

ApplicationContext 中的监听器的注册方式除了继承来自 SpringApplication 中的外,还会在 refresh 阶段获得所有的类型为 ApplicationListener 的 Bean,注册到事件广播器中。

总结

  1. Spring Boot 为了能够灵活扩展启动过程,使用了事件监听机制,在启动的不同阶段发出不同的事件(ApplicationEvent)。事件广播器(ApplicationEventMulticaster)在发出这些事件的同时,会获得当前注册在应用上的事件监听器(ApplicationListener),依次执行每个监听器的回调(onApplicationEvent) 方法。这就是 Spring Boot 启动过程中的事件分发与监听机制。

  2. Spring Boot 为了将“阶段”与“事件”解耦,“事件”与“监听器”解耦,使用 SpringFactoriesLoader 读取 META-INF/spring.factories 文件,获取指定接口的实现类,这是 SPI 动态加载模式,也是 Spring 中通用的实现项目级别解耦的方式。

  3. 事件监听器(ApplicationListener)的实现有三种,第一种是在 classpath 路径下的 META-INF/spring.factories 文件中填写接口和实现类的全名,多个实现的话用逗号分隔。第二种是在 Spring Boot 启动代码中手动添加初始化器,第三种是在 application.properties 中配置 context.listener.classes。

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