13.SpringShell自定义Runner-后台运行单条命令

SpringShell 应用启动时, 会自动创建两个ApplicationRunner组件: ScriptShellApplicationRunner 和 InteractiveShellApplicationRunner, 其中ScriptShellApplicationRunner 用来支持启动直接运行脚本方式, InteractiveShellApplicationRunner 用来支持交互式启动方式. 当我们需要新增一种运行方式时, 那么可以通过自定义ApplicationRunner 实现. 深入了解ScriptShellApplicationRunner 和 InteractiveShellApplicationRunner的逻辑, 可阅读笔者的下一篇博客.

1. SpringShell应用默认运行方式

1.1 交互式运行

$ java -jar ./target/spring-shell-0.0.1-SNAPSHOT.jar
# 省略启动信息...

shell:>add 2 3
5
shell:>exit

1.2 脚本运行

$ java -jar ./target/spring-shell-0.0.1-SNAPSHOT.jar
# 省略启动信息...

5
2
4

1.3 笔者期望新增运行方式

$ java -jar ./target/spring-shell-0.0.1-SNAPSHOT.jar
# 省略启动信息...

3
7

2. 自定义ApplicationRunner

  1. 自定义Input, 实现Input接口, InputProvider.readInput()方法需要返回一个Input类型的值
  2. 自定义InputProvider, 实现InputProvider接口, Shell.run()方法会调用InputProvider.readInput()获取执行名
  3. 自定义ApplicationRunner, 实现ApplicationRunner接口, 因为容器初始化完成之后会执行所有ApplicationRunner 的run方法
  4. 设定ApplicationRunner的优先级, 笔者设定位于默认两个ApplicationRunner 直接, 这个很关键, 直接影响核心代码的编写.

2.1 自定义Input

SpringShell 源码提供了ParsedLineInput, 但是类权限为包级别, 笔者访问不到. 因此复制源码自成一类.

/**
 * @Description: 自定义行解析, 复制的底层代码: org.springframework.shell.jline.ParsedLineInput
 * @author: zongf
 * @date: 2019-01-28 11:18
 */
class MyParsedLineInput implements Input {

    private final ParsedLine parsedLine;

    MyParsedLineInput(ParsedLine parsedLine) {
        this.parsedLine = parsedLine;
    }

    @Override
    public String rawText() {
        return parsedLine.line();
    }

    @Override
    public List<String> words() {
        return sanitizeInput(parsedLine.words());
    }

    static List<String> sanitizeInput(List<String> words) {
        words = words.stream()
                .map(s -> s.replaceAll("^\\n+|\\n+$", ""))
                .map(s -> s.replaceAll("\\n+", " "))
                .collect(Collectors.toList());
        return words;
    }
}

2.2 自定义InputProvider

  • Shell.run 方法会循环调用InputProvider的readInput()方法, 当readInput()方法返回为null时, 终止循环
  • 笔者使用Queue结构来存储启动参数中所有的命令, 执行一条命令移除一条命令, 命令执行完之后返回null.
/**
 * @Description: 读取命令
 * @author: zongf
 * @date: 2019-01-28 11:19
 */
class MyInputProvider implements InputProvider {

    // 存储要执行的所有命令
    private Queue<String> commands;

    private final Parser parser;

    public MyInputProvider(Parser parser, Queue<String> commands) {
        this.parser = parser;
        this.commands = commands;
    }

    @Override
    public Input readInput() {
        String command = commands.poll();
        if (command == null) {
            // return null 时退出应用
            return null;
        }else {
            ParsedLine parsedLine = parser.parse(command, command.length());
            return new MyParsedLineInput(parsedLine);
        }
    }
}

2.3 自定义ApplicationRuuner

  • 自定义实现ApplicationRunner 接口的实现类
  • 将自定义ApplicationRunner 注册为Spring的一个组件, 即使用@Component修饰
  • 设置自定义Runner运行顺序, 保证运行顺序为: ScriptShellApplicationRunner > 自定义Runner > InteractiveShellApplicationRunner, 使用@Order 限制. 只有这样能让脚本参数和命令参数共存.
  • 延迟注入内置属性: Parser, Shell, enviroment
/**
 * @Description: 自定义命令Runner, 启动应用后直接执行多条命令, 执行后结束
 * @author: zongf
 * @date: 2019-01-28 09:58
 */
@Component
@Order(InteractiveShellApplicationRunner.PRECEDENCE - 50)  // order 越小, 越先执行
public class MyCommandsRunner implements ApplicationRunner {

    @Lazy
    @Autowired
    private Parser parser;

    @Lazy
    @Autowired
    private Shell shell;

