在日常开发中,有一些特殊的任务需要在系统启动时执行,例如数据库初始化、配置文件加载、数据字典等操作。在传统的spring项目中,我们一般使用Listener或@PostConstruct注解来解决。除此之外,Spring Boot提供了两种解决方案:CommandLineRunner 和 ApplicationRunner。CommandLineRunner和ApplicationRunner基本一致,差别主要体现在参数和执行顺序上。
1、ApplicationRunner 和 CommandLineRunner 的区别
SpringBoot 项目通过 main 方法中的 SpringApplication.run 方法启动,其实在 SpringApplication.run 方法执行之前还可以执行一些启动任务。具体的方式是实现 ApplicationRunner 或者 CommandLineRunner 这两个接口。这两个接口的区别如下:
- ApplicationRunner 和 CommandLineRunner 接口都只有一个 run 方法。这两个 run 方法的参数不同,其中 ApplicationRunner 接口 run 方法的参数是 ApplicationArguments 类型的,CommandLineRunner 接口 run 方法的参数是 String 数组类型的。
- ApplicationRunner 接口的 run 方法比 CommandLineRunner 接口的 run 方法要先执行。
2、CommandLineRunner 接口
Spring Boot项目在启动时会遍历所有 CommandLineRunner 的实现类并调用其中的run方法,如果整个系统中有多个 CommandLineRunner 的实现类,可以使用 @Order 注解对这些实现类的调用顺序进行排序。
- @Order(1)注解用来描述CommandLineRunner的执行顺序,数字越小越先执行。
- run方法中是调用的核心逻辑,参数是系统启动时传入的参数,即入口类中main方法的参数(在调用SpringApplication.run方法时被传给CommandLineRunner的run方法)。
@Component
@Order(1)
public class MyCommandLineRunner1 implements CommandLineRunner {
@Override
public void run(String... args) {
System.out.println("Runner1>>>" + Arrays.toString(args));
}
}
3、ApplicationRunner 接口
3.1 使用方式
ApplicationRunner 用法和 CommandLineRunner 基本一致。项目在启动时会遍历所有的 ApplicationRunner 的实现类并调用其中的 run 方法。
如果整个系统中有多个 ApplicationRunner 的实现类,同样可以使用 @Order 注解对这些实现类的调用顺序进行排序(数字越小越先执行)。
- @Order注解依然是用来描述执行顺序的,数字越小越优先执行。
ApplicationRunner 与 CommandLineRunner 的区别主要体现在 run 方法的参数上。不同于 CommandLineRunner 中的 run 方法的 String 数组参数,ApplicationRunner 里 run 方法的参数是一个 ApplicationArguments 对象。
ApplicationArguments 区分选项参数和非选项参数:
- 对于非选项参数:我们可以通过 ApplicationArguments 的 getNonOptionArgs() 方法获取,获取到的是一个数组(这里获取的参数就是入口类中main方法接收的参数)。
- 对于选项参数:可以通过 ApplicationArguments 的 getOptionNames() 方法获取所有选项名称。通过 getOptionValues() 方法获取实际值(它会返回一个列表字符串)。ApplicationArguments中的getOptionNames方法用来获取项目启动命令行中参数的key,如果将本项目打成jar包,运行java -jar xxx.jar –name=xc命令来启动项目,此时getOptionNames方法获取到的就是name,而getOptionValues方法则是获取相应的value。
@Component
@Order(1)
public class MyApplicationRunner1 implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
List<String> nonOptionArgs = args.getNonOptionArgs();
System.out.println("1-nonOptionArgs>>>" + nonOptionArgs);
Set<String> optionNames = args.getOptionNames();
for (String optionName : optionNames) {
System.out.println("1-key:" + optionName + ";value:" + args.getOptionValues(optionName));
}
}
}
3.2 测试
jar命令启动方式
java -jar xc-springboot-0.0.2-SNAPSHOT.jar --name=xc --age=18 a1 a2
- –name=xc --age=18都属于getOptionNames/getOptionValues范畴。
- 后面的 a1 a2 可以通过getNonOptionArgs方法获取,获取到的是一个数组,相当于上文提到的运行时配置的ProgramArguments。
idea启动方式
测试结果
4、CommandLineRunner 和 ApplicationRunner 接口同时使用
4.1 分别实现两个接口
在springboot项目中编写一个类实现 CommandLineRunner 接口。
@Commponent
public class MyRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("我会在项目启动的时候运行");
}
}
在同一个springboot项目中编写一个类实现 ApplicationRunner 接口。
@Component
public class MyRunner2 implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("我也随着项目启动而启动啦");
}
}
启动springboot项目后的执行结果:
2022-06-18 10:30:43.899 INFO 9560 --- [ main] com.lili.TestCsdnApplication : Starting TestCsdnApplication using Java 1.8.0_151 on DESKTOP-GEUFILT with PID 9560 (D:\studySpace\idea_workspace4\testCsdn\target\classes started by YLi_Jing in D:\studySpace\idea_workspace4\testCsdn)
2022-06-18 10:30:43.903 INFO 9560 --- [ main] com.lili.TestCsdnApplication : No active profile set, falling back to 1 default profile: "default"
2022-06-18 10:30:44.638 INFO 9560 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2022-06-18 10:30:44.645 INFO 9560 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2022-06-18 10:30:44.645 INFO 9560 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.63]
2022-06-18 10:30:44.761 INFO 9560 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2022-06-18 10:30:44.761 INFO 9560 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 809 ms
2022-06-18 10:30:45.038 INFO 9560 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2022-06-18 10:30:45.046 INFO 9560 --- [ main] com.lili.TestCsdnApplication : Started TestCsdnApplication in 1.503 seconds (JVM running for 2.374)
我也随着项目启动而启动啦
我会在项目启动的时候运行
可以看到 ApplicationRunner 接口的 run 方法比 CommandLineRunner 接口的 run 方法要先执行。
4.2 同时实现两个接口
在springboot项目中编写一个类同时实现 CommandLineRunner 和 ApplicationRunner 接口。
public class Test1 implements CommandLineRunner, ApplicationRunner {
@Override
public void run(String... args) throws Exception {
log.info("Test1:CommandLineRunner:run " + args[0]);
}
@Override
public void run(ApplicationArguments args) throws Exception {
log.info("Test1:ApplicationRunner:run " + args.getSourceArgs()[0]);
}
}
在同一个springboot项目中编写另一个类同时实现 CommandLineRunner 和 ApplicationRunner 接口。
public class Test2 implements CommandLineRunner, ApplicationRunner {
@Override
public void run(String... args) throws Exception {
log.info("Test2:CommandLineRunner:run " + args[1]);
}
@Override
public void run(ApplicationArguments args) throws Exception {
log.info("Test2:ApplicationRunner:run " + args.getSourceArgs()[1]);
}
}
在同一个springboot项目中的 DemoApplication 上也完成同样的代码。
public class DemoApplication implements CommandLineRunner, ApplicationRunner {
然后我们打包,通过命令行启动该 jar 包。
java -jar demo-0.0.1-SNAPSHOT.jar abc xyz
输出结果如下:
cn.coderup.demo.DemoApplication : DemoApplication:ApplicationRunner:run xyz
cn.coderup.demo.DemoApplication : DemoApplication:CommandLineRunner:run abc
cn.coderup.demo.Test2 : Test2:ApplicationRunner:run xyz
cn.coderup.demo.Test2 : Test2:CommandLineRunner:run xyz
cn.coderup.demo.Test1 : Test1:ApplicationRunner:run abc
cn.coderup.demo.Test1 : Test1:CommandLineRunner:run abc
输出结果的顺序是,DemoApplication -> Test2 -> Test1。
4.3 同时实现两个接口且调整顺序
些时候我们需要按照指定的顺序执行每个类中的 ApplicationRunner 和 CommandLIneRunner 中的 run 方法,这时候可以使用 @Order 注解,或者实现 Ordered 接口。两种方法我们都使用。
给 Test1 的类上增加 @Order 注解,代码如下:
@Order(value=1)
public class Test1 implements CommandLineRunner, ApplicationRunner {
给 DemoApplication 也增加 @Order 注解,代码如下:
@Order(value=3)
public class DemoApplication implements CommandLineRunner, ApplicationRunner {
让 Test2 类实现 Ordered 接口,代码如下:
public class Test2 implements CommandLineRunner, ApplicationRunner, Ordered {
@Override
public int getOrder() {
return 2;
}
...
}
重新打包执行,运行结果如下:
cn.coderup.demo.Test1 : Test1:ApplicationRunner:run abc
cn.coderup.demo.Test1 : Test1:CommandLineRunner:run abc
cn.coderup.demo.Test2 : Test2:ApplicationRunner:run xyz
cn.coderup.demo.Test2 : Test2:CommandLineRunner:run xyz
cn.coderup.demo.DemoApplication : DemoApplication:ApplicationRunner:run xyz
cn.coderup.demo.DemoApplication : DemoApplication:CommandLineRunner:run abc
可以看到执行顺序是按照我们指定的顺序进行了输出,Test1 -> Test2 -> DemoApplication。
5、@PostContruct 方式
@PostContruct是spring框架的注解,在方法上加该注解会在项目启动的时候执行该方法,也可以理解为在spring容器初始化的时候执行该方法。可作为一些数据的常规化加载,比如数据字典之类的。
/**
* 项目启动时,初始化定时器
*/
@PostConstruct
public void init(){
List<Job> jobList = jobDao.selectJobAll();
for (Job job : jobList){
CronTrigger cronTrigger = ScheduleUtils.getCronTrigger(scheduler, job.getJobId());
// 如果不存在,则创建
if (cronTrigger == null) {
ScheduleUtils.createScheduleJob(scheduler, job);
} else {
ScheduleUtils.updateScheduleJob(scheduler, job);
}
}
}
上述代码表示在项目启动时,Spring IOC容器初始化创建之后,Bean初始化之前和销毁之前,执行@PostConstruct注解的方法。一般用于一些项目初始化的设定。比如Spring IOC Container 初始化之后,用@PostConstruct注解Quartz的 CronTrigger 用于初始化定时器(向定时器中添加定时启动的JOB)。那么项目运行时就能自动的运行CronTrigger 中的job了。
评论区