一、背景
在日常开发中,某些情况下可能需要为服务提供一个命令行工具(CLI),方便运维、调试或者远程调用业务接口。
假设我们有一个 Spring Boot 服务,提供多个接口,例如:
- 获取用户列表:
/users?type=admin - 获取角色列表:
/roles?level=manager - 获取系统状态:
/system/status - 批量导入数据:
/data/import - 生成报表:
/report/generate
传统做法:
- 每个接口在 CLI 客户端写一条命令
- CLI 方法直接调用服务端 REST 接口
- 硬编码的服务地址和参数格式
问题:
- 接口多时,CLI 方法数量激增,代码冗余严重
- 每次新增接口都需要修改客户端,发布新版本
- 维护成本高,不同环境的配置分散
- 缺乏统一的认证、授权和日志机制
- 开发效率低下,重复劳动多
解决方案 :通用命令 + 动态分发
- CLI 只维护一条通用命令
exec - 根据参数动态路由到服务端对应的 Service Bean
- 服务端统一管理,支持动态扩展
- 一次开发,多处复用
二、方案设计
1. 核心架构
我们设计了一套基于 Spring Boot + Spring Shell 的通用CLI系统,采用分层架构设计:
scss
客户端(Spring Shell) <--HTTP--> 服务端(Spring Boot)
| |
通用命令exec 统一控制器(/cli)
| |
动态参数 动态Bean分发
| |
单一入口命令 多个CommandHandler
| |
REST通信 业务逻辑处理
设计原则
单一职责 :客户端只负责命令解析和HTTP通信,服务端只负责业务逻辑 开闭原则 :对扩展开放(新增服务),对修改关闭(不需改客户端) 依赖倒置 :依赖抽象的CommandHandler接口,而非具体实现 最小知识:客户端无需知道服务端的具体实现细节
2. 客户端设计
在 CLI 客户端定义一条通用命令 exec:
java
@ShellComponent
public class ExecCommand {
@ShellMethod(key = "exec", value = "执行远程服务命令")
public String executeCommand(
@ShellOption(value = {"", "service"}, help = "服务名称") String serviceName,
@ShellOption(value = "--args", help = "命令参数", arity = 100) String[] args) {
// 构建请求并发送到服务端
CommandRequest request = new CommandRequest(serviceName, Arrays.asList(args));
return httpClient.post("/cli", request);
}
}
使用示例:
shell
> exec userService --args list
user1, user2, user3
> exec roleService --args users admin
role1, role2
> exec systemService --args status
系统正常运行
3. 服务端设计
服务端提供统一接口 /cli,根据服务名动态分发:
java
@RestController
@RequestMapping("/cli")
public class CliController {
@Autowired
private ApplicationContext applicationContext;
@PostMapping
public String execute(@RequestBody CommandRequest request) {
String serviceName = request.getService();
String[] args = request.getArgs().toArray(new String[0]);
// 动态获取 Service Bean
Object serviceBean = applicationContext.getBean(serviceName);
// 执行命令
if (serviceBean instanceof CommandHandler handler) {
return handler.handle(args);
}
return "服务未找到";
}
}
4. 统一接口规范
所有需要通过CLI调用的服务都必须实现 CommandHandler 接口:
java
public interface CommandHandler {
String handle(String[] args);
default String getDescription() { return "命令描述"; }
default String getUsage() { return "使用说明"; }
}
示例服务实现:
java
@Service("userService")
public class UserService implements CommandHandler {
@Override
public String handle(String[] args) {
if (args.length == 0) return getUsage();
switch (args[0]) {
case "list":
return listUsers(args.length > 1 ? args[1] : null);
case "get":
return getUser(args[1]);
default:
return "未知命令: " + args[0];
}
}
private String listUsers(String type) {
// 实现获取用户列表逻辑
return "用户列表...";
}
}
三、方案优势
1. 客户端统一命令
- Shell 只需维护一条
exec命令 - 新增服务无需修改客户端代码
2. 服务端动态分发
- 新增接口无需修改 CLI
- 统一接口入口便于权限控制与日志审计
3. 易扩展
- 支持任意参数数量、类型
- 可结合 OpenAPI 自动生成命令提示与帮助信息
4. 逻辑解耦
- CLI 仅做命令解析和 HTTP 调用
- 业务逻辑完全在服务端
四、安全控制
1. 服务白名单
通过配置文件限制可访问的服务:
yaml
cli:
allowed-services:
- userService
- roleService
- systemService
2. 参数验证
使用 Spring Validation 进行请求参数校验,防止恶意输入。
3. 访问日志
记录所有CLI调用,便于审计和问题追踪:
java
logger.info("CLI请求 - 服务: {}, 参数: {}, 来源: {}",
serviceName, Arrays.toString(args), httpRequest.getRemoteAddr());
五、实际应用场景
1. 运维场景
shell
# 查看系统状态
exec systemService --args status
# 重启服务
exec serviceManager --args restart userService
# 查看日志
exec logService --args tail 100 error
2. 调试场景
shell
# 查看用户详情
exec userService --args get 123
# 测试接口
exec testService --args simulate /api/orders
# 清理缓存
exec cacheService --args clear all
3. 批量操作
shell
# 批量导入用户
exec userService --args import users.csv
# 批量更新角色
exec roleService --args batchUpdate role-mapping.json
六、扩展功能
1. 交互增强
- Tab 补全:自动补全服务名和参数
- 命令历史:保存执行历史,支持上下键浏览
- 颜色输出:不同类型信息使用不同颜色显示
2. 结果格式化
java
private String formatResponse(String data) {
try {
Object json = objectMapper.readValue(data, Object.class);
return objectMapper.writerWithDefaultPrettyPrinter()
.writeValueAsString(json);
} catch (Exception e) {
return data;
}
}
3. 脚本模式
支持从文件执行命令序列:
shell
exec script --args commands.txt
七、总结
本文介绍的"通用命令+动态分发"方案,通过Spring Boot + Spring Shell构建,使用单一 exec 命令实现多服务动态调用,大幅简化了CLI系统的维护复杂度。
ruby
https://github.com/yuboon/java-examples/tree/master/springboot-cli