用Spring Boot开发命令行执行程序

通常使用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.CommandLineRunnerorg.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());

你可能感兴趣的:(用Spring Boot开发命令行执行程序)