详解ApplicationRunner和CommandLineRunner

一、前言

springBoot框架项目,有时候有预加载数据需求------提前加载到缓存中或类的属性中,并且希望执行操作的时间是在容器启动末尾时间执行操作。比如笔者工作中遇到了一个预加载redis中的缓存数据,加载为java对象。针对这种场景,SpringBoot提供了两个接口,分别是CommandLineRunner和ApplicationRunner。两个接口都在spring-boot的jar包中(spring-boot的jar包依附关系:spring-boot<-spring-boot-starter<-spring-boot-starter-web),项目只需要依赖spring-boot-starter-web的jar便可使用。

当程序启动时,我们传给 main() 方法的参数可以被实现 CommandLineRunner 和 ApplicationRunner 接口的类的 run() 方法访问,即可接收启动服务时传过来的参数,自动执行各自接口中的run方法,完成一些初始化动作。我们可以创建多个实现 CommandLineRunner 和 ApplicationRunner 接口的类。为了使他们按一定顺序执行,可以使用 @Order 注解或实现 Ordered 接口。

二、ApplicationRunner和CommandLineRunner的比较

共同点

  1. 自动执行: 只需实现相应的接口并将其作为一个Bean注册到Spring容器中(通常是通过@Component注解),Spring Boot在应用启动后会自动调用这些接口的run方法。

  2. 访问命令行参数: 两个接口的run方法都接收一个ApplicationArguments对象,该对象提供了访问启动时传递给应用的命令行参数的方法。

ApplicationRunner

  1. 更细粒度的参数处理: 相比于CommandLineRunner,ApplicationRunner提供了更详细的参数处理API,如区分非选项参数(nonOptionArgs)和选项参数(getOptionNames、getOptionValues),这使得在处理复杂命令行参数时更加灵活方便。

  2. 用途: 当你需要根据不同的命令行参数执行不同的初始化逻辑时,ApplicationRunner提供了更丰富的功能来解析和处理这些参数。

CommandLineRunner

  1. 简单直接: 如果你的需求只是简单地执行一些初始化逻辑,且不需要对命令行参数做精细处理,CommandLineRunner是一个更简洁的选择。它的run方法直接接受ApplicationArguments,但通常用于较为基本的参数检查或打印帮助信息。

  2. 用途: 对于那些只需要知道是否有参数被传入,或者参数数量,而不需要详细解析每个参数的应用场景,使用CommandLineRunner会更加直接。

选择ApplicationRunner还是CommandLineRunner取决于你的具体需求:如果你需要对命令行参数进行更细致的操作和解析,选择ApplicationRunner。如果你的需求简单,仅需执行一些基础的启动任务,且对命令行参数的处理不复杂,则CommandLineRunner足矣。两者都非常适用于执行应用程序启动初期的初始化或设置任务。下面对这两个接口做详细解析和案例分析。

三、CommandLineRunner接口

源码如下:

java 复制代码
@FunctionalInterface
public interface CommandLineRunner {

	/**
	 * Callback used to run the bean.
	 * @param args incoming main method arguments
	 * @throws Exception on error
	 */
	void run(String... args) throws Exception;

}

从源码看到参数为String数组,即可以传多个参数,以数组形式接收。

自定义一个类实现 CommandLineRunner 接口:

java 复制代码
package com.hulei.runner;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class MyCommandLineRunner implements CommandLineRunner {

    private static final Logger logger = LoggerFactory.getLogger(MyCommandLineRunner.class);

    @Override
    public void run(String... args) {
        String strArgs = String.join("|", args);
        logger.info("Application started with arguments:{}", strArgs);
    }
}

笔者使用的是IDEA工具(IDEA必须要学会使用,如果不会,我认为是极其不专业的java开发),直接右击package打包


现在选择使用命令java -jar的方式启动这个jar包,可以找到项目所在目录,直接cmd进入这个target下执行命令:

java 复制代码
java -jar Add-WaterMark-0.0.1-SNAPSHOT.jar data1 data2 data2

其中data1、data2、data3就是启动时输入的参数,笔者这里直接使用IDEA的teminal控制台启动了,但是要先把Terminal的Shell path设置成cmd.exe路径。

在上面package打包后,打开一个terminal终端,输入以下命令

java 复制代码
cd target
java -jar Add-WaterMark-0.0.1-SNAPSHOT.jar data1 data2 data3

按下Enter回车执行,执行结果如下:

控制台并没有出现 MyCommandLineRunner类中打印参数的信息

java 复制代码
logger.info("MyCommandLineRunner started with arguments:{}", strArgs);

经过排查发现笔者把目录建错了,没有建在启动类 AddWaterMarkApplication 所在的直接根目录watermark下,导致无法扫描到 MyCommandLineRunner 类,因为**@SpringBootApplication**默认只扫描定义了该注解的类所在的包及其所有子包下的组件。

做法一是把runner文件目录转移到watermark下,做法二如下:

在@SpringBootApplication注解中加入基础包扫描,手动写入需要扫描的包路径

此时再关闭terminal终端,重新package打包,重新打开terminal终端执行启动命令

看到MyCommandLineRunner中的run方法成功执行了,打印出我们启动时添加的启动参数

四、ApplicationRunner接口

源码如下

java 复制代码
@FunctionalInterface
public interface ApplicationRunner extends Runner {

