[CLI/Java] 基于 Java 构建 CLI 命令行工具的解决方案

1 需求背景

  • 排查项目中的各类数据问题,测试人员和开发人员都耗费大量时间,不如将这部分排查工作所需的程序工具化。

  • 为此,稍微调研了一下cli命令行构建框架。

  • 综合之下,目前笔者选择的 picocli 框架。感觉其框架的易用性更好。

  • cli 命令行工具与业务工程模块的区别在于:

  1. 参数/请求的功能、输入数据,来源于使用者及其电脑本地。

参数和数据的识别,是一个需要考虑的问题。

  1. 部署位置:电脑本地
  2. 使用人员:后台支持人员,而非最终的平台用户(这种情况极少)。

2 需求描述

  • 在 Java 项目中实现 CLI(命令行工具)可以通过使用框架如 Picocli 或 Apache Commons CLI 来快速完成。

这些框架提供了强大的功能来解析命令行参数、生成帮助信息等。

3 解决方案: 使用 Picocli 框架

框架介绍

  • Picocli 是一个功能强大且易用的命令行工具开发框架,支持注解驱动开发

  • URL

实现步骤

Step1 添加依赖

  • 在 Maven 的 pom.xml 文件中引入 Picocli:
<dependency>
  <groupId>info.picocli</groupId>
  <artifactId>picocli</artifactId>
  <version>4.7.6</version>
</dependency>

Step2 创建命令类

  • 使用注解定义命令和选项:
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;

@Command(name = "example", mixinStandardHelpOptions = true, description = "示例 CLI 工具")
public class ExampleCLI implements Runnable {

	@Option(names = {"-n", "--name"}, description = "用户名称")
	private String name;

	@Override
	public void run() {
		System.out.println("Hello, " + (name != null ? name : "World") + "!");
	}

	public static void main(String[] args) {
		int exitCode = new CommandLine(new ExampleCLI()).execute(args);
		System.exit(exitCode);
	}
}

Step3 运行程序

  • 编译并运行程序:
java -jar example.jar --name=John

