Spring Shell是Spring生态系统中的一个强大组件,它允许开发者轻松创建功能丰富的命令行应用程序。
当与Spring Boot结合使用时,可以快速构建出专业级别的CLI工具。
本文将介绍如何在Spring Boot项目中集成和使用Spring Shell,开发自定义命令,以及一些高级特性。
1. Spring Shell简介
Spring Shell是一个交互式shell框架,它提供了一种通过命令行与应用程序交互的方式。
它支持自动补全、帮助文档生成、命令历史和各种交互式功能,使命令行工具更加用户友好。
Spring Shell的主要特性包括:
- 类似于Bash的交互体验
- Tab键自动补全功能
- 内置帮助系统
- 命令历史记录
- 参数验证和转换
- 命令分组和可扩展性
2. 在SpringBoot项目中集成Spring Shell
添加依赖
首先,在您的Spring Boot项目的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. 开发自定义命令
在Spring Shell中,命令是通过在标有@ShellComponent
注解的类中创建标有@ShellMethod
注解的方法来定义的。
创建基本命令
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("计算两个数字的和")
public int add(int a, int b) {
return a + b;
}
// 注意: 如果方法名为驼峰式命名,则shell使用时需要使用-分隔符,如addBig -> add-big
@ShellMethod(value = "计算两个数字的和,注意大小写")
public int addBig(int a, int b) {
return a + b;
}
// 关于驼峰命名也可以使用key属性进行指定,则shell使用时仍然是驼峰式命名,如addSmall -> addSmall
@ShellMethod(value = "计算两个数字的和,注意大小写",key = {"addSmall"})
public int addSmall(int a, int b) {
return a + b;
}
}
使用上面的代码,当您运行应用程序时,将可以使用hello
和add
命令:
shell:>hello 世界
你好, 世界!
shell:>add 5 3
8
命令参数配置
Spring Shell提供了丰富的参数配置选项:
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellOption;
@ShellComponent
public class AdvancedCommands {
@ShellMethod("使用高级参数选项的示例")
public String greet(
@ShellOption(defaultValue = "世界") String name,
@ShellOption(help = "决定是否使用大写", defaultValue = "false") boolean uppercase
) {
String greeting = "你好, " + name + "!";
return uppercase ? greeting.toUpperCase() : greeting;
}
}
4. 高级功能
命令分组
您可以使用@ShellCommandGroup
注解对命令进行分组:
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellCommandGroup;
@ShellComponent
@ShellCommandGroup("文件操作")
public class FileCommands {
@ShellMethod("列出目录内容")
public String ls(String path) {
// 实现列出目录内容的逻辑
return "列出 " + path + " 的内容";
}
@ShellMethod("创建新目录")
public String mkdir(String dirName) {
// 实现创建目录的逻辑
return "创建目录: " + 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("登录系统")
public String login(String username, String password) {
// 实现登录逻辑
this.loggedIn = true;
return "用户 " + username + " 已登录";
}
@ShellMethod("查看敏感信息")
public String query() {
return "这是敏感信息,只有登录后才能查看";
}
@ShellMethodAvailability("query")
public Availability viewSecretInfoAvailability() {
return loggedIn
? Availability.available()
: Availability.unavailable("您需要先登录才能查看敏感信息");
}
}
自定义提示符
您可以通过实现PromptProvider
接口来自定义提示符:
@Bean
public PromptProvider shellPromptProvider() {
return () -> new AttributedString("my-shell:>", AttributedStyle.DEFAULT.foreground(AttributedStyle.YELLOW));
}
5. 一个工具示例
下面是一个更完整的示例,展示如何创建一个简单的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("设置基础URL")
public String setBaseUrl(String url) {
this.baseUrl = url;
return "基础URL已设置为: " + 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 + " 个请求头";
}
@ShellMethod("显示当前配置")
public String showConfig() {
StringBuilder sb = new StringBuilder();
sb.append("基础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("发送GET请求")
public String get(
@ShellOption(help = "请求的路径或完整URL") String path,
@ShellOption(help = "是否显示响应头", 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("发送POST请求")
public String post(
@ShellOption(help = "请求的路径或完整URL") String path,
@ShellOption(help = "POST请求体(JSON)") String body,
@ShellOption(help = "是否显示响应头", defaultValue = "false") boolean showHeaders
) {
String url = buildUrl(path);
requestHistory.add("POST " + url);
try {
HttpRequest request = HttpRequest.post(url);
addHeadersToRequest(request);
// 添加Content-Type请求头,如果没有设置
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("发送PUT请求")
public String put(
@ShellOption(help = "请求的路径或完整URL") String path,
@ShellOption(help = "PUT请求体(JSON)") String body,
@ShellOption(help = "是否显示响应头", defaultValue = "false") boolean showHeaders
) {
String url = buildUrl(path);
requestHistory.add("PUT " + url);
try {
HttpRequest request = HttpRequest.put(url);
addHeadersToRequest(request);
// 添加Content-Type请求头,如果没有设置
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("发送DELETE请求")
public String delete(
@ShellOption(help = "请求的路径或完整URL") String path,
@ShellOption(help = "是否显示响应头", 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 + " 字节,保存路径: " + savePath;
} catch (Exception e) {
return "文件下载失败: " + e.getMessage();
}
}
@ShellMethod("上传文件")
public String upload(
@ShellOption(help = "上传URL") String url,
@ShellOption(help = "文件参数名") String paramName,
@ShellOption(help = "文件路径") String filePath,
@ShellOption(help = "是否显示响应头", 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("状态码: ").append(response.getStatus()).append("\n");
if (showHeaders) {
sb.append("响应头:\n");
response.headers().forEach((name, values) -> {
sb.append(" ").append(name).append(": ");
sb.append(String.join(", ", values)).append("\n");
});
sb.append("\n");
}
sb.append("响应体:\n");
// 尝试格式化JSON响应体
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,直接使用原始响应体
}
}
sb.append(body);
return sb.toString();
}
}
6. 内置命令清单
Spring Shell 默认提供以下内置命令,这些命令在应用启动后可直接使用,无需额外配置。各命令功能及典型用法如下
命令名称 | 功能描述 | 典型用法 |
---|---|---|
help | 查看所有可用命令及其描述(输入 help <命令> 可查看特定命令详情) | shell:> help``shell:> help add |
clear | 清空控制台输出(支持快捷键 Ctrl + L ) | shell:> clear |
exit / quit | 退出 Shell 应用(二者功能相同) | shell:> exit |
script | 从文件批量执行命令(需提供绝对路径) | shell:> script /tmp/commands.txt |
stacktrace | 显示最近一次异常的完整堆栈信息(默认只显示简略错误) | 发生异常后输入:`shell:> stacktrace`` |
history | 显示历史命令 | shell:> history |