最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Java Picocli 實(shí)戰(zhàn)指南:用注解寫出好用的命令行工具

 更新時(shí)間:2026年07月29日 09:31:06   作者:唐青楓  
使用Java的Picocli庫來創(chuàng)建命令行工具是一種高效且簡潔的方法,Picocli是一個(gè)小巧的庫,它允許你通過注解的方式來定義命令行接口,從而使得命令行工具的創(chuàng)建變得非常簡單,下面,我將通過一個(gè)實(shí)戰(zhàn)示例來詳細(xì)說明如何使用Picocli編寫一個(gè)好用的命令行工具

簡介

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 框架
JavaPicocli
GoCobra
Pythonargparse / Click / Typer
Node.jsCommander
.NETSystem.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) RunnableCallable<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 類型命令行傳值
Stringhello
int / Integer8080
long / Long10000
booleantrue / false,或布爾開關(guān)
File./a.txt
Path./a.txt
URLhttps://example.com
URIfile:///tmp/a.txt
DurationPT10S
LocalDate2026-01-01
EnumJSON

如果內(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,比如:

  • UserService
  • OrderService
  • Repository
  • 配置文件
  • 數(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)PicocliApache 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)存不足問題的解決方案

    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)簽的用法及說明

    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&錯(cuò)誤的解決方法

    這篇文章主要介紹了關(guān)于Redis鍵值出現(xiàn)\xac\xed\x00\x05t\x00&的解決方法,出現(xiàn)該問題的原因是, redis template向redis存放使用java對象序列化的值,序列化方式和string的一般方式不同,需要的朋友可以參考下
    2023-08-08
  • springboot整合minio的超詳細(xì)教程

    springboot整合minio的超詳細(xì)教程

    在很多互聯(lián)網(wǎng)產(chǎn)品應(yīng)用中,都涉及到各種與文件存儲(chǔ)相關(guān)的業(yè)務(wù),隨著技術(shù)的發(fā)展,關(guān)于如何解決分布式文件存儲(chǔ)也有了比較成熟的方案,比如私有云部署下可以考慮fastdfs,阿里云對象存儲(chǔ)oss,七牛云等,本篇將為你介紹另一種文件存儲(chǔ)方式,即MinIO,需要的朋友可以參考下
    2023-12-12
  • SpringBoot項(xiàng)目部署到騰訊云的實(shí)現(xiàn)步驟

    SpringBoot項(xiàng)目部署到騰訊云的實(shí)現(xiàn)步驟

    本文主要介紹了SpringBoot項(xiàng)目部署到騰訊云的實(shí)現(xiàn)步驟,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-01-01
  • Mybatis 將table表名作為參數(shù)傳入操作

    Mybatis 將table表名作為參數(shù)傳入操作

    這篇文章主要介紹了Mybatis 將table表名作為參數(shù)傳入操作,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-12-12
  • Maven 集成 groovy 腳本插件gmavenplus-plugin詳解

    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中過濾器和攔截器的區(qū)別有哪些

    Java中過濾器和攔截器的區(qū)別有哪些

    過濾器和攔截器非常相似,但是它們有很大的區(qū)別,這篇文章主要介紹了Java中過濾器和攔截器的區(qū)別有哪些的相關(guān)資料,文中介紹的非常詳細(xì),需要的朋友可以參考下
    2025-09-09
  • Java?JDBC高封裝Util類的項(xiàng)目實(shí)踐

    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ù)條件查詢傳入的值為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

最新評論

翼城县| 灵川县| 普兰店市| 孝昌县| 炎陵县| 电白县| 大余县| 塔河县| 固原市| 新竹市| 天峨县| 樟树市| 榆社县| 金堂县| 梅河口市| 垣曲县| 中山市| 连平县| 安仁县| 牡丹江市| 安西县| 永川市| 武安市| 新丰县| 清徐县| 湖南省| 湟中县| 青浦区| 三亚市| 景泰县| 曲麻莱县| 沈丘县| 安龙县| 兴海县| 玉田县| 泰顺县| 祁阳县| 华宁县| 长武县| 昭苏县| 宁德市|