[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 參考文獻
本文作者:
千千寰宇
本文鏈接: http://www.rzrgm.cn/johnnyzen
關于博文:評論和私信會在第一時間回復,或直接私信我。
版權聲明:本博客所有文章除特別聲明外,均采用 BY-NC-SA 許可協議。轉載請注明出處!
日常交流:大數據與軟件開發-QQ交流群: 774386015 【入群二維碼】參見左下角。您的支持、鼓勵是博主技術寫作的重要動力!
本文鏈接: http://www.rzrgm.cn/johnnyzen
關于博文:評論和私信會在第一時間回復,或直接私信我。
版權聲明:本博客所有文章除特別聲明外,均采用 BY-NC-SA 許可協議。轉載請注明出處!
日常交流:大數據與軟件開發-QQ交流群: 774386015 【入群二維碼】參見左下角。您的支持、鼓勵是博主技術寫作的重要動力!

浙公網安備 33010602011771號