picocli-入门

https://picocli.info/#_introduction

前言

相信每个Java程序员都曾使用过Scanner ,因编写出一个命令行程序而兴奋不已。
命令行程序也颇为实用,然而,使用Java来编写一个功能强大的命令行程序却并不容易,主要有以下几方面的痛点:

  • 没有成熟的框架来封装参数接收、参数提示以及参数校验
  • 很难处理参数的互斥以及特定命令的相互依赖关系
  • 无法进行命令自动补全
  • 由于JVM解释执行字节码,并且JIT无法在短时执行中发挥作用,Java命令行程序启动缓慢
  • 集成SpringBoot及其它组件后,启动更加缓慢

上述这些问题都可以使用Picocli来解决

引用:https://blog.csdn.net/qq_40419564/article/details/115290878

Picocli 基本介绍

Every main method deserves picocli!
Picocli aims to be the easiest way to create rich command line applications that can run on and off the JVM.

入门

引入maven依赖

<dependency>
     <groupId>info.picocli</groupId>
     <artifactId>picocli</artifactId>
     <version>4.6.3</version>
 </dependency>

应用demo

@CommandLine.Command(name = "checksum", mixinStandardHelpOptions = true, version = "checksum 4.0",
        description = "Prints the checksum (SHA-256 by default) of a file to STDOUT.")
public class CheckSum implements Callable<Integer> {

    @CommandLine.Parameters(index = "0", description = "The file whose checksum to calculate.")
    private File file;

    @CommandLine.Option(names = {"-a", "--algorithm"}, description = "MD5, SHA-1, SHA-256, ...")
    private String algorithm = "SHA-256";

    @Override
    public Integer call() throws Exception { // your business logic goes here...
        byte[] fileContents = Files.readAllBytes(file.toPath());
        byte[] digest = MessageDigest.getInstance(algorithm).digest(fileContents);
        System.out.printf("%0" + (digest.length*2) + "x%n", new BigInteger(1, digest));
        return 0;
    }

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

maven打包

<build>
        <finalName>demo1</finalName>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifest>
                            <mainClass>cn.jhs.CheckSum</mainClass>
                        </manifest>
                    </archive>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

执行命令mvn clean package

Command

> java -jar target/demo1-jar-with-dependencies.jar                 
Missing required parameter: '<file>'
Usage: checksum [-hV] [-a=<algorithm>] <file>
Prints the checksum (SHA-256 by default) of a file to STDOUT.
      <file>      The file whose checksum to calculate.
  -a, --algorithm=<algorithm>
                  MD5, SHA-1, SHA-256, ...
  -h, --help      Show this help message and exit.
  -V, --version   Print version information and exit.

> echo "hello" > hello.txt
> java -jar target/demo1-jar-with-dependencies.jar hello.txt            
5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03
使用别名
> alias checksum=java\ -jar\ target/demo1-jar-with-dependencies.jar

> checksum hello.txt -a SHA-1
f572d396fae9206628714fb2ce00f72e94f2258f


Options and Parameters

Command line arguments可以分为optionspositional parameters
- option有一个名称

  • positional parameters位置参数通常是 option后面的值,但它们可能是混合的。

在这里插入图片描述

Options

option必须有一个或多个名称。 Picocli 允许您使用任何您想要的选项名称。

默认情况下,option名称区分大小写.

class Tar {
    @Option(names = "-c", description = "create a new archive")
    boolean create;

    @Option(names = { "-f", "--file" }, paramLabel = "ARCHIVE", description = "the archive file")
    File archive;

    @Parameters(paramLabel = "FILE", description = "one or more files to archive")
    File[] files;

    @Option(names = { "-h", "--help" }, usageHelp = true, description = "display a help message")
    private boolean helpRequested = false;
}

TestCase

String[] args = { "-c", "--file", "result.tar", "file1.txt", "file2.txt" };
Tar tar = new Tar();
new CommandLine(tar).parseArgs(args);

assert !tar.helpRequested;
assert  tar.create;
assert  tar.archive.equals(new File("result.tar"));
assert  Arrays.equals(tar.files, new File[] {new File("file1.txt"), new File("file2.txt")});

Interactive (Password) Options

对于标记为Interactive Optionspositional parameters,会提示用户在控制台上输入一个值。

交互式

class Login implements Callable<Integer> {
    @Option(names = {"-u", "--user"}, description = "User name")
    String user;

	//响应式 Option
    @Option(names = {"-p", "--password"}, description = "Passphrase", interactive = true)
    char[] password;

    public Integer call() throws Exception {
        byte[] bytes = new byte[password.length];
        for (int i = 0; i < bytes.length; i++) { bytes[i] = (byte) password[i]; }

        MessageDigest md = MessageDigest.getInstance("SHA-256");
        md.update(bytes);

        System.out.printf("Hi %s, your password is hashed to %s.%n", user, base64(md.digest()));

        // null out the arrays when done
        Arrays.fill(bytes, (byte) 0);
        Arrays.fill(password, ' ');

        return 0;
    }

