使用 Spring Shell 開發 Java 命令行應用

提到 Java,大家都會想到 Java 在服務器端應用開發中的使用。實際上,Java 在命令行應用的開發中也有一席之地。在很多情況下,相對於圖形用戶界面來說,命令行界面響應速度快,所佔用的系統資源少。在與用戶進行交互的場景比較單一時,命令行界面是更好的選擇。命令行界面有其固定的交互模式。通常是由用戶輸入一系列的參數,在執行之後把相應的結果在控制檯輸出。命令行應用通常需要處理輸入參數的傳遞和驗證、輸出結果的格式化等任務。Spring Shell 可以幫助簡化這些常見的任務,讓開發人員專注於實現應用的業務邏輯。本文對 Spring Shell 進行詳細的介紹。

Spring Shell 入門

最簡單的創建 Spring Shell 應用的方式是使用 Spring Boot。從 Spring Initializr 網站(http://start.spring.io/)上創建一個新的基於 Apache Maven 的 Spring Boot 應用,然後添加 Spring Shell 相關的依賴即可。本文介紹的是 Spring Shell 2.0.0.M2 版本,目前還只是 Milestone 版本,因此需要在 pom.xml 中添加 Spring 提供的包含 Milestone 版本工件的 Maven 倉庫,如代碼清單 1 所示。否則的話,Maven 會無法找到相應的工件。

清單 1. 添加 Spring Shell 的 Maven 倉庫
1
2
3
4
5
6
7
< repositories >
   < repository >
     < id >spring-milestone</ id >
     < name >Spring Repository</ name >
     < url >https://repo.spring.io/milestone</ url >
   </ repository >
</ repositories >

在添加了 Spring Shell 的 Maven 倉庫之後,可以在 Spring Boot 項目中添加對於spring-shell-starter的依賴,如代碼清單 2 所示。

清單 2. 添加 Spring Shell 所需 Maven 依賴
1
2
3
4
5
< 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. 輸出問候語的命令行應用
1
2
3
4
5
6
7
8
9
10
11
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
 
@ShellComponent
public class GreetingApp {
 
   @ShellMethod("Say hi")
   public String sayHi(String name) {
     return String.format("Hi %s", name);
   }
}

接下來我們運行該應用。運行起來之後,該應用直接進入命令行提示界面,我們可以輸入 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. 包含多個參數的命令方法
1
2
3
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. 指定參數名稱
1
2
3
4
@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. 指定方法名稱和參數默認值
1
2
3
4
5
6
7
@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. 參數對應多個值
1
2
3
4
@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. 校驗參數
1
2
3
4
5
6
7
8
9
10
11
12
@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.springframework.shell.result.IterableResultHandler。IterableResultHandler 負責處理 Iterable 類型的結果對象。對於 Iterable 中包含的每個對象,把實際的處理請求代理給另外一個 ResultHandler 來完成。IterableResultHandler 默認的代理實現是類 org.springframework.shell.result.TypeHierarchyResultHandler。TypeHierarchyResultHandler 其實是一個複合的處理器,它會把對於不同類型結果的 ResultHandler 接口的實現進行註冊,然後根據結果的類型來選擇相應的處理器實現。如果找不到類型完全匹配的處理器實現,則會沿着結果類型的層次結構樹往上查找,直到找到對應的處理器實現。Spring Shell 提供了對於 Object 類型結果的處理實現類 org.springframework.shell.result.DefaultResultHandler,因此所有的結果類型都可以得到處理。DefaultResultHandler 所做的處理只是把 Object 類型轉換成 String,然後輸出到控制檯。

瞭解了 Spring Shell 對於結果的處理方式之後,我們可以添加自己所需要的特定結果類型的處理實現。代碼清單 9 給了一個作爲示例的處理結果類 PrefixedResult。PrefixedResult 中包含一個前綴 prefix 和實際的結果 result。

清單 9. 帶前綴的處理結果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class PrefixedResult {
   private final String prefix;
   private final String result;
 
   public PrefixedResult(String prefix, String result) {
     this.prefix = prefix;
     this.result = result;
   }
 
   public String getPrefix() {
     return prefix;
   }
 
   public String getResult() {
     return result;
   }
}

在代碼清單 10 中,我們爲 PrefixedResult 添加了具體的處理器實現。該實現也非常簡單,只是把結果按照某個格式進行輸出。

清單 10. PrefixedResult 對應的處理器實現
1
2
3
4
5
6
7
8
@Component
public class PrefixedResultHandler implements ResultHandler< PrefixedResult > {
 
   @Override
   public void handleResult(PrefixedResult result) {
     System.out.printf("%s --> %s%n", result.getPrefix(), result.getResult());
   }
}

在代碼清單 11 中,命令方法 resultHandler 返回的是一個 PrefixedResult 對象,因此會被代碼清單 10 中的處理器來進行處理,輸出相應的結果。

清單 11. 使用 PrefixedResult 的命令
1
2
3
4
5
6
7
@ShellComponent
public class CustomResultHandlerApp {
   @ShellMethod("Result handler")
   public PrefixedResult resultHandler() {
     return new PrefixedResult("PRE", "Hello!");
   }
}

代碼清單 12 給出了具體的命令運行結果。

清單 12. 命令的處理結果
1
2
myshell=>result-handler
PRE --> Hello!

自定義提示符

在啓動命令行應用時,會發現該應用使用的是默認提示符"shell:>"。該提示符是可以定製的,只需要提供接口 org.springframework.shell.jline.PromptProvider 的實現即可。接口 PromptProvider 中只有一個方法,用來返回類型爲 org.jline.utils.AttributedString 的提示符。在代碼清單 13 中,我們定義了一個 PromptProvider 接口的實現類,並使用"myshell=>"作爲提示符,而且顏色爲藍色。

清單 13. 自定義提示符
1
2
3
4
5
@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. 動態命令可用性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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
1
2
3
4
5
6
7
8
9
10
11
12
13
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
1
2
3
4
5
6
7
8
9
10
11
12
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"));
10
11
12
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) {
     return users.get(id);
   }
}

在代碼清單 17 中,UserConverter 實現了 Spring 中的 Converter 接口並添加了從 String 到 User 對象的轉換邏輯,即通過 UserService 來進行查找�

相關文章
相關標籤/搜索