Picocli 完全指南
一、概述
1.1 什么是 Picocli?
Picocli 是一个用于 Java 和 JVM 语言的现代化命令行应用程序框架。它提供了简洁的注解式 API,让开发者能够快速构建功能强大、用户友好的命令行工具。由 Remko Popma 创建,采用 Apache 2.0 开源许可证。
核心特点:
- 🎯 注解驱动:使用
@Command、@Option、@Parameters等注解 - 📝 自动生成帮助:ANSI 彩色输出,美观易用
- 🔍 类型安全:支持任意类型的选项和参数
- 🚀 零依赖:单个 JAR 文件,无外部依赖
- ⚡ 高性能:快速解析,低内存占用
- 🌐 多语言支持:Java、Groovy、Kotlin、Scala 等 JVM 语言
- 🛠️ 功能丰富:子命令、自动补全、配置文件、国际化
1.2 为什么选择 Picocli?
传统命令行解析的问题:
❌ Apache Commons CLI
// 代码冗长,不够直观
Options options = new Options();
options.addOption("h", "help", false, "Show help");
options.addOption(Option.builder("p")
.longOpt("port")
.hasArg()
.desc("Port number")
.build());
CommandLineParser parser = new DefaultParser();
CommandLine cmd = parser.parse(options, args);
❌ JCommander
// 需要单独的类,注解不够灵活
@Parameter(names = {"-p", "--port"})
private Integer port;
@Parameter(names = {"-h", "--help"}, help = true)
private boolean help;
❌ 手动解析
// 容易出错,难以维护
for (int i = 0; i < args.length; i++) {
if (args[i].equals("-p")) {
port = Integer.parseInt(args[++i]);
} else if (args[i].equals("-h")) {
showHelp();
}
}
Picocli 的优势:
✅ 简洁优雅
@Command(name = "myapp", mixinStandardHelpOptions = true, version = "1.0")
public class MyApp implements Callable<Integer> {
@Option(names = {"-p", "--port"}, description = "Port number")
private int port = 8080;
@Option(names = {"-h", "--host"}, description = "Host name")
private String host = "localhost";
@Override
public Integer call() throws Exception {
System.out.println("Connecting to " + host + ":" + port);
return 0;
}
public static void main(String[] args) {
int exitCode = new CommandLine(new MyApp()).execute(args);
System.exit(exitCode);
}
}
✅ 自动生成帮助
Usage: myapp [-hV] [-p=<port>] [-h=<host>]
-h, --host=<host> Host name (default: localhost)
-p, --port=<port> Port number (default: 8080)
-h, --help Show this help message and exit.
-V, --version Print version information and exit.
✅ 类型安全
- 自动类型转换(String → int, double, File, Date 等)
- 编译时检查
- 清晰的错误提示
1.3 适用场景
- ✅ CLI 工具开发:devops 工具、数据处理工具
- ✅ 微服务管理:服务启动、配置管理
- ✅ 构建工具插件:Maven/Gradle 插件
- ✅ 系统管理工具:监控、部署、备份
- ✅ 数据导入导出:ETL 工具、数据迁移
- ✅ 测试工具:性能测试、集成测试
二、核心概念
2.1 基本架构
┌─────────────────────────────────────┐
│ CommandLine Application │
├─────────────────────────────────────┤
│ @Command (命令定义) │
├─────────────────────────────────────┤
│ @Option (选项参数) │
│ @Parameters (位置参数) │
├─────────────────────────────────────┤
│ Subcommands (子命令) │
├─────────────────────────────────────┤
│ Type Conversion (类型转换) │
├─────────────────────────────────────┤
│ Help & Usage (帮助信息) │
├─────────────────────────────────────┤
│ Exit Code (退出码) │
└─────────────────────────────────────┘
2.2 核心注解
1. @Command - 命令定义
定义一个命令行命令的基本信息。
@Command(
name = "git", // 命令名称
description = "Version control", // 描述
mixinStandardHelpOptions = true, // 混合标准帮助选项
version = "git 2.30", // 版本信息
subcommands = { // 子命令
Commit.class,
Push.class,
Pull.class
}
)
public class Git {
// ...
}
常用属性:
name: 命令名称description: 简短描述footer: 页脚文本mixinStandardHelpOptions: 是否包含-h和-Vversion: 版本信息subcommands: 子命令类数组exitCodeOnInvalidInput: 无效输入时的退出码exitCodeOnExecutionException: 执行异常时的退出码
2. @Option - 选项参数
定义命名选项(以 - 或 -- 开头)。
@Option(
names = {"-p", "--port"}, // 选项名称
description = "Port number", // 描述
required = false, // 是否必需
defaultValue = "8080", // 默认值
paramLabel = "<PORT>", // 参数标签
arity = "1", // 参数个数
interactive = false // 是否交互式输入
)
private int port;
常用属性:
names: 选项名称数组(支持短选项和长选项)description: 选项描述required: 是否必需defaultValue: 默认值paramLabel: 参数标签(用于帮助信息)arity: 参数个数("0..1", "1..", "" 等)interactive: 是否从控制台读取(隐藏输入)echoChar: 交互式输入的回显字符(如密码显示*)split: 分隔符(用于多个值)type: 参数类型converter: 自定义转换器
3. @Parameters - 位置参数
定义不带名称的位置参数。
@Parameters(
index = "0", // 参数索引
description = "Input file", // 描述
arity = "1", // 参数个数
paramLabel = "<FILE>" // 参数标签
)
private File inputFile;
@Parameters(index = "1..*")
private List<String> extraArgs;
常用属性:
index: 参数索引("0", "1", "0..1", "*" 等)description: 参数描述arity: 参数个数paramLabel: 参数标签type: 参数类型
4. @Mixin - 混合选项
复用一组选项定义。
// 定义可混入的选项组
public class VerboseMixin {
@Option(names = {"-v", "--verbose"}, description = "Verbose mode")
private boolean verbose;
@Option(names = {"-q", "--quiet"}, description = "Quiet mode")
private boolean quiet;
}
// 在命令中使用
@Command(name = "myapp")
public class MyApp {
@Mixin
private VerboseMixin verboseMixin;
}
5. @Spec - 命令规范
访问命令的元数据。
@Command(name = "myapp")
public class MyApp implements Runnable {
@Spec
private CommandSpec spec;
@Override
public void run() {
// 访问命令名称
String name = spec.name();
// 访问所有选项
for (OptionSpec option : spec.options()) {
System.out.println(option.longestName());
}
// 访问父命令
CommandSpec parent = spec.parent();
}
}
2.3 数据类型支持
Picocli 支持丰富的数据类型自动转换:
内置类型
// 基本类型
@Option(names = "-i") private int intValue;
@Option(names = "-l") private long longValue;
@Option(names = "-d") private double doubleValue;
@Option(names = "-b") private boolean boolValue;
// 包装类型
@Option(names = "-I") private Integer integerValue;
// 字符串
@Option(names = "-s") private String stringValue;
// 文件
@Option(names = "-f") private File file;
@Option(names = "-P") private Path path;
// 日期时间
@Option(names = "-date") private Date date;
@Option(names = "-localdate") private LocalDate localDate;
@Option(names = "-instant") private Instant instant;
// 集合
@Option(names = "-list") private List<String> list;
@Option(names = "-set") private Set<Integer> set;
@Option(names = "-map") private Map<String, Integer> map;
// 枚举
enum LogLevel { DEBUG, INFO, WARN, ERROR }
@Option(names = "-level") private LogLevel logLevel;
// URI/URL
@Option(names = "-uri") private URI uri;
@Option(names = "-url") private URL url;
// 正则表达式
@Option(names = "-pattern") private Pattern pattern;
// BigInteger/BigDecimal
@Option(names = "-big") private BigInteger bigInteger;
@Option(names = "-decimal") private BigDecimal bigDecimal;
自定义类型转换器
// 方法 1:实现 ITypeConverter 接口
public class ColorConverter implements ITypeConverter<Color> {
@Override
public Color convert(String value) throws Exception {
switch (value.toLowerCase()) {
case "red": return Color.RED;
case "green": return Color.GREEN;
case "blue": return Color.BLUE;
default: throw new ParameterException(
spec.commandLine(),
"Invalid color: " + value
);
}
}
}
@Option(names = "-color", converter = ColorConverter.class)
private Color color;
// 方法 2:注册全局转换器
CommandLine cmd = new CommandLine(new MyApp());
cmd.registerConverter(Color.class, ColorConverter::new);
三、实现原理
3.1 解析流程
命令行输入
↓
┌──────────────────┐
│ Tokenizer │ ← 词法分析,拆分参数
└────────┬─────────┘
↓
┌──────────────────┐
│ Parser │ ← 语法分析,匹配选项
└────────┬─────────┘
↓
┌──────────────────┐
│ Type Converter │ ← 类型转换
└────────┬─────────┘
↓
┌──────────────────┐
│ Validation │ ← 参数验证
└────────┬─────────┘
↓
┌──────────────────┐
│ Execution │ ← 执行业务逻辑
└────────┬─────────┘
↓
退出码
3.2 核心组件
1. CommandLine - 核心类
// 创建 CommandLine 实例
MyApp app = new MyApp();
CommandLine cmd = new CommandLine(app);
// 解析并执行
int exitCode = cmd.execute(args);
// 或者分步执行
ParseResult parseResult = cmd.parseArgs(args);
if (parseResult.isUsageHelpRequested()) {
cmd.usage(System.out);
} else if (parseResult.isVersionHelpRequested()) {
cmd.printVersionHelp(System.out);
} else {
app.run();
}
主要方法:
execute(String...): 解析并执行,返回退出码parseArgs(String...): 仅解析,不执行usage(PrintWriter): 打印帮助信息printVersionHelp(PrintWriter): 打印版本信息registerConverter(Class, ITypeConverter): 注册类型转换器addSubcommand(String, Object): 添加子命令
2. ParseResult - 解析结果
ParseResult result = cmd.parseArgs(args);
// 检查是否请求帮助
if (result.isUsageHelpRequested()) {
cmd.usage(System.out);
return;
}
// 获取选项值
if (result.hasMatchedOption("port")) {
int port = result.matchedOptionValue("port", 8080);
}
// 获取位置参数
List<File> files = result.matchedPositionalValues(0);
// 获取子命令
if (result.hasSubcommand()) {
ParseResult subResult = result.subcommand();
}
3. CommandSpec - 命令规范
@Command(name = "myapp")
public class MyApp {
@Spec
CommandSpec spec;
public void init() {
// 访问命令元数据
String name = spec.name();
List<OptionSpec> options = spec.options();
List<PositionalParamSpec> params = spec.positionalParameters();
// 动态添加选项
spec.addOption(OptionSpec.builder("-x")
.description("Dynamic option")
.build());
}
}
3.3 注解处理机制
原理:反射 + 注解扫描
// Picocli 内部伪代码
public class CommandLine {
private final Object userObject;
private final CommandSpec commandSpec;
public CommandLine(Object userObject) {
this.userObject = userObject;
this.commandSpec = CommandSpec.forAnnotatedObject(userObject);
}
// 扫描注解
private CommandSpec scanAnnotations(Object obj) {
CommandSpec spec = new CommandSpec();
Class<?> clazz = obj.getClass();
// 扫描 @Command
Command command = clazz.getAnnotation(Command.class);
if (command != null) {
spec.name(command.name());
spec.description(command.description());
}
// 扫描字段注解
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(Option.class)) {
Option option = field.getAnnotation(Option.class);
spec.addOption(createOptionSpec(field, option));
}
if (field.isAnnotationPresent(Parameters.class)) {
Parameters params = field.getAnnotation(Parameters.class);
spec.addPositionalParam(createParamSpec(field, params));
}
}
// 扫描方法注解
for (Method method : clazz.getDeclaredMethods()) {
if (method.isAnnotationPresent(Option.class)) {
// 处理方法级别的选项
}
}
return spec;
}
}
3.4 参数解析算法
1. 词法分析(Tokenization)
// 输入: myapp -p 8080 --host=localhost file1 file2
// 拆分 tokens:
// [0] "myapp" → 命令名
// [1] "-p" → 短选项
// [2] "8080" → 选项值
// [3] "--host=localhost" → 长选项(= 分隔)
// [4] "file1" → 位置参数
// [5] "file2" → 位置参数
规则:
- 空格分隔参数
=连接选项名和值--后的所有内容都是位置参数- 引用字符串保持完整(
"hello world")
2. 语法分析(Parsing)
// 伪代码:解析流程
for (Token token : tokens) {
if (token.startsWith("--")) {
// 长选项
String optionName = token.substring(2).split("=")[0];
String value = token.contains("=") ? token.split("=")[1] : nextToken();
matchAndAssign(optionName, value);
} else if (token.startsWith("-") && token.length() > 1) {
// 短选项
String optionName = token.substring(1, 2);
String value = token.length() > 2 ? token.substring(2) : nextToken();
matchAndAssign(optionName, value);
} else {
// 位置参数
assignPositional(token);
}
}
3. 类型转换(Type Conversion)
// 伪代码:类型转换
public Object convertValue(String value, Class<?> targetType) {
// 查找注册的转换器
ITypeConverter<?> converter = converters.get(targetType);
if (converter != null) {
return converter.convert(value);
}
// 尝试内置转换
if (targetType == int.class || targetType == Integer.class) {
return Integer.parseInt(value);
}
if (targetType == boolean.class || targetType == Boolean.class) {
return Boolean.parseBoolean(value);
}
if (targetType == File.class) {
return new File(value);
}
// ... 其他类型
throw new IllegalArgumentException("Cannot convert to " + targetType);
}
3.5 帮助信息生成
原理:模板渲染 + ANSI 着色
// 帮助信息生成流程
public void usage(PrintWriter writer) {
// 1. 构建帮助模型
Help help = new Help(commandSpec, ansi());
// 2. 渲染各部分
writer.println(help.synopsis()); // 用法摘要
writer.println(help.description()); // 描述
writer.println(help.optionList()); // 选项列表
writer.println(help.parameterList()); // 参数列表
writer.println(help.footer()); // 页脚
}
// ANSI 颜色示例
@|green Usage:|@ myapp [@|cyan -p|@ <port>]
@|yellow -p, --port|@ Port number
帮助信息结构:
Usage: myapp [-hV] [-p=<port>] [-h=<host>] [<files>...]
Description text here.
[<files>...] Positional parameters
-h, --host=<host> Host name (default: localhost)
-p, --port=<port> Port number (default: 8080)
-h, --help Show this help message and exit.
-V, --version Print version information and exit.
Footer text here.
3.6 命令执行调用链路
完整执行流程图
用户输入: java MyApp -p 8080 --host=localhost file1.txt
↓
┌─────────────────────────────────────────┐
│ 1. main() 入口 │
│ new CommandLine(new MyApp()) │
│ .execute(args) │
└──────────────┬──────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 2. CommandLine 构造函数 │
│ - 创建 CommandSpec │
│ - 扫描注解 (@Command, @Option...) │
│ - 构建元数据模型 │
└──────────────┬──────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 3. execute(args) │
│ ├─ parseArgs(args) ← 解析参数 │
│ ├─ 检查帮助/版本请求 │
│ └─ executeUserObject() ← 执行 │
└──────────────┬──────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 4. parseArgs(args) - 参数解析 │
│ ├─ Tokenizer.tokenize() │
│ │ └─ 拆分命令行字符串为 tokens │
│ ├─ Parser.parse() │
│ │ ├─ 匹配选项名 │
│ │ ├─ 验证 arity │
│ │ └─ 绑定到字段 │
│ ├─ TypeConverter.convert() │
│ │ └─ String → 目标类型 │
│ └─ 返回 ParseResult │
└──────────────┬──────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 5. 检查特殊请求 │
│ ├─ isUsageHelpRequested()? │
│ │ └─ 是 → usage() → 打印帮助 │
│ ├─ isVersionHelpRequested()? │
│ │ └─ 是 → printVersionHelp() │
│ └─ 否 → 继续执行 │
└──────────────┬──────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 6. executeUserObject() - 执行业务逻辑 │
│ ├─ 判断用户对象类型: │
│ │ ├─ Callable? → call() │
│ │ ├─ Runnable? → run() │
│ │ └─ 其他 → 直接返回 │
│ ├─ 捕获异常 │
│ └─ 返回退出码 │
└──────────────┬──────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 7. 返回退出码 │
│ ├─ 0: 成功 │
│ ├─ 1: 业务错误 │
│ ├─ 2: 无效输入 │
│ └─ 其他: 自定义错误 │
└─────────────────────────────────────────┘
详细调用栈示例
// 用户代码
public static void main(String[] args) {
int exitCode = new CommandLine(new MyApp()).execute(args);
System.exit(exitCode);
}
// === 调用链路开始 ===
// 第 1 层:CommandLine 构造
public CommandLine(Object userObject) {
this(userObject, new Factory.Default());
}
public CommandLine(Object userObject, IFactory factory) {
this.userObject = userObject;
this.commandSpec = CommandSpec.forAnnotatedObject(userObject, factory);
// ↑ 这里会扫描所有注解,构建命令规范
}
// 第 2 层:CommandSpec.forAnnotatedObject()
public static CommandSpec forAnnotatedObject(Object obj, IFactory factory) {
CommandSpec spec = new CommandSpec();
spec.userObject(obj);
// 扫描类级别的 @Command 注解
Command command = obj.getClass().getAnnotation(Command.class);
if (command != null) {
applyCommandAnnotation(spec, command);
}
// 扫描字段级别的 @Option 和 @Parameters
for (Field field : obj.getClass().getDeclaredFields()) {
if (field.isAnnotationPresent(Option.class)) {
Option option = field.getAnnotation(Option.class);
OptionSpec optionSpec = createOptionSpec(field, option, factory);
spec.add(optionSpec);
}
if (field.isAnnotationPresent(Parameters.class)) {
Parameters params = field.getAnnotation(Parameters.class);
PositionalParamSpec paramSpec = createParamSpec(field, params, factory);
spec.add(paramSpec);
}
}
// 扫描方法级别的注解
for (Method method : obj.getClass().getDeclaredMethods()) {
if (method.isAnnotationPresent(Option.class)) {
// 处理方法级别的选项
}
}
return spec;
}
// 第 3 层:execute() 方法
public int execute(String... args) {
try {
// 解析参数
ParseResult parseResult = parseArgs(args);
// 检查是否请求帮助
if (parseResult.isUsageHelpRequested()) {
usage(System.out);
return exitCodeOnUsageHelp;
}
// 检查是否请求版本
if (parseResult.isVersionHelpRequested()) {
printVersionHelp(System.out);
return exitCodeOnVersionHelp;
}
// 执行用户对象
return executeUserObject(parseResult);
} catch (ParameterException ex) {
// 参数错误
handleParameterException(ex);
return exitCodeOnInvalidInput;
} catch (ExecutionException ex) {
// 执行异常
handleExecutionException(ex);
return exitCodeOnExecutionException;
}
}
// 第 4 层:parseArgs() - 参数解析
public ParseResult parseArgs(String... args) {
// 1. 词法分析:拆分 tokens
List<CommandLine.ArgSpec> matched = new ArrayList<>();
Stack<String> arguments = new Stack<>();
Collections.addAll(arguments, args);
// 2. 创建解析器
Parser parser = new Parser(this);
// 3. 执行解析
ParseResult result = parser.parse(arguments, matched);
return result;
}
// 第 5 层:Parser.parse() - 核心解析逻辑
class Parser {
public ParseResult parse(Stack<String> args, List<ArgSpec> matched) {
while (!args.isEmpty()) {
String arg = args.pop();
if (arg.startsWith("--")) {
// 长选项处理
handleLongOption(arg, args, matched);
} else if (arg.startsWith("-") && arg.length() > 1) {
// 短选项处理
handleShortOption(arg, args, matched);
} else {
// 位置参数处理
handlePositional(arg, matched);
}
}
return new ParseResult(matched);
}
private void handleLongOption(String arg, Stack<String> args, List<ArgSpec> matched) {
// 解析 --option=value 或 --option value
String[] parts = arg.split("=", 2);
String optionName = parts[0].substring(2); // 去掉 "--"
// 查找匹配的 OptionSpec
OptionSpec optionSpec = findOptionByName(optionName);
// 获取值
String value;
if (parts.length == 2) {
value = parts[1]; // --option=value
} else {
value = args.pop(); // --option value
}
// 类型转换并赋值
Object convertedValue = convert(value, optionSpec.type());
optionSpec.setValue(convertedValue);
matched.add(optionSpec);
}
}
// 第 6 层:TypeConverter - 类型转换
public Object convert(String value, Class<?> targetType) {
// 查找注册的转换器
ITypeConverter<?> converter = converters.get(targetType);
if (converter != null) {
return converter.convert(value);
}
// 尝试内置转换
if (targetType == int.class || targetType == Integer.class) {
return Integer.parseInt(value);
}
if (targetType == boolean.class || targetType == Boolean.class) {
return Boolean.parseBoolean(value);
}
if (targetType == File.class) {
return new File(value);
}
if (targetType == LocalDate.class) {
return LocalDate.parse(value);
}
// ... 更多类型
throw new IllegalArgumentException("Cannot convert to " + targetType);
}
// 第 7 层:executeUserObject() - 执行业务逻辑
private int executeUserObject(ParseResult parseResult) throws Exception {
Object userObject = getCommandSpec().userObject();
// 判断用户对象类型
if (userObject instanceof Callable) {
// Callable 类型:有返回值
Callable<?> callable = (Callable<?>) userObject;
Object result = callable.call();
// 如果返回 Integer,作为退出码
if (result instanceof Integer) {
return (Integer) result;
}
return 0;
} else if (userObject instanceof Runnable) {
// Runnable 类型:无返回值
Runnable runnable = (Runnable) userObject;
runnable.run();
return 0;
} else {
// 其他类型:直接返回
return 0;
}
}
实际执行示例追踪
// 定义命令
@Command(name = "myapp", mixinStandardHelpOptions = true)
public class MyApp implements Callable<Integer> {
@Option(names = {"-p", "--port"}, defaultValue = "8080")
private int port;
@Option(names = {"-h", "--host"}, defaultValue = "localhost")
private String host;
@Parameters(index = "0")
private File inputFile;
@Override
public Integer call() throws Exception {
System.out.println("Connecting to " + host + ":" + port);
System.out.println("Processing file: " + inputFile);
return 0;
}
public static void main(String[] args) {
int exitCode = new CommandLine(new MyApp()).execute(args);
System.exit(exitCode);
}
}
// 执行:java MyApp -p 9090 --host=example.com data.txt
执行过程追踪:
步骤 1: 构造 CommandLine
├─ 创建 CommandSpec
├─ 扫描 @Command 注解
│ └─ name = "myapp"
├─ 扫描 @Option 字段
│ ├─ port: names=["-p", "--port"], defaultValue="8080"
│ └─ host: names=["-h", "--host"], defaultValue="localhost"
├─ 扫描 @Parameters 字段
│ └─ inputFile: index="0"
└─ 添加标准帮助选项 (-h, -V)
步骤 2: 调用 execute("-p", "9090", "--host=example.com", "data.txt")
├─ 调用 parseArgs()
│ ├─ Tokenizer 拆分:
│ │ ["-p", "9090", "--host=example.com", "data.txt"]
│ ├─ Parser 解析:
│ │ ├─ "-p" → 匹配 port 选项
│ │ │ └─ 读取下一个 token "9090"
│ │ │ └─ 转换为 int: 9090
│ │ │ └─ 设置 port = 9090
│ │ ├─ "--host=example.com" → 匹配 host 选项
│ │ │ └─ 分割得到 "example.com"
│ │ │ └─ 保持 String 类型
│ │ │ └─ 设置 host = "example.com"
│ │ └─ "data.txt" → 位置参数 index=0
│ │ └─ 转换为 File 对象
│ │ └─ 设置 inputFile = new File("data.txt")
│ └─ 返回 ParseResult
│
├─ 检查帮助请求: false
├─ 检查版本请求: false
│
└─ 调用 executeUserObject()
├─ 检测到 MyApp 实现 Callable<Integer>
├─ 调用 MyApp.call()
│ ├─ 输出: "Connecting to example.com:9090"
│ ├─ 输出: "Processing file: data.txt"
│ └─ 返回: 0
└─ 返回退出码: 0
步骤 3: System.exit(0)
子命令执行链路
@Command(name = "git", subcommands = {Git.Commit.class, Git.Push.class})
public class Git implements Runnable {
public void run() { System.out.println("Git"); }
@Command(name = "commit")
static class Commit implements Runnable {
@Option(names = "-m") private String message;
public void run() { System.out.println("Commit: " + message); }
}
@Command(name = "push")
static class Push implements Runnable {
public void run() { System.out.println("Push"); }
}
}
// 执行:java git commit -m "fix bug"
子命令执行流程:
步骤 1: 构造主命令 Git
├─ 创建 Git 的 CommandSpec
├─ 注册子命令:
│ ├─ "commit" → Commit.class
│ └─ "push" → Push.class
└─ 递归构造子命令的 CommandSpec
步骤 2: 调用 execute("commit", "-m", "fix bug")
├─ parseArgs() 解析
│ ├─ 识别 "commit" 为子命令
│ └─ 委托给 Commit 的 Parser 解析 "-m fix bug"
│
├─ 获取子命令的 ParseResult
├─ 检查帮助/版本请求
│
└─ executeUserObject(Commit)
├─ 检测到 Commit 实现 Runnable
├─ 调用 Commit.run()
│ └─ 输出: "Commit: fix bug"
└─ 返回退出码: 0
关键时序图
sequenceDiagram
participant User as 用户
participant Main as main()
participant CL as CommandLine
participant Spec as CommandSpec
participant Parser as Parser
participant Converter as TypeConverter
participant App as 用户应用
User->>Main: java MyApp -p 9090
Main->>CL: new CommandLine(new MyApp())
CL->>Spec: forAnnotatedObject(MyApp)
Spec->>Spec: 扫描 @Command
Spec->>Spec: 扫描 @Option/@Parameters
Spec-->>CL: 返回 CommandSpec
Main->>CL: execute(args)
CL->>Parser: parseArgs(args)
Parser->>Parser: tokenize()
Parser->>Parser: match options
Parser->>Converter: convert(value, type)
Converter-->>Parser: 转换后的值
Parser->>Parser: bind to fields
Parser-->>CL: ParseResult
CL->>CL: check help/version
CL->>App: executeUserObject()
alt Callable
App->>App: call()
else Runnable
App->>App: run()
end
App-->>CL: 退出码
CL-->>Main: 退出码
Main->>User: System.exit(code)
性能优化点
在调用链路中的优化策略:
// 1. 缓存 CommandSpec(避免重复扫描注解)
private static final CommandLine CMD = new CommandLine(new MyApp());
// 2. 预注册常用类型转换器
static {
CMD.registerConverter(LocalDate.class, LocalDate::parse);
CMD.registerConverter(Path.class, Paths::get);
}
// 3. 使用工厂模式减少反射开销
IFactory factory = new IFactory() {
@Override
public <K> K create(Class<K> clazz) throws Exception {
// 可以使用依赖注入框架
return applicationContext.getBean(clazz);
}
};
CommandLine cmd = new CommandLine(new MyApp(), factory);
// 4. 惰性加载子命令
@Command(name = "git")
public class Git {
@CommandLine.Spec
CommandSpec spec;
// 只在需要时加载子命令
private void loadSubcommands() {
if (spec.subcommands().isEmpty()) {
spec.addSubcommand("commit", new Commit());
spec.addSubcommand("push", new Push());
}
}
}
四、完整示例
4.1 基础示例
import picocli.CommandLine;
import picocli.CommandLine.*;
import java.io.File;
import java.util.concurrent.Callable;
@Command(
name = "grep",
mixinStandardHelpOptions = true,
version = "grep 1.0",
description = "Search for patterns in files"
)
public class Grep implements Callable<Integer> {
@Parameters(index = "0", description = "Pattern to search for")
private String pattern;
@Parameters(index = "1..*", description = "Files to search", arity = "1..*")
private File[] files;
@Option(names = {"-i", "--ignore-case"}, description = "Ignore case")
private boolean ignoreCase;
@Option(names = {"-n", "--line-number"}, description = "Show line numbers")
private boolean showLineNumbers;
@Option(names = {"-c", "--count"}, description = "Show count only")
private boolean showCount;
@Override
public Integer call() throws Exception {
int totalMatches = 0;
for (File file : files) {
int matches = searchInFile(file);
totalMatches += matches;
if (!showCount) {
System.out.println(matches + " matches in " + file.getName());
}
}
if (showCount) {
System.out.println("Total: " + totalMatches + " matches");
}
return 0;
}
private int searchInFile(File file) {
// 实现搜索逻辑
return 0;
}
public static void main(String[] args) {
int exitCode = new CommandLine(new Grep()).execute(args);
System.exit(exitCode);
}
}
使用:
# 编译
javac -cp picocli-4.7.5.jar Grep.java
# 运行
java -cp .:picocli-4.7.5.jar Grep -i "error" *.log
# 查看帮助
java -cp .:picocli-4.7.5.jar Grep --help
4.2 子命令示例
@Command(
name = "git",
description = "Version control system",
subcommands = {
Git.Commit.class,
Git.Push.class,
Git.Pull.class,
Git.Status.class
}
)
public class Git implements Runnable {
@Option(names = {"-C"}, description = "Run as if git was started in <path>")
private File directory;
@Override
public void run() {
System.out.println("Git command");
}
@Command(name = "commit", description = "Record changes")
static class Commit implements Runnable {
@Option(names = {"-m"}, description = "Commit message", required = true)
private String message;
@Option(names = {"-a"}, description = "Add all files")
private boolean addAll;
@Override
public void run() {
System.out.println("Committing: " + message);
if (addAll) {
System.out.println("Adding all files");
}
}
}
@Command(name = "push", description = "Update remote refs")
static class Push implements Runnable {
@Parameters(description = "Remote name", defaultValue = "origin")
private String remote;
@Parameters(description = "Branch name", defaultValue = "main")
private String branch;
@Option(names = {"-f", "--force"}, description = "Force push")
private boolean force;
@Override
public void run() {
System.out.println("Pushing to " + remote + "/" + branch);
if (force) {
System.out.println("Force push!");
}
}
}
@Command(name = "pull", description = "Fetch and merge")
static class Pull implements Runnable {
@Override
public void run() {
System.out.println("Pulling changes");
}
}
@Command(name = "status", description = "Show working tree status")
static class Status implements Runnable {
@Override
public void run() {
System.out.println("On branch main");
System.out.println("nothing to commit, working tree clean");
}
}
public static void main(String[] args) {
int exitCode = new CommandLine(new Git()).execute(args);
System.exit(exitCode);
}
}
使用:
# 查看主命令帮助
java Git --help
# 查看子命令帮助
java Git commit --help
# 执行子命令
java Git commit -m "Initial commit" -a
java Git push origin main
java Git status
4.3 交互式输入示例
@Command(name = "login", description = "Login to system")
public class Login implements Callable<Integer> {
@Option(names = {"-u", "--username"}, description = "Username", required = true)
private String username;
@Option(
names = {"-p", "--password"},
description = "Password",
interactive = true, // 交互式输入
echoChar = '*', // 回显字符
required = true
)
private char[] password;
@Option(names = {"-r", "--remember"}, description = "Remember me")
private boolean rememberMe;
@Override
public Integer call() throws Exception {
System.out.println("Logging in as: " + username);
// 验证密码
if (authenticate(username, password)) {
System.out.println("Login successful!");
if (rememberMe) {
saveCredentials(username, password);
}
return 0;
} else {
System.err.println("Invalid credentials");
return 1;
}
}
private boolean authenticate(String user, char[] pass) {
// 认证逻辑
return true;
}
private void saveCredentials(String user, char[] pass) {
// 保存凭证
}
public static void main(String[] args) {
int exitCode = new CommandLine(new Login()).execute(args);
System.exit(exitCode);
}
}
使用:
java Login -u admin -p
# 会提示输入密码,显示为 ***
4.4 Spring Boot 集成示例
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import picocli.CommandLine;
@Component
@CommandLine.Command(
name = "data-import",
description = "Import data from CSV"
)
public class DataImportCommand implements CommandLineRunner, Callable<Integer> {
@CommandLine.Option(
names = {"-f", "--file"},
description = "CSV file to import",
required = true
)
private File csvFile;
@CommandLine.Option(
names = {"-t", "--table"},
description = "Target table name",
required = true
)
private String tableName;
@CommandLine.Option(
names = {"-b", "--batch-size"},
description = "Batch size for insert",
defaultValue = "1000"
)
private int batchSize;
@Autowired
private DataService dataService;
@Override
public Integer call() throws Exception {
System.out.println("Importing data from: " + csvFile);
System.out.println("Target table: " + tableName);
System.out.println("Batch size: " + batchSize);
int imported = dataService.importCsv(csvFile, tableName, batchSize);
System.out.println("Successfully imported " + imported + " records");
return 0;
}
@Override
public void run(String... args) throws Exception {
// Spring Boot 启动时执行
new CommandLine(this).execute(args);
}
}
4.5 自定义转换器示例
// 自定义类型
public class DateRange {
private LocalDate start;
private LocalDate end;
public DateRange(LocalDate start, LocalDate end) {
this.start = start;
this.end = end;
}
// getters...
}
// 自定义转换器
public class DateRangeConverter implements ITypeConverter<DateRange> {
@Override
public DateRange convert(String value) throws Exception {
String[] parts = value.split("/");
if (parts.length != 2) {
throw new ParameterException(
spec.commandLine(),
"Date range must be in format: yyyy-MM-dd/yyyy-MM-dd"
);
}
LocalDate start = LocalDate.parse(parts[0]);
LocalDate end = LocalDate.parse(parts[1]);
if (end.isBefore(start)) {
throw new ParameterException(
spec.commandLine(),
"End date must be after start date"
);
}
return new DateRange(start, end);
}
}
// 使用自定义转换器
@Command(name = "report")
public class ReportGenerator implements Callable<Integer> {
@Option(
names = {"-d", "--date-range"},
description = "Date range (yyyy-MM-dd/yyyy-MM-dd)",
converter = DateRangeConverter.class,
required = true
)
private DateRange dateRange;
@Override
public Integer call() throws Exception {
System.out.println("Generating report from " +
dateRange.getStart() + " to " + dateRange.getEnd());
return 0;
}
}
五、高级特性
5.1 自动补全
Bash 自动补全
# 生成补全脚本
java -cp picocli-4.7.5.jar picocli.AutoComplete myapp
# 安装补全
source myapp_completion.sh
# 现在可以使用 Tab 补全
myapp --<TAB>
myapp --ho<TAB> # 自动补全为 --host
生成的补全脚本示例:
_myapp() {
local cur prev opts
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
opts="-h -V -p --port -h --host --help --version"
case "${prev}" in
-p|--port)
COMPREPLY=( $(compgen -W "" -- ${cur}) )
return 0
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
}
complete -F _myapp myapp
5.2 配置文件支持
@Command(name = "myapp")
public class MyApp implements Callable<Integer> {
@Option(names = {"-c", "--config"}, description = "Config file")
private File configFile;
@Option(names = {"-p", "--port"}, description = "Port")
private int port = 8080;
@Option(names = {"-h", "--host"}, description = "Host")
private String host = "localhost";
@Override
public Integer call() throws Exception {
// 从配置文件加载
if (configFile != null && configFile.exists()) {
Properties props = new Properties();
props.load(new FileReader(configFile));
port = Integer.parseInt(props.getProperty("port", String.valueOf(port)));
host = props.getProperty("host", host);
}
System.out.println("Starting on " + host + ":" + port);
return 0;
}
}
配置文件(app.properties):
port=9090
host=0.0.0.0
使用:
myapp --config app.properties
5.3 国际化(i18n)
// 资源束
ResourceBundle bundle = ResourceBundle.getBundle("messages", Locale.CHINA);
CommandLine cmd = new CommandLine(new MyApp());
cmd.setResourceBundle(bundle);
// messages_zh_CN.properties
myapp.description=我的应用程序
myapp.port.description=端口号
myapp.host.description=主机名
5.4 退出码管理
@Command(
name = "myapp",
exitCodeOnInvalidInput = 2,
exitCodeOnExecutionException = 3
)
public class MyApp implements Callable<Integer> {
@Override
public Integer call() throws Exception {
try {
// 业务逻辑
return 0; // 成功
} catch (BusinessException e) {
System.err.println("Business error: " + e.getMessage());
return 1; // 业务错误
} catch (Exception e) {
System.err.println("System error: " + e.getMessage());
return 3; // 系统错误
}
}
}
标准退出码:
0: 成功1: 业务逻辑错误2: 无效输入(Picocli 自动返回)3+: 自定义错误
六、最佳实践
6.1 代码组织
src/main/java/com/example/myapp/
├── MyApp.java # 主命令
├── commands/ # 子命令
│ ├── CreateCommand.java
│ ├── DeleteCommand.java
│ └── UpdateCommand.java
├── options/ # 可复用选项
│ ├── VerboseOption.java
│ └── OutputOption.java
├── converters/ # 自定义转换器
│ ├── DateRangeConverter.java
│ └── ColorConverter.java
└── model/ # 数据模型
├── Config.java
└── Result.java
6.2 选项设计原则
1. 短选项和长选项
// ✅ 推荐:同时提供短选项和长选项
@Option(names = {"-p", "--port"})
// ❌ 避免:只有长选项
@Option(names = {"--port"})
2. 合理的默认值
// ✅ 推荐:提供合理的默认值
@Option(names = "-p", defaultValue = "8080")
private int port;
// ❌ 避免:无默认值的非必需选项
@Option(names = "-p")
private Integer port; // 可能为 null
3. 清晰的描述
// ✅ 推荐:详细描述
@Option(
names = "-p",
description = "Port number to listen on (default: ${DEFAULT-VALUE})"
)
// ❌ 避免:模糊描述
@Option(names = "-p", description = "Port")
6.3 错误处理
@Command(name = "myapp")
public class MyApp implements Callable<Integer> {
@Override
public Integer call() throws Exception {
try {
validateInputs();
execute();
return 0;
} catch (ValidationException e) {
System.err.println("Validation error: " + e.getMessage());
return 2;
} catch (IOException e) {
System.err.println("IO error: " + e.getMessage());
log.debug("Stack trace", e);
return 1;
} catch (Exception e) {
System.err.println("Unexpected error: " + e.getMessage());
log.error("Error", e);
return 3;
}
}
}
6.4 性能优化
// ✅ 推荐:重用 CommandLine 实例
public class AppFactory {
private static final CommandLine CMD = new CommandLine(new MyApp());
public static CommandLine getCommandLine() {
return CMD;
}
}
// ❌ 避免:每次创建新实例
public static void main(String[] args) {
new CommandLine(new MyApp()).execute(args); // 每次都创建
}
七、与其他框架对比
7.1 对比表格
| 特性 | Picocli | Commons CLI | JCommander | Argparse4j |
|---|---|---|---|---|
| 注解支持 | ✅ | ❌ | ✅ | ✅ |
| 子命令 | ✅ | ❌ | ✅ | ✅ |
| 自动帮助 | ✅ | ❌ | ⚠️ | ✅ |
| ANSI 颜色 | ✅ | ❌ | ❌ | ❌ |
| 类型转换 | ✅ | ⚠️ | ✅ | ✅ |
| 自动补全 | ✅ | ❌ | ❌ | ❌ |
| 零依赖 | ✅ | ❌ | ✅ | ❌ |
| 活跃度 | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
| 文档质量 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
7.2 选型建议
选择 Picocli:
- ✅ 新项目开发
- ✅ 需要子命令支持
- ✅ 注重用户体验(彩色帮助、自动补全)
- ✅ 希望零依赖
选择 Commons CLI:
- ✅ 遗留项目维护
- ✅ 简单的参数解析
- ✅ 已有 Apache 生态
选择 JCommander:
- ✅ TestNG 项目
- ✅ 简单的注解需求
八、常见问题
8.1 技术问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 选项未识别 | 注解错误 | 检查 @Option 和 @Parameters |
| 类型转换失败 | 格式错误 | 实现自定义转换器 |
| 帮助信息乱码 | 编码问题 | 设置 UTF-8 编码 |
| 子命令不生效 | 未注册 | 在 @Command 中声明 |
| 默认值无效 | 类型不匹配 | 确保默认值类型正确 |
8.2 调试技巧
// 启用调试日志
System.setProperty("picocli.trace", "DEBUG");
// 查看解析结果
CommandLine cmd = new CommandLine(new MyApp());
ParseResult result = cmd.parseArgs(args);
System.out.println(result);
// 打印详细帮助
cmd.usage(System.out, Help.Ansi.ON);
九、总结
9.1 Picocli 的核心价值
✨ 简洁:注解驱动,代码量少
🎨 美观:ANSI 彩色帮助信息
🚀 高效:零依赖,高性能
🔧 灵活:支持复杂场景
📚 完善:文档齐全,社区活跃
9.2 学习路径
入门:
1. 理解基本注解(@Command, @Option, @Parameters)
2. 编写简单命令行工具
3. 生成帮助信息
进阶:
4. 实现子命令
5. 自定义类型转换器
6. 集成到 Spring Boot
高级:
7. 实现自动补全
8. 国际化支持
9. 性能优化和最佳实践
9.3 参考资源
官方文档:
- 官网:picocli.info/
- GitHub:github.com/remkop/pico…
- User Manual:picocli.info/#_user_manu…
示例项目:
- Picocli Examples:github.com/remkop/pico…
相关工具:
- GraalVM Native Image:将 Picocli 应用编译为原生可执行文件
- Maven Plugin:自动化构建
文档版本: v1.0
最后更新: 2026-05-02