    private String base64(byte[] arr) { /* ... */ }
}

testcase
执行命令:new CommandLine(new Login()).execute("-u", "user123", "-p");
然后会提示用户输入一个值:

Enter value for --password (Passphrase):

在 Java 6 或更高版本上运行时,用户输入不会回显到控制台。
用户输入密码值并按下回车后,将call()调用该方法,该方法将打印如下内容:

Hi user123, your passphrase is hashed to 75K3eLr+dx6JJFuJ7LwIpEpOFmwGZZkRiB84PURz6U8=.

#### 可选的 Interactive 
默认情况下,` Interactive  Options`会导致应用程序等待标准输入上的输入。
对于即需要以交互方式 又需要以批处理模式运行的命令,如果该选项**可以选择的**,这将很有用。

**arity**
```java
@Option(names = "--user")
String user;

@Option(names = "--password", arity = "0..1", interactive = true)
char[] password;
  • 通过以下输入,密码字段将被初始化为“123”,而不提示用户输入:--password 123 --user Joe
  • 但是,如果未指定密码,--password --user Joe , 则会提示用户输入密码。

Short (POSIX) Options

class ClusteredShortOptions {
    @Option(names = "-a") boolean aaa;
    @Option(names = "-b") boolean bbb;
    @Option(names = "-c") boolean ccc;
    @Option(names = "-f") String  file;
}

以下命令行参数都是等效的,解析它们会得到相同的结果:

<command> -abcfInputFile.txt
<command> -abcf=InputFile.txt
<command> -abc -f=InputFile.txt
<command> -ab -cf=InputFile.txt
<command> -a -b -c -fInputFile.txt
<command> -a -b -c -f InputFile.txt
<command> -a -b -c -f=InputFile.txt
...

Boolean Options

Boolean Options通常不需要参数:在命令行中指定选项名称就足够了。

class BooleanOptions {
    @Option(names = "-x") boolean x;
}
  • x 的值默认为 false,
  • 如果在命令行上指定了-x,则设置为 true(与默认值相反)。
  • 如果在命令行上多次指定-x,则 x 的值保持为true

Negatable Options-否定选项

@Command(name = "negatable-options-demo")
class NegatableOptionsDemo {
    @Option(names = "--verbose",           negatable = true) boolean verbose;
    @Option(names = "-XX:+PrintGCDetails", negatable = true) boolean printGCDetails;
    @Option(names = "-XX:-UseG1GC",        negatable = true) boolean useG1GC = true;
}

上述示例的使用帮助如下所示:

Usage: negatable-options-demo 
		[--[no-]verbose] 
		[-XX:(+|-)PrintGCDetails]
      	[-XX:(+|-)UseG1GC]
      	
      --[no-]verbose     Show verbose output
      -XX:(+|-)PrintGCDetails Prints GC details
	  -XX:(+|-)UseG1GC   Use G1 algorithm for GC


Positional Parameters

Explicit Index - 显式索引

使用[0,+oo)索引属性来准确指定要捕获的参数。数组或集合字段可以捕获多个值。

class PositionalParameters {
    @Parameters(index = "0")    InetAddress host;
    @Parameters(index = "1")    int port;
    @Parameters(index = "2..*") File[] files;

    @Parameters(hidden = true)  // "hidden": don't show this parameter in usage help message
    List<String> allParameters; // no "index" attribute: captures _all_ arguments
}

testcase

String[] args = { "localhost", "12345", "file1.txt", "file2.txt" };
PositionalParameters params = CommandLine.populateCommand(new PositionalParameters(), args);

assert params.host.getHostName().equals("localhost");
assert params.port == 12345;
assert Arrays.equals(params.files, new File[] {new File("file1.txt"), new File("file2.txt")});

assert params.allParameters.equals(Arrays.asList(args));

Omitting the Index -省略索引

可以省略 index 属性。

  • 对于多值位置参数(数组或集合),省略 index 属性意味着该字段捕获所有位置参数(相当于 index = "0..*")。
  • 对于单值位置参数
    • 在 picocli 4.3 之前,单值位置参数的默认索引也是 index = "0..*",即使只有一个值(通常是第一个参数) 可以被捕获。
    • 从 4.3 版开始,picocli 根据同一命令中定义的其他位置参数自动分配索引。**Automatic Parameter Indexes **

Mixing Options and Positional Parameters

 class Mixed {
    @Parameters
    List<String> positional;

    @Option(names = "-o")
    List<String> options;
}

testcase

 String[] args = { "param0", "-o", "AAA", "param1", "param2", "-o", "BBB", "param3" };
Mixed mixed = new Mixed();
new CommandLine(mixed).parseArgs(args);

assert mixed.positional.equals(Arrays.asList("param0", "param1", "param2", "param3");
assert mixed.options.equals   (Arrays.asList("AAA", "BBB"));

Double dash (–)

当命令行参数之一只是两个破折号而没有附加任何字符 (--) 时,picocli 将所有后续参数解释为Positional Parameters,甚至是与选项名称匹配的参数

class DoubleDashDemo {
    @Option(names = "-v")     boolean verbose;
    @Option(names = "-files") List<String> files;
    @Parameters               List<String> params;
}

testcase

String[] args = { "-v", "--", "-files", "file1", "file2" };
DoubleDashDemo demo = new DoubleDashDemo();
new CommandLine(demo).parseArgs(args);

assert demo.verbose;
assert demo.files == null;
assert demo.params.equals(Arrays.asList("-files", "file1", "file2"));

@-files

长命令行的参数文件

假设有文件:/home/foo/args,内容如下

# This line is a comment and is ignored.
ABC -option=123
'X Y Z'

执行命令: java MyCommand @/home/foo/args
等价于执行:java MyCommand ABC -option=123 "X Y Z"

encoding
若要执行命令: java -DFile.encoding=UTF8 MyCommand ABC -option=123 "X Y Z"
可以通过:

SET JAVA_TOOL_OPTIONS=-Dfile.encoding=UTF8
java MyCommand ABC -option=123 "X Y Z"
@-files Usage Help: showAtFileInUsageHelp
@Command(name = "myapp", showAtFileInUsageHelp = true,
         mixinStandardHelpOptions = true, description = "Example command.")
class MyApp {
    @Parameters(description = "A file.") File file;
}

执行命令结果如下:

Usage: myapp [-hV] [@<filename>...] <file>
Example command.
      [@<filename>...]   One or more argument files containing options.
      <file>             A file.
  -h, --help             Show this help message and exit.
  -V, --version          Print version information and exit.

增加了-h -V选项。



Subcommands

Subcommands复杂的命令行工具,例如著名的 git 工具,有许多子命令(例如,commit、push 等),每个子命令都有自己的一组选项和位置参数。 Picocli 使得使用subcommandssub-subcommands的命令变得非常容易,达到任何深度。

例1

@Command(name = "foo", subcommands = Bar.class)
class Foo implements Callable<Integer> {
    @Option(names = "-x") int x;

    @Override public Integer call() {
        System.out.printf("hi from foo, x=%d%n", x);
        boolean ok = true;
        return ok ? 0 : 1; // exit code
    }

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

@Command(name = "bar", description = "I'm a subcommand of `foo`")
class Bar implements Callable<Integer> {
    @Option(names = "-y") int y;

    @Override public Integer call() {
        System.out.printf("hi from bar, y=%d%n", y);
        return 23;
    }

    @Command(name = "baz", description = "I'm a subcommand of `bar`")
    int baz(@Option(names = "-z") int z) {
        System.out.printf("hi from baz, z=%d%n", z);
        return 45;
    }
}

testcase`

alias foo='java Foo'

$ foo -x 123
hi from foo, x=123

#check the exit code
$ echo $?
0

###########
$ foo -x 123 bar -y=456
hi from bar, y=456

#check the exit code
$ echo $?
23

###########
foo bar baz -z=789
hi from baz, z=789

#check the exit code
$ echo $?
45

声明式注册subcommand

@Command(subcommands = {
    GitStatus.class,
    GitCommit.class,
    GitAdd.class,
    GitBranch.class,
    GitCheckout.class,
    GitClone.class,
    GitDiff.class,
    GitMerge.class,
    GitPush.class,
    GitRebase.class,
    GitTag.class
})
public class Git { /* ... */ }

编程式注册subcommand

CommandLine commandLine = new CommandLine(new Git())
        .addSubcommand("status",   new GitStatus())
        .addSubcommand("commit",   new GitCommit())
        .addSubcommand("add",      new GitAdd())
        .addSubcommand("branch",   new GitBranch())
        .addSubcommand("checkout", new GitCheckout())
        .addSubcommand("clone",    new GitClone())
        .addSubcommand("diff",     new GitDiff())
        .addSubcommand("merge",    new GitMerge())
        .addSubcommand("push",     new GitPush())
        .addSubcommand("rebase",   new GitRebase())
        .addSubcommand("tag",      new GitTag());


SpringBoot集成

从 4.0 版开始,picocli 通过在picocli-spring-boot-starter模块中提供自定义工厂来支持 Spring Boot。

下面的 Spring Boot 示例应用程序为邮件客户端提供了一个命令行界面,可用于使用 SMTP 服务器发送电子邮件。收件人地址和主题行可以作为选项给出,而消息正文可以指定为参数文本。

MAVEN依赖

<dependency>
  <groupId>info.picocli</groupId>
  <artifactId>picocli-spring-boot-starter</artifactId>
  <version>4.6.3</version>
</dependency>

import org.springframework.boot.*;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import picocli.CommandLine;
import picocli.CommandLine.IFactory;

@SpringBootApplication
public class MySpringMailer implements CommandLineRunner, ExitCodeGenerator {
	//1. 自动注入 PicocliSpringFactory
    private IFactory factory;    
	
