Spring Shell開發(fā)自定義命令行工具詳解
Spring Shell是Spring生態(tài)系統(tǒng)中的一個(gè)強(qiáng)大組件,它允許開發(fā)者輕松創(chuàng)建功能豐富的命令行應(yīng)用程序。
當(dāng)與Spring Boot結(jié)合使用時(shí),可以快速構(gòu)建出專業(yè)級別的CLI工具。
本文將介紹如何在Spring Boot項(xiàng)目中集成和使用Spring Shell,開發(fā)自定義命令,以及一些高級特性。
1. Spring Shell簡介

Spring Shell是一個(gè)交互式shell框架,它提供了一種通過命令行與應(yīng)用程序交互的方式。
它支持自動(dòng)補(bǔ)全、幫助文檔生成、命令歷史和各種交互式功能,使命令行工具更加用戶友好。
Spring Shell的主要特性包括:
- 類似于Bash的交互體驗(yàn)
- Tab鍵自動(dòng)補(bǔ)全功能
- 內(nèi)置幫助系統(tǒng)
- 命令歷史記錄
- 參數(shù)驗(yàn)證和轉(zhuǎn)換
- 命令分組和可擴(kuò)展性
2. 在SpringBoot項(xiàng)目中集成Spring Shell
添加依賴
首先,在您的Spring Boot項(xiàng)目的pom.xml中添加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 https://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>3.4.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>demo</groupId>
<artifactId>springboot-cmd</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.shell</groupId>
<artifactId>spring-shell-starter</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.18</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>21</source>
<target>21</target>
<encoding>utf-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
基本配置
@SpringBootApplication
public class ShellToolApplication {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(ShellToolApplication.class);
application.setBannerMode(Banner.Mode.OFF);
application.run(args);
}
@Bean
public PromptProvider shellPromptProvider() {
return () -> new AttributedString("boot-shell:>", AttributedStyle.DEFAULT.foreground(AttributedStyle.YELLOW));
}
}
spring:
shell:
interactive:
enabled: true
3. 開發(fā)自定義命令
在Spring Shell中,命令是通過在標(biāo)有@ShellComponent注解的類中創(chuàng)建標(biāo)有@ShellMethod注解的方法來定義的。
創(chuàng)建基本命令
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
@ShellComponent
public class MyCommands {
@ShellMethod("顯示歡迎消息")
public String hello(String name) {
return "你好, " + name + "!";
}
@ShellMethod("計(jì)算兩個(gè)數(shù)字的和")
public int add(int a, int b) {
return a + b;
}
// 注意: 如果方法名為駝峰式命名,則shell使用時(shí)需要使用-分隔符,如addBig -> add-big
@ShellMethod(value = "計(jì)算兩個(gè)數(shù)字的和,注意大小寫")
public int addBig(int a, int b) {
return a + b;
}
// 關(guān)于駝峰命名也可以使用key屬性進(jìn)行指定,則shell使用時(shí)仍然是駝峰式命名,如addSmall -> addSmall
@ShellMethod(value = "計(jì)算兩個(gè)數(shù)字的和,注意大小寫",key = {"addSmall"})
public int addSmall(int a, int b) {
return a + b;
}
}
使用上面的代碼,當(dāng)您運(yùn)行應(yīng)用程序時(shí),將可以使用hello和add命令:
shell:>hello 世界
你好, 世界!
shell:>add 5 3
8
命令參數(shù)配置
Spring Shell提供了豐富的參數(shù)配置選項(xiàng):
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellOption;
@ShellComponent
public class AdvancedCommands {
@ShellMethod("使用高級參數(shù)選項(xiàng)的示例")
public String greet(
@ShellOption(defaultValue = "世界") String name,
@ShellOption(help = "決定是否使用大寫", defaultValue = "false") boolean uppercase
) {
String greeting = "你好, " + name + "!";
return uppercase ? greeting.toUpperCase() : greeting;
}
}
4. 高級功能
命令分組
您可以使用@ShellCommandGroup注解對命令進(jìn)行分組:
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellCommandGroup;
@ShellComponent
@ShellCommandGroup("文件操作")
public class FileCommands {
@ShellMethod("列出目錄內(nèi)容")
public String ls(String path) {
// 實(shí)現(xiàn)列出目錄內(nèi)容的邏輯
return "列出 " + path + " 的內(nèi)容";
}
@ShellMethod("創(chuàng)建新目錄")
public String mkdir(String dirName) {
// 實(shí)現(xiàn)創(chuàng)建目錄的邏輯
return "創(chuàng)建目錄: " + dirName;
}
}
命令可用性控制
可以使用Availability來控制命令在不同情況下的可用性:
import org.springframework.shell.Availability;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellMethodAvailability;
@ShellComponent
public class SecurityCommands {
private boolean loggedIn = false;
@ShellMethod("登錄系統(tǒng)")
public String login(String username, String password) {
// 實(shí)現(xiàn)登錄邏輯
this.loggedIn = true;
return "用戶 " + username + " 已登錄";
}
@ShellMethod("查看敏感信息")
public String query() {
return "這是敏感信息,只有登錄后才能查看";
}
@ShellMethodAvailability("query")
public Availability viewSecretInfoAvailability() {
return loggedIn
? Availability.available()
: Availability.unavailable("您需要先登錄才能查看敏感信息");
}
}
自定義提示符
您可以通過實(shí)現(xiàn)PromptProvider接口來自定義提示符:
@Bean
public PromptProvider shellPromptProvider() {
return () -> new AttributedString("my-shell:>", AttributedStyle.DEFAULT.foreground(AttributedStyle.YELLOW));
}
5. 一個(gè)工具示例
下面是一個(gè)更完整的示例,展示如何創(chuàng)建一個(gè)簡單的HTTP訪問CLI工具
package com.example.cmd.shell;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import org.springframework.shell.standard.ShellCommandGroup;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellOption;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ShellComponent
@ShellCommandGroup("HTTP請求")
public class HttpClientCommands {
private String baseUrl = "";
private final Map<String, String> headers = new HashMap<>();
private final List<String> requestHistory = new ArrayList<>();
@ShellMethod("設(shè)置基礎(chǔ)URL")
public String setBaseUrl(String url) {
this.baseUrl = url;
return "基礎(chǔ)URL已設(shè)置為: " + url;
}
@ShellMethod("添加HTTP請求頭")
public String addHeader(String name, String value) {
headers.put(name, value);
return "已添加請求頭: " + name + " = " + value;
}
@ShellMethod("清除所有HTTP請求頭")
public String clearHeaders() {
int count = headers.size();
headers.clear();
return "已清除 " + count + " 個(gè)請求頭";
}
@ShellMethod("顯示當(dāng)前配置")
public String showConfig() {
StringBuilder sb = new StringBuilder();
sb.append("基礎(chǔ)URL: ").append(baseUrl).append("\n");
sb.append("請求頭:\n");
if (headers.isEmpty()) {
sb.append(" (無)\n");
} else {
headers.forEach((name, value) ->
sb.append(" ").append(name).append(": ").append(value).append("\n"));
}
return sb.toString();
}
@ShellMethod("發(fā)送GET請求")
public String get(
@ShellOption(help = "請求的路徑或完整URL") String path,
@ShellOption(help = "是否顯示響應(yīng)頭", defaultValue = "false") boolean showHeaders
) {
String url = buildUrl(path);
requestHistory.add("GET " + url);
try {
HttpRequest request = HttpRequest.get(url);
addHeadersToRequest(request);
HttpResponse response = request.execute();
return formatResponse(response, showHeaders);
} catch (Exception e) {
return "請求失敗: " + e.getMessage();
}
}
@ShellMethod("發(fā)送POST請求")
public String post(
@ShellOption(help = "請求的路徑或完整URL") String path,
@ShellOption(help = "POST請求體(JSON)") String body,
@ShellOption(help = "是否顯示響應(yīng)頭", defaultValue = "false") boolean showHeaders
) {
String url = buildUrl(path);
requestHistory.add("POST " + url);
try {
HttpRequest request = HttpRequest.post(url);
addHeadersToRequest(request);
// 添加Content-Type請求頭,如果沒有設(shè)置
if (!headers.containsKey("Content-Type")) {
request.header("Content-Type", "application/json");
}
HttpResponse response = request.body(body).execute();
return formatResponse(response, showHeaders);
} catch (Exception e) {
return "請求失敗: " + e.getMessage();
}
}
@ShellMethod("發(fā)送PUT請求")
public String put(
@ShellOption(help = "請求的路徑或完整URL") String path,
@ShellOption(help = "PUT請求體(JSON)") String body,
@ShellOption(help = "是否顯示響應(yīng)頭", defaultValue = "false") boolean showHeaders
) {
String url = buildUrl(path);
requestHistory.add("PUT " + url);
try {
HttpRequest request = HttpRequest.put(url);
addHeadersToRequest(request);
// 添加Content-Type請求頭,如果沒有設(shè)置
if (!headers.containsKey("Content-Type")) {
request.header("Content-Type", "application/json");
}
HttpResponse response = request.body(body).execute();
return formatResponse(response, showHeaders);
} catch (Exception e) {
return "請求失敗: " + e.getMessage();
}
}
@ShellMethod("發(fā)送DELETE請求")
public String delete(
@ShellOption(help = "請求的路徑或完整URL") String path,
@ShellOption(help = "是否顯示響應(yīng)頭", defaultValue = "false") boolean showHeaders
) {
String url = buildUrl(path);
requestHistory.add("DELETE " + url);
try {
HttpRequest request = HttpRequest.delete(url);
addHeadersToRequest(request);
HttpResponse response = request.execute();
return formatResponse(response, showHeaders);
} catch (Exception e) {
return "請求失敗: " + e.getMessage();
}
}
@ShellMethod("下載文件")
public String download(
@ShellOption(help = "文件URL") String url,
@ShellOption(help = "保存路徑") String savePath
) {
requestHistory.add("DOWNLOAD " + url);
try {
long fileSize = HttpUtil.downloadFile(url, savePath);
return "文件下載成功,大小: " + fileSize + " 字節(jié),保存路徑: " + savePath;
} catch (Exception e) {
return "文件下載失敗: " + e.getMessage();
}
}
@ShellMethod("上傳文件")
public String upload(
@ShellOption(help = "上傳URL") String url,
@ShellOption(help = "文件參數(shù)名") String paramName,
@ShellOption(help = "文件路徑") String filePath,
@ShellOption(help = "是否顯示響應(yīng)頭", defaultValue = "false") boolean showHeaders
) {
requestHistory.add("UPLOAD " + url);
try {
HttpRequest request = HttpRequest.post(url);
addHeadersToRequest(request);
request.form(paramName, new java.io.File(filePath));
HttpResponse response = request.execute();
return formatResponse(response, showHeaders);
} catch (Exception e) {
return "文件上傳失敗: " + e.getMessage();
}
}
@ShellMethod("格式化JSON")
public String formatJson(
@ShellOption(help = "JSON字符串") String json
) {
try {
return JSONUtil.formatJsonStr(json);
} catch (Exception e) {
return "JSON格式化失敗: " + e.getMessage();
}
}
private String buildUrl(String path) {
if (path.toLowerCase().startsWith("http")) {
return path;
}
if (baseUrl.isEmpty()) {
return path;
}
if (baseUrl.endsWith("/") && path.startsWith("/")) {
return baseUrl + path.substring(1);
} else if (!baseUrl.endsWith("/") && !path.startsWith("/")) {
return baseUrl + "/" + path;
} else {
return baseUrl + path;
}
}
private void addHeadersToRequest(HttpRequest request) {
headers.forEach(request::header);
}
private String formatResponse(HttpResponse response, boolean showHeaders) {
StringBuilder sb = new StringBuilder();
sb.append("狀態(tài)碼: ").append(response.getStatus()).append("\n");
if (showHeaders) {
sb.append("響應(yīng)頭:\n");
response.headers().forEach((name, values) -> {
sb.append(" ").append(name).append(": ");
sb.append(String.join(", ", values)).append("\n");
});
sb.append("\n");
}
sb.append("響應(yīng)體:\n");
// 嘗試格式化JSON響應(yīng)體
String body = response.body();
if (body != null && !body.isEmpty()) {
try {
if (body.trim().startsWith("{") || body.trim().startsWith("[")) {
body = JSONUtil.formatJsonStr(body);
}
} catch (Exception ignored) {
// 如果不是有效的JSON,直接使用原始響應(yīng)體
}
}
sb.append(body);
return sb.toString();
}
}
6. 內(nèi)置命令清單
Spring Shell 默認(rèn)提供以下內(nèi)置命令,這些命令在應(yīng)用啟動(dòng)后可直接使用,無需額外配置。各命令功能及典型用法如下
| 命令名稱 | 功能描述 | 典型用法 |
|---|---|---|
| help | 查看所有可用命令及其描述(輸入 help <命令>可查看特定命令詳情) | shell:> help``shell:> help add |
| clear | 清空控制臺輸出(支持快捷鍵 Ctrl + L) | shell:> clear |
| exit/ quit | 退出 Shell 應(yīng)用(二者功能相同) | shell:> exit |
| script | 從文件批量執(zhí)行命令(需提供絕對路徑) | shell:> script /tmp/commands.txt |
| stacktrace | 顯示最近一次異常的完整堆棧信息(默認(rèn)只顯示簡略錯(cuò)誤) | 發(fā)生異常后輸入:`shell:> stacktrace`` |
| history | 顯示歷史命令 | shell:> history |
到此這篇關(guān)于Spring Shell開發(fā)自定義命令行工具詳解的文章就介紹到這了,更多相關(guān)Spring Shell命令行內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Mybatis-Plus3.2.0 MetaObjectHandler 無法進(jìn)行公共字段全局填充
這篇文章主要介紹了Mybatis-Plus3.2.0 MetaObjectHandler 無法進(jìn)行公共字段全局填充,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11
Springboot升級至2.4.0中出現(xiàn)的跨域問題分析及修改方案
這篇文章主要介紹了Springboot升級至2.4.0中出現(xiàn)的跨域問題分析及修改方案,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-12-12
SpringCloud中的Feign遠(yuǎn)程調(diào)用接口傳參失敗問題
這篇文章主要介紹了SpringCloud中的Feign遠(yuǎn)程調(diào)用接口傳參失敗問題,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03
SpringBoot?快速實(shí)現(xiàn)?api?接口加解密功能
在項(xiàng)目中,為了保證數(shù)據(jù)的安全,我們常常會(huì)對傳遞的數(shù)據(jù)進(jìn)行加密,Spring?Boot接口加密,可以對返回值、參數(shù)值通過注解的方式自動(dòng)加解密,這篇文章主要介紹了SpringBoot?快速實(shí)現(xiàn)?api?接口加解密功能,感興趣的朋友一起看看吧2023-10-10
java中map和對象互轉(zhuǎn)工具類的實(shí)現(xiàn)示例
這篇文章主要介紹了java中map和對象互轉(zhuǎn)工具類的實(shí)現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08
SpringBoot自定義線程池,執(zhí)行定時(shí)任務(wù)方式
這篇文章主要介紹了SpringBoot自定義線程池,執(zhí)行定時(shí)任務(wù)方式,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-04-04

