[CLI/Java] 基于 Java 构建 CLI 命令行工具的解决方案
1 需求背景
-
排查项目中的各类数据问题,测试人员和开发人员都耗费大量时间,不如将这部分排查工作所需的程序工具化。
-
为此,稍微调研了一下cli命令行构建框架。
-
综合之下,目前笔者选择的 picocli 框架。感觉其框架的易用性更好。
-
cli 命令行工具与业务工程模块的区别在于:
- 参数/请求的功能、输入数据,来源于使用者及其电脑本地。
参数和数据的识别,是一个需要考虑的问题。
- 部署位置:电脑本地
- 使用人员:后台支持人员,而非最终的平台用户(这种情况极少)。
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> <!– Any name that makes sense –>-->
<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 推荐文献
- 用 Java 做个命令行程序,太简单了!-腾讯云开发者社区 - 腾讯云
- java - Apache Commons CLI:构建命令行应用的利器 - Segmentfault
- 怎么基于Java编写一个CLI工具_native-image class - CSDN
X 参考文献

本文作者:
千千寰宇
本文链接: https://www.cnblogs.com/johnnyzen
关于博文:评论和私信会在第一时间回复,或直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
日常交流:大数据与软件开发-QQ交流群: 774386015 【入群二维码】参见左下角。您的支持、鼓励是博主技术写作的重要动力!
本文链接: https://www.cnblogs.com/johnnyzen
关于博文:评论和私信会在第一时间回复,或直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
日常交流:大数据与软件开发-QQ交流群: 774386015 【入群二维码】参见左下角。您的支持、鼓励是博主技术写作的重要动力!