	//2. 将有后面的@CommandLine.Command 注入    
    private MailCommand mailCommand; 
    private int exitCode;

    // constructor injection
    MySpringMailer(IFactory factory, MailCommand mailCommand) {
        this.factory = factory;
        this.mailCommand = mailCommand;
    }

    @Override
    public void run(String... args) {
        // let picocli parse command line args and run the business logic
        exitCode = new CommandLine(mailCommand, factory).execute(args);
    }

    @Override
    public int getExitCode() {
        return exitCode;
    }

    public static void main(String[] args) {
        // SpringApplication.exit() 关闭容器。
        //System.exit()关闭虚拟机。
        //作用: 在执行完spring任务后`mailCommand.run()`,关闭spring容器,关闭虚拟机。
        System.exit(SpringApplication.exit(SpringApplication.run(MySpringMailer.class, args)));
    }
}

Command

import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
import picocli.CommandLine.*;
import java.util.List;
import java.util.concurrent.Callable;

@Component// 1.注册到spring容器 
@Command(name = "mailCommand")
public class MailCommand implements Callable<Integer> {

    @Autowired private IMailService mailService; //3。由spring注入

    @Option(names = "--to", description = "email(s) of recipient(s)", required = true)
    List<String> to;

    @Option(names = "--subject", description = "Subject")
    String subject;

