Spring Shell入門介紹

2Simple發表於2019-06-21

目錄

Spring Shell是什麼

Spring Shell是Spring生態中的一員,用於開發命令列應用程式,官網:https://projects.spring.io/spring-shell/ 。
Spring Shell構建在JLine之上,整合Bean Validation API實現命令引數校驗。
從2.0版本開始,Spring Shell還可以非常方便地與Spring Boot進行整合,直接使用Spring Boot提供的一些非常實用的功能(如:打包可執行jar檔案)。
Spring Shell是什麼

入門實踐

使用Spring Shell非常簡單,直接新增對應的依賴配置即可,而為了使用Spring Boot提供的便利性,通常都是與Spring Boot整合使用。

基礎配置

整合Spring Boot本質上就是先新建一個Spring Boot工程,然後新增Spring Shell依賴即可,如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>chench.org.extra</groupId>
    <artifactId>test-springshell</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>test-springshell</name>
    <description>Test Spring Shell</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!-- 在Spring Boot專案中新增Spring Shell依賴 -->
        <dependency>
            <groupId>org.springframework.shell</groupId>
            <artifactId>spring-shell-starter</artifactId>
            <version>2.0.0.RELEASE</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <!-- 打包可執行檔案 -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

新增完上述配置之後,一個基於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: default
2019-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:>help
AVAILABLE COMMANDS

Built-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命令退出應用程式。

簡單示例

按照國際慣例,通過編寫一個簡單的“Hello,World!”程式來介紹Spring Shell的相關概念。

@ShellComponent
public 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:>help
AVAILABLE COMMANDS

Built-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 World
hello, World!
shell:>

至此,一個簡單的基於Spring Shell的命令列互動應用就完成了,下面對Spring Shell中的相關元件進行詳細介紹。

註解@ShellMethod

預設情況下,使用註解@ShellMethod修飾的Java方法名稱就是具體的互動命令名稱,如上述示例。
追溯註解@ShellMethod原始碼:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface ShellMethod {
    String INHERITED = "";

    String[] key() default {};    // 設定命令名稱

    String value() default "";    // 設定命名描述

    String prefix() default "--"; // 設定命令引數字首,預設為“--”

    String group() default "";    // 設定命令分組
}

還可以使用註解@ShellMethod的屬性key設定命令名稱(注意:可以為一個命令設定多個名稱)。

@ShellComponent
public 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:>help
AVAILABLE COMMANDS

Built-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 2
1 + 2 = 3
shell:>sum 1 2
1 + 2 = 3
shell:>sum --a 1 --b 2  # 使用帶命令引數字首的方式
1 + 2 = 3
shell:>

顯然,使用註解@ShellMethod的key屬性可以為方法指定多個命令名稱,而且,此時方法名不再是可用的命令了。
除了可以自定義命令名稱,還可以自定義命令引數字首(預設為“--”)和命令分組(預設為命令對應Java方法所在的類名稱)。

// 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:>help
AVAILABLE COMMANDS

Built-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 2
Too 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 2
1 + 2 = 3
shell:>

顯然,命令分組為自定義的“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 3
a=1, b=2, c=3
shell:>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 3
a=1, b=2, c=3
shell:>

使用註解@ShellOption還可以為命令引數指定多個名稱:

@ShellMethod("Echo command help")
public void myhelp(@ShellOption({"-C", "--command"}) String cmd) {
    System.out.println(cmd);
}
shell:>myhelp action
action
shell:>myhelp -C action
action
shell:>myhelp --command action
action

設定引數預設值

還可以使用註解@ShellOption通過屬性“defaultValue”為引數指定預設值。

