目录
Spring Shell是什么
Spring Shell是Spring生态中的一员,用于开发命令行应用程序,官网:https://projects.spring.io/spring-shell/ 。
Spring Shell构建在之上,集成实现命令参数校验。 从2.0版本开始,Spring Shell还可以非常方便地与Spring Boot进行集成,直接使用Spring Boot提供的一些非常实用的功能(如:打包可执行jar文件)。入门实践
使用Spring Shell非常简单,直接添加对应的依赖配置即可,而为了使用Spring Boot提供的便利性,通常都是与Spring Boot集成使用。
基础配置
集成本质上就是先新建一个Spring Boot工程,然后添加Spring Shell依赖即可,如下所示:
4.0.0 org.springframework.boot spring-boot-starter-parent 2.1.6.RELEASE chench.org.extra test-springshell 0.0.1-SNAPSHOT test-springshell Test Spring Shell 1.8 org.springframework.boot spring-boot-starter org.springframework.shell spring-shell-starter 2.0.0.RELEASE org.springframework.boot spring-boot-maven-plugin
添加完上述配置之后,一个基于Spring Boot的使用Spring Shell开发命令行应用程序的基础开发框架已经搭建完毕,打包运行:
$ mvn clean package -Dmaven.test.skip=true$ java -jar test-springshell-0.0.1-SNAPSHOT.jar . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.1.6.RELEASE)2019-06-21 11:23:54.966 INFO 11286 --- [ main] c.o.e.t.TestSpringshellApplication : Starting TestSpringshellApplication v0.0.1-SNAPSHOT on chench9-pc with PID 11286 (/home/chench9/sun/workspace/test-springshell/target/test-springshell-0.0.1-SNAPSHOT.jar started by chench9 in /home/chench9)2019-06-21 11:23:54.970 INFO 11286 --- [ main] c.o.e.t.TestSpringshellApplication : No active profile set, falling back to default profiles: default2019-06-21 11:23:56.457 INFO 11286 --- [ main] c.o.e.t.TestSpringshellApplication : Started TestSpringshellApplication in 2.26 seconds (JVM running for 2.771)shell:>
显然,使用Spring Shell开发的命令行应用程序与其他普通应用不同,启动之后停留在命令交互界面,等待用户输入。
目前还没有编写任何与业务相关的代码,输入help
命令看看。 shell:>helpAVAILABLE COMMANDSBuilt-In Commands clear: Clear the shell screen. exit, quit: Exit the shell. help: Display help about available commands. script: Read and execute commands from a file. stacktrace: Display the full stacktrace of the last error.shell:>
可以看到,Spring Shell已经内置了一些常用的命令,如:help
命令显示帮助信息,clear
命令清空命令行界面,exit
退出应用。
exit
命令退出应用程序。 实际上,Spring Shell默认就集成了Spring Boot。
如下,我们在pom.xml文件只添加Spring Shell依赖配置(不明确配置依赖Spring Boot):org.springframework.shell spring-shell-starter 2.0.0.RELEASE
项目依赖关系如下图所示:
简单示例
按照国际惯例,通过编写一个简单的“Hello,World!”程序来介绍Spring Shell的相关概念。
@ShellComponentpublic class HelloWorld { @ShellMethod("Say hello") public void hello(String name) { System.out.println("hello, " + name + "!"); }}
如上所示,HellWorld
是一个非常简单的Java类,在Spring Shell应用中Java类需要使用注解@ShellComponent
来修饰,类中的方法使用注解@ShellMethod
表示为一个具体的命令。
help
命令之后将会看到,默认情况下在Java类中定义的方法名就是在交互界面中可以使用的命令名称。 shell:>helpAVAILABLE COMMANDSBuilt-In Commands clear: Clear the shell screen. exit, quit: Exit the shell. help: Display help about available commands. script: Read and execute commands from a file. stacktrace: Display the full stacktrace of the last error.Hello World # 命令所属组名 hello: Say hello # 具体的命令shell:>hello Worldhello, World!shell:>
至此,一个简单的基于Spring Shell的命令行交互应用就完成了,下面对Spring Shell中的相关组件进行详细介绍。
注解@ShellMethod
默认情况下,使用注解@ShellMethod
修饰的Java方法名称就是具体的交互命令名称,如上述示例。
@ShellMethod
源码: @Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD})@Documentedpublic @interface ShellMethod { String INHERITED = ""; String[] key() default {}; // 设置命令名称 String value() default ""; // 设置命名描述 String prefix() default "--"; // 设置命令参数前缀,默认为“--” String group() default ""; // 设置命令分组}
还可以使用注解@ShellMethod
的属性key设置命令名称(注意:可以为一个命令设置多个名称)。
@ShellComponentpublic class Calculator { // 为一个命令指定多个名称 @ShellMethod(value = "Add numbers.", key = {"sum", "addition"}) public void add(int a, int b) { int sum = a + b; System.out.println(String.format("%d + %d = %d", a, b, sum)); }}
shell:>helpAVAILABLE COMMANDSBuilt-In Commands clear: Clear the shell screen. exit, quit: Exit the shell. help: Display help about available commands. script: Read and execute commands from a file. stacktrace: Display the full stacktrace of the last error.Calculator addition, sum: Add numbers.shell:>addition 1 21 + 2 = 3shell:>sum 1 21 + 2 = 3shell:>sum --a 1 --b 2 # 使用带命令参数前缀的方式1 + 2 = 3shell:>
显然,使用注解@ShellMethod
的key属性可以为方法指定多个命令名称,而且,此时方法名不再是可用的命令了。
// 1.使用属性value定义命令描述// 2.使用属性key定义命令名称// 3.使用属性prefix定义参数前缀// 4.使用属性group定义命令分组@ShellMethod(value = "Add numbers.", key = {"sum", "addition"}, prefix = "-", group = "Cal")public void add(int a, int b) { int sum = a + b; System.out.println(String.format("%d + %d = %d", a, b, sum));}
如下为自定义了注解@ShellMethod
各个属性之后的结果:
shell:>helpAVAILABLE COMMANDSBuilt-In Commands clear: Clear the shell screen. exit, quit: Exit the shell. help: Display help about available commands. script: Read and execute commands from a file. stacktrace: Display the full stacktrace of the last error.Cal addition, sum: Add numbers.shell:>sum --a 1 --b 2Too many arguments: the following could not be mapped to parameters: '--b 2'Details of the error have been omitted. You can use the stacktrace command to print the full stacktrace.shell:>sum -a 1 -b 21 + 2 = 3shell:>
显然,命令分组为自定义的“Cal”,命令参数前缀为自定义的“-”(此时将不能再使用默认的参数前缀“--”)。
注解@ShellOption
注解@ShellMethod
应用在Java方法上对命令进行定制,还可以使用注解@ShellOption
对命令参数进行定制。
自定义参数名称
@ShellMethod("Echo params")public void echo(int a, int b, @ShellOption("--third") int c) { System.out.println(String.format("a=%d, b=%d, c=%d", a, b, c));}
如上所示,使用注解@ShellOption
为第三个参数指定名称为“third”。
shell:>echo 1 2 3a=1, b=2, c=3shell:>echo --a 1 --b 2 --c 3 # 显然,当明确指定了参数名称之后,必须使用指定的名称Too many arguments: the following could not be mapped to parameters: '3'Details of the error have been omitted. You can use the stacktrace command to print the full stacktrace.shell:>echo --a 1 --b 2 --third 3a=1, b=2, c=3shell:>
使用注解@ShellOption
还可以为命令参数指定多个名称:
@ShellMethod("Echo command help")public void myhelp(@ShellOption({"-C", "--command"}) String cmd) { System.out.println(cmd);}
shell:>myhelp actionactionshell:>myhelp -C actionactionshell:>myhelp --command actionaction
设置参数默认值
还可以使用注解@ShellOption
通过属性“defaultValue”为参数指定默认值。
@ShellMethod("Say hello")public void hello(@ShellOption(defaultValue = "World") String name) { System.out.println("hello, " + name + "!");}
shell:>hello # 显然,当参数值为空时使用默认值hello, World!shell:>hello zhangsanhello, zhangsan!
为一个参数传递多个值
通常,一个命令参数只对应一个值,如果希望为一个参数传递多个值(对应Java中的数组或集合),可以使用注解@ShellOption
的属性arity指定参数值的个数。
// 参数为一个数组@ShellMethod("Add by array")public void addByArray(@ShellOption(arity = 3) int[] numbers) { int sum = 0; for(int number : numbers) { sum += number; } System.out.println(String.format("sum=%d", sum));}
shell:>add-by-array 1 2 3sum=6shell:>add-by-array --numbers 1 2 3sum=6shell:>add-by-array --numbers 1 2 3 4 # 传递的参数个数超过arity属性值时报错Too many arguments: the following could not be mapped to parameters: '4'Details of the error have been omitted. You can use the stacktrace command to print the full stacktrace.
// 参数为集合@ShellMethod("Add by list")public void addByList(@ShellOption(arity = 3) Listnumbers) { int s = 0; for(int number : numbers) { s += number; } System.out.println(String.format("s=%d", s));}
shell:>add-by-list 1 2 3s=6shell:>add-by-list --numbers 1 2 3s=6shell:>add-by-list --numbers 1 2 3 4 # 传递的参数个数超过arity属性值时报错Too many arguments: the following could not be mapped to parameters: '4'Details of the error have been omitted. You can use the stacktrace command to print the full stacktrace.
注意: 传递的参数个数不能大于@ShellOption
属性arity设置的值。
对布尔参数的特殊处理
// 参数为Boolean类型@ShellMethod("Shutdown action")public void shutdown(boolean shutdown) { System.out.println(String.format("shutdown=%s", shutdown));}
shell:>shutdown shutdown=falseshell:>shutdown --shutdownshutdown=trueshell:>shutdown --shutdown trueToo many arguments: the following could not be mapped to parameters: 'true'Details of the error have been omitted. You can use the stacktrace command to print the full stacktrace.
从上述示例可以知道,对于布尔类型的参数,默认值为false,当明确传递参数名时,值为true。
注意: 对于布尔参数值处理比较特别,无需像普通参数一样传递参数值,否则报错。带空格的参数处理
Spring Shell使用空格来分割参数,当需要传递带空格的参数时,需要将参数使用引号(单引号或者双引号)引起来。
// 带空格的参数需要使用引号引起来@ShellMethod("Echo.")public void echo(String what) { System.out.println(what);}
shell:>echo "Hello,World!"Hello,World!shell:>echo 'Hello,World!'Hello,World!shell:>echo "Hello,\"World!\""Hello,"World!"shell:>echo '\"Hello,World!\"'"Hello,World!"shell:>echo Hello World # 当参数值中包含空格时,需要使用引号引起来,否则报错Too many arguments: the following could not be mapped to parameters: 'World'Details of the error have been omitted. You can use the stacktrace command to print the full stacktrace.
参数校验
Spring Shell集成了,可用来实现参数校验。可支持参数校验的类型很多,如:是否为空,长度,最大值,最小值等等。
实现参数校验也是通过注解实现的,常用的参数校验注解有:@Size
(校验参数长度),@Max
(校验参数最大值),@Min
(校验参数最小值),@Pattern
(支持自定义正则表达式校验规则)。 // 使用@Size注解校验参数长度@ShellMethod("Change password")public void changePwd(@Size(min = 6, max = 30) String pwd) { System.out.println(pwd);}
shell:>change-pwd 123 # 当参数长度小于最小值6时报错The following constraints were not met: --pwd string : size must be between 6 and 30 (You passed '123')shell:>change-pwd 1234567890123456789012345678901 # 当参数长度大于最大值30时报错The following constraints were not met: --pwd string : size must be between 6 and 30 (You passed '1234567890123456789012345678901')shell:>change-pwd 1234567890 # 参数在指定范围是成功1234567890
Spring Shell支持的参数注解如下图所示:
动态命令可用性
如果存在这样一种场景:命令A是否可以执行需要依赖命令B的执行结果,换言之,当命令B的执行结果不满足条件时不允许执行命令A。
Spring Shell针对这个需求也做了支持,翻译为:动态命令可用性(Dynamic Command Availability),具体实现有2种方式。 这个概念理解起来有些生硬,简而言之:命令必须满足特定条件时才能被执行,也就说命令必须满足特定条件才可用。因为这个“特定条件”是在动态变化的,所以叫做“动态命令可用性”。为单一命令提供动态可用性
为单一命令提供动态可用性支持通过控制方法命名来实现。
@ShellComponentpublic class Downloader { private boolean connected = false; @ShellMethod("Connect server") public void connect() { connected = true; } @ShellMethod("Download file") public void download() { System.out.println("Downloaded."); } // 为命令download提供可用行支持 public Availability downloadAvailability() { return connected ? Availability.available():Availability.unavailable("you are not connected"); }}
shell:>download # download命令依赖connect命令的执行结果,因此在执行connect命令成功之前直接调用download命令时报错Command 'download' exists but is not currently available because you are not connectedDetails of the error have been omitted. You can use the stacktrace command to print the full stacktrace.shell:>connect shell:>download # 在执行命令connect成功之后再执行download命令时成功Downloaded.
显然,在这种方式下,必须为需要实现动态可用性的命令提供一个对应名称的方法(方法名必须是:“命令名 + Availability”,如:downloadAvailability),且方法的返回值必须为org.springframework.shell.Availability
对象。
为多个命令提供动态可用性
如果需要为多个命令提供动态可用性支持,使用注解@ShellMethodAvailability
才是比较明智的。
@ShellMethodAvailability
的使用方式又有2种: 1.在命令方法上使用@ShellMethodAvailability
指定提供动态可用性支持的方法名 private boolean connected = false;@ShellMethod("Connect server")public void connect() { connected = true;}@ShellMethod("Download")@ShellMethodAvailability({"connectCheck"})public void download() { System.out.println("Downloaded.");}@ShellMethod("Upload")@ShellMethodAvailability({"connectCheck"})public void upload() { System.out.println("Uploaded.");}public Availability connectCheck() { return connected ? Availability.available():Availability.unavailable("you are not connected");}
如上所示,在命令方法download()
和upload()
通过注解@ShellMethodAvailability
指定提供命令动态性实现的方法名:connectCheck,这样就可以很方便地实现使用一个方法为多个命令提供动态可用性支持。
2.直接在提供命令动态可用性支持的方法上使用注解@ShellMethodAvailability
指定命令方法名
另外一种实现用一个方法为多个命令提供动态可用性实现的方式是:直接在命令动态可用性方法上使用注解@ShellMethodAvailability
指定对应的命令方法名。
@ShellMethod("Download")public void download() { System.out.println("Downloaded.");}@ShellMethod("Upload")public void upload() { System.out.println("Uploaded.");}// 直接在提供命令动态可用性的方法上通过注解`@ShellMethodAvailability`指定命令方法名@ShellMethodAvailability({"download", "upload"})public Availability connectCheck() { return connected ? Availability.available():Availability.unavailable("you are not connected");}
命令动态可用性小结
1.使用了动态命令可用性的命令会在交互界面中显示一个星号提示,明确提示该命令的执行需要依赖指定状态(通常是其他命令的执行结果)。
Downloader connect: Connect server * download: Download # download和upload命令的执行都需要依赖指定状态 * upload: Upload# 说明标注星号的命令不可用,可以通过help命令查看帮助信息Commands marked with (*) are currently unavailable.Type `help` to learn more.shell:>help download # 通过help命令查看指定命令的帮助信息NAME download - DownloadSYNOPSYS download CURRENTLY UNAVAILABLE This command is currently not available because you are not connected.
2.不论如何,提供动态命令可用性的方法返回值必须是org.springframework.shell.Availability
类型对象。
命令分组
Spring Shell管理命令分组有3种实现方式,分别是:默认以类名为组名,使用注解@ShellMethod
的group属性指定组名,使用注解@ShellCommandGroup
指定组名。
默认命令分组规则
命令所在的组为其对应方法所在的Java类名称按驼峰法则分隔的名称(如:“HelloWord”为类名,则其中的命令组名为“Hello Word”),这是默认的命令组管理方式。
Hello World # 默认的命令组管理方式 hello: Say hello
使用@ShellMethod注解的group属性指定分组
通过注解@ShellMethod
的group属性指定命令所属的组名
@ShellComponentpublic class Cmd1 { @ShellMethod(value = "Cmd1 action1", group = "CMD") public void action11() { System.out.println("cmd1 action1"); } @ShellMethod(value = "Cmd1 action2", group = "CMD") public void action12() { System.out.println("cmd1 action2"); }}@ShellComponentpublic class Cmd2 { @ShellMethod(value = "Cmd2 action1", group = "CMD") public void action21() { System.out.println("cmd2 action1"); } @ShellMethod(value = "Cmd2 action2", group = "CMD") public void action22() { System.out.println("cmd2 action2"); }}
shell:>helpAVAILABLE COMMANDSCMD action11: Cmd1 action1 action12: Cmd1 action2 action21: Cmd2 action1 action22: Cmd2 action2
显然,使用注解@ShellMethod
的group属性可以将不同类的不同命令指定到同一个命令组下。
使用@ShellCommandGroup注解指定分组
在使用注解@ShellCommandGroup
指定命令分组时有2种方法:
@ShellCommandGroup
指定组名,则该类下的所有命令都属于该组 @ShellComponent@ShellCommandGroup("CMD")public class Cmd1 { @ShellMethod(value = "Cmd1 action1") public void action11() { System.out.println("cmd1 action1"); } @ShellMethod(value = "Cmd1 action2") public void action12() { System.out.println("cmd1 action2"); }}@ShellComponent@ShellCommandGroup("CMD")public class Cmd2 { @ShellMethod(value = "Cmd2 action1") public void action21() { System.out.println("cmd2 action1"); } @ShellMethod(value = "Cmd2 action2") public void action22() { System.out.println("cmd2 action2"); }}
# 使用注解@ShellCommandGroup将多个类中的命令指定到一个组下shell:>helpAVAILABLE COMMANDSCMD action11: Cmd1 action1 action12: Cmd1 action2 action21: Cmd2 action1 action22: Cmd2 action2
方法二: 在package-info.java
中使用注解@ShellCommandGroup
指定整个包下的所有类中的命令为一个组。
如下图所示,Cmd1.java和Cmd2.java都在包chench.org.extra.testspringshell.group
下,package-info.java
为对应的包描述类。
package chench.org.extra.testspringshell.group;import org.springframework.shell.standard.ShellComponent;import org.springframework.shell.standard.ShellMethod;@ShellComponentpublic class Cmd1 { @ShellMethod(value = "Cmd1 action1") public void action11() { System.out.println("cmd1 action1"); } @ShellMethod(value = "Cmd1 action2") public void action12() { System.out.println("cmd1 action2"); }}
Cmd2.java
package chench.org.extra.testspringshell.group;import org.springframework.shell.standard.ShellComponent;import org.springframework.shell.standard.ShellMethod;@ShellComponentpublic class Cmd2 { @ShellMethod(value = "Cmd2 action1") public void action21() { System.out.println("cmd2 action1"); } @ShellMethod(value = "Cmd2 action2") public void action22() { System.out.println("cmd2 action2"); }}
package-info.java:
// 在Java包描述类中通过注解`@ShellCommandGroup`为该包下的所有类中的命令指定统一组名@ShellCommandGroup("CMD")package chench.org.extra.testspringshell.group;import org.springframework.shell.standard.ShellCommandGroup;
shell:>helpAVAILABLE COMMANDSCMD action11: Cmd1 action1 action12: Cmd1 action2 action21: Cmd2 action1 action22: Cmd2 action2
注意: 通过注解@ShellCommandGroup
指定的命令分组可以被注解@ShellMethod
的group属性指定的组名覆盖。
内置命令
Spring Shell提供了5个内置命令:
shell:>helpAVAILABLE COMMANDSBuilt-In Commands clear: Clear the shell screen. # 清空命令行界面 exit, quit: Exit the shell. # 退出应用 help: Display help about available commands. # 显示帮助信息 script: Read and execute commands from a file. # 从文件中读取并执行批量命令 stacktrace: Display the full stacktrace of the last error. # 报错时读取异常堆栈信息
写在最后
Spring Shell大大简化了使用Java开发基于命令行交互应用的步骤,只需要简单配置,再使用相关注解就可以开发一个命令行应用了。
同时,Spring Shell还内置了一些有用的命令,如:help
,clear
,stacktrace
,exit
等。 另外,Spring Shell还支持实用TAB键补全命令,非常方便。 最后,需要特别注意: Spring Shell不允许出现同名的命令(虽然命令对应的同名方法虽然在不同的Java类中被允许,不会出现编译错误,但是运行时将报错,从而无法正确启动应用程序)。即:下面的情形是不允许的。
@ShellComponentpublic class Cmd1 { @ShellMethod(value = "Cmd1 action1") public void action1() { System.out.println("cmd1 action1"); } @ShellMethod(value = "Cmd1 action2") public void action2() { System.out.println("cmd1 action2"); }}@ShellComponentpublic class Cmd2 { @ShellMethod(value = "Cmd2 action1") public void action1() { System.out.println("cmd2 action1"); } @ShellMethod(value = "Cmd2 action2") public void action2() { System.out.println("cmd2 action2"); }}
除非使用注解@ShellMethod
的key属性不同的命令指定为不同的名称,如下所示:
// 使用注解`@ShellMethod`的key属性不同的命令指定为不同的名称@ShellComponentpublic class Cmd1 { @ShellMethod(value = "Cmd1 action1", key = {"cmd11"}) public void action1() { System.out.println("cmd1 action1"); } @ShellMethod(value = "Cmd1 action2", key = {"cmd12"}) public void action2() { System.out.println("cmd1 action2"); }}@ShellComponentpublic class Cmd2 { @ShellMethod(value = "Cmd2 action1", key = {"cmd21"}) public void action1() { System.out.println("cmd2 action1"); } @ShellMethod(value = "Cmd2 action2", key = {"cmd22"}) public void action2() { System.out.println("cmd2 action2"); }}
shell:>helpAVAILABLE COMMANDSCMD cmd11: Cmd1 action1 cmd12: Cmd1 action2 cmd21: Cmd2 action1 cmd22: Cmd2 action2