通过SpringShell开发Java命令⾏应⽤
提到 Java,⼤家都会想到 Java 在服务器端应⽤开发中的使⽤。实际上,Java 在命令⾏应⽤的开发中也有⼀席之地。在很多情况下,相对于图形⽤户界⾯来说,命令⾏界⾯响应速度快,所占⽤的系统资源少。在与⽤户进⾏交互的场景⽐较单⼀时,命令⾏界⾯是更好的选择。命令⾏界⾯有其固定的交互模式。通常是由⽤户输⼊⼀系列的参数,在执⾏之后把相应的结果在控制台输出。命令⾏应⽤通常需要处理输⼊参数的传递和验证、输出结果的格式化等任务。Spring Shell 可以帮助简化这些常见的任务,让开发⼈员专注于实现应⽤的业务逻辑。本⽂对 Spring Shell 进⾏详细的介绍。
Spring Shell ⼊门
清单 1. 添加 Spring Shell 的 Maven 仓库
<repositories>
<repository>
<id>spring-milestone</id>
<name>Spring Repository</name>
<url>repo.spring.io/milestone</url>
</repository>
</repositories>
在添加了 Spring Shell 的 Maven 仓库之后,可以在 Spring Boot 项⽬中添加对于spring-shell-starter 的依赖,如代码清单 2 所⽰。
清单 2. 添加 Spring Shell 所需 Maven 依赖
<dependency>
<groupId>org.springframework.shell</groupId>
<artifactId>spring-shell-starter</artifactId>
<version>2.0.0.M2</version>
</dependency>
我们接着可以创建第⼀个基于 Spring Shell 的命令⾏应⽤。该应⽤根据输⼊的参数来输出相应的问候语,完整的代码如清单 3所⽰。从代码清单 3 中可以看到,在 Spring Shell 的帮助下,完整的实现代码⾮常简单。代码的核⼼是两个注解:
@ShellComponent 声明类GreetingApp 是⼀个 Spring Shell 的组件;@ShellMethod 表⽰⽅法 sayHi 是可以在命令⾏运⾏的命令。该⽅法的参数 name 是命令⾏的输⼊参数,⽽其返回值是命令⾏执⾏的结果。
清单 3. 输出问候语的命令⾏应⽤
<dependency>
<groupId>org.springframework.shell</groupId>
<artifactId>spring-shell-starter</artifactId>
<version>2.0.0.M2</version>
</dependency>
接下来我们运⾏该应⽤。运⾏起来之后,该应⽤直接进⼊命令⾏提⽰界⾯,我们可以输⼊ help 来输出使⽤帮助。help 是Spring Shell 提供的众多内置命令之⼀,在列出的命令中,可以看到我们创建的 say-hi 命令。我们输⼊"say-hi Alex"来运⾏该命令,可以看到输出的结果"Hi Alex"。如果我们直接输⼊"say-hi",会看到输出的错误信息,告诉我们参数"--name"是必须的。从上⾯的例⼦可以看出,在 Spring Shell 的帮助下,创建⼀个命令⾏应⽤是⾮常简单的。很多实⽤功能都已经默认提供了。在使⽤ Spring Initializr 创建的 Spring Boot 项⽬中,默认提供了⼀个单元测试⽤例。这个默认的单元测试⽤例与 Spring Shell 在使⽤时存在冲突。在进⾏代码清单 3 中的项⽬的 Maven 构建时,该测试⽤例需要被禁⽤,否则构建过程会卡住。
参数传递与校验
下⾯我们讨论 Spring Shell 中的参数传递和校验。Spring Shell ⽀持两种不同类型的参数,分别是命名参数和位置参数。命名参数有名称,可以通过类似--arg 的⽅式来指定;位置参数则按照其在⽅法的参数列表中的出现位置来进⾏匹配。命名参数和位置参数可以混合起来使⽤,不过命名参数的优先级更⾼,会⾸先匹配命名参数。每个参数都有默认的名称,与⽅法中的对应的参数名称⼀致。
在代码清单 4 中的⽅法有 3 个参数 a、b 和 c。在调⽤该命令时,可以使⽤"echo1 --a 1 --b 2 --c 3",也可以使⽤"echo1 --a 1 2 3"或"echo1 1 3 --b 2"。其效果都是分别把 1,2 和 3 赋值给 a、b 和 c。
清单 4. 包含多个参数的命令⽅法
@ShellMethod("Echo1")
public String echo1(int a, int b, int c) {
return String.format("a = %d, b = %d, c = %d", a, b, c);
}
如果不希望使⽤⽅法的参数名称作为命令对应参数的名称,可以通过@ShellOption 来标注所要使⽤的⼀个或多个参数名称。
我们可以通过指定多个参数名称来提供不同的别名。在代码清单 5 中,为参数 b 指定了⼀个名称 boy。可以通过"echo2 1 --boy 2 3"来调⽤。
清单 5. 指定参数名称
@ShellMethod("Echo2")
public String echo2(int a, @ShellOption("--boy") int b, int c) {
return String.format("a = %d, b = %d, c = %d", a, b, c);
}
对于命名参数,默认使⽤的是"--"作为前缀,可以通过@ShellMethod 的属性 prefix 来设置不同的前缀。⽅法对应的命令的名称默认是从⽅法名称⾃动得到的,可以通过属性 key 来设置不同的名称,属性 value 表⽰的是命令的描述信息。如果参数是可选的,可以通过@ShellOption 的属性 defaultValue 来设置默认值。在代码清单 6 中,我们为⽅法 withDefault 指定了⼀个命令名称 default,同时为参数 value 指定了默认值"Hello"。如果直接运⾏命令"default",输出的结果是"Value: Hello";如果运⾏命令"default 123",则输出的结果是"Value: 123"。
清单 6. 指定⽅法名称和参数默认值
@ShellComponent
public class NameAndDefaultValueApp {
@ShellMethod(key = "default", value = "With default value")
public void withDefault(@ShellOption(defaultValue = "Hello") final String value) {
System.out.printf("Value: %s%n", value);
}
}
⼀个参数可以对应多个值。通过@ShellOption 属性 arity 可以指定⼀个参数所对应的值的数量。这些参数会被添加到⼀个数组中,可以在⽅法中访问。在代码清单 7 中,⽅法 echo3 的参数 numbers 的 arity 值是 3,因此可以映射 3 个参数。在运⾏命令"echo3 1 2 3"时,输出的结果是"a = 1, b =2, c = 3"。
清单 7. 参数对应多个值
@ShellMethod("Echo3")
public String echo3(@ShellOption(arity = 3) int[] numbers) {
return String.format("a = %d, b = %d, c = %d", numbers[0], numbers[1], numbers[2]);
}
如果参数的类型是布尔类型 Boolean,在调⽤的时候不需要给出对应的值。当参数出现时就表⽰值为 true。
Spring Shell ⽀持对参数的值使⽤ Bean Validation API 进⾏验证。⽐如我们可以⽤@Size 来限制字符串的长度,⽤@Min 和@Max 来限制数值的⼤⼩,如代码清单 8 所⽰。
清单 8. 校验参数
@ShellComponent
public class ParametersValidationApp {
@ShellMethod("String size")
public String stringSize(@Size(min = 3, max = 16) String name) {
return String.format("Your name is %s", name);
}
@ShellMethod("Number range")
public String numberRange(@Min(10) @Max(100) int number) {
return String.format("The number is %s", number);
}
}
结果处理
Spring Shell 在运⾏时,内部有⼀个处理循环。在每个循环的执⾏过程中,⾸先读取⽤户的输⼊,然后进⾏相应的处理,最后再把处理的结果输出。这其中的结果处理是由 org.springframework.shell.ResultHandler 接⼝来实现的。Spring Shell 中内置提供了对于不同类型结果的处理实现。命令执⾏的结果可能有很多种:如果⽤户输⼊的参数错误,输出的结果应该是相应的提⽰信息;如果在命令的执⾏过程中出现了错误,则需要输出相应的错误信息;⽤户也可能直接退出命令⾏。Spring Shell 默认使⽤的处理实现是类 org.sult.IterableResultHandler。IterableResultHandler 负责处理 Iterable 类型的结果对象。对于 Iterable 中包含的每个对象,把实际的处理请求代理给另外⼀个 ResultHandler 来完成。IterableResultHandler 默认的代理实现是类 org.sult.TypeHierarchy
ResultHandler。TypeHierarchyResultHandler 其实是⼀个复合的处理器,它会把对于不同类型结果的 ResultHandler 接⼝的实现进⾏注册,然后根据结果的类型来选择相应的处理器实现。如果不到类型完全匹配的处理器实现,则会沿着结果类型的层次结构树往上查,直到到对应的处理器实现。Spring Shell 提供了对于 Object 类型结果的处理实现类
org.sult.DefaultResultHandler,因此所有的结果类型都可以得到处理。DefaultResultHandler 所做的处理只是把 Object 类型转换成 String,然后输出到控制台。
了解了 Spring Shell 对于结果的处理⽅式之后,我们可以添加⾃⼰所需要的特定结果类型的处理实现。代码清单 9 给了⼀个作为⽰例的处理结果类 PrefixedResult。PrefixedResult 中包含⼀个前缀 prefix 和实际的结果 result。
清单 9. 带前缀的处理结果
public class PrefixedResult {
private final String prefix;
private final String result;
public PrefixedResult(String prefix, String result) {
this.prefix = prefix;
}
public String getPrefix() {
return prefix;
}
public String getResult() {
return result;
}
}
在代码清单 10 中,我们为 PrefixedResult 添加了具体的处理器实现。该实现也⾮常简单,只是把结果按照某个格式进⾏输出。
清单 10. PrefixedResult 对应的处理器实现
@Component
public class PrefixedResultHandler implements ResultHandler<PrefixedResult> {
@Override
public void handleResult(PrefixedResult result) {
System.out.printf("%s --> %s%n", Prefix(), Result());
}
}
在代码清单 11 中,命令⽅法 resultHandler 返回的是⼀个 PrefixedResult 对象,因此会被代码清单 10 中的处理器来进⾏处理,输出相应的结果。
清单 11. 使⽤ PrefixedResult 的命令
@ShellComponent
public class CustomResultHandlerApp {
@ShellMethod("Result handler")
public PrefixedResult resultHandler() {
return new PrefixedResult("PRE", "Hello!");
}
}
代码清单 12 给出了具体的命令运⾏结果。
清单 12. 命令的处理结果
myshell=>result-handler
PRE --> Hello!
⾃定义提⽰符
在启动命令⾏应⽤时,会发现该应⽤使⽤的是默认提⽰符"shell:>"。该提⽰符是可以定制的,只需要提供接⼝
org.springframework.shell.jline.PromptProvider 的实现即可。接⼝ PromptProvider 中只有⼀个⽅法,⽤来返回类型为
org.jline.utils.AttributedString 的提⽰符。在代码清单 13 中,我们定义了⼀个 PromptProvider 接⼝的实现类,并使⽤"myshell=>"作为提⽰符,⽽且颜⾊为蓝⾊。
清单 13. ⾃定义提⽰符
@Bean
public PromptProvider promptProvider() {
return () -> new AttributedString("myshell=>",
AttributedStyle.DEFAULT.foreground(AttributedStyle.BLUE));
}
动态命令可⽤性
前⾯所创建的命令都是⼀直可⽤的。只要应⽤启动起来,就可以使⽤这些命令。不过有些命令的可⽤性可能取决于应⽤的内部状态,只有内部状态满⾜时,才可以使⽤这些命令。对于这些命令,Spring Shell 提供了类
org.springframework.shell.Availability 来表⽰命令的可⽤性。通过类 Availability 的静态⽅法 available()和 unavailable()来分别创建表⽰命令可⽤和不可⽤的 Availability 对象。
在代码清单 14 中,我们创建了两个命令⽅法 runOnce()和 runAgain()。变量 run 作为内部状态。在运⾏ runOnce()之后,变量run 的值变为 true。命令 runAgain 的可⽤性由⽅法 runAgainAvailability()来确定。该⽅法根据变量 run 的值来决定 runAgain 是否可⽤。按照命名惯例,检查命令可⽤性的⽅法的名称是在命令⽅法名称之后加上 Availability 后缀。如果需要使⽤不同的⽅法名称,或是由⼀个检查⽅法控制多个⽅法,可以在检查⽅法上添加注解@ShellMethodAvailability 来声明其控制的⽅法名称。
清单 14. 动态命令可⽤性
@ShellComponent
public class RunTwiceToEnableApp {
private boolean run = false;
@ShellMethod("Run once")
public void runOnce() {
this.run = true;
}
@ShellMethod("Run again")
public void runAgain() {
System.out.println("Run!");
}
public Availability runAgainAvailability() {
return run
Availability.available()
: Availability.unavailable("You should run runOnce first!");
}
}
输⼊参数转换
之前的@ShellMethod 标注的⽅法使⽤的都是简单类型的参数。Spring Shell 通过 Spring 框架的类型转换系统来进⾏参数类型的转换。Spring 框架已经内置提供了对常⽤类型的转换逻辑,包括原始类型、String 类型、数组类型、集合类型、Java 8的 Optional 类型、以及⽇期和时间类型等。我们可以通过 Spring 框架提供的扩展机制来添加⾃定义的转换实现。
代码清单 15 中的 User 类是作为⽰例的⼀个领域对象,包含了 id 和 name 两个属性。
清单 15. User
public class User {
private final String id;
private final String name;
public User(String id, String name) {
this.id = id;
this.name = name;
}
public String getName() {
return name;
}
}
代码清单 16 中的 UserService ⽤来根据 id 来查对应的 User 对象。作为⽰例,UserService 只是简单使⽤⼀个 HashMap 来保存作为测试的 User 对象。
清单 16. UserService
public class UserService {
private final Map<String, User> users = new HashMap<>();
public UserService() {
users.put("alex", new User("alex", "Alex"));
users.put("bob", new User("bob", "Bob"));
}
public User findUser(String id) {
(id);
}
}
在代码清单 17 中,UserConverter 实现了 Spring 中的 Converter 接⼝并添加了从 String 到 User 对象的转换逻辑,即通过UserService 来进⾏查。
清单 17. 使⽤类型转换
@Component
public class UserConverter implements Converter<String, User> {
private final UserService userService = new UserService();
@Override
public User convert(String source) {
return userService.findUser(source);
}
}
在代码清单 18 中,命令⽅法 user 的参数是 User 类型。当运⾏命令"user alex"时,输⼊参数 alex 会通过代码清单 17 中的类型转换服务转换成对应的 User 对象,然后输出 User 对象的属性值 name。如果不到与输⼊参数值对应的 User 对象,则输出"User not found"。
清单 18. 使⽤类型转换的命令
@ShellComponent
public class UserCommandApp {
@ShellMethod("User")
public void user(final User user) {
if (user != null) {
System.out.Name());
} else {
System.out.println("User not found");
}
}
}
命令组织⽅式
当创建很多个命令时,需要有⼀种把这些命令组织起来。Spring Shell 提供了不同的⽅式来对命令进⾏分组。处于同⼀分组的命令会在 help 命令输出的帮助中出现在⼀起。默认情况下,同⼀个类中的命令会被添加到同⼀分组中。默认的分组名称根据对应的 Java 类名来⾃动⽣成。除了默认分组之外,还可以显式的设置分组。可以使⽤@ShellMethod 注解的属性 group 来指定分组名称;还可以为包含命令的类添加注解@ShellCommandGroup,则该类中的所有命令都在由@ShellCommandGroup 指定的分组中;还可以把@ShellCommandGroup 注解添加到包声明中,则该包中的所有命令都在由@ShellCommandGroup 指定的分组中。
在代码清单 19 中,通过@ShellCommandGroup 为命令所在类添加了⾃定义的分组名称 Special。其中的⽅法 command2 则通过@ShellMethod 的 group 属性指定了不同的分组名称"Basic Group"。
清单 19. 组织命令
@ShellComponentvalidation框架
@ShellCommandGroup("Special")
public class CommandsGroupApp {
@ShellMethod("Command1")
public void command1() {}
@ShellMethod(value = "Command2", group = "Basic Group")
public void command2() {}
}
图 1 显⽰了⽰例应⽤的 help 命令的输出结果,从中可以看到命令的分组情况。
图 1. 所有的命令列表
commands.png
内置命令
Spring Shell 提供了很多内置的命令,如下所⽰。
运⾏ help 命令可以列出来应⽤中的所有命令和对应的描述信息。
运⾏ clear 命令可以进⾏清屏操作。
运⾏ exit 命令可以退出命令⾏应⽤。
运⾏ script 命令可以执⾏⼀个⽂件中包含的所有命令。
如果不需要某个内置命令,可以通过把上下⽂环境中的属性 spring.shellmand.<command>.enabled 的值设为 false 来禁⽤。如果希望禁⽤全部的内置命令,可以把 spring-shell-standard-commands 从 Maven 依赖中排除,如代码清单 20 所⽰。
清单 20. 排除内置命令对应的 Maven 依赖
<dependency>
<groupId>org.springframework.shell</groupId>
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论