案例实践: xxx-common-helper

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>xxx-common-resource</artifactId>
        <groupId>cn.xxx.bd</groupId>
        <version>1.4.15-SNAPSHOT</version>
        <relativePath>../pom.xml</relativePath>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>xxx-common-helper</artifactId>
    <packaging>jar</packaging>

    <name>xxx-common-helper</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>

        <slf4j.version>1.7.25</slf4j.version>
        <log4j.version>2.20.0</log4j.version>
        <lombok.version>1.18.22</lombok.version>

        <xxx-common-resource.version>1.4.15-SNAPSHOT</xxx-common-resource.version>
        <picocli.version>4.7.6</picocli.version>

        <maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
        <maven-jar-plugin.version>3.2.0</maven-jar-plugin.version>
        <maven-shade-plugin.version>3.2.4</maven-shade-plugin.version>
        <maven-assembly-plugin.version>3.3.0</maven-assembly-plugin.version>

    </properties>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>

        <!-- log -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>${log4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>${log4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
            <version>${log4j.version}</version>
        </dependency>
        <!-- log [end] -->

        <!-- xxx-common-resource [start] -->
        <dependency>
            <groupId>cn.xxx.bd</groupId>
            <artifactId>xxx-common-utils</artifactId>
            <version>${xxx-common-resource.version}</version>
            <exclusions>
                <exclusion>
                    <artifactId>slf4j-log4j12</artifactId>
                    <groupId>org.slf4j</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>kafka-clients</artifactId>
                    <groupId>org.apache.kafka</groupId>
                </exclusion>
                <exclusion>
                    <groupId>mysql</groupId>
                    <artifactId>mysql-connector-java</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>cn.xxx.bd</groupId>
            <artifactId>xxx-common-pojo</artifactId>
        </dependency>
        <!-- xxx-common-resource [end] -->

        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>${fastjson2.version}</version>
        </dependency>

        <!-- command framework -->
        <dependency>
            <groupId>info.picocli</groupId>
            <artifactId>picocli</artifactId>
            <version>${picocli.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>${maven-shade-plugin.version}</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <!--                            <shadedArtifactAttached>true</shadedArtifactAttached>-->
                            <!--                            <shadedClassifierName>fat</shadedClassifierName> &lt;!&ndash; Any name that makes sense &ndash;&gt;-->
                            <artifactSet>
                                <excludes>
                                    <!--<exclude>com.google.code.findbugs:jsr305</exclude>-->
                                    <!--             在使用kafka生产消息时,不要过滤                       -->
                                    <!--<exclude>org.slf4j:*</exclude>-->
                                    <!--<exclude>log4j:*</exclude>-->
                                </excludes>
                            </artifactSet>
                            <filters>
                                <filter>
                                    <!-- Do not copy the signatures in the META-INF folder. Otherwise,
                                        this might cause SecurityExceptions when using the JAR. -->
                                    <artifact>*:*</artifact>
                                    <excludes>
                                        <exclude>META-INF/*.SF</exclude>
                                        <exclude>META-INF/*.DSA</exclude>
                                        <exclude>META-INF/*.RSA</exclude>
                                    </excludes>
                                </filter>
                            </filters>
                            <transformers>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ApacheNoticeResourceTransformer">
                                    <projectName>Apache Flink</projectName>
                                    <encoding>${project.build.sourceEncoding}</encoding>
                                </transformer>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>cn.xxx.bd.common.helper.xxxCommandHelper</mainClass>
                                </transformer>
                            </transformers>
                            <!--                            <minimizeJar>true</minimizeJar>-->
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>${maven-compiler-plugin.version}</version>
                <configuration>
                    <source>${maven.compiler.source}</source>
                    <target>${maven.compiler.target}</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

基础组件

Constants

package cn.xxx.bd.common.helper.constants;

/**
 * @author xxxxxx
 * @version v1.0
 * @description ...
 * @refrence-doc
 * @gpt-promt
 */
public class Constants {
    public final static String CHARSET = "UTF-8";

    public static class Parameters {
        public static String COMMAND_PARAM = "command";
    }
}

CommandEnum

package cn.xxx.bd.common.helper.enums;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author xxxxxx
 * @version v1.0
 * @description ...
 * @refrence-doc
 * @gpt-promt
 */
public enum CommandEnum {
    FILE_COMPRESS("file-compress", "文件解压缩");

    private String command;
    private String description;

    private final static String COMMAND_PARAM = "command";
    private final static String DESCRIPTION_PARAM = "description";

    CommandEnum(String command, String description) {
        this.command = command;
        this.description = description;
    }
    public static CommandEnum findByCommand(String command) {
        for (CommandEnum type : values()) {
            if (type.getCommand().equals(command)) {
                return type;
            }
        }
        return null;
    }
    public String getCommand() {
        return this.command;
    }

    public String getDescription() {
        return this.description;
    }

    public static List<Map<String, String>> toList() {
        List<Map<String, String>> list = new ArrayList();//Lists.newArrayList()其实和new ArrayList()几乎一模
        for (CommandEnum item : CommandEnum.values()) {
            Map<String, String> map = new HashMap<String, String>();
            map.put(CommandEnum.COMMAND_PARAM, item.getCommand());
            map.put(CommandEnum.DESCRIPTION_PARAM, item.getDescription());
            list.add(map);
        }
        return list;
    }

}

CommandParameters

package cn.xxx.bd.common.helper.entity;

import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import picocli.CommandLine;

import java.io.File;

/**
 * @author xxxxxx
 * @version v1.0
 * @description 命令行辅助工具的所有必填参数和可选参数
 * @refrence-doc
 * @gpt-promt
 */
@Data
@NoArgsConstructor
@ToString
public class CommandParameters {
    //必填参数 @CommandLine.Parameters
    @CommandLine.Parameters(index = "0", description = "功能命令")//命令行的第1个参数
    public String command;

    //选填参数 @CommandLine.Option
    @CommandLine.Option(names = {"-tc", "--timeConsuming"}, description = "输出耗时信息")
    public Boolean timeConsuming;

    //压缩算法,可选值: "zstd" / "gzip"
    @CommandLine.Option(names = {"--compress"}, description = "压缩算法")
    public String compress;

    @CommandLine.Option(names = {"--decompress"}, description = "压缩算法")
    public String decompress;

    @CommandLine.Option(names = {"-if", "--inputFile"}, description = "输入文件")
    private File inputFile;

    @CommandLine.Option(names = {"-of", "--outputFile"}, description = "输出文件")
    private File outputFile;

}

AbstractXxxCommandHelper

package cn.xxx.bd.common.helper.commands;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import picocli.CommandLine;

/**
 * @author xxxxxx
 * @version v1.0
 * @description ...
 * @refrence-doc
 * @gpt-promt
 */
@Slf4j
@Data
@ToString
@NoArgsConstructor
public abstract class AbstractXxxCommandHelper implements Runnable {
    /**
     * 功能命令 := 用户输入的功能 := 用户输入的命令
     */
    //@CommandLine.Option(names = {"-cmd", "--command"}, description = "功能命令")
    //@CommandLine.Parameters(index = "0", description = "功能命令")//命令行的第1个参数 //必填参数 @CommandLine.Parameters
    protected String command;

    protected Boolean timeConsuming = false;

    public AbstractXxxCommandHelper(String command) {
        this.command = command;
    }

    public abstract Object execute();

    //参数校验
    public abstract Boolean validate();

    @Override
    public void run(){
        Long startTime = System.currentTimeMillis();
        execute();
        Long endTime = System.currentTimeMillis();
        if(!timeConsuming){
            log.info("Execute the command(`{}`) finished, and its time consuming : {}ms", command, endTime - startTime );
        }
    }
}

XxxCommandHelper extends AbstractXxxCommandHelper (Entry Class)

package cn.xxx.bd.common.helper;

import cn.xxx.bd.common.helper.commands.AbstractXxxCommandHelper;
import cn.xxx.bd.common.helper.commands.file.FileCompressHelper;
import cn.xxx.bd.common.helper.entity.CommandParameters;
import cn.xxx.bd.common.helper.enums.CommandEnum;
import com.alibaba.fastjson2.JSON;
import picocli.CommandLine;

/**
 * 大数据命令行辅助工具
 * @reference-doc
 * [1] Java 命令行参数解析方式探索(三):Picocli - juejin - https://xie.infoq.cn/article/1da75f148f8f6941793eab7ef
 */
public abstract class XxxCommandHelper extends AbstractXxxCommandHelper {
    public static void main( String[] args ) {
        //java.util.HashMap@6c284af is not a command: it has no @Command, @Option, @Parameters or @Unmatched annotations
        //Map<String, String> commandParameters = new HashMap<>();
        CommandParameters commandParameters = new CommandParameters();

        CommandLine commandLine = new CommandLine(commandParameters);
        CommandLine.Parxxxult argsParxxxult = commandLine.parseArgs(args);
        AbstractXxxCommandHelper commandHelper = getCommandHelper(commandParameters);

        commandHelper.execute();
        System.exit(1);
        //int exitCode = commandLine.execute(args);
        //System.exit(exitCode);
    }

    private static AbstractXxxCommandHelper getCommandHelper(CommandParameters commandParameters){
        AbstractXxxCommandHelper commandHelper = null;
        CommandEnum commandEnum = CommandEnum.findByCommand( commandParameters.getCommand() );

        switch( commandEnum ) {
            case FILE_COMPRESS: {
                commandHelper = new FileCompressHelper(
                    commandParameters.getCompress()
                    , commandParameters.getDecompress()
                    , commandParameters.getInputFile()
                    , commandParameters.getOutputFile()
                );
                break;
            }
            default:
                throw new RuntimeException(String.format("Not support the command(%s) now!params:%s", commandParameters.getCommand(), JSON.toJSONString( commandParameters ) ) );
        }

        return commandHelper;
    }
}

FileCompressHelper

package cn.xxx.bd.common.helper.commands.file;

import cn.xxx.bd.common.helper.xxxCommandHelper;
import cn.xxx.bd.common.helper.constants.Constants;
import cn.xxx.bd.utils.BytesUtil;
import cn.xxx.bd.utils.FileUtils;
import cn.xxx.bd.utils.ZstdUtils;
import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;

import java.io.*;
import java.nio.charset.Charset;

/**
 * @author xxxxxx
 * @version v1.0
 * @description 文件解压缩工具
 * @refrence-doc
 * @gpt-promt
 */
//@Command(name = "file-compress", mixinStandardHelpOptions = true, description = "文件解压缩工具 CLI 工具")
@Slf4j
public class FileCompressHelper extends xxxCommandHelper {

    //压缩算法
    //@Option(names = {"--compress"}, description = "用户名称")
    private String compress;

    //解压算法
    private String decompress;

    //输入文件 := 待解压缩的文件
    @NonNull
    private File inputFile;

    //输入文件 := 待解压缩的文件
    @NonNull
    private File outputFile;

    public FileCompressHelper(String compress, String decompress, File inputFile, File outputFile) {
        this.compress = compress;
        this.decompress = decompress;
        this.inputFile = inputFile;
        this.outputFile = outputFile;
        this.validate();
    }


    @Override
    public Object execute() {
        if( this.compress != null && this.decompress == null ) {//压缩
            compress();
        } else if( this.compress == null && this.decompress != null ){//解压
            decompress();
        } else {//不知道操作目的
            throw new RuntimeException( String.format( "Not known operation!compress:%s, decompress:%s", this.compress, this.decompress ) );
        }
        return null;
    }

    /**
     * 压缩
     */
    private void compress() {
        //TODO
    }

    /**
     * 解压
     */
    @SneakyThrows
    private void decompress(){
        InputStream inputFileStream = new FileInputStream( this.inputFile );
        String compressedContentHex = FileUtils.readFile2Str( inputFileStream );//读取 .hex-bin 格式的文件为文本字符串
        inputFileStream.close();
        byte [] compressedContentBytes = BytesUtil.hexStringToByteArray( compressedContentHex );
        byte [] decompressBytes = new byte [] {};
        String decompressContent = null;

        switch (this.decompress) {
            case "zstd": {
                decompressBytes = compressedContentHex == null ? decompressBytes : ZstdUtils.decompress( compressedContentBytes );
                decompressContent = new String( decompressBytes , Charset.forName( Constants.CHARSET ) );
                break;
            }
            default: {
                throw new RuntimeException("Not support the decompress algorithm!decompress:" + decompress);
            }
        }

        Boolean createNewFileResult = null;
        if(!this.outputFile.exists()){
            createNewFileResult = this.outputFile.createNewFile();
        }
        OutputStream outputFileStream = new FileOutputStream( this.outputFile );
        outputFileStream.write( decompressBytes );
        outputFileStream.flush();
        outputFileStream.close();
        log.info("Decompress success!decompress:{}, inputFile:{}, outputFile(createNewFileResult:{}):{}", this.decompress, this.inputFile.getAbsoluteFile(), createNewFileResult, this.outputFile.getAbsoluteFile());
    }

    @Override
    public Boolean validate(){
        if(this.inputFile == null){
            throw new RuntimeException("input file param is empty!");
        }
        if(this.outputFile == null){
            throw new RuntimeException("output file param is empty!");
        }
        if( this.compress == null && this.decompress == null ) {
            throw new RuntimeException("compress and decompress param is empty!");
        }
        if( this.compress != null && this.decompress != null ) {
            throw new RuntimeException("both compress and decompress param are not empty!");
        }
        return true;
    }
}

Use CASE

  • CASE 支持 .hex-bin 文件解压
  • IDEA:
file-compress --decompress "zstd" --inputFile "E:\tmp_data\20250827182449.zstd.hex-bin" --outputFile "E:\tmp_data\20250827182449.json"
  • Shell:
java -jar xxx-common-helper-1.4.15-SNAPSHOT.jar file-compress --decompress "zstd" --inputFile "20250827182449.zstd.hex-bin" --outputFile "20250827182449.json"

4 解决方案: 使用 Apache Commons CLI

框架介绍

  • Apache Commons CLI 是一个轻量级库,适合处理简单的命令行参数。

  • URL

实现步骤

Step1 添加依赖

  • 在 Maven 的 pom.xml 文件中引入 Apache Commons CLI:
<dependency>
  <groupId>commons-cli</groupId>
  <artifactId>commons-cli</artifactId>
  <version>1.9.0</version>
</dependency>

Step2 定义和解析选项

  • 使用 Options 和 CommandLineParser 定义和解析参数:
import org.apache.commons.cli.*;

public class ExampleCLI {
	public static void main(String[] args) {
		Options options = new Options();
		options.addOption("n", "name", true, "用户名称");
		options.addOption("h", "help", false, "显示帮助信息");

		CommandLineParser parser = new DefaultParser();
		HelpFormatter formatter = new HelpFormatter();

		try {
			CommandLine cmd = parser.parse(options, args);

			if (cmd.hasOption("help")) {
				formatter.printHelp("example", options);
				return;
			}

			String name = cmd.getOptionValue("name", "World");
			System.out.println("Hello, " + name + "!");

		} catch (ParseException e) {
			System.out.println(e.getMessage());
			formatter.printHelp("example", options);
		}
	}
}

Step3 运行程序

  • 编译并运行程序:
java -jar example.jar -n John

Z 最佳实践

  • 选择框架:Picocli 更适合复杂场景的 CLI 工具开发,而 Apache Commons CLI 更适合简单场景
  • 生成可执行文件:结合 GraalVM 的 native-image 工具,可以将 Java 程序编译为跨平台的可执行文件。
  • 提供帮助信息:确保工具支持 --help 参数,方便用户了解使用方法。

Y 推荐文献

X 参考文献

posted @ 2025-08-27 14:42  千千寰宇  阅读(37)  评论(0)    收藏  举报