Java Picocli 實(shí)戰(zhàn)指南:用注解寫出好用的命令行工具
簡介
Picocli 是 Java 生態(tài)里很常用的命令行工具開發(fā)庫。
它主要解決這些問題:
解析命令行參數(shù) 處理短選項(xiàng)和長選項(xiàng) 支持位置參數(shù) 自動(dòng)做類型轉(zhuǎn)換 生成幫助文檔 生成版本信息 支持子命令 支持命令補(bǔ)全 支持打包成可執(zhí)行 Jar 支持 GraalVM Native Image
一句話概括:
Picocli 用注解描述命令、選項(xiàng)和參數(shù),把 String[] args 解析成 Java 對象。
沒有 Picocli 時(shí),命令行參數(shù)通常要手動(dòng)解析:
public static void main(String[] args) {
for (int i = 0; i < args.length; i++) {
if ("--file".equals(args[i])) {
String file = args[++i];
System.out.println(file);
}
}
}
這種寫法很快會(huì)變亂:
--file a.txt--file=a.txt-f a.txt-abc- 必填參數(shù)
- 默認(rèn)值
- 參數(shù)類型轉(zhuǎn)換
- 參數(shù)錯(cuò)誤提示
--help- 子命令
Picocli 把這些通用能力封裝好了。
Picocli 適合什么場景
常見場景如下:
| 場景 | 示例 |
|---|---|
| 開發(fā)者工具 | 代碼生成、項(xiàng)目初始化、接口調(diào)試 |
| 運(yùn)維工具 | 發(fā)布、備份、清理、巡檢 |
| 數(shù)據(jù)處理工具 | CSV 轉(zhuǎn)換、日志分析、批量導(dǎo)入 |
| 管理腳本 | 用戶管理、配置檢查、任務(wù)觸發(fā) |
| Spring Boot CLI | 復(fù)用 Service 做命令行管理工具 |
| 本地小工具 | 文件重命名、圖片處理、目錄掃描 |
它和一些常見 CLI 工具的定位類似:
| 語言 | 常見 CLI 框架 |
|---|---|
| Java | Picocli |
| Go | Cobra |
| Python | argparse / Click / Typer |
| Node.js | Commander |
| .NET | System.CommandLine |
Maven 依賴
當(dāng)前 picocli 最新穩(wěn)定版本是 4.7.7。
Maven:
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
<version>4.7.7</version>
</dependency>Gradle Kotlin DSL:
dependencies {
implementation("info.picocli:picocli:4.7.7")
}
如果要生成 GraalVM Native Image 配置、命令補(bǔ)全腳本等,可以再加 picocli-codegen。
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli-codegen</artifactId>
<version>4.7.7</version>
<scope>provided</scope>
</dependency>核心注解
Picocli 最常用的注解不多。
| 注解 | 作用 |
|---|---|
@Command | 定義一個(gè)命令 |
@Option | 定義選項(xiàng),比如 -f、--file |
@Parameters | 定義位置參數(shù),比如 copy a.txt b.txt 里的兩個(gè)文件 |
@Mixin | 復(fù)用一組公共選項(xiàng) |
@ParentCommand | 在子命令里拿到父命令對象 |
@Spec | 拿到當(dāng)前命令的元數(shù)據(jù) |
一個(gè)命令類通常實(shí)現(xiàn) Runnable 或 Callable<Integer>。
Runnable:只執(zhí)行邏輯,不關(guān)心返回值 Callable<Integer>:可以返回退出碼,更適合正式 CLI
正式工具更推薦 Callable<Integer>。
退出碼約定:
0:成功 非 0:失敗
第一個(gè) Demo:hello 命令
先寫一個(gè)最簡單的命令。
目標(biāo):
java -jar hello.jar 張三
輸出:
Hello, 張三
代碼:
package com.example.picoclidemo;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Parameters;
import java.util.concurrent.Callable;
@Command(
name = "hello",
description = "打印問候語",
mixinStandardHelpOptions = true,
version = "hello 1.0.0"
)
public class HelloCommand implements Callable<Integer> {
@Parameters(index = "0", description = "名稱")
private String name;
@Override
public Integer call() {
System.out.println("Hello, " + name);
return 0;
}
public static void main(String[] args) {
int exitCode = new CommandLine(new HelloCommand()).execute(args);
System.exit(exitCode);
}
}運(yùn)行:
java com.example.picoclidemo.HelloCommand 張三
輸出:
Hello, 張三
查看幫助:
java com.example.picoclidemo.HelloCommand --help
輸出類似:
Usage: hello [-hV] <name>
打印問候語
<name> 名稱
-h, --help Show this help message and exit.
-V, --version Print version information and exit.mixinStandardHelpOptions = true 會(huì)自動(dòng)添加:
-h, --help -V, --version
這兩個(gè)選項(xiàng)幾乎每個(gè)正式 CLI 都應(yīng)該有。
Option 和 Parameters 的區(qū)別
命令行參數(shù)大致分兩類。
Option
帶名字的參數(shù)叫選項(xiàng)。
比如:
tool --file app.log --limit 100 --verbose
對應(yīng) Picocli:
@Option(names = {"-f", "--file"}, description = "文件路徑")
private File file;
@Option(names = {"-l", "--limit"}, defaultValue = "100", description = "最多處理多少行")
private int limit;
@Option(names = {"-v", "--verbose"}, description = "輸出詳細(xì)日志")
private boolean verbose;Parameters
沒有名字、靠位置識(shí)別的參數(shù)叫位置參數(shù)。
比如:
copy source.txt target.txt
對應(yīng) Picocli:
@Parameters(index = "0", description = "源文件") private File source; @Parameters(index = "1", description = "目標(biāo)文件") private File target;
簡單判斷:
有 - 或 -- 前綴:@Option 沒有前綴,靠順序:@Parameters
實(shí)戰(zhàn) Demo:文件統(tǒng)計(jì)工具
下面做一個(gè)稍微實(shí)用點(diǎn)的工具:統(tǒng)計(jì)文本文件。
支持功能:
統(tǒng)計(jì)行數(shù) 統(tǒng)計(jì)字符數(shù) 忽略空行 限制最多讀取多少行 輸出詳細(xì)信息
命令示例:
textstat README.md --chars --ignore-blank --limit 1000 -v
命令類
package com.example.picoclidemo;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.Callable;
@Command(
name = "textstat",
description = "統(tǒng)計(jì)文本文件的行數(shù)和字符數(shù)",
mixinStandardHelpOptions = true,
version = "textstat 1.0.0"
)
public class TextStatCommand implements Callable<Integer> {
@Parameters(index = "0", description = "要統(tǒng)計(jì)的文本文件")
private Path file;
@Option(names = {"-c", "--chars"}, description = "統(tǒng)計(jì)字符數(shù)")
private boolean countChars;
@Option(names = {"--ignore-blank"}, description = "忽略空行")
private boolean ignoreBlank;
@Option(names = {"-l", "--limit"}, defaultValue = "0", description = "最多讀取多少行,0 表示不限制")
private int limit;
@Option(names = {"-v", "--verbose"}, description = "輸出詳細(xì)信息")
private boolean verbose;
@Override
public Integer call() {
if (!Files.exists(file)) {
System.err.println("文件不存在: " + file);
return 2;
}
if (!Files.isRegularFile(file)) {
System.err.println("不是普通文件: " + file);
return 2;
}
try {
List<String> lines = Files.readAllLines(file, StandardCharsets.UTF_8);
if (limit > 0 && lines.size() > limit) {
lines = lines.subList(0, limit);
}
long lineCount = lines.stream()
.filter(line -> !ignoreBlank || !line.isBlank())
.count();
System.out.println("lines=" + lineCount);
if (countChars) {
int chars = lines.stream()
.filter(line -> !ignoreBlank || !line.isBlank())
.mapToInt(String::length)
.sum();
System.out.println("chars=" + chars);
}
if (verbose) {
System.out.println("file=" + file.toAbsolutePath());
System.out.println("limit=" + limit);
System.out.println("ignoreBlank=" + ignoreBlank);
}
return 0;
} catch (IOException e) {
System.err.println("讀取文件失敗: " + e.getMessage());
return 1;
}
}
public static void main(String[] args) {
int exitCode = new CommandLine(new TextStatCommand()).execute(args);
System.exit(exitCode);
}
}運(yùn)行:
java com.example.picoclidemo.TextStatCommand README.md
輸出:
lines=128
統(tǒng)計(jì)字符數(shù):
java com.example.picoclidemo.TextStatCommand README.md --chars
輸出:
lines=128 chars=5042
忽略空行并輸出詳細(xì)信息:
java com.example.picoclidemo.TextStatCommand README.md --ignore-blank -v
輸出:
lines=96 file=/Users/test/project/README.md limit=0 ignoreBlank=true
缺少文件參數(shù):
java com.example.picoclidemo.TextStatCommand
Picocli 會(huì)直接提示參數(shù)缺失,并打印用法。
常用 Option 寫法
必填選項(xiàng)
@Option(names = {"-u", "--username"}, required = true, description = "用戶名")
private String username;
沒傳會(huì)報(bào)錯(cuò):
Missing required option: '--username=<username>'
默認(rèn)值
@Option(names = {"-p", "--port"}, defaultValue = "8080", description = "端口,默認(rèn) ${DEFAULT-VALUE}")
private int port;
幫助信息里可以用:
${DEFAULT-VALUE}
顯示默認(rèn)值。
布爾開關(guān)
@Option(names = {"-v", "--verbose"}, description = "詳細(xì)輸出")
private boolean verbose;
只要命令里出現(xiàn) -v,值就是 true。
tool -v tool --verbose
多值參數(shù)
@Option(names = {"-t", "--tag"}, description = "標(biāo)簽,可重復(fù)")
private List<String> tags;
運(yùn)行:
tool -t java -t cli -t demo
結(jié)果:
[java, cli, demo]
也可以用 split:
@Option(names = "--tags", split = ",", description = "逗號(hào)分隔的標(biāo)簽") private List<String> tags;
運(yùn)行:
tool --tags java,cli,demo
可選值
有些選項(xiàng)傳不傳值都可以。
比如:
tool --color tool --color=always tool --color=never
寫法:
@Option(
names = "--color",
arity = "0..1",
fallbackValue = "always",
defaultValue = "auto",
description = "顏色模式: auto, always, never"
)
private String color;
含義:
不傳 --color:auto 只傳 --color:always 傳 --color=never:never
枚舉參數(shù)
enum OutputFormat {
TEXT, JSON, CSV
}
@Option(names = {"-f", "--format"}, defaultValue = "TEXT", description = "輸出格式: ${COMPLETION-CANDIDATES}")
private OutputFormat format;COMPLETION-CANDIDATES 會(huì)顯示候選值。
運(yùn)行:
tool --format JSON
位置參數(shù)
單個(gè)位置參數(shù):
@Parameters(index = "0", description = "輸入文件") private Path input;
多個(gè)位置參數(shù):
@Parameters(index = "0", description = "源文件") private Path source; @Parameters(index = "1", description = "目標(biāo)文件") private Path target;
剩余參數(shù):
@Parameters(index = "0..*", description = "文件列表") private List<Path> files;
運(yùn)行:
tool a.txt b.txt c.txt
類型轉(zhuǎn)換
Picocli 內(nèi)置了很多常見類型轉(zhuǎn)換。
常見類型:
| Java 類型 | 命令行傳值 |
|---|---|
String | hello |
int / Integer | 8080 |
long / Long | 10000 |
boolean | true / false,或布爾開關(guān) |
File | ./a.txt |
Path | ./a.txt |
URL | https://example.com |
URI | file:///tmp/a.txt |
Duration | PT10S |
LocalDate | 2026-01-01 |
Enum | JSON |
如果內(nèi)置轉(zhuǎn)換不夠,可以寫自定義轉(zhuǎn)換器。
自定義 LocalDate 轉(zhuǎn)換器
假設(shè)日期想支持:
2026-07-18
轉(zhuǎn)換器:
package com.example.picoclidemo;
import picocli.CommandLine.ITypeConverter;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public class LocalDateConverter implements ITypeConverter<LocalDate> {
@Override
public LocalDate convert(String value) {
return LocalDate.parse(value, DateTimeFormatter.ISO_LOCAL_DATE);
}
}使用:
@Option(names = "--date", converter = LocalDateConverter.class, description = "日期,格式 yyyy-MM-dd") private LocalDate date;
運(yùn)行:
tool --date 2026-07-18
子命令 Demo:filecli
復(fù)雜 CLI 一般不是一個(gè)命令解決所有問題,而是類似:
git add git commit kubectl get pods docker image ls
Picocli 支持子命令。
下面做一個(gè) filecli:
filecli stat README.md filecli copy a.txt b.txt --force filecli delete temp.log --dry-run
主命令
package com.example.picoclidemo.filecli;
import picocli.CommandLine;
import picocli.CommandLine.Command;
@Command(
name = "filecli",
description = "文件處理命令行工具",
mixinStandardHelpOptions = true,
version = "filecli 1.0.0",
subcommands = {
StatCommand.class,
CopyCommand.class,
DeleteCommand.class
}
)
public class FileCliCommand implements Runnable {
@Override
public void run() {
CommandLine.usage(this, System.out);
}
public static void main(String[] args) {
int exitCode = new CommandLine(new FileCliCommand()).execute(args);
System.exit(exitCode);
}
}stat 子命令
package com.example.picoclidemo.filecli;
import picocli.CommandLine.Command;
import picocli.CommandLine.Parameters;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.Callable;
@Command(name = "stat", description = "查看文件基本信息")
public class StatCommand implements Callable<Integer> {
@Parameters(index = "0", description = "文件路徑")
private Path file;
@Override
public Integer call() throws Exception {
if (!Files.exists(file)) {
System.err.println("文件不存在: " + file);
return 2;
}
System.out.println("path=" + file.toAbsolutePath());
System.out.println("size=" + Files.size(file));
System.out.println("directory=" + Files.isDirectory(file));
System.out.println("regularFile=" + Files.isRegularFile(file));
return 0;
}
}copy 子命令
package com.example.picoclidemo.filecli;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.concurrent.Callable;
@Command(name = "copy", description = "復(fù)制文件")
public class CopyCommand implements Callable<Integer> {
@Parameters(index = "0", description = "源文件")
private Path source;
@Parameters(index = "1", description = "目標(biāo)文件")
private Path target;
@Option(names = {"-f", "--force"}, description = "覆蓋已存在文件")
private boolean force;
@Override
public Integer call() throws Exception {
if (!Files.exists(source)) {
System.err.println("源文件不存在: " + source);
return 2;
}
if (Files.exists(target) && !force) {
System.err.println("目標(biāo)文件已存在,使用 --force 覆蓋: " + target);
return 3;
}
if (force) {
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
} else {
Files.copy(source, target);
}
System.out.println("復(fù)制完成: " + source + " -> " + target);
return 0;
}
}delete 子命令
package com.example.picoclidemo.filecli;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.Callable;
@Command(name = "delete", description = "刪除文件")
public class DeleteCommand implements Callable<Integer> {
@Parameters(index = "0", description = "文件路徑")
private Path file;
@Option(names = "--dry-run", description = "只打印將要執(zhí)行的操作,不真正刪除")
private boolean dryRun;
@Override
public Integer call() throws Exception {
if (!Files.exists(file)) {
System.err.println("文件不存在: " + file);
return 2;
}
if (dryRun) {
System.out.println("[dry-run] 將刪除: " + file.toAbsolutePath());
return 0;
}
Files.delete(file);
System.out.println("刪除完成: " + file.toAbsolutePath());
return 0;
}
}運(yùn)行:
java com.example.picoclidemo.filecli.FileCliCommand stat README.md java com.example.picoclidemo.filecli.FileCliCommand copy a.txt b.txt --force java com.example.picoclidemo.filecli.FileCliCommand delete temp.log --dry-run
查看子命令幫助:
java com.example.picoclidemo.filecli.FileCliCommand copy --help
輸出類似:
Usage: filecli copy [-f] <source> <target>
復(fù)制文件
<source> 源文件
<target> 目標(biāo)文件
-f, --force 覆蓋已存在文件
復(fù)用公共選項(xiàng):Mixin
很多子命令都會(huì)有公共參數(shù),比如:
--verbose --profile --config
可以用 @Mixin 復(fù)用。
公共選項(xiàng):
package com.example.picoclidemo.filecli;
import picocli.CommandLine.Option;
import java.nio.file.Path;
public class CommonOptions {
@Option(names = {"-v", "--verbose"}, description = "輸出詳細(xì)日志")
boolean verbose;
@Option(names = "--config", description = "配置文件路徑")
Path config;
}子命令使用:
@Mixin private CommonOptions commonOptions;
完整示例:
@Command(name = "stat", description = "查看文件基本信息")
public class StatCommand implements Callable<Integer> {
@Mixin
private CommonOptions commonOptions;
@Parameters(index = "0", description = "文件路徑")
private Path file;
@Override
public Integer call() throws Exception {
if (commonOptions.verbose) {
System.out.println("config=" + commonOptions.config);
}
System.out.println("path=" + file.toAbsolutePath());
return 0;
}
}運(yùn)行:
filecli stat README.md --verbose --config ./app.properties
交互式輸入
密碼、Token 這類敏感參數(shù)不適合直接寫在命令里。
可以使用交互式輸入:
@Option(names = "--username", required = true, description = "用戶名")
private String username;
@Option(
names = "--password",
interactive = true,
arity = "0..1",
description = "密碼,不在命令行中明文顯示"
)
private char[] password;運(yùn)行:
tool --username admin --password
控制臺(tái)會(huì)提示輸入密碼。
相比:
tool --password 123456
交互式輸入更適合敏感信息。
原因是命令行參數(shù)可能被歷史記錄、進(jìn)程列表、審計(jì)日志記錄下來。
參數(shù)校驗(yàn)
Picocli 本身會(huì)做一些基本校驗(yàn):
- 必填選項(xiàng)
- 參數(shù)個(gè)數(shù)
- 類型轉(zhuǎn)換
- 枚舉值是否合法
比如:
@Option(names = "--port", required = true) private int port;
傳入:
tool --port abc
會(huì)提示無法轉(zhuǎn)換成整數(shù)。
更復(fù)雜的業(yè)務(wù)校驗(yàn)可以放到 call() 里。
示例:
@Option(names = "--port", defaultValue = "8080", description = "端口")
private int port;
@Override
public Integer call() {
if (port < 1 || port > 65535) {
System.err.println("端口范圍必須是 1 到 65535");
return 2;
}
return 0;
}也可以使用自定義轉(zhuǎn)換器提前攔截非法值。
錯(cuò)誤處理和退出碼
CommandLine.execute(args) 會(huì)返回退出碼。
常見做法:
public static void main(String[] args) {
int exitCode = new CommandLine(new AppCommand()).execute(args);
System.exit(exitCode);
}
常見退出碼可以這樣約定:
| 退出碼 | 含義 |
|---|---|
0 | 成功 |
1 | 普通執(zhí)行失敗 |
2 | 參數(shù)或輸入文件問題 |
3 | 目標(biāo)已存在、狀態(tài)沖突 |
Picocli 內(nèi)置常量:
CommandLine.ExitCode.OK CommandLine.ExitCode.USAGE CommandLine.ExitCode.SOFTWARE
示例:
return CommandLine.ExitCode.OK;
如果需要統(tǒng)一處理異常,可以配置異常處理器:
CommandLine commandLine = new CommandLine(new FileCliCommand());
commandLine.setExecutionExceptionHandler((ex, cmd, parseResult) -> {
cmd.getErr().println("執(zhí)行失敗: " + ex.getMessage());
return 1;
});
int exitCode = commandLine.execute(args);
System.exit(exitCode);幫助文檔寫好一點(diǎn)
命令行工具的幫助信息很重要。
推薦給 @Command 寫清楚這些字段:
@Command(
name = "filecli",
description = "文件處理命令行工具",
synopsisHeading = "%n用法:%n ",
descriptionHeading = "%n說明:%n",
optionListHeading = "%n選項(xiàng):%n",
parameterListHeading = "%n參數(shù):%n",
commandListHeading = "%n子命令:%n",
mixinStandardHelpOptions = true,
version = "filecli 1.0.0"
)
選項(xiàng)描述里可以使用變量:
@Option(
names = "--format",
defaultValue = "TEXT",
description = "輸出格式: ${COMPLETION-CANDIDATES},默認(rèn) ${DEFAULT-VALUE}"
)
private OutputFormat format;
常見變量:
| 變量 | 說明 |
|---|---|
${DEFAULT-VALUE} | 默認(rèn)值 |
${COMPLETION-CANDIDATES} | 候選值 |
${FALLBACK-VALUE} | 可選值參數(shù)的 fallback 值 |
命令補(bǔ)全
Picocli 可以生成命令補(bǔ)全腳本。
補(bǔ)全能力包括:
- 命令名
- 子命令
- 選項(xiàng)
- 枚舉候選值
添加 codegen 依賴后,可以生成腳本。
常見方式是在主命令里加入 AutoComplete.GenerateCompletion 子命令:
@Command(
name = "filecli",
mixinStandardHelpOptions = true,
subcommands = {
StatCommand.class,
CopyCommand.class,
DeleteCommand.class,
picocli.AutoComplete.GenerateCompletion.class
}
)
public class FileCliCommand implements Runnable {
@Override
public void run() {
CommandLine.usage(this, System.out);
}
}生成腳本:
java -cp "target/filecli.jar" com.example.picoclidemo.filecli.FileCliCommand generate-completion
也可以用 picocli.AutoComplete 工具類按命令類生成。
不同 Shell 的安裝方式不同,常見是把生成腳本放到 Bash、Zsh 或 Fish 的補(bǔ)全目錄里。
打包成可執(zhí)行 Jar
命令行工具通常希望這樣運(yùn)行:
java -jar filecli.jar stat README.md
可以用 Maven Shade Plugin 打包 fat jar。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.example.picoclidemo.filecli.FileCliCommand</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>打包:
mvn clean package
運(yùn)行:
java -jar target/filecli-1.0.0.jar stat README.md
為了像普通命令一樣運(yùn)行,可以加一個(gè)腳本。
filecli:
#!/usr/bin/env sh java -jar /opt/filecli/filecli-1.0.0.jar "$@"
授權(quán):
chmod +x filecli
運(yùn)行:
./filecli stat README.md
GraalVM Native Image
CLI 工具很適合打成 Native Image。
好處:
- 啟動(dòng)快
- 不需要目標(biāo)機(jī)器安裝 JVM
- 分發(fā)更像普通二進(jìn)制命令
Picocli 對 GraalVM Native Image 支持比較好。
Maven Native Build Tools 示例:
<profiles>
<profile>
<id>native</id>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>1.1.2</version>
<extensions>true</extensions>
<configuration>
<mainClass>com.example.picoclidemo.filecli.FileCliCommand</mainClass>
<imageName>filecli</imageName>
</configuration>
<executions>
<execution>
<id>build-native</id>
<goals>
<goal>compile-no-fork</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>構(gòu)建:
mvn -Pnative package
運(yùn)行:
./target/filecli stat README.md
Native Image 構(gòu)建需要安裝 GraalVM 和本地 C 編譯工具鏈。
如果項(xiàng)目引入了反射、動(dòng)態(tài)代理、資源文件,可能還需要額外配置。純 Picocli 小工具通常比較順。
Spring Boot 集成
如果 CLI 需要復(fù)用 Spring Bean,比如:
UserServiceOrderServiceRepository- 配置文件
- 數(shù)據(jù)源
可以使用 picocli-spring-boot-starter。
依賴:
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli-spring-boot-starter</artifactId>
<version>4.7.7</version>
</dependency>命令類:
package com.example.bootcli;
import org.springframework.stereotype.Component;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import java.util.concurrent.Callable;
@Component
@Command(name = "user", description = "用戶管理命令", mixinStandardHelpOptions = true)
public class UserCommand implements Callable<Integer> {
private final UserService userService;
@Option(names = "--list", description = "列出用戶")
private boolean list;
public UserCommand(UserService userService) {
this.userService = userService;
}
@Override
public Integer call() {
if (list) {
userService.findAll().forEach(System.out::println);
return 0;
}
System.out.println("沒有指定操作");
return 2;
}
}啟動(dòng)類:
package com.example.bootcli;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import picocli.CommandLine;
import picocli.CommandLine.IFactory;
@SpringBootApplication
public class BootCliApplication implements CommandLineRunner {
private final UserCommand userCommand;
private final IFactory factory;
public BootCliApplication(UserCommand userCommand, IFactory factory) {
this.userCommand = userCommand;
this.factory = factory;
}
public static void main(String[] args) {
SpringApplication.run(BootCliApplication.class, args);
}
@Override
public void run(String... args) {
int exitCode = new CommandLine(userCommand, factory).execute(args);
System.exit(exitCode);
}
}運(yùn)行:
java -jar boot-cli.jar --list
這種方式的關(guān)鍵點(diǎn)是:
命令類交給 Spring 管理 CommandLine 使用 Spring 提供的 IFactory 創(chuàng)建對象 子命令、轉(zhuǎn)換器等也可以走 Spring 注入
如果只是一個(gè)很小的 CLI,不一定需要 Spring Boot。Spring Boot 啟動(dòng)成本更高,適合需要復(fù)用完整業(yè)務(wù)容器的場景。
單元測試
CLI 工具也應(yīng)該測試。
Picocli 測試很方便,可以直接構(gòu)造命令對象,捕獲輸出。
示例命令:
@Command(name = "hello")
class HelloCommand implements Callable<Integer> {
@Parameters(index = "0")
String name;
PrintWriter out = new PrintWriter(System.out, true);
@Override
public Integer call() {
out.println("Hello, " + name);
return 0;
}
}測試:
package com.example.picoclidemo;
import org.junit.jupiter.api.Test;
import picocli.CommandLine;
import java.io.PrintWriter;
import java.io.StringWriter;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class HelloCommandTest {
@Test
void shouldPrintHello() {
HelloCommand command = new HelloCommand();
StringWriter output = new StringWriter();
command.out = new PrintWriter(output);
int exitCode = new CommandLine(command).execute("張三");
assertEquals(0, exitCode);
assertTrue(output.toString().contains("Hello, 張三"));
}
}也可以直接測試錯(cuò)誤參數(shù):
@Test
void shouldReturnUsageExitCodeWhenMissingName() {
int exitCode = new CommandLine(new HelloCommand()).execute();
assertEquals(CommandLine.ExitCode.USAGE, exitCode);
}和 Apache Commons CLI 的區(qū)別
Java 里還有一個(gè)老牌庫叫 Apache Commons CLI。
兩者大概區(qū)別:
| 對比項(xiàng) | Picocli | Apache Commons CLI |
|---|---|---|
| 開發(fā)方式 | 注解驅(qū)動(dòng),也支持編程式 API | 編程式 API |
| 幫助文檔 | 自動(dòng)生成能力強(qiáng) | 相對基礎(chǔ) |
| 子命令 | 支持較好 | 需要自行組織 |
| 類型轉(zhuǎn)換 | 內(nèi)置很多類型 | 通常手動(dòng)取字符串再轉(zhuǎn)換 |
| GraalVM | 支持較好 | 不是主要賣點(diǎn) |
| 適合場景 | 完整 CLI 工具 | 簡單參數(shù)解析 |
新寫命令行工具,Picocli 更省事。
如果只是解析幾個(gè)參數(shù),Commons CLI 也夠用。
常見坑
main 方法里沒有 System.exit
錯(cuò)誤寫法:
new CommandLine(new AppCommand()).execute(args);
腳本調(diào)用時(shí)無法正確拿到退出碼。
推薦:
int exitCode = new CommandLine(new AppCommand()).execute(args); System.exit(exitCode);
還在使用 CommandLine.run 或 call
舊示例里經(jīng)常看到:
CommandLine.run(new AppCommand(), args); CommandLine.call(new AppCommand(), args);
這些便捷方法在 Picocli 4.x 里已經(jīng)不推薦。
推薦:
new CommandLine(new AppCommand()).execute(args);
布爾參數(shù)寫成 Boolean 但沒有默認(rèn)值
@Option(names = "--verbose") private Boolean verbose;
如果沒傳,值是 null。
大部分布爾開關(guān)用基本類型更簡單:
@Option(names = "--verbose") private boolean verbose;
位置參數(shù)順序不清楚
@Parameters private Path source; @Parameters private Path target;
多個(gè)位置參數(shù)建議明確 index:
@Parameters(index = "0") private Path source; @Parameters(index = "1") private Path target;
選項(xiàng)名太隨意
不建議寫一堆只有短選項(xiàng)的參數(shù):
-a -b -c -x
正式工具最好短選項(xiàng)和長選項(xiàng)都提供:
@Option(names = {"-f", "--file"})
@Option(names = {"-o", "--output"})
@Option(names = {"-v", "--verbose"})
長選項(xiàng)更適合腳本,可讀性更好。
密碼直接從參數(shù)傳入
不推薦:
tool --password 123456
更推薦:
tool --password
配合:
@Option(names = "--password", interactive = true, arity = "0..1") private char[] password;
總結(jié)
Picocli 可以按這條線理解:
@Command 定義命令 @Option 定義具名選項(xiàng) @Parameters 定義位置參數(shù) CommandLine.execute 負(fù)責(zé)解析和執(zhí)行 Callable<Integer> 返回退出碼 subcommands 組織復(fù)雜工具 Mixin 復(fù)用公共參數(shù)
日常開發(fā)可以按這個(gè)順序落地:
- 先寫單命令 Demo
- 再補(bǔ)齊
--help和--version - 參數(shù)盡量用明確類型,比如
Path、int、Enum - 復(fù)雜工具拆成子命令
- 公共選項(xiàng)用
@Mixin - 正式發(fā)布時(shí)打包成可執(zhí)行 Jar
- 對啟動(dòng)速度敏感時(shí)考慮 Native Image
- 需要復(fù)用業(yè)務(wù) Bean 時(shí)接入 Spring Boot
Picocli 的價(jià)值不是“把參數(shù)解析換成注解”這么簡單。真正省事的地方在于幫助文檔、錯(cuò)誤提示、子命令、類型轉(zhuǎn)換、退出碼這些細(xì)節(jié)都已經(jīng)有一套成熟約定。命令行工具越往后做,越能感受到這些約定帶來的穩(wěn)定性。
到此這篇關(guān)于Java Picocli 實(shí)戰(zhàn)指南:用注解寫出好用的命令行工具的文章就介紹到這了,更多相關(guān)Java Picocli命令行工具內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Cursor IDE中SpringBoot項(xiàng)目啟動(dòng)內(nèi)存不足問題的解決方案
在CursorIDE中運(yùn)行SpringBoot項(xiàng)目時(shí),可能出現(xiàn)啟動(dòng)失敗、報(bào)錯(cuò)等問題,這些問題通常與JVM內(nèi)存設(shè)置不當(dāng)有關(guān),本文總結(jié)了多種解決方案,需要的朋友可以參考下2026-04-04
Spring中的@AliasFor標(biāo)簽的用法及說明
Spring框架中的@AliasFor標(biāo)簽用于定義注解屬性的別名,使得在同一個(gè)注解中可以使用相同的屬性值,從而提高代碼的可讀性和可維護(hù)性2026-03-03
關(guān)于Redis鍵值出現(xiàn)\xac\xed\x00\x05t\x00&錯(cuò)誤的解決方法
這篇文章主要介紹了關(guān)于Redis鍵值出現(xiàn)\xac\xed\x00\x05t\x00&的解決方法,出現(xiàn)該問題的原因是, redis template向redis存放使用java對象序列化的值,序列化方式和string的一般方式不同,需要的朋友可以參考下2023-08-08
SpringBoot項(xiàng)目部署到騰訊云的實(shí)現(xiàn)步驟
本文主要介紹了SpringBoot項(xiàng)目部署到騰訊云的實(shí)現(xiàn)步驟,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01
Maven 集成 groovy 腳本插件gmavenplus-plugin詳解
文章介紹了在Maven構(gòu)建過程中使用Groovy腳本進(jìn)行動(dòng)態(tài)配置的示例,重點(diǎn)解析了如何在`<build>`配置片段中綁定腳本到`initialize`生命周期階段,并詳細(xì)說明了腳本執(zhí)行流程、常見使用場景以及潛在問題和優(yōu)化建議,感興趣的朋友跟隨小編一起看看吧2025-12-12
Java?JDBC高封裝Util類的項(xiàng)目實(shí)踐
這篇文章主要介紹了Java?JDBC高封裝Util類的項(xiàng)目實(shí)踐,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-08-08
MyBatis參數(shù)條件查詢傳入的值為0時(shí)的判斷問題及解決
MyBatis參數(shù)條件查詢解析,條件為空判斷處理;當(dāng)傳入?yún)?shù)為Integer類型且值為0時(shí),MyBatis會(huì)將其轉(zhuǎn)為空串;解析過程涉及sqlNode處理與OgnlOps類的double值轉(zhuǎn)換,幫助開發(fā)提供參考2026-06-06

