我们有这样的需求,当spring初始化完成后,我们要做些业务上的初始化,比如说开任务等。
一、通过查看spring启动代码,我们发现了spring会通过Listener
的方式,在容器初始化完成后会发送一个Event,实现了Listener
就可以在spring初始化完成后拿到spring上下文,进行我们的业务操作。
二、实操
我们采用的方式就是实现ApplicationListener接口,泛型使用ContextRefreshedEvent,即在applicationContext完成refresh的时候接收事件。
package com;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.jdbc.core.JdbcTemplate;
/**
* 应用启动成功后开始启动消费者线程
* @author jxk
*/
@Configuration
public class ApplicationStartup implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
JdbcTemplate template;
private static final Logger LOGGER =
LoggerFactory.getLogger(ApplicationStartup.class);
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
//获取spring上下文
ApplicationContext context = contextRefreshedEvent.getApplicationContext();
LOGGER.info("开始做一些事情");
JdbcTemplate template1 = context.getBean(JdbcTemplate.class);
System.out.println(template1.toString());
System.out.println(template.toString());
}
}
以下开始稍微的多思考一下。
1.spring究竟是在什么时候发出的event事件。
2.这里的listenter会不会重复触发
3.如果我有多个listener怎么办,是不是有什么方式可以指定多个listener的顺序
下面我来记录我的查找历程。
1. 源码使用springboot 2.2.0版本,从 SpringApplication.run(Config.class,args) 开始。查询调用栈整理如下:
SpringApplication.java
//开始run
(new SpringApplication(primarySources)).run(args);
//方法里关键代码,准备环境,准备工厂,准备context,->刷新context
this.refreshContext(context);
//去刷新
this.refresh(context);
//去刷新
((AbstractApplicationContext)applicationContext).refresh();
AbstractApplicationContext.java
//方法里面关键代码,bean后置工厂处理器,后置处理器,onRefresh,注册listenter,->完成刷新
this.finishRefresh();
//发布event
this.publishEvent((ApplicationEvent)(new ContextRefreshedEvent(this)));
在publishEvent()方法中我们也可以看到spring还会向上找parent的,如果有parent,它会发送parent事件
protected void publishEvent(Object event, @Nullable ResolvableType eventType) {
Assert.notNull(event, "Event must not be null");
Object applicationEvent;
if (event instanceof ApplicationEvent) {
applicationEvent = (ApplicationEvent)event;
} else {
applicationEvent = new PayloadApplicationEvent(this, event);
if (eventType == null) {
eventType = ((PayloadApplicationEvent)applicationEvent).getResolvableType();
}
}
if (this.earlyApplicationEvents != null) {
this.earlyApplicationEvents.add(applicationEvent);
} else {
this.getApplicationEventMulticaster().multicastEvent((ApplicationEvent)applicationEvent, eventType);
}
if (this.parent != null) {
if (this.parent instanceof AbstractApplicationContext) {
((AbstractApplicationContext)this.parent).publishEvent(event, eventType);
} else {
this.parent.publishEvent(event);
}
}
}
2.如上我们可以知道,重复触发理论上是可行的,只要我加载多个applicationContext就可以完成重复触发,或者说,refresh(context) 就会触发。做一个简单的示例,再加载完成context后,我手动在加载一次,看下触发情况,改动只需要一个main方法。
@EnableAutoConfiguration
@SpringBootApplication(scanBasePackages = {"com"})
public class Main {
private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationStartup.class);
public static void main(String[] args) {
LOGGER.info("let's begin");
//spring初始化
ApplicationContext context =SpringApplication.run( Config.class, args);
//手动再来一次
ApplicationContext contexts = new AnnotationConfigApplicationContext();
//注册一下
((AnnotationConfigApplicationContext) contexts).register(Config.class);
//refresh一次
((AnnotationConfigApplicationContext) contexts).refresh();
LOGGER.info( "完成");
}
}
我们来看看打印情况
ok,事实验证了,那么问题来了,我只想初始化一次,多少次加载,我都只想加载一次。如何做到呢。
方案一,使用类似乐观锁的方案,锁上一次就再不触发,改造的是自定义ApplicationListener接口
@Configuration
public class ApplicationStartup implements ApplicationListener<ContextRefreshedEvent> {
private volatile AtomicBoolean isInit=new AtomicBoolean(false);
private static final Logger LOGGER =
LoggerFactory.getLogger(ApplicationStartup.class);
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
//防止重复触发,使用原子操作,将尝试将isInit的值 从false->true
if(!isInit.compareAndSet(false,true)) {
return;
}
//获取spring上下文
ApplicationContext context = contextRefreshedEvent.getApplicationContext();
LOGGER.info("开始做一些事情");
//代码完善处,如果出现异常,要将isInit 改为false,否则都触发不了了。
}
}
这个方案很简单,也是存在问题的,比如我要做的事情是有关于第二次初始化涉及到的东西,这里在第一次执行完就不会执行,达不到我们的目标。防止重复触发之前context instanceof AnnotationConfigApplicationContext,确定是自己想要的context,然后在操作更好。
3.既然是监听,一定有顺序的,如果想要实现顺序处理,有处理顺序的要求,就要使用SmartApplicationListener 类了。
@Configuration
public class ApplicationStartup implements SmartApplicationListener {
private volatile AtomicBoolean isInit=new AtomicBoolean(false);
private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationStartup.class);
//我只想要ContextRefreshedEvent,其他event不要
@Override
public boolean supportsEventType(Class<? extends ApplicationEvent> aClass) {
return aClass==ContextRefreshedEvent.class;
}
//值越小,就先触发
@Override
public int getOrder() {
return 0;
}
//准备接受所有来源的的event
@Override
public boolean supportsSourceType(Class<?> sourceType) {
return true;
}
@Override
public void onApplicationEvent(ApplicationEvent applicationEvent) {
ApplicationContext context = null;
ContextRefreshedEvent event = (ContextRefreshedEvent)applicationEvent;
context = event.getApplicationContext();
//不是我的context不要
if (context==null||context instanceof AnnotationConfigApplicationContext){
return;
}
//防止重复触发,使用原子操作,将尝试将isInit的值 从false->true
if(!isInit.compareAndSet(false,true)) {
return;
}
LOGGER.info("开始做一些事情");
//代码完善处,如果出现异常,要将isInit 改为false,否则都触发不了了。
}
}
题外,其他类型的event事件,以及自定义event事件,使用后整理发上来。