	/**
	 * Callback used to run the bean.
	 * @param args incoming application arguments
	 * @throws Exception on error
	 */
	void run(ApplicationArguments args) throws Exception;

}```

自定义一个类MyApplicationRunner实现ApplicationRunner接口

```java
package com.hulei.runner;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;


@Component
public class MyApplicationRunner implements ApplicationRunner {

    private static final Logger logger = LoggerFactory.getLogger(MyApplicationRunner.class);

    @Override
    public void run(ApplicationArguments args) {
        String strArgs = String.join("|", args.getSourceArgs());
        logger.info("MyApplicationRunner started with arguments:{}", strArgs);

    }
}

按照之前的做法执行相同的命令,可以看到MyApplicationRunner 中run方法也成功执行

ApplicationArguments是对参数(main方法)做了进一步的处理,可以解析--name=value的,我们就可以通过name来获取value(而CommandLineRunner只是获取--name=value,获取的是一个整体字符串,并不能解析参数,如果想对参数做精细化处理,ApplicationRunner 更为合适),可以接收--foo=bar这样的参数。

--getOptionNames()方法可以得到foo这样的key的集合。

--getOptionValues(String name)方法可以得到bar这样的集合的value。

比如输入的参数命令如下

java 复制代码
java -jar Add-WaterMark-0.0.1-SNAPSHOT.jar --doo=bar --developer.name=hulei

ApplicationRunner 就可以根据doo和developer.name这两个key获取后面的值bar 和hulei

下面对MyApplicationRunner改造下:

java 复制代码
@Component
public class MyApplicationRunner implements ApplicationRunner {

    private static final Logger logger = LoggerFactory.getLogger(MyApplicationRunner.class);

    @Override
    public void run(ApplicationArguments args) {
        logger.info("===MyApplicationRunner==={}", Arrays.asList(args.getSourceArgs()));
        logger.info("===getOptionNames========{}",args.getOptionNames());
        logger.info("===getOptionValues======={}",args.getOptionValues("doo"));
        logger.info("==getOptionValues========{}",args.getOptionValues("developer.name"));

    }
}

可以看到成功按照键值对的形式解析了我们启动时输入的参数

而CommandLineRunner做不到精细解析,只能打印一个字符串参数

五、Order控制执行顺序

在 spring boot 程序中,我们可以使用不止一个实现 CommandLineRunner 和 ApplicationRunner 的 bean。为了有序执行这些 bean 的 run() 方法,可以使用 @Order 注解或 Ordered 接口。例子中我们创建了两个实现 CommandLineRunner 接口的 bean 和两个实现 ApplicationRunner 接口的 bean。可以使用 @Order 注解按顺序执行这四个 bean。

  1. CommandLineRunnerBean1.java
java 复制代码
@Component
@Order(1)
public class CommandLineRunnerBean1 implements CommandLineRunner {
    @Override
    public void run(String... args) {
        System.out.println("CommandLineRunnerBean 1");
    }
}
  1. ApplicationRunnerBean1.java
java 复制代码
@Component
@Order(2)
public class ApplicationRunnerBean1 implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments arg0) throws Exception {
        System.out.println("ApplicationRunnerBean 1");
    }
}
  1. CommandLineRunnerBean2.java
java 复制代码
@Component
@Order(3)
public class CommandLineRunnerBean2 implements CommandLineRunner {
    @Override
    public void run(String... args) {
        System.out.println("CommandLineRunnerBean 2");
    }
}
  1. ApplicationRunnerBean2.java
java 复制代码
@Component
@Order(4)
public class ApplicationRunnerBean2 implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments arg0) throws Exception {
        System.out.println("ApplicationRunnerBean 2");
    }
}

这几个类上都有注解@Order,并且按照1,2,3,4的顺序规定了启动顺序,启动结果如下:

六、总结

本篇文章详细介绍了ApplicationRunnerCommandLineRunner两个接口的用法和区别,掌握这两个强大的接口可以为我们的日常开发带来很大便利,比如笔者开发中有个获取字典值的操作,从redis中获取,需要转为java对象Map,才能使用。而从redis中获取转为java对象这个过程长达十秒(可能redis是分布式的,对象本身也比较大,再加上带宽限制这个主要原因)我无法忍受,所以笔者在启动服务的时候就从分布式缓存redis中获取到这个大对象,转成java对象,后续就能在应用中直接使用了。

相关推荐
2401_8574396939 分钟前
SpringBoot框架在资产管理中的应用
java·spring boot·后端
怀旧66641 分钟前
spring boot 项目配置https服务
java·spring boot·后端·学习·个人开发·1024程序员节
哎呦没4 小时前
SpringBoot框架下的资产管理自动化
java·spring boot·后端
2401_857600954 小时前
SpringBoot框架的企业资产管理自动化
spring boot·后端·自动化
NiNg_1_2348 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
种树人202408198 小时前
如何在 Spring Boot 中启用定时任务
spring boot
苹果醋311 小时前
Java8->Java19的初步探索
java·运维·spring boot·mysql·nginx
Wx-bishekaifayuan11 小时前
django电商易购系统-计算机设计毕业源码61059
java·spring boot·spring·spring cloud·django·sqlite·guava
customer0811 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
Yaml412 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理