@ShellMethod("Say hello")
public void hello(@ShellOption(defaultValue = "World") String name) {
    System.out.println("hello, " + name + "!");
}
shell:>hello          # 顯然,當引數值為空時使用預設值
hello, World!
shell:>hello zhangsan
hello, 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 3
sum=6
shell:>add-by-array --numbers 1 2 3
sum=6
shell:>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) List<Integer> numbers) {
    int s = 0;
    for(int number : numbers) {
        s += number;
    }
    System.out.println(String.format("s=%d", s));
}
shell:>add-by-list 1 2 3
s=6
shell:>add-by-list --numbers 1 2 3
s=6
shell:>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=false
shell:>shutdown --shutdown
shutdown=true
shell:>shutdown --shutdown true
Too 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整合了Bean Validation API,可用來實現引數校驗。可支援引數校驗的型別很多,如:是否為空,長度,最大值,最小值等等。
實現引數校驗也是通過註解實現的,常用的引數校驗註解有:@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支援的引數註解如下圖所示:
Spring Shell支援的引數校驗註解

動態命令可用性

如果存在這樣一種場景:命令A是否可以執行需要依賴命令B的執行結果,換言之,當命令B的執行結果不滿足條件時不允許執行命令A。
Spring Shell針對這個需求也做了支援,翻譯為:動態命令可用性(Dynamic Command Availability),具體實現有2種方式。
這個概念理解起來有些生硬,簡而言之:命令必須滿足特定條件時才能被執行,也就說命令必須滿足特定條件才可用。因為這個“特定條件”是在動態變化的,所以叫做“動態命令可用性”。

為單一命令提供動態可用性

為單一命令提供動態可用性支援通過控制方法命名來實現。

@ShellComponent
public 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 connected
Details 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

2.不論如何,提供動態命令可用性的方法返回值必須是org.springframework.shell.Availability型別物件。

命令分組

Spring Shell管理命令分組有3種實現方式,分別是:預設以類名為組名,使用註解@ShellMethod的group屬性指定組名,使用註解@ShellCommandGroup指定組名。

預設命令分組規則

命令所在的組為其對應方法所在的Java類名稱按駝峰法則分隔的名稱(如:“HelloWord”為類名,則其中的命令組名為“Hello Word”),這是預設的命令組管理方式。

Hello World      # 預設的命令組管理方式
        hello: Say hello

使用@ShellMethod註解的group屬性指定分組

通過註解@ShellMethod的group屬性指定命令所屬的組名

@ShellComponent
public 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");
    }
}

@ShellComponent
public 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:>help
AVAILABLE COMMANDS

CMD
        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:>help
AVAILABLE COMMANDS

CMD
        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為對應的包描述類。
通過包描述類對命令進行分組
Cmd1.java:

package chench.org.extra.testspringshell.group;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
@ShellComponent
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");
    }
}

Cmd2.java

package chench.org.extra.testspringshell.group;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
@ShellComponent
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");
    }
}

package-info.java:

// 在Java包描述類中通過註解`@ShellCommandGroup`為該包下的所有類中的命令指定統一組名
@ShellCommandGroup("CMD")
package chench.org.extra.testspringshell.group;
import org.springframework.shell.standard.ShellCommandGroup;
shell:>help
AVAILABLE COMMANDS

CMD
        action11: Cmd1 action1
        action12: Cmd1 action2
        action21: Cmd2 action1
        action22: Cmd2 action2

注意: 通過註解@ShellCommandGroup指定的命令分組可以被註解@ShellMethod的group屬性指定的組名覆蓋。

內建命令

Spring Shell提供了5個內建命令:

shell:>help
AVAILABLE COMMANDS

Built-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還內建了一些有用的命令,如:helpclearstacktraceexit等。
另外,Spring Shell還支援實用TAB鍵補全命令,非常方便。

最後,需要特別注意: Spring Shell不允許出現同名的命令(雖然命令對應的同名方法雖然在不同的Java類中被允許,不會出現編譯錯誤,但是執行時將報錯,從而無法正確啟動應用程式)。即:下面的情形是不允許的。

@ShellComponent
public 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");
    }
}

@ShellComponent
public 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屬性不同的命令指定為不同的名稱
@ShellComponent
public 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");
    }
}

@ShellComponent
public 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:>help
AVAILABLE COMMANDS

CMD
        cmd11: Cmd1 action1
        cmd12: Cmd1 action2
        cmd21: Cmd2 action1
        cmd22: Cmd2 action2

相關文章