    @Parameters(description = "Message to be sent")
    String[] body = {};

    public Integer call() throws Exception {
        mailService.sendMessage(to, subject, String.join(" ", body));  //2.业务逻辑
        return 0;
    }
}

testcase
执行命令:

mvnw spring-boot:run
# ...
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.3.5.RELEASE)
# ...
#2020-11-01 19:35:31.084  INFO 15724 --- [           main] info.picocli.demo.MySpringMailer         : Started MySpringMailer in 0.821 seconds (JVM running for 1.131)
Missing required option: '--to=<to>'
Usage: mailCommand [--subject=<subject>] --to=<to> [--to=<to>]... [<body>...]
      [<body>...]           Message to be sent
      --subject=<subject>   Subject
      --to=<to>             email(s) of recipient(s)

补充:

SpringBoot.exit()

springboot-application-exit

SpringApplication提供了一个exit静态方法,用于关闭Spring容器,该方法还有一个参数exitCodeGenerators表示ExitCodeGenerator接口的数组。ExitCodeGenerator接口是一个生成退出码exitCode的生成器。

public static int exit(ApplicationContext context,
		ExitCodeGenerator... exitCodeGenerators) {
	Assert.notNull(context, "Context must not be null");
	int exitCode = 0; // 默认的退出码是0
	try {
		try {
			// 构造ExitCodeGenerator集合
			ExitCodeGenerators generators = new ExitCodeGenerators();
			// 获得Spring容器中所有的ExitCodeGenerator类型的bean
			Collection<ExitCodeGenerator> beans = context
					.getBeansOfType(ExitCodeGenerator.class).values();
			// 集合加上参数中的ExitCodeGenerator数组
			generators.addAll(exitCodeGenerators);
			// 集合加上Spring容器中的ExitCodeGenerator集合
			generators.addAll(beans);
			// 遍历每个ExitCodeGenerator,得到最终的退出码exitCode
			// 这里每个ExitCodeGenerator生成的退出码如果比0大,那么取最大的
			// 如果比0小,那么取最小的
			exitCode = generators.getExitCode();
			if (exitCode != 0) { // 如果退出码exitCode不为0,发布ExitCodeEvent事件
				context.publishEvent(new ExitCodeEvent(context, exitCode));
			}
		}
		finally {
			// 关闭Spring容器
			close(context);
		}

	}
	catch (Exception ex) {
		ex.printStackTrace();
		exitCode = (exitCode == 0 ? 1 : exitCode);
	}
	return exitCode;
}

假设有一个controller方法:

@Autowired
private ApplicationContext applicationContext;

@PostMapping("/stop")
public String stop() {
		// 加上自己的权限验证
    SpringApplication.exit(applicationContext);
    return "ok";
}

System.exit(X)

System.exit(status):无论 status 为何值都会退出程序

  • System.exit(0); 整个程序正常退出,会执行注册的shutdown-hooks.
  • System.exit(非0); 整个程序异常退出
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值