前言
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
修饰。
在@ShellComponent
class中,每一个命令参数定义和执行逻辑对应着一个@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
- 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
本博客为作者原创,欢迎大家参与讨论和批评指正。如需转载请注明出处。