Spring shell 简易使用指南

前言

Java的很多开源项目中使用到了交互式命令行方式操作。交互式命令行有很多种实现方式。其中Spring shell是一种极其简单易用的交互式命令行框架。

本篇为大家带来使用Spring shell编写交互式命令行应用的简易指南。

环境准备

我们以Spring shell 2.1.1版本为例讲解Spring shell的使用方式。

Spring shell项目依赖于Springboot,它们的项目结构式完全相同的。

在Maven的pom.xml文件中加入如下依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.shell</groupId>
        <artifactId>spring-shell-starter</artifactId>
        <version>2.1.1</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <version>${springboot.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>2.7.2</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <version>2.7.2</version>
            <executions>
                <execution>
                    <goals>
                        <goal>repackage</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

注意:Spring shell 2.x.x相对于1.x.x版本大升级之后用法改变较大。本篇不适合1.x.x版本的使用方式。

环境已准备完毕,接下来我们开始使用Spring shell。

命令编写

我们已最简单的echo命令为例,用户在echo后面输入什么就返回什么。

@ShellComponent
public class EchoCommand {
    @ShellMethod("Echo command")
    public String echo(String str) {
        return str;
    }
}

我们运行Springboot主类,可以在命令行中使用刚刚创建出的echo命令。

通常来说如果交互式命令行应用支持的命令很多,这些命令是需要归类的。归为同一类的命令在使用help命令打印帮助信息的时候会被显示在一起。归类的方式是将相关的命令定义在同一个class内。这个class需要使用@ShellComponent修饰。

@ShellComponentclass中,每一个命令参数定义和执行逻辑对应着一个@ShellMethod注解修饰的方法。

@ShellMethod注解的属性:

  • key: 自定义命令。默认为注解修饰的方法名,为数组类型,即同一个方法可以指定多个名称。如果不指定,默认命令和方法名相同。如果方法名是驼峰结构,与之对应的命令是横线分割全小写的命令方式。例如方法名为doSomething,对应的命令为do-something
  • value: 方法的描述,相当于帮助信息,可以使用help命令打印出各个方法的描述。

例如:

@ShellMethod(key = {"echo", "say"}, value = "Echo command")
public String echo(String str) {
    return str;
}

这种写法我们既可以使用echo,又可以使用say命令来调用echo方法。

传递命令参数的方式也可以修改。可以使用@ShellOption注解。它包含如下属性:

  • value: 数组类型,用来指定这个参数的key。例如@ShellOption(value = {"--parallelism", "-p"}) Integer parallelism,在调用的时候传参既可以用--parallelism 10也可以使用-p 10
  • help: 参数的帮助信息。
  • defaultValue: 参数的默认值。如果不指定默认值,则这个参数是必需(mandatory)参数。如果需要设置一个参数为可选(optional),并且不想指定默认值(默认值为null),可以使用defaultValue = ShellOption.NULL

指定命名参数

@ShellMethod(key = {"echo", "say"}, value = "Echo command")
public String echo(@ShellOption(value = {"--content", "-c"}) String str) {
    return str;
}

可以支持使用echo --content something或者echo -c something方式传入参数str。

指定默认值

@ShellMethod(key = {"echo", "say"}, value = "Echo command")
public String echo(@ShellOption(defaultValue = "something") String str) {
    return str;
}

指定了str参数默认值为something

注意:如果不指定默认值,则这个参数是必需(mandatory)参数。如果需要设置一个参数为可选(optional),并且不想指定默认值(默认值为null),可以使用defaultValue = ShellOption.NULL

参数校验

一个例子如下:

@ShellMethod("Password")
public String password(@Size(min = 6, max = 16) String pwd) {
    return pwd;
}

@Size注解可以校验参数字符串长度范围。上面的例子确保字符串长度在6到16之间。

如果参数校验失败,命令行会显示校验提示信息。校验注解自带了默认的提示信息模板,通常可以满足要求。如果需要自定义提示信息,可以添加message属性,例如:

@Size(min = 6, max = 16, message = "Length is illegal")

除了@Size之外,Spring还提供了很多其他的校验逻辑注解。具体可以查看javax.validation.constraints包下面的注解定义。

包含:

  • AssertFalse
  • AssertTrue
  • DecimalMax
  • DecimalMin
  • Digits
  • Email
  • Future
  • FutureOrPresent
  • Max
  • Min
  • Negative
  • NegativeOrZero
  • NotBlank
  • NotEmpty
  • NotNull
  • Past
  • PastOrPresent
  • Pattern
  • Positive
  • PositiveOrZero
  • Size

比如我们要求参数必须为正整数,可以这么写:

@ShellMethod("Positive")
public Integer positive(@Positive Integer integer) {
    return integer;
}

命令可用性

Spring shell交互式命令行可以支持多级命令,也就是说某些命令必须在一定的状态下才可以执行。这要求了我们能够定义命令的可用性。

我们使用一个例子来说明。代码如下:

private boolean isTestEnabled = false;

@ShellMethod("enable test")
public String enableTest() {
    isTestEnabled = true;
    return "Test enabled";
}

@ShellMethod("disable test")
public String disableTest() {
    isTestEnabled = false;
    return "Test disabled";
}

@ShellMethod("test")
public String test() {
    return "Test command";
}

public Availability testAvailability() {
    return isTestEnabled ? Availability.available() : Availability.unavailable("enable-test has not been executed");
}

这个例子中test命令默认是无法使用的。必须先执行enable-test命令启用它之后才能够调用。执行disable-test命令可以再次禁用它。

这里需要注意:判定命令是否可用的方法是testAvailability。该方法的命名规则为方法名 + Availability。方法需要返回Availability类型。如果命令不可用,需要编写一句话解释不可用原因。具体写法请见上面例子。

自定义

自定义Prompt

自定义命令提示符的方法只需要定义一个bean实现PromptProvider接口,例如:

@Component
public class MyPromptProvider implements PromptProvider {
    @Override
    public AttributedString getPrompt() {
        return new AttributedString("Hello -> ", AttributedStyle.BOLD.foreground(AttributedStyle.GREEN));
    }
}

配置上面代码后,启动spring shell应用,命令提示符会变为Hello ->

其中AttributedStyle.BOLD.foreground(AttributedStyle.GREEN)决定了命令提示符的显示风格和颜色。

getPrompt方法在每次需要显示提示符的时候都会被调用,所以说在一些复杂的场景,我们可以实现命令行处于不同状态时显示不同的提示符。

自定义banner

交互式命令应用启动的时候会打印出一些标志或者提示信息,这称之为banner。Spring shell允许我们自定义banner。

方法为保存自定义banner内容到banner.txt文件,放置在项目resources目录下。重启Springboot应用可以观察到banner修改生效。

配置

配置history log

Spring shell支持将命令执行历史记录输出为文件。配置方法如下。

编写或者修改resources/application.yaml,增加:

spring:
  shell:
    history:
      enabled: true
      name: xxx-history.log

单元测试

Spring shell的单元测试类需要添加:

@SpringBootTest(properties = {"spring.shell.interactive.enabled=false", "spring.shell.command.script.enabled=false"})

注解。

具体的编写模板如下所示:

@SpringBootTest(properties = {"spring.shell.interactive.enabled=false", "spring.shell.command.script.enabled=false"})
public class SomeTest {
    @Autowired
    private Shell shell;
    
    @Test
    public void test1() {
        // 在java代码中运行shell method的方法
        // 返回的数据类型为shell method方法返回的数据类型
        // 需要注意的是,需要判断方法返回的数据类型
        Object result = shell.evaluate(() -> "some command");
        
        if (result instanceof Throwable) {
            // 如果result为Throwable类型,说明方法执行过程中发生了异常
        }
    }
}

注意:使用上面的方式调用shell.evaluate存在一个问题,它无法解析命令行某个参数值存在空格的问题,即便参数值使用引号引起来,原因为源码中按照空格split字符串。例如someCommand --sql 'select * from table'。如果需要支持这种写法,需要自己编写Input接口的实现类,覆盖默认的words方法。如下所示:

public interface MockCommandLineInput extends Input {
    @Override
    default List<String> words() {
        if (null == rawText() || rawText().isEmpty()) {
            return Collections.emptyList();
        }
        boolean isInQuote = false;
        List<String> result = new ArrayList<>();
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < rawText().length(); i++) {
            char c = rawText().charAt(i);
            if (' ' == c && !isInQuote) {
                if (stringBuilder.length() != 0) {
                    result.add(stringBuilder.toString());
                    stringBuilder.delete(0, stringBuilder.length());
                }
            } else if ('\'' == c || '"' == c) {
                if (isInQuote) {
                    isInQuote = false;
                    result.add(stringBuilder.toString());
                    stringBuilder.delete(0, stringBuilder.length());
                } else {
                    isInQuote = true;
                }
            } else {
                stringBuilder.append(c);
            }
        }
        return result;
    }
}

上面的MockCommandLineInput方法重写了父接口Input中的words方法,它不会拆分单引号和双引号引起来的字符串。

最终使用shell.evaluate的方法如下所示:

Object result = shell.evaluate((MockCommandLineInput) () -> "someCommand --sql 'select * from table'");

执行外部命令

我们可以将需要spring shell执行的一连串命令存放到文件中,例如/path/to/command-file.txt,然后通过spring shell批量执行它们。

java -jar xxx.jar script --file /path/to/command-file.txt

参考文献

https://docs.spring.io/spring-shell/docs/current/reference/htmlsingle

https://docs.spring.io/spring-shell/docs/2.1.1/site/reference/htmlsingle

本博客为作者原创,欢迎大家参与讨论和批评指正。如需转载请注明出处。

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

推荐阅读更多精彩内容