    @Lazy
    @Autowired
    private ConfigurableEnvironment environment;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        //过滤掉所有@开头的参数, @开头的参数交给ScriptShellApplicationRunner 处理
        List<String> cmds = args.getNonOptionArgs().stream()
                .filter(s -> !s.startsWith("@"))
                .collect(Collectors.toList());

        //如果启动参数中, 命令不为空, 则执行
        if (cmds != null && cmds.size() > 0) {
            // 关闭交互式应用: 确保关闭应用后, 直接退出应用, 不停留在交互式窗口中
            InteractiveShellApplicationRunner.disable(environment);

            // 将所有命令存储到队列中
            Queue<String> queue = new PriorityQueue<>();
            queue.addAll(cmds);

            // 执行命令
            shell.run(new MyInputProvider(parser, queue));
        }
    }
}

3. 测试

新增自定义ApplicationRunner 之后, SpringShell应用便有了四种运行方式

3.1 测试直接运行命令

$ java -jar ./target/spring-shell-0.0.1-SNAPSHOT.jar "add 2 3"
Welcom to :
    __  _____  _______ __  __________    __
   /  |/  /\ \/ / ___// / / / ____/ /   / /
  / /|_/ /  \  /\__ \/ /_/ / __/ / /   / /
 / /  / /   / /___/ / __  / /___/ /___/ /___
/_/  /_/   /_//____/_/ /_/_____/_____/_____/
Version: 0.0.1-SNAPSHOT
Author: zongf
Date: 2019-01-26

# 应用启动之后, 直接执行了add 命令, 命令执行之后, 退出应用程序
5

3.2 测试直接运行脚本

$ java -jar ./target/spring-shell-0.0.1-SNAPSHOT.jar @/tmp/zongf/script
Welcom to :
    __  _____  _______ __  __________    __
   /  |/  /\ \/ / ___// / / / ____/ /   / /
  / /|_/ /  \  /\__ \/ /_/ / __/ / /   / /
 / /  / /   / /___/ / __  / /___/ /___/ /___
/_/  /_/   /_//____/_/ /_/_____/_____/_____/
Version: 0.0.1-SNAPSHOT
Author: zongf
Date: 2019-01-26

# 应用启动之后, 直接运行脚本中定义的命令
5
2
4

3.3 测试混合运行

需要注意的是, 会先执行所有的脚本,从才会执行所有命令. 因为ScriptShellApplicationRunner 的优先级在自定义Runner优先级之前.

$ java -jar ./target/spring-shell-0.0.1-SNAPSHOT.jar @/tmp/zongf/script "add 20 30" @/tmp/zongf/script
Welcom to :
    __  _____  _______ __  __________    __
   /  |/  /\ \/ / ___// / / / ____/ /   / /
  / /|_/ /  \  /\__ \/ /_/ / __/ / /   / /
 / /  / /   / /___/ / __  / /___/ /___/ /___
/_/  /_/   /_//____/_/ /_/_____/_____/_____/
Version: 0.0.1-SNAPSHOT
Author: zongf
Date: 2019-01-26

# 执行第一个脚本
5
2
4
# 执行第二个脚本
5
2
4
# 执行自定义命令
50

3.4 测试交互式运行

zongf@zongf-E570 spring-shell $ java -jar ./target/spring-shell-0.0.1-SNAPSHOT.jar
Welcom to :
    __  _____  _______ __  __________    __
   /  |/  /\ \/ / ___// / / / ____/ /   / /
  / /|_/ /  \  /\__ \/ /_/ / __/ / /   / /
 / /  / /   / /___/ / __  / /___/ /___/ /___
/_/  /_/   /_//____/_/ /_/_____/_____/_____/

Version: 0.0.1-SNAPSHOT
Author: zongf
Date: 2019-01-26

# 应用启动之后, 处于交互式窗口之中
shell:>add 2 3
5
shell:>

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容

  • 一、Python简介和环境搭建以及pip的安装 4课时实验课主要内容 【Python简介】: Python 是一个...
    _小老虎_阅读 5,713评论 0 10
  • 关于Mongodb的全面总结 MongoDB的内部构造《MongoDB The Definitive Guide》...
    中v中阅读 31,893评论 2 89
  • Robot Framework官方教程(一)入门Robot Framework官方教程(二)测试数据语法Robot...
    程序员文集阅读 269,487评论 0 84
  • 腾讯新闻,1月28日新闻的标题是:除夕夜 全球的寺庙都被挤爆了 咱们中国人有13亿人口,又是出了名爱扎堆凑热闹,...
    昕城阅读 151评论 0 0
  • 髌骨疼痛 髌骨股骨疼痛症候群初期在行走、上下楼梯、蹲下站起时膝盖会出现疼痛的现象,严重时会有行走间突然膝盖无法出力...
    琢磨啥呢阅读 183评论 0 0