JCommander + AutoService打造带子命令的Java命令行应用

需求

最近想将自己的一个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/

猜你喜欢

转载自blog.csdn.net/jgku/article/details/132110314