Springcloud环境下SpringContext的启动情况

1,327 阅读5分钟

前言

本来想在Spring容器中用ApplicationListener处理一下ContextRefreshedEvent事件,原先以为直接写一个ApplicationListener的即可,没想到执行的时候发现这个listener执行了两次事件更新,印象中一个容器只会发送一次类似的映像事件,怎么会执行三次的? 接下来调试一下源码看看。

代码分析

自己写了一个如下的listener, 监听ContextRefreshedEvent

public class DemoListener implements ApplicationListener<ContextRefreshedEvent>{

    private ApplicationContext context;

    public static final AtomicInteger NUM = new AtomicInteger(0);

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        log.info("listener has execute {} times!!!!", NUM.incrementAndGet());
    }
}

第一次触发时机

堆栈图如下:

第一次触发
具体分析一下代码执行路径:

  1. 自己启动的容器(也就是我们SpringApplication.run出来的那个容器) 发送事件ApplicationEnvironmentPreparedEvent
  2. 事件被唯一的SpringApplicationRunListener监听到,也就是EventPublishingRunListener,然后这个listener委托一个SimpleApplicationEventMulticaster把这个事件广播出去,其实就是一个监听器模式,让所有注册了的listener响应这个事件
  3. 这里可以看到BootstrapApplicationListener响应了这个事件,执行了bootstrapServiceContext新开了一个id为bootstrap的上下文
  4. 新启动的bootstrap application context开始run
  5. bootstrap application context 触发ContextRefreshedEvent事件,被自定义的DemoListener监听到,然后执行对应代码 流程如下:

这就意味着其实我们是启动了另外一个bootstrap context,然后这个context本身refresh的时候发送事件被Demolistener监听到了。 我们可以具体看一下BootstrapApplicationListener.bootstrapServiceContext()代码:

private ConfigurableApplicationContext bootstrapServiceContext(
			ConfigurableEnvironment environment, final SpringApplication application,
			String configName) {
		StandardEnvironment bootstrapEnvironment = new StandardEnvironment();
		MutablePropertySources bootstrapProperties = bootstrapEnvironment
				.getPropertySources();
		for (PropertySource<?> source : bootstrapProperties) {
			bootstrapProperties.remove(source.getName());
		}
		String configLocation = environment
				.resolvePlaceholders("${spring.cloud.bootstrap.location:}");
		Map<String, Object> bootstrapMap = new HashMap<>();
		//这里表示了这个context会去加载bootstrap.properties放入环境中
		bootstrapMap.put("spring.config.name", configName);
		bootstrapMap.put("spring.main.web-application-type", "none");
		if (StringUtils.hasText(configLocation)) {
			bootstrapMap.put("spring.config.location", configLocation);
		}
		bootstrapProperties.addFirst(
				new MapPropertySource(BOOTSTRAP_PROPERTY_SOURCE_NAME, bootstrapMap));
		for (PropertySource<?> source : environment.getPropertySources()) {
			if (source instanceof StubPropertySource) {
				continue;
			}
			bootstrapProperties.addLast(source);
		}
		SpringApplicationBuilder builder = new SpringApplicationBuilder()
				.profiles(environment.getActiveProfiles()).bannerMode(Mode.OFF)
				.environment(bootstrapEnvironment)
				.registerShutdownHook(false).logStartupInfo(false)
				.web(WebApplicationType.NONE);
		final SpringApplication builderApplication = builder.application();
		if (builderApplication.getMainApplicationClass() == null) {
			builder.main(application.getMainApplicationClass());
		}
		if (environment.getPropertySources().contains("refreshArgs")) {
			builderApplication
					.setListeners(filterListeners(builderApplication.getListeners()));
		}
		builder.sources(BootstrapImportSelectorConfiguration.class);
		final ConfigurableApplicationContext context = builder.run();
	
		context.setId("bootstrap");
		// 设置一个Initializer给当前的application context,目的是为了让它执行的时候设置期父容器为我们新建的这个bootstrap context
		addAncestorInitializer(application, context);
		bootstrapProperties.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME);
		// 将此次bootstrap context加载的环境信息共享给application context
		mergeDefaultProperties(environment.getPropertySources(), bootstrapProperties);
		return context;
	}

