通常使用Spring Boot,我们都是开发服务程序,是一种启动后就不停止的。如果想要用Spring Boot开发一次性执行的程序,该怎么设计呢?
基于Spring Boot,而不是纯粹的jdk开发一次性执行程序,有什么区别呢?那就是可以利用Springframework的特性都可以被使用。依赖反转、spring expression、日志、测试等都可以快速搭建起来。这不就是Spring Boot的宗旨吗?
1. 基于ApplicationRunner/CommandLineRunner
1.1 创建最基本的Spring Boot项目
首先从Spring initializr 上创建一个最简单的Spring Boot项目,不需要添加任何依赖,就会生成类似下方的项目文件。
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.3.3.RELEASE
com.example
demo-cli-app
0.0.1-SNAPSHOT
demo-cli-app
Demo project for Spring Boot CommandLineRunner and ApplicationRunner
1.8
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
org.springframework.boot
spring-boot-maven-plugin
1.2 ApplicationRunner/CommandLineRunner
我们可以根据需要实现ApplicationRunner或CommandLineRunner接口,那么启动Spring Boot应用时就会执行我们的逻辑,并在全部执行结束后退出。
package com.example.democliapp.runner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* @author jackeylv
* @date 2020-08-14 10:13
*/
@Order(value = 21)
@Component
public class AppRunnerOne implements ApplicationRunner {
private static final Logger logger = LoggerFactory.getLogger(AppRunnerOne.class);
@Override
public void run(ApplicationArguments args) throws Exception {
// 命令行参数是 --p1=2 --p2=3 --p3 4 --p1=34
// getSourceArgs的结果,与CommandLineRunner一样
// Running with --p1=2|--p2=3|--p3|4|--p1=34
logger.info("Running with {}", String.join("|", args.getSourceArgs()));
// p1 = [2, 34]
logger.info("p1 = {}", args.getOptionValues("p1"));
// non-exist = null
logger.info("non-exist = {}", args.getOptionValues("non-exist"));
// names [p1, p2, p3]
logger.info("names {}", args.getOptionNames());
// NonOptionArgs [4]
logger.info("NonOptionArgs {}", args.getNonOptionArgs());
}
}
package com.example.democliapp.runner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* CommandLineRunner会在SpringBootApplication启动以后执行。
*
* @author jackeylv
* @date 2020-08-14 10:05
*/
@Order(value = 1)
@Component
public class CommandOne implements CommandLineRunner {
private static final Logger logger = LoggerFactory.getLogger(CommandOne.class);
@Override
public void run(String... args) throws Exception {
logger.info("Running with {}", String.join("|", args));
}
}
1.3 这些Runner 是怎么执行的?
package com.example.democliapp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author jackeylv
*/
@SpringBootApplication
public class DemoCliAppApplication {
private static final Logger logger = LoggerFactory.getLogger(DemoCliAppApplication.class);
public static void main(String[] args) {
//Step 0. [main thread]这部分代码是最早开始执行的,会早于Spring-boot本身
String strArgs = String.join("|", args);
logger.info("starting the application with args: {}", strArgs);
// Step 1. [main thread]启动SpringApplication
// Step 2. [main thread] Step 1中启动时扫描发现的CommandLineRunner ApplicationRunner都会被执行。
SpringApplication.run(DemoCliAppApplication.class, args);
// Step 3. [main thread]执行到此。
logger.info("Application is started, any others?");
}
}
把程序跑起来我们就发现:
- SpringBoot程序和各种Runner都是在一个线程里面运行,这里都是main thread。
- 各种Runner都是在Spring Boot启动以后执行。
- 以上两部分都是通过
SpringApplication.run(DemoCliAppApplication.class, args);
这一句完成
2. 其他
2.1 Runner的Order
我们可以用@Order(value= ?)
来调整不同Runner的执行顺序,可以达到我们的业务诉求。
我们从SpringApplication的run方法进入,可以找到这些Runner是如何被调用执行的。关键就在于
org.springframework.boot.SpringApplication#callRunners
方法,以及该方法内部的org.springframework.core.annotation.AnnotationAwareOrderComparator
排序方法。跟踪到这个类的父类org.springframework.core.OrderComparator
说明中,可以看到具体的排序规则:
Comparator implementation for Ordered objects, sorting by order value ascending, respectively by priority descending.
- PriorityOrdered Objects
PriorityOrdered objects will be sorted with higher priority than plain Ordered objects.- Same Order Objects
Objects that have the same order value will be sorted with arbitrary ordering with respect to other objects with the same order value.- Non-ordered Objects
Any object that does not provide its own order value is implicitly assigned a value of
Ordered.LOWEST_PRECEDENCE, thus ending up at the end of a sorted collection in arbitrary order with > respect to other objects with the same order value.
覆盖了有Order,没Order的各种情况,包括使用org.springframework.core.PriorityOrdered
注解的方式都统一在这个java.util.Comparator
中定义和实现。
2.2 Runner的差异
在实际应用中两种Runner有什么差异呢?仅仅差异在入参。可以详见org.springframework.boot.CommandLineRunner
和org.springframework.boot.ApplicationRunner
的定义。
CommandLineRunner | ApplicationRunner | |
---|---|---|
定义 | void run(String... args) throws Exception; |
void run(ApplicationArguments args) throws Exception; |
建议用ApplicationRunner
,因为不需要自己做命令参数解析,而且功能也完全覆盖CommandLineRunner
。
举例子,看下方的输出即可。
// 命令行参数是 --p1=2 --p2=3 --p3 4 --p1=34
// getSourceArgs的结果,与CommandLineRunner一样
// Running with --p1=2|--p2=3|--p3|4|--p1=34
logger.info("Running with {}", String.join("|", args.getSourceArgs()));
// p1 = [2, 34]
logger.info("p1 = {}", args.getOptionValues("p1"));
// non-exist = null
logger.info("non-exist = {}", args.getOptionValues("non-exist"));
// names [p1, p2, p3]
logger.info("names {}", args.getOptionNames());
// NonOptionArgs [4]
logger.info("NonOptionArgs {}", args.getNonOptionArgs());