图源:简书 (jianshu.com)
从前文可以知道,作为 Ioc 容器的 ApplicationContext,需要进行一系列步骤来初始化以最终就绪(对于 Web 应用来说就是可以提供Http服务)。
这些步骤大概可以分为以下内容:
Environment
。ApplicationContextInitializers
被调用)。Spring 用一系列事件来表示这些行为,并且在框架内通过发布和监听相应的事件来完成各种任务。
这些事件可以用下图表示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OPYhbrEC-1686566310630)(D:\image\typora\Application-events.png)]
因此,如果我们要在 Spring Application 启动时候的特定阶段执行某段代码,可以通过监听相应事件的方式来完成。
绘图时少了一个事件,如果最后 Application 启动失败,会产生一个
ApplicationFailedEvent
事件。
最简单的方式是创建一个实现了ApplicationListener
接口的 Spring Bean:
@Log4j2
@Configuration
public class WebConfig {
@Bean
public ApplicationListener<ContextRefreshedEvent> contextRefreshedEventApplicationListener(){
return event -> log.debug("ContextRefreshedEvent is called.");
}
@Bean
public ApplicationListener<WebServerInitializedEvent> webServerInitializedEventApplicationListener(){
return event -> log.debug("WebServerInitializedEvent is called.");
}
@Bean
public ApplicationListener<ApplicationStartedEvent> applicationStartedEventApplicationListener(){
return event -> log.debug("ApplicationStartedEvent is called.");
}
@Bean
public ApplicationListener<AvailabilityChangeEvent> applicationAliveListener(){
return event -> {
AvailabilityState state = event.getState();
if (state == LivenessState.CORRECT){
log.debug("AvailabilityChangeEvent is called, and now Application is lived.");
}
};
}
@Bean
public ApplicationListener<ApplicationReadyEvent> applicationReadyEventApplicationListener(){
return event -> log.debug("ApplicationReadyEvent is called.");
}
@Bean
public ApplicationListener<AvailabilityChangeEvent> applicationAllReadyListener(){
return event -> {
AvailabilityState state = event.getState();
if (state == ReadinessState.ACCEPTING_TRAFFIC){
log.debug("AvailabilityChangeEvent is called, and now application is all ready.");
}
};
}
}
执行上面这个示例就能看到事件依次被调用的过程。
需要注意的是,这种方式只对部分事件有用,对于某些“早期”事件,比如ApplicationStartingEvent
,事件发生的时候 ApplicationContext 还没有创建和初始化,更别提加载 bean 定义了,因此即使你定义了相应事件的监听器 bean,相应的代码也不可能会被执行。
如果我们要监听这些早期事件,可以:
@SpringBootApplication
@Log4j2
public class IniApplication {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(IniApplication.class);
application.addListeners(applicationStartedEventApplicationListener(),
applicationEnvironmentPreparedEventApplicationListener(),
applicationContextInitializedEventApplicationListener(),
applicationPreparedEventApplicationListener());
application.run(args);
}
private static ApplicationListener<ApplicationStartingEvent> applicationStartedEventApplicationListener(){
return event -> System.out.println("ApplicationStartingEvent is called.");
}
private static ApplicationListener<ApplicationEnvironmentPreparedEvent> applicationEnvironmentPreparedEventApplicationListener(){
return event -> log.debug("ApplicationEnvironmentPreparedEvent is called.");
}
private static ApplicationListener<ApplicationContextInitializedEvent> applicationContextInitializedEventApplicationListener(){
return event -> log.debug("ApplicationContextInitializedEvent is called.");
}
private static ApplicationListener<ApplicationPreparedEvent> applicationPreparedEventApplicationListener(){
return event -> log.debug("ApplicationPreparedEvent is called.");
}
}
示例中的
ApplicationStartingEvent
事件监听器没有使用log.debug
输出日志,因为实际测试发现这样做不会产生任何输出(也没有报错),只能猜测是此阶段日志模块还没有被正常加载。如果有谁了解更多,可以留言说明,谢谢。
除了通过SpringApplication.addListeners()
添加监听器,还可以通过SpringApplicationBuilder.listeners()
添加:
@SpringBootApplication
@Log4j2
public class IniApplication {
public static void main(String[] args) {
new SpringApplicationBuilder()
.sources(IniApplication.class)
.listeners(applicationEnvironmentPreparedEventApplicationListener(),
applicationStartedEventApplicationListener(),
applicationContextInitializedEventApplicationListener(),
applicationPreparedEventApplicationListener())
.run(args);
}
// ...
}
所有的事件监听器都是在主线程上依次执行的,因此很容易为它们指定一个顺序,以控制监听同一事件的监听器的先后执行顺序。
比如:
@Log4j2
@Configuration
public class WebConfig {
@Order(1)
@Bean
public ApplicationListener<ApplicationReadyEvent> applicationReadyEventApplicationListener1(){
return event -> log.debug("ApplicationReadyEvent1 is called.");
}
@Order(2)
@Bean
public ApplicationListener<ApplicationReadyEvent> applicationReadyEventApplicationListener2(){
return event -> log.debug("ApplicationReadyEvent2 is called.");
}
}
两个都是监听ApplicationReadyEvent
事件的监听器,其中用@Order(1)
标记的监听器会先于@Order(2)
标记的监听器被执行。
Application 事件是通过 Spring 框架的事件发布机制发布的,该机制确保了如果 ApplicationContext 是一个多层级的,那么一个子级的 ApplicationContext 产生的事件同样会发布给其父容器。在这种结构下,我们可能需要在监听器中区分事件是由子容器还是当前容器产生的,这点可以通过对比事件关联的 ApplicationContext 以及当前的 ApplicationContext 来区分:
@Component
@Log4j2
public class MyApplicationStartedEventListener implements ApplicationListener<ApplicationStartedEvent> {
private final ApplicationContext applicationContext;
public MyApplicationStartedEventListener(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Override
public void onApplicationEvent(ApplicationStartedEvent event) {
ApplicationContext eventCtx = event.getApplicationContext();
if (eventCtx == applicationContext){
//当前 ApplicationContext 发送的事件
log.debug("Current ctx's ApplicationStartedEvent is called.");
}
else{
log.debug("Sub ctx's ApplicationStartedEvent is called.");
}
}
}
在这个示例中,可以简单地通过依赖注入在 bean 中获取当前的ApplicationContext
,如果无法通过这种方式获取(比如 context),可以通过实现ApplicationContextAware
来注入。
更多关于 Aware 接口的说明见从零开始 Spring Boot 27:IoC - 红茶的个人站点 (icexmoon.cn)。
就像前面展示的,如果要在 Spring Application 初始化的某个阶段执行代码,我们只需要使用相应事件的监听器即可。但 Spring 官方并不推荐在事件监听器中运行潜在的耗时任务,因为这些监听器实际上都是在主线程上依次执行的,如果其中某个监听器的执行比较耗时,就会拖累整个 Spring Application 启动。
因此,Spring 官方推荐使用ApplicationRunner
或CommandLineRuner
接口完成某些需要在 Spring Application应用初始化后,但还未真正工作(对于Web应用来说就是执行Http响应)前需要完成的任务。
比如:
@Log4j2
@Configuration
public class WebConfig {
// ...
@Bean
public ApplicationRunner applicationRunner() {
return args -> log.debug("ApplicationRunner is called, args:%s".formatted(args));
}
@Bean
public CommandLineRunner commandLineRunner() {
return args -> log.debug("commandLineRunner is called, args:%s".formatted(Arrays.toString(args)));
}
}
观察输出:
... AvailabilityChangeEvent is called, and now Application is lived.
... ApplicationRunner is called,args:
... commandLineRunner is called, args:[]
... ApplicationReadyEvent1 is called.
... ApplicationReadyEvent2 is called.
可以看到,就像上面的 Application 事件流程图中表述的,ApplicationRunner
和CommandLineRuner
都是在Application 处于活动状态后,以及ApplicationReadyEvent
事件发生前被调用的。
ApplicationRunner
和CommandLineRuner
没有本质上的区别,唯一的区别是它们接收的参数类型不同:
@FunctionalInterface
public interface ApplicationRunner {
void run(ApplicationArguments args) throws Exception;
}
@FunctionalInterface
public interface CommandLineRunner {
void run(String... args) throws Exception;
}
查看源码就可以发现,实际上String... args
就是ApplicationArguments.getSourceArgs()
:
public class SpringApplication {
// ...
private void callRunner(CommandLineRunner runner, ApplicationArguments args) {
try {
runner.run(args.getSourceArgs());
} catch (Exception var4) {
throw new IllegalStateException("Failed to execute CommandLineRunner", var4);
}
}
// ...
}
而ApplicationArguments
实际上是通过封装 Java 的命令行参数获得的:
public class SpringApplication {
public ConfigurableApplicationContext run(String... args) {
// ...
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// ...
}
// ...
}
DefaultApplicationArguments
的主要源码:
public class DefaultApplicationArguments implements ApplicationArguments {
private final DefaultApplicationArguments.Source source;
private final String[] args;
public DefaultApplicationArguments(String... args) {
Assert.notNull(args, "Args must not be null");
this.source = new DefaultApplicationArguments.Source(args);
this.args = args;
}
public String[] getSourceArgs() {
return this.args;
}
// ...
}
因此,使用封装后的ApplicationArguments
作为命令行参数的ApplicationRuner
相比CommandLineRuner
在处理命令行参数时更灵活,比如:
@Bean
public ApplicationRunner applicationRunner() {
return args -> {
String s = Arrays.toString(args.getSourceArgs());
log.debug("ApplicationRunner is called, source args:%s".formatted(s));
log.debug("ApplicationRunner is called, non option args:%s".formatted(args.getNonOptionArgs()));
log.debug("ApplicationRunner is called, option names:%s".formatted(args.getOptionNames()));
log.debug("ApplicationRunner is called, option values:%s".formatted(args.getOptionValues("spring.profiles.active")));
};
}
添加命令行参数并运行:
./java -jar D:\workspace\learn_spring_boot\ch37\ini-application\target\ini-application-0.0.1-SNAPSHOT.jar --spring.profiles.activ
可以看到类似下面的输出:
... source args:[--spring.profiles.active=test]
... non option args:[]
... option names:[spring.profiles.active]
... option values:[test]
可以看到,ApplicationArguments
有以下用于处理命令行参数的方法:
getSourceArgs
,用于获取原始的命令行参数(带--
前缀)getOptionNames
,用于获取命令行参数的key(不带--
前缀)getOptionValues
,用于获取指定参数的值(可能有多个)getNonOptionArgs
,获取非选项参数(不带key的)虽然 Spring 官方建议使用ApplicationRuner
或CommandLineRuner
执行比较耗时的任务,但实际上查看源码就会发现,相应的代码依然是在主线程上执行,并没有采用并发,因此同样会拖慢整个 Application 的创建和初始化。
比如下面的示例:
@Log4j2
@Component
public class MyCommandLineRuner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
log.debug("MyCommandLineRuner is begin.");
new LongTimeTask().run();
log.debug("MyCommandLineRuner is end.");
}
private static class LongTimeTask implements Runnable {
private Random random = new Random();
@Override
public void run() {
int delay = random.nextInt(10) + 1;
try {
Thread.sleep((20 + delay) * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
这里的内嵌类LongTimeTask
代表一个比较耗时的任务,具体用Thread.sleep
进行模拟,在执行时会阻塞当前进程20~30秒。
最初的示例我们在主线程直接执行这个任务(new LongTimeTask().run()
),这样做将导致应用启动后输出会卡在MyCommandLineRuner is begin.
,之后很长时间才能看到后续的日志打印和输出。
如果有需要的话,当然可以用单独的线程来执行这个耗时任务,比如:
@Log4j2
@Component
public class MyCommandLineRuner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
log.debug("MyCommandLineRuner is begin.");
new Thread(new LongTimeTask()).start();
log.debug("MyCommandLineRuner is end.");
}
// ...
}
现在这个耗时任务并不会影响到主线程的执行,所有的事件日志都很多全部输出。
一切都看起来很美妙,但真的如此吗?
让我们回顾一下 Spring 的事件设置,事件及其监听器的调用是有着先后顺序的意义的,比如ContextRefreshedEvent
会在ApplicationPreparedEvent
之后以及ApplicationStartedEvent
之前发生。
换言之,一个ContextRefreshedEvent
监听器也应当在ApplicationStartedEvent
事件发生前完成调用。
如果我们使用了多线程,就可能无法确保这一点。比如在上面这个示例中,显然后续的ApplicationReadyEvent
和AvailabilityChangeEvent
都已经触发,但CommandLineRunner
触发的子线程依然没有完成调用。
如果应用中的后续监听器或者业务代码依赖于CommandLineRunner
中的任务完成结果,就很可能出现问题。
我们可以通过同步主线程和子线程来解决这个问题:
@Log4j2
@Component
public class MyCommandLineRuner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
log.debug("MyCommandLineRuner is begin.");
Thread thread = new Thread(new LongTimeTask());
thread.start();
thread.join();
log.debug("MyCommandLineRuner is end.");
}
// ...
}
当然,这里实质上在效率方面已经“退化”成了单线程,这种情况下使用多线程是得不偿失的,反而可能造成性能浪费。但如果有多个耗时任务可以并行执行,此时就显得很有意义:
@Log4j2
@Component
public class MyCommandLineRuner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
log.debug("MyCommandLineRuner is begin.");
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new LongTimeTask());
thread.start();
threads.add(thread);
}
for (Thread t : threads) {
t.join();
}
log.debug("MyCommandLineRuner is end.");
}
// ...
}
当然,这里只是一个最简单的演示,实际使用中使用线程池和Future
会更好。
关于 Java 并发的更多内容,可以阅读我的系列文章:
- Java学习笔记21:并发(1) - 红茶的个人站点 (icexmoon.cn)
- Java学习笔记22:并发(2) - 红茶的个人站点 (icexmoon.cn)
- Java编程笔记23:并发(3) - 红茶的个人站点 (icexmoon.cn)
- Java编程笔记24:并发(4) - 红茶的个人站点 (icexmoon.cn)
总结一下,在 Application 事件监听或者 CommandLineRunner、ApplicationRunner 中使用多线程需要额外注意,要明确这里的子线程处理结果会不会影响到后续事件监听或者程序运行,如果是,要么放弃使用多线程,要么进行线程同步。
从这个角度考虑,或许 Spring 框架在这里没有使用多线程调用是有意为之。
The End,谢谢阅读。
本文的所有示例代码可以从这里获取。