需求
最近想将自己的一个Java应用包装成命令行工具,看了几个库,最后选取了JCommander,结合AutoService库,实现了带子命令的工具,方便扩展新的子命令。
子命令放在同一包下,实现相同的接口,通过java.util.ServiceLocator加载。
Java命令行工具库
常用的几个库为:
-
JCommander
项目地址: https://github.com/cbeust/jcommander
Star: 1010 Fork: 227
文档地址: http://jcommander.org/ -
picocli
地址: https://github.com/remkop/picocli
Star: 336, Fork: 32
示例:https://github.com/kakawait/picocli-spring-boot-starter -
Commons CLI
地址: https://commons.apache.org/proper/commons-cli/
地址: 来自apache common的开源项目
更新: 最后一次更新是1.5-SNAPSHOT,是在2017年6月8日 -
Args4j
项目地址: https://0github.com/kohsuke/args4j
Star: 570 Fork: 151
文档地址: http://args4j.kohsuke.org/sample.html
活跃程度: 最后一次更新为2年之前
依赖库
<!-- https://mvnrepository.com/artifact/com.google.auto.service/auto-service -->
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<version>1.1.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.beust/jcommander -->
<dependency>
<groupId>com.beust</groupId>
<artifactId>jcommander</artifactId>
<version>1.82</version>
</dependency>
定义各个子命令
Create子命令:
@AutoService(Command.class)
@Parameters(
commandNames = {
CREATE_CMD },
commandDescription = "create a new ebook."
)
@Getter
public class CreateCommand implements Command{
@Parameter(names = {
"--indexUrl", "-u" })
public String indexUrl;
@Override
public void execute() throws CommandException {
System.out.println("create a new book from " + indexUrl);
}
}
Fetch子命令:
@AutoService(Command.class)
@Parameters(
commandNames = {
FETCH_CMD},
commandDescription = "fetch some articles from website."
)
@Getter
public class FetchCommand implements Command {
@Parameter(names = {
"--indexUrl", "-u"})
public String indexUrl;
@Override
public void execute() throws CommandException {
System.out.println("fetch articles from " + indexUrl);
}
}
都实现了接口Command:
public interface Command {
default Collection<Command> commands() {
return null;
}
void execute() throws CommandException;
}
注意,子命令支持嵌套的子命令。当然一般用不到。
主类CLI
public class CLI {
static final String CREATE_CMD = "create";
static final String FETCH_CMD = "fetch";
@Parameter(names = {
"-h", "--help" }, help = true)
private boolean help;
public void exec(String[] args) {
final JCommander.Builder builder = JCommander.newBuilder().addObject(new CLI());
final JCommander jCommander = builder.build();
ServiceLoader.load(Command.class).forEach(command -> CLI.registerCommand(jCommander, command));
JCommander leafCommander = jCommander;
try {
jCommander.parse(args);
final String rootVerb = jCommander.getParsedCommand();
final JCommander rootCommander = jCommander.getCommands().get(rootVerb);
if (rootCommander == null) {
jCommander.usage();
System.exit(1);
}
leafCommander = rootCommander;
do {
final String subVerb = leafCommander.getParsedCommand();
final JCommander subCommander = leafCommander.getCommands().get(subVerb);
if (subCommander != null)
leafCommander = subCommander;
else
break;
} while (true);
final Command command = (Command) leafCommander.getObjects().get(0);
command.execute();
} catch (final CommandException e) {
System.err.printf("%1$s: %2$s. See '%1$s --help'.%n", leafCommander.getProgramName(), e.getMessage());
System.exit(e.getStatus());
} catch (final Exception e) {
System.err.printf("%1$s: %2$s. See '%1$s --help'.%n", leafCommander.getProgramName(), e.getMessage());
System.exit(1);
}
}
private static final void registerCommand(final JCommander jCommander, final Command command) {
jCommander.addCommand(command);
final Parameters commandParameters = command.getClass().getAnnotation(Parameters.class);
if (commandParameters == null || commandParameters.commandNames().length == 0)
return;
final JCommander subCommander = jCommander.getCommands().get(commandParameters.commandNames()[0]);
final Collection<Command> subCommands = command.commands();
if (subCommands != null)
subCommands.forEach(subCommand -> CLI.registerCommand(subCommander, subCommand));
}
public static void main(final String[] args) {
CLI cli = new CLI();
cli.exec(args);
}
}
原理很简单:ServiceLocator加载了所有Command的实现类,然后根据子命令调用相应的Command类。AutoService的好处就是不必自己去创建META-INF/services下的相关文件。
测试一下
@Test
public void testCreateCommand() {
CLI cli = new CLI();
String[] argv = {
"create", "-u", "http://www.sina.com.cn"};
cli.exec(argv);
}
@Test
public void testFetchCommand() {
CLI cli = new CLI();
String[] argv = {
"fetch", "-u", "http://www.csdn.cn"};
cli.exec(argv);
}
参考文档
- https://gist.github.com/mkarg/9d9ca23e6da32b47c7fadaf10ae16ba6
- https://pedrorijo.com/blog/java-service-loader/