一、Spring Shell简介
并非所有应用程序都需要花哨的Web用户界面!有时候,使用交互式终端与应用程序进行交互是完成任务的最合适方式。Spring Shell允许用户轻松创建这样的可运行应用程序,用户可以输入文本命令,这些命令将被执行直到程序终止。Spring Shell项目提供了创建REPL(读取、评估、打印循环)的基础设施,使开发人员能够使用熟悉的Spring编程模型专注于命令的实现。高级功能如解析、TAB补全、输出着色、精美的ASCII艺术表格显示、输入转换和验证等都是免费提供的,开发人员只需关注核心命令逻辑。
二、使用Spring Shell - 入门
1. 编写一个简单的Spring Boot应用
从版本2开始,Spring Shell进行了全面重写,并考虑了各种增强功能,其中之一是与Spring Boot的轻松集成,不过这并不是强制要求。为了本教程的目的,我们使用start.spring.io创建一个简单的Spring Boot应用。这个最小的应用只依赖于spring-boot-starter
,并配置了spring-boot-maven-plugin
,生成一个可执行的超级JAR。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
...
</dependencies>
2. 添加Spring Shell依赖
使用Spring Shell最简单的方法是依赖spring-shell-starter
工件。它包含了使用Spring Shell所需的一切,并且与Spring Boot配合良好,根据需要仅配置必要的Bean。
<dependency>
<groupId>org.springframework.shell</groupId>
<artifactId>spring-shell-starter</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
由于添加了这个依赖,Spring Shell会启动REPL。在整个教程中,你需要跳过测试构建(-DskipTests
),或者删除start.spring.io生成的示例集成测试。否则,集成测试会创建Spring ApplicationContext
,根据你的构建工具,可能会陷入评估循环或因空指针异常而崩溃。
3. 创建第一个命令
现在是时候添加我们的第一个命令了。创建一个新类(可以随意命名),并使用@ShellComponent
进行注解(这是@Component
的一种变体,用于限制扫描候选命令的类集)。然后,创建一个add
方法,该方法接受两个整数(a
和b
)并返回它们的和。使用@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;
}
}
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
并按回车键退出。
三、编写自己的命令
1. 使用标准API
Spring Shell将方法转换为实际shell命令的方式是完全可插拔的,但从Spring Shell 2.x开始,推荐的编写命令的方式是使用本节描述的新API(即所谓的标准API)。使用标准API,Bean上的方法将被转换为可执行命令,前提是:
Bean类带有
@ShellComponent
注解,用于限制被考虑的Bean集。方法带有
@ShellMethod
注解。
@ShellComponent
是一个原型注解,本身用@Component
进行了元注解。因此,除了过滤机制外,它还可以用于声明Bean(例如使用@ComponentScan
)。注解的value
属性可以自定义创建的Bean的名称。
2. 文档的重要性
@ShellMethod
注解唯一必需的属性是其value
属性,应该用于编写一个简短的、一句话的命令描述。这很重要,这样用户可以在不离开shell的情况下获得关于命令的一致帮助(请参阅help
命令的集成文档)。命令描述应该简短,最好一到两句话,为了更好的一致性,建议以大写字母开头并以句点结尾。
3. 自定义命令名称
默认情况下,不需要为命令指定键(即在shell中调用它的单词)。方法的名称将用作命令键,将驼峰命名转换为破折号分隔的GNU风格名称(例如,sayHello()
将变为say-hello
)。但是,也可以使用注解的key
属性显式设置命令键,如下所示:
@ShellMethod(value = "Add numbers.", key = "sum")
public int add(int a, int b) {
return a + b;
}
key
属性接受多个值。如果为单个方法设置多个键,则该命令将使用这些不同的别名进行注册。命令键可以包含几乎任何字符,包括空格。但在命名时,请记住用户通常喜欢一致性(例如,避免混合使用破折号命名和空格命名)。
四、调用命令
1. 按名称参数与位置参数
如前所述,使用@ShellMethod
装饰方法是创建命令的唯一要求。用户可以通过两种方式设置方法参数的值:
使用参数键(例如
--arg value
),这种方法称为“按名称”参数。不使用键,只需按照方法签名中参数出现的顺序设置参数值(“位置”参数)。
这两种方法可以混合使用,命名参数始终优先(因为它们不太容易产生歧义)。例如,对于以下命令:
@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
2. 自定义命名参数键
默认情况下,推导命名参数键的策略是使用方法签名的Java名称,并在前面加上两个破折号(--
)。可以通过两种方式进行自定义:
使用
@ShellMethod
注解的prefix()
属性更改整个方法的默认前缀。使用
@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);
}
对于这样的设置,可能的参数键将是-a
、-b
和--third
。可以为单个参数指定多个键,这些键将是指定同一参数的互斥方式(因此只能使用其中一个)。例如,内置help
命令的签名如下:
@ShellMethod("Describe a command.")
public String help(@ShellOption({"-C", "--command"}) String command) {
...
}
3. 可选参数和默认值
Spring Shell允许为参数提供默认值,这样用户可以省略这些参数。例如:
@ShellMethod("Say hello.")
public String greet(@ShellOption(defaultValue="World") String who) {
return "Hello " + who;
}
现在,greet
命令可以像greet Mother
(或greet --who Mother
)一样调用,但也可以这样调用:
shell:>greet
Hello World
4. 参数arity
到目前为止,我们一直假设每个参数对应于用户输入的单个单词。但有时,参数值可能需要是多值的。这可以通过@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
使用按名称参数方法时,键不应重复。例如,shell:>add --numbers 1 --numbers 2 --numbers 3.3
是无效的。
5. 布尔参数的特殊处理
对于参数arity,布尔参数(即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
这种特殊处理与默认值规范配合良好。虽然布尔参数的默认值通常为false
,但你可以指定其他值(例如@ShellOption(defaultValue="true")
),行为将反转(即不指定参数将导致值为true
,指定标志将导致值为false
)。如果希望允许用户指定值(并放弃标志方法),则可以使用注解强制arity
为1
。
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
支持单引号和双引号允许用户轻松地将一种引号嵌入到值中。如果用户需要嵌入用于引用整个参数的相同类型的引号,则转义序列使用反斜杠(\
)字符。也可以在不使用引号时转义空格字符。
7. 与Shell交互
Spring Shell基于JLine库构建,因此带来了许多不错的交互功能,其中一些在本节中详细介绍。
TAB补全:Spring Shell几乎在所有可能的地方都支持TAB补全。例如,如果有一个
echo
命令,用户按下e
、c
、TAB
,则echo
将出现。如果有多个以ec
开头的命令,用户将被提示选择(使用TAB
或Shift + TAB
导航,按ENTER
选择)。补全不仅适用于命令键,还适用于参数键(--arg
),甚至参数值(如果应用程序开发人员注册了适当的Bean)。行继续:如果命令及其参数太长,无法在屏幕上很好地显示,用户可以将其分段,以反斜杠(
\
)字符结束一行,然后按ENTER
并在下一行继续。提交整个命令时,这将被解析为用户在换行处输入了一个空格。如果用户打开了引号(请参阅引号处理)并在引号内按ENTER
,行继续也会自动触发。键盘快捷键:Spring Shell应用程序受益于许多你在使用常规操作系统Shell时可能已经熟悉的键盘快捷键,这些快捷键借鉴自Emacs。值得注意的快捷键包括
Ctrl + r
进行反向搜索,Ctrl + a
和Ctrl + e
分别移动到行的开头和结尾,或Esc f
和Esc b
一次向前(或向后)移动一个单词。
五、验证命令参数
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,都可以通过适配器实现(请参阅支持其他API)。
六、动态命令可用性
有时,由于应用程序的内部状态,已注册的命令可能没有意义。例如,可能有一个download
命令,但只有在用户使用connect
连接到远程服务器后才能使用。如果用户尝试使用download
命令,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");
}
}
在这个例子中,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.
当前不可用命令的信息也会在集成帮助中使用。命令不可用时提供的原因应该在“Because …”之后读起来通顺,最好不要以大写字母开头,也不要添加句点。如果出于某种原因,以命令方法的名称命名可用性方法不合适,可以使用@ShellMethodAvailability
提供显式名称。此外,如果同一个类中的多个命令共享相同的内部状态,并且应该同时可用或不可用,可以将@ShellMethodAvailabilty
注解放在可用性方法上,并指定它控制的命令名称。
七、组织命令
当你的shell开始提供大量功能时,可能会有很多命令,这可能会让用户感到困惑。用户输入help
时,会看到一个按字母顺序排列的令人生畏的命令列表,这可能并不总是有意义的。为了缓解这个问题,Spring Shell允许将命令分组,并且有合理的默认设置。相关的命令将最终放在同一个组中(例如User Management Commands
),并在帮助屏幕和其他地方一起显示。
默认情况下,命令将根据它们实现的类进行分组,将驼峰命名的类名转换为单独的单词(例如,URLRelatedCommands
变为URL Related Commands
)。这是一个非常合理的默认设置,因为相关的命令通常已经在同一个类中,因为它们需要使用相同的协作对象。
如果这种行为不适合你,可以按以下优先级顺序覆盖命令的组:
在
@ShellMethod
注解中指定group()
。在定义命令的类上放置
@ShellCommandGroup
,这将应用于该类中定义的所有命令(除非如上所述被覆盖)。在定义命令的包(通过
package-info.java
)上放置@ShellCommandGroup
,这将应用于该包中定义的所有命令(除非在方法或类级别被覆盖)。
例如:
public class UserCommands {
@ShellCommand(value = "This command ends up in the 'User Commands' group")
public void foo() {}
@ShellCommand(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() {}
}
八、内置命令
任何使用spring-shell-starter
工件(或更准确地说,spring-shell-standard-commands
依赖项)构建的应用程序都带有一组内置命令。这些命令可以单独覆盖或禁用(请参阅覆盖或禁用内置命令),如果不进行修改,本节将描述它们的行为。
1. help
命令 - 集成文档
运行shell应用程序通常意味着用户处于图形受限的环境中。尽管在手机时代我们总是连接到网络,但访问Web浏览器或任何其他丰富的UI应用程序(如PDF查看器)可能并不总是可行的。因此,shell命令正确地进行自我文档化非常重要,这就是help
命令的作用。
输入help
+ ENTER
将列出shell已知的所有命令(包括不可用的命令)以及它们的简短描述:
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 <command>
将显示有关命令的更详细信息,包括可用的参数、它们的类型以及是否为必需参数等。例如,对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>]
2. clear
命令 - 清屏
clear
命令的作用正如你所期望的那样,它会清除屏幕,将提示符重置到左上角。
3. quit
/exit
命令 - 退出Shell
quit
命令(也别名为exit
)只是请求shell退出,优雅地关闭Spring应用程序上下文。如果不进行覆盖,JLine的History
Bean将把所有执行的命令历史记录写入磁盘,以便下次启动时可以再次使用(请参阅与Shell交互)。
4. stacktrace
命令 - 显示错误详细信息
当命令代码中发生异常时,shell会捕获它并显示一个简单的单行消息,以免向用户提供过多信息。但在某些情况下,了解具体发生了什么很重要(特别是如果异常有嵌套原因)。为此,Spring Shell会记住最后发生的异常,用户可以稍后使用stacktrace
命令在控制台打印所有详细信息。
5. script
命令 - 运行一批命令
script
命令接受一个本地文件作为参数,并将依次重放其中找到的命令。从文件中读取的行为与交互式shell中完全相同,因此以//
开头的行将被视为注释并忽略,以\
结尾的行将触发行继续。
九、自定义Shell
1. 覆盖或禁用内置命令
Spring Shell提供的内置命令用于完成许多(如果不是全部)shell应用程序需要的日常任务。如果你对它们的行为不满意,可以禁用或覆盖它们。
禁用所有内置命令:如果你根本不需要内置命令,有一种简单的方法可以“禁用”它们:不包含它们!可以在
spring-shell-standard-commands
上使用Maven排除,或者如果你选择性地包含Spring Shell依赖项,不要引入该依赖项。
<dependency>
<groupId>org.springframework.shell</groupId>
<artifactId>spring-shell-starter</artifactId>
<version>2.0.0.RELEASE</version>
<exclusions>
<exclusion>
<groupId>org.springframework.shell</groupId>
<artifactId>spring-shell-standard-commands</artifactId>
</exclusion>
</exclusions>
</dependency>
禁用特定命令:要禁用单个内置命令,只需在应用程序的
Environment
中将spring.shell.command.<command>.enabled
属性设置为false
。一种简单的方法是在main()
入口点向Spring Boot应用程序传递额外的参数。
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);
}
覆盖特定命令:如果你不想禁用命令,而是想提供自己的实现,可以通过以下两种方式之一:
如上所述禁用命令,并以相同的名称注册你的实现。
让你的实现类实现
<Command>.Command
接口。例如,如何覆盖clear
命令:
public class MyClear implements Clear.Command {
@ShellCommand("Clear the screen, only better.")
public void clear() {
// ...
}
}
如果你觉得自己对标准命令的实现对社区有价值,请考虑在github.com/spring-projects/spring-shell上提交拉取请求。或者,在自行进行任何更改之前,你可以在项目中打开一个问题。反馈总是受欢迎的!
2. 结果处理程序和提示提供程序
提示提供程序
每次命令调用后,shell会等待用户的新输入,并显示一个黄色的提示符:shell:>
。可以通过注册一个PromptProvider
类型的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();
}
}
3. 自定义命令行选项行为
Spring Shell带有两个默认的Spring Boot ApplicationRunners
:
InteractiveShellApplicationRunner
引导Shell REPL。它设置JLine基础设施并最终调用Shell.run()
。ScriptShellApplicationRunner
查找以@
开头的程序参数,假设这些是本地文件名,并尝试运行其中包含的命令(与script
命令具有相同的语义),然后退出进程(通过有效地禁用InteractiveShellApplicationRunner
)。
如果这种行为不适合你,只需提供一个(或多个)ApplicationRunner
类型的Bean,并可选地禁用标准的Bean。你可以从ScriptShellApplicationRunner
中获取灵感:
@Order(InteractiveShellApplicationRunner.PRECEDENCE - 100) // Runs before InteractiveShellApplicationRunner
public class ScriptShellApplicationRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
List<File> scriptsToRun = args.getNonOptionArgs().stream()
.filter(s -> s.startsWith("@"))
.map(s -> new File(s.substring(1)))
.collect(Collectors.toList());
boolean batchEnabled = environment.getProperty(SPRING_SHELL_SCRIPT_ENABLED, boolean.class, true);
if (!scriptsToRun.isEmpty() && batchEnabled) {
InteractiveShellApplicationRunner.disable(environment);
for (File file : scriptsToRun) {
try (Reader reader = new FileReader(file);
FileInputProvider inputProvider = new FileInputProvider(reader, parser)) {
shell.run(inputProvider);
}
}
}
}
...
}
4. 自定义参数转换
从文本输入到实际方法参数的转换使用标准的Spring转换机制。Spring Shell安装一个新的DefaultConversionService
(启用内置转换器),并将它在应用程序上下文中找到的任何Converter<S, T>
、GenericConverter
或ConverterFactory<S, T>
类型的Bean注册到其中。
这意味着很容易自定义转换为你自己的Foo
类型的对象:只需在上下文中安装一个Converter<String, Foo>
Bean。
@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);
}
}
需要注意的是,最好让你的toString()
实现返回用于创建对象实例的相反内容。这是因为当值验证失败时,Spring Shell会打印如下信息:
The following constraints were not met:
--arg <type> : <message> (You passed '<value.toString()>')
有关更多信息,请参阅验证命令参数。如果你想进一步自定义ConversionService
,可以将默认的注入到你的代码中并以某种方式对其进行操作,或者完全用你自己的覆盖它(需要手动注册自定义转换器)。Spring Shell使用的ConversionService
需要被限定为"spring-shell"
。
十、扩展Spring Shell
文档中还提到了Spring Shell对Spring Shell 1和JCommander的支持,以及发现可以作为命令的方法、解析参数值和支持TAB补全等方面,但具体细节文档未详细展开。
通过以上内容,你应该对Spring Shell有了从入门到精通的全面了解,可以开始使用Spring Shell开发自己的交互式命令行应用程序了。希望这篇博客对你有所帮助!