Spring Shell 参考文档
不是所有应用都需要漂亮的web用户界面,有时很多场景也需要使用终端进行交互。
Spring Shell可以很容易创建使用文本命令进行终端交互的应用。Spring Shell项目提供了创建REPL (Read, Eval, Print Loop)的基础架构,让开发者使用熟悉的spring编程模型集中命令业务实现。
一些高级特性如,命令解析,TAB自动完成,彩色输出,漂亮的ascii艺术表格展示,输入转换和验证默认都可以使用,开发者仅需考虑核心命令业务实现。
使用Spring Shell
1 快速入门
为了理解Spring Shell提供哪些功能,我们先写一个小的Shell应用,包括简单的两个数求和应用。
1.1 写个简单Spring Boot 应用
从 2版本开始,Spring Shell完全从头重写,增强各种功能,其中之一是很容易集成Spring Boot,虽然不是必须使用。为了示例说明,我们需要创建一个简单的Spring Boot应用,仅需要依赖spring-boot-starter,并配置spring-boot-maven-plugin,生成可执行jar:
...
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
...
1.2 增加Spring Shell依赖
最简单的方式是依赖spring-shell-starter,自动提供Spring Shell所需的全部功能,并且可以很好地与Boot进行整合,只需要配置必要的bean。
<dependency>
<groupId>org.springframework.shell</groupId>
<artifactId>spring-shell-starter</artifactId>
<version>2.0.1.RELEASE</version>
</dependency>
1.3 第一个命令
下面开始增加第一个命令,创建新的类(类名称你自己定),并增加@ShellComponent注解(@Component的变体,用于扫描作为候选命令类)。
然后添加add方法,带有两个int类型参数并返回两者之和。在方法上增加@ShellMethod注解,提供对命令的描述(仅需要提供的信息描述)。
package com.example.demo;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellComponent;
@ShellComponent
public class MyCommands {
@ShellMethod("Add two integers together.")
public int add(int a, int b) {
return a + b;
}
}
1.4 启动并测试
构建应用并启动生成jar,使用下面命令:
./mvnw clean install -DskipTests
[...]
java -jar target/demo-0.0.1-SNAPSHOT.jar
执行命令会出现下图的欢迎界面(这时Spring Boot提供的,用户可以自定义):
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.5.6.RELEASE)
shell:>
下面是shell:>命令提示符,提示可以输入命令,输入 add 1 2 ,然后回车,出现神奇的结果!
shell:>add 1 2
3
尝试测试下Shell功能(有help命令),测试完成后输入exit然后回车。
下面文档将进行深入完整Spring Shell编程模型。
2 实现自定义命令
Spring Shell 映射shell 命令值方法的方式完全是可插拔的,但Spring Shell2.x建议使用新的API方式实现命令,即本节采用的方式,也称为标准API。
使用标准API,带有注解的bean将转为可执行命令。
- 使用@ShellComponent 注解的bean类,被限制为命令实现类。
- 使用 @ShellMethod注解的方法实现具体命令。
@ShellComponent注解是原型注解,其本身是用@Component进行注释的。这样可以对声明的bean实现过滤机制(如:@ComponentScan)。可以通过注解的value属性自定义创建bean的名称。
2.1 关于文档
@ShellMethod注解唯一必须有值的是value属性,一般是简单一句话描述命令实现功能。这很重要,用户无需离开命令行界面就可以获得帮助。
命令的描述应该简短,仅需要一两句话。为了保持一致性,建议以大写字母开头,以点结尾。
2.2 自定义命令名称
一般无需指定命令的key属性(用于在shell中执行的单词),缺省情况下使用方法的名称作为命令的key,并把骆驼命名法转为中划线分割方式,即gnu-style。如:sayHello()会变为say-hello。
但也可以使用key属性显示指定命令的名称,示例如下:
@ShellMethod(value = "Add numbers.", key = "sum")
public int add(int a, int b) {
return a + b;
}
key属性可以接受多个值,如果你给一个方法设置多个值,那么命令将使用它们注册多个别名。
命令的key开是任何字符,包括空格。但是需要提醒的是,采用一致性的方式命名用户通常容易接受(例如,避免中划线和空格混合的命名方式等)。
3 执行命令
3.1 名称参数与位置参数
上节我们看到,使用@ShellMethod注解方面创建命令仅需唯一value属性,这样用户可以使用两种不同的方法设置方法参数值:
- 使用参数key(如,–arg value),称为命名参数。
- 不指定key,仅按照方法申明参数的顺序设置参数值,称为位置参数。
这两种方式也可以混合使用,命名参数总是优先(命名参数歧义可能性小)。下面,通过示例说明:
@ShellMethod("Display stuff.")
public String echo(int a, int b, int c) {
return String.format("You said a=%d, b=%d, c=%d", a, b, c);
}
下面所有调用方式都是等价,通过输出结果可以表明:
shell:>echo 1 2 3 ①
You said a=1, b=2, c=3
shell:>echo --a 1 --b 2 --c 3 ②
You said a=1, b=2, c=3
shell:>echo --b 2 --c 3 --a 1 ③
You said a=1, b=2, c=3
shell:>echo --a 1 2 3 ④
You said a=1, b=2, c=3
shell:>echo 1 --c 3 2 ⑤
You said a=1, b=2, c=3
① 使用了位置参数
② 完全使用命名参数
③ 命名参数可以根据需要重新拍下
④ 混合两种方式
⑤ 非命名参数按出现顺序进行解析
3.2 自定义命名参数key
上节我们看到,参数名称默认策略是java 方法申明的参数名称,前缀带两个中划线(–),可以通过两种方式自定义:
-
在@ShellMethod注解中通过prefix属性可以改变整个方法缺省的参数前缀。
-
为了覆盖所有参数key的前缀方式,可以使用@ShellOption注解。
请看下面代码示例:
@ShellMethod(value = "Display stuff.", prefix="-")
public String echo(int a, int b, @ShellOption("--third") int c) {
return String.format("You said a=%d, b=%d, c=%d", a, b, c);
}
上面示例设置后,参数key为-a,-b和–third。
也可以给一个参数指定多个key,如果这样,这些方法将是指定相同参数的互斥方法(因此只能使用其中一)。下面示例是内置help命令的方法签名:
@ShellMethod("Describe a command.")
public String help(@ShellOption({"-C", "--command"}) String command) {
...
}
3.3 可选参数与缺省值
Spring Shell可以给参数指定缺省值,用户执行命令时可以忽略这些参数:
@ShellMethod("Say hello.")
public String greet(@ShellOption(defaultValue="World") String who) {
return "Hello " + who;
}
现在greet命令可以这样greet Month(或 greet --who Mother)执行,但下面方式也可以:
shell:>greet
Hello World
3.4 多值参数
到目前为止,每个参数总是对应一个用户输入值。有时需要一个参数映射多个值,可以通过@ShellOption注解的arity属性进行标识。使用集合或数组类型,该属性指定需要有几个值:
@ShellMethod("Add Numbers.")
public float add(@ShellOption(arity=3) float[] numbers) {
return numbers[0] + numbers[1] + numbers[2];
}
下面两种方式执行都可以:
shell:>add 1 2 3.3
6.3
shell:>add --numbers 1 2 3.3
6.3
使用命名参数时,key不能重复。下面执行方式不正确:
shell:>add --numbers 1 --numbers 2 --numbers 3.3
3.5 特殊处理Boolean类型参数
针对参数与值一致性,通常Boolean类型参数默认会特殊处理。Boolean(boolean,与java.lang.Boolean一样)参数行为就好像其arity属性缺省值为0,允许用户使用“标记”方法设置它们值。请看示例:
@ShellMethod("Terminate the system.")
public String shutdown(boolean force) {
return "You said " + force;
}
执行并验证:
shell:>shutdown
You said false
shell:>shutdown --force
You said true
这种特殊处理遵循缺省值规范。虽然boolean类型参数缺省值为false,但也可以通过@ShellOption(defaultValue=“true”))进行设置为true,主要其功能反转(即不知道参数时值为true,而指定标识结果为false)。
通过arity()=0显示指定,避免用户带参数值(如:shutdown --force true)。如果你希望带上标识参数方法,指定属性arity值为1注解force参数 :
@ShellMethod("Terminate the system.") public String shutdown(@ShellOption(arity=1, defaultValue="false") boolean force) { return "You said " + force; }
3.6 处理引号
Spring Shell接受用户输入,使用空格分割标记为单词。如果需要输入包括空格的参数值,则需要引号括起来,使用’或”都可以,并作为整体给参数赋值:
@ShellMethod("Prints what has been entered.")
public String echo(String what) {
return "You said " + what;
}
shell:>echo Hello
You said Hello
shell:>echo 'Hello'
You said Hello
shell:>echo 'Hello World'
You said Hello World
shell:>echo "Hello World"
You said Hello World
混合两种引号可以实现值中包含引号:
shell:>echo "I'm here!"
You said I'm here!
shell:>echo 'He said "Hi!"'
You said He said "Hi!"
如何用户需要嵌入引用整个值的引号,需要使用转义符“\”:
shell:>echo 'I\'m here!'
You said I'm here!
shell:>echo "He said \"Hi!\""
You said He said "Hi!"
shell:>echo I\'m here!
You said I'm here!
如何不想使用引号,也可以使用转义符实现值中包含空格:
shell:>echo This\ is\ a\ single\ value
You said This is a single value
3.7 命令行交互
Spring Shell基于JLine库构建,包括许多很好用的交互特性,下面举例详细说明。
首先要说的是,Spring Shell几乎所有地方都支持TAB键自动完成功能。假如有echo命令,用户输入e,c,TAB,那么echo将自动出现。如果多个命令以ec开头,会提示用户进行选择(使用TAB或Shift+TAB进行浏览,按回车键选择)。
但自动完成并不仅用于命令。如果开发人员注册了适当的bean,它还可以用于参数键(—arg)甚至参数值。
Spring Shell另一个不错的特性是支持换行。如果命令及参数太长,在屏幕上不能很好显示,用户可以会对其进行分段,使用反斜杠()字符结束一行,然后按回车键并在下一行继续。当提交整个命令时,将会把换行符解析为空格。
shell:>register module --type source --name foo \ ①
> --uri file:///tmp/bar
Successfully registered module 'source:foo'
① 命令在下一行继续。
用户输入开始引号,然后回车,并继续在引号内输入,也会自动触发换行行为:
shell:>echo "Hello ①
dquote> World"
You said Hello World
① 此处用户按下回车。
最后,Spring Shell 也提供了你已经熟悉操作系统Shell的快捷键,来自Emacs。最有名快捷键是ctrl+r执行反向搜索,Ctrl+a,ctrl+e在行开始和结束处各自移动,或Esc f 和 Esc b 前后一次移动一个单词。
4 验证命令参数
Spring Shell 集成了 Bean Validation API,支持通过注解方便地验证命令参数。
在参数上的注解和方法级的注解一样,在命令执行之前会触发参数验证,请看示例:
@ShellMethod("Change password.")
public String changePassword(@Size(min = 8, max = 40) String password) {
return "Password successfully set to " + password;
}
下面是测试结果:
shell:>change-password hello
The following constraints were not met:
--password string : size must be between 8 and 40 (You passed 'hello')
注意:bean 验证可以应用于所有命名实现,无论使用的是标准API或其他API.
5 动态启用禁用命令
由于应用内部状态缘故,一些已经注册的命令有时不能使用。假如有一下载命令,但仅仅当用户已经连接了远程服务器后才能使用。如果用户尝试使用下载命令,shell应该优雅地提示该命令不存在,但不是每次都不工作。Spring Shell给开发者提供了该功能,甚至可以增加简短描述不工作的原因。
有三种可能的方式控制有效性。它们都利用无参方法返回Availability的实例。先从简单示例开始:
@ShellComponent
public class MyCommands {
private boolean connected;
@ShellMethod("Connect to the server.")
public void connect(String user, String password) {
[...]
connected = true;
}
@ShellMethod("Download the nuclear codes.")
public void download() {
[...]
}
public Availability downloadAvailability() {
return connected
? Availability.available()
: Availability.unavailable("you are not connected");
}
}
你看到connect方法用于连接服务器(细节忽略),当连接后通过connected变量切换命令状态。只有用户已经连接后download命令才有效,因为提供了与download命令方法完全相同的方法,其名称中带有Availability后缀。该方法返回Availability实例,使用两个工厂方法中一个构建。一旦命令不可用,会提供说明。现在当没有连接服务器时用户尝试调用download命令,会返回下面信息:
shell:>download
Command 'download' exists but is not currently available because you are not connected.
Details of the error have been omitted. You can use the stacktrace command to print the full stacktrace.
当前无需信息也可以被help集成,后面会提及。
"Because …"之后的命令不可用提示信息应该很容易读。最后不要以大写字母开后,也不要加最后点。
有时一些场景使用availability 作为方法名后缀不能满足需求,Shell提供@ShellMethodAvailability注解在显示意义的方法上,如下代码:
@ShellMethod("Download the nuclear codes.")
@ShellMethodAvailability("availabilityCheck") ①
public void download() {
[...]
}
public Availability availabilityCheck() {
return connected
? Availability.available()
: Availability.unavailable("you are not connected");
}
① 无需名称匹配。
最后,经常一个类中几个命令共享相同的内部状态,即同时有效或无效。我们不需要在每个方法上增加@ShellMethodAvailability注解,Shell 可以在控制方法上增加@ShellMethodAvailability注解,同时多个指定需要控制命令的名称:
@ShellMethod("Download the nuclear codes.")
public void download() {
[...]
}
@ShellMethod("Disconnect from the server.")
public void disconnect() {
[...]
}
@ShellMethodAvailability({"download", "disconnect"})
public Availability availabilityCheck() {
return connected
? Availability.available()
: Availability.unavailable("you are not connected");
}
@ShellMethodAvailability注解的value属性缺省值为“*”,作为通配符匹配所有命令名称。这很容易通过控制方法启用或禁用类中所有命令,示例如下:
@ShellComponent
public class Toggles {
@ShellMethodAvailability
public Availability availabilityOnWeekdays() {
return Calendar.getInstance().get(DAY_OF_WEEK) == SUNDAY
? Availability.available()
: Availability.unavailable("today is not Sunday");
}
@ShellMethod
public void foo() {}
@ShellMethod
public void bar() {}
}
Spring Shell对如何组织类和实现命令没有强制很多限制。但通常在同一类定义相关的命令是好的实践,这样有利于控制命令可用性。
6 组织命令
当你的应用提供非常多的功能时,可能需要提供很多命令,这很容易让用户困惑。输入help命令将看到大量的命令,按照默认字母顺序排列并没有实际意义。
为了避免,Spring Shell 提供了给命令分组功能,并提供适当的缺省值。业务相关的命令一般在相同的组中(如:用户管理组命令),help命令中或其他地方集中展示。
默认情况下,命令根据其实现类进行分组,把骆驼命名的类名称转成各自单词(如,URLRelatedCommands 转成 URL Related Commands)。一般情况下,这没有问题,因为相关命令通常在一个类中,这些命名使用相同的协同对象。当这些不能满足需求时,我们可以重写命令的group,有几种方法可以实现,按优先顺序:
-
在@ShellMethod 注解中指定group属性。
-
在定义命令的类上增加@ShellCommandGroup注解。这会影响该类中定义的所有命令,除非采用上面方式覆盖。
-
在命令定义的包上增加@ShellCommandGroup注解,这会影响包下所有定义命令,除非使用上面两种方式覆盖。
这是一个简单示例:
public class UserCommands {
@ShellMethod(value = "This command ends up in the 'User Commands' group")
public void foo() {}
@ShellMethod(value = "This command ends up in the 'Other Commands' group",
group = "Other Commands")
public void bar() {}
}
...
@ShellCommandGroup("Other Commands")
public class SomeCommands {
@ShellMethod(value = "This one is in 'Other Commands'")
public void wizz() {}
@ShellMethod(value = "And this one is 'Yet Another Group'",
group = "Yet Another Group")
public void last() {}
}
7 内置命令
使用spring-shell-starter依赖(或更准确地说,使用spring-shell-standard-commands依赖)的应用默认会拥有一组内置命令。这些命令可以被重写或禁用(后面章节会讲解),本节我们会描述他们的作用。
7.1 使用help命令集成帮助文档
运行shell程序通常意味着用户是在缺少图形界面环境。虽然我们生活在移动互联网时代,但并不意味着我们总是可以访问web浏览器或丰富ui的应用(如pdf阅读器)。因此,shell命令可以自己显示文档非常重要,help命令可以提供帮助。
输入help + ENTER将列出所有已知命令(包括禁用命令)以及命令的简短描述:
shell:>help
AVAILABLE COMMANDS
add: Add numbers together.
* authenticate: Authenticate with the system.
* blow-up: Blow Everything up.
clear: Clear the shell screen.
connect: Connect to the system
disconnect: Disconnect from the system.
exit, quit: Exit the shell.
help: Display help about available commands.
register module: Register a new module.
script: Read and execute commands from a file.
stacktrace: Display the full stacktrace of the last error.
Commands marked with (*) are currently unavailable.
Type `help <command>` to learn more.
输入 help 将显示命令的更多细节信息,包括有效参数,以及其类型、是否为必需等。下面示例展示help命令应用于自身:
shell:>help help
NAME
help - Display help about available commands.
SYNOPSYS
help [[-C] string]
OPTIONS
-C or --command string
The command to obtain help for. [Optional, default = <none>]
7.2 清屏命令
clear命令清除屏幕,并在左上角重新设置提示符。
7.3 退出Shell
quit命令(也可以是其别名exit)请求退出shell,优雅地关闭Srping 应用上下文。如果没有覆盖,JLine Historybean将写所有以及执行命令的历史至磁盘,这样下次启动时保持有效。
7.4 显示错误细节
当命令内部代码发生异常时,shell捕获并显示简单的单行消息,以便不向用户提供过多信息。但有时需要理解到底发生了什么(尤其是在异常有嵌套原因的情况下)是非常重要。
为此,Spring Shell收集最后发生异常,之后用户可以使用stacktrace命令在控制台打印所有细节信息。
7.5 运行批处理
script 命令介绍本地文件作为参数,然后一次性重新执行文件中所有命令。从文件中读取命令行为与shell交互很相似,因此,以//开头的行是注释,会被忽略;而以\ 结尾的行 会触发续行。
8 自定义Shell
8.1 覆盖或禁用内置命令
内置命令是Spring Shell提供的常用任务命令,但不是所有应用都需要。如果不能满足你的需要,可以禁用或重载其功能,本节详细说明。
- 禁用所有内置命令
如果你根本需要内置命令,那么有个简单方法禁用他们:不引入它们,使用maven排除spring-shell-standard-commands,或有选择地依赖Spring Shell,而不是一次性包括所有。
<dependency>
<groupId>org.springframework.shell</groupId>
<artifactId>spring-shell-starter</artifactId>
<version>2.0.1.RELEASE</version>
<exclusions>
<exclusion>
<groupId>org.springframework.shell</groupId>
<artifactId>spring-shell-standard-commands</artifactId>
</exclusion>
</exclusion>
</dependency>
- 禁用特定内置命令
为了禁用特定命令,可以给应用环境中设置spring.shell.command..enabled 属性为false。简单的实现方法为在Boot应用的main方法入口点中传递另外参数:
public static void main(String[] args) throws Exception {
String[] disabledCommands = {"--spring.shell.command.help.enabled=false"}; ①
String[] fullArgs = StringUtils.concatenateStringArrays(args, disabledCommands);
SpringApplication.run(MyApp.class, fullArgs);
}
① 会禁用help命令。
- 覆盖特定的命令
如果你不想禁用命令,而是提供自己的实现,下面方法可以实现:
- 通过上节提供的方法禁用命令,然后以相同的名称注册自己的实现。
- 让你的命令类实现.Command接口。下面示例展示了如何覆盖clear命令:
public class MyClear implements Clear.Command {
@ShellMethod("Clear the screen, only better.")
public void clear() {
// ...
}
}
8.2 命令行提示
每个命令执行后,shell等待用户输入新的命令,显示一个提示:
shell:>
可以通过注册ProptProvider类型的bean自定义命令提示。该bean使用内部状态决定给用户显示什么样的提示,也能使用JLine 的AttributedCharSequence 显示漂亮的ANSI文本。
下面是模拟示例:
@Component
public class CustomPromptProvider implements PromptProvider {
private ConnectionDetails connection;
@Override
public AttributedString getPrompt() {
if (connection != null) {
return new AttributedString(connection.getHost() + ":>",
AttributedStyle.DEFAULT.foreground(AttributedStyle.YELLOW));
}
else {
return new AttributedString("server-unknown:>",
AttributedStyle.DEFAULT.foreground(AttributedStyle.RED));
}
}
@EventListener
public void handle(ConnectionUpdatedEvent event) {
this.connection = event.getConnectionDetails();
}
}
8.3
8.4 自定义参数转换
标准的Spring 转换机制实现从输入文本到方法参数类型转换。Spring Shell 使用DefaultConversionService (内置转换机制),可以给它注册任何在spring上下文中的类型bean转换器,如: Converter<S, T>, GenericConverter or ConverterFactory<S, T>。这就意味着很容易给特定类型自定义转换器,如有Foo类型,则在上下文中注册Converter<String, Foo>。
@ShellComponent
class ConversionCommands {
@ShellMethod("Shows conversion using Spring converter")
public String conversionExample(DomainObject object) {
return object.getClass();
}
}
class DomainObject {
private final String value;
DomainObject(String value) {
this.value = value;
}
public String toString() {
return value;
}
}
@Component
class CustomDomainConverter implements Converter<String, DomainObject> {
@Override
public DomainObject convert(String source) {
return new DomainObject(source);
}
}