这里可以看出,首先这个新启动的context是WebApplicationType.NONE的,同时是被设置成当前启动的application context的父容器的。它只会只会扫描BootstrapImportSelectorConfiguration这个class,看对应的代码就清除其实它只引入BootstrapConfiguration类以及配置项spring.cloud.bootstrap.sources配置的类,也就是说当前真正项目需要实例化的所有bean依然还是在我们启动的那个application context 中进行实例化。 因此,这次触发的Demolistener其实是bootstrap context里面实例化的listener。之后我们全局搜索一下org.springframework.cloud.bootstrap.BootstrapApplicationListener,发现其实这个类被声明在spring-cloud-contextspring.factories文件里面(实际上因为引入spring-cloud-starter带来的引入)。那么以为着,如果我们使用springboot的时候引入spring-cloud-starter之后,会相对应的启动一个bootstrap context,用来作为当前启动的容器的父容器。

第二次触发时机

那么第二次的堆栈就显而易见了

就是当前我们启动的application context 所触发的事件,其实也就是我们需求上最想要的那一次事件

第三次触发时机

从前面了解到,是多启动了一个容器,而引发了事件多执行一次,但是分属于不同的listener,一个是bootstrap context里面的listener执行的,一个是application context执行的,那第三次的触发是为什么呢?难道又启动了一个容器?我们可以看下第三次的执行堆栈

可以看到其实是两个不同的context触发了两次publishEvent,代码如下:

if (this.earlyApplicationEvents != null) {
    this.earlyApplicationEvents.add(applicationEvent);
}
else {
    // 调用multicaster发送容器事件
    getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
}

// 如果父容器存在,则叫父容器触发一次对应的容器事件
if (this.parent != null) {
    if (this.parent instanceof AbstractApplicationContext) {
        ((AbstractApplicationContext) this.parent).publishEvent(event, eventType);
    }
    else {
        this.parent.publishEvent(event);
    }
}

这里可以看出其实是application context触发事件之后,先发送一遍给自己的listener,然后看下自己有没有父容器,有的话再发送一遍这个事件给父容器,当然咯,事件的source肯定是触发的容器咯,也就是application context。 同时,我们可以看到这里是通过context 调用multicaster 把事件发出去的,所以这里会有一个判断是否有父容器的逻辑。第一次触发的时候实际上是通过EventPublishingRunListener 调用multicaster把事件发出去的。所以说,如果是我们的listener监听的其他事件,不一定是说只执行三次,要看具体情况分析。例如ContextRefreshedEvent这类容器的事件,spring里面肯定都是通过context去发送,那么这种情况下肯定会有奇数次的(因为父容器会执行两遍)。

总结

因此,如果写一个只在application context 执行一次的listener就很重要,注意以下几点:

  1. 只把listener注册在父容器或子容器
  2. 判断event里面过来的context是不是我们想要的context。例如可以根据contextId.equals("bootstrap")来判断是不是自己想要的容器。也可以通过context.getParent() == null判断,但是我们知道这个父容器是引入springcloud而带来的,如果我们没有引入,那么这个代码就不一定有用了,因为此时的application context也是没有parent的。
  3. 判断是不是已经执行过一遍listener里面的逻辑了。这个主要是防止通过context发送事件之后又传播到父容器再执行一遍(第三次触发)。

ps: 后续我还监听了ApplicationPreparedEvent,发现了执行了五次,里面是org.springframework.cloud.context.restart.RestartListener 在 ContextRefresh阶段发送了一次,应该是为了context 手工refresh的时候再通知一遍监听ApplicationPreparedEvent的listener来保证正确执行