侧边栏壁纸
博主头像
孔子说JAVA博主等级

成功只是一只沦落在鸡窝里的鹰,成功永远属于自信且有毅力的人!

  • 累计撰写 285 篇文章
  • 累计创建 125 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

SpringBoot系统启动时执行任务的多种方式

孔子说JAVA
2022-08-21 / 0 评论 / 0 点赞 / 47 阅读 / 7,218 字 / 正在检测是否收录...

在日常开发中,有一些特殊的任务需要在系统启动时执行,例如数据库初始化、配置文件加载、数据字典等操作。在传统的spring项目中,我们一般使用Listener或@PostConstruct注解来解决。除此之外,Spring Boot提供了两种解决方案:CommandLineRunner 和 ApplicationRunner。CommandLineRunner和ApplicationRunner基本一致,差别主要体现在参数和执行顺序上。

1、ApplicationRunner 和 CommandLineRunner 的区别

SpringBoot 项目通过 main 方法中的 SpringApplication.run 方法启动,其实在 SpringApplication.run 方法执行之前还可以执行一些启动任务。具体的方式是实现 ApplicationRunner 或者 CommandLineRunner 这两个接口。这两个接口的区别如下:

  1. ApplicationRunner 和 CommandLineRunner 接口都只有一个 run 方法。这两个 run 方法的参数不同,其中 ApplicationRunner 接口 run 方法的参数是 ApplicationArguments 类型的,CommandLineRunner 接口 run 方法的参数是 String 数组类型的。
  2. 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启动方式

image-1660904998925

测试结果

image-1660905009218

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了。

0

评论区