当前位置: 首页 > 工具软件 > JCommander > 使用案例 >

JCommander学习总结

全流觞
2023-12-01

JCommander介绍

JCommander是一个小型的java框架,它用来解析命令行参数。

比如下面的代码:

 import com.beust.jcommander.Parameter;
 ​
 public class Args {
   @Parameter
   private List<String> parameters = new ArrayList<>();
 ​
   @Parameter(names = { "-log", "-verbose" }, description = "Level of verbosity")
   private Integer verbose = 1;
 ​
   @Parameter(names = "-groups", description = "Comma-separated list of group names to be run")
   private String groups;
 ​
   @Parameter(names = "-debug", description = "Debug mode")
   private boolean debug = false;
 }

这是一个参数类,里面封装了一些命令行参数。

下面我们可以用JCommander来解析它:

 Args args = new Args();
 String[] argv = { "-log", "2", "-groups", "unit" };
 JCommander.newBuilder()
   .addObject(args)
   .build()
   .parse(argv);
 ​
 Assert.assertEquals(jct.verbose.intValue(), 2);

这里还有一个例子:

 class Main {
     @Parameter(names={"--length", "-l"})
     int length;
     @Parameter(names={"--pattern", "-p"})
     int pattern;
 ​
     public static void main(String ... argv) {
         Main main = new Main();
         JCommander.newBuilder()
             .addObject(main)
             .build()
             .parse(argv);
         main.run();
     }
 ​
     public void run() {
         System.out.printf("%d %d", length, pattern);
     }
 }

这里需要用带参数的启动方式来启动Main方法

选项类型

用来表示你的参数的字段,可以是任何类型,像是基础类型(Integer,Boolean等等),你也可以自己类型转换器来支持更多其他的类型,比如文件类型等等。

1. Boolean

当一个Parameter注解被加在了Boolean类型的上面的时候,JCommander会将它解析为一个没有其他附加参数的参数选项。

 @Parameter(names = "-debug", description = "Debug mode")
 private boolean debug = false;

像这样的参数不需要其他附加参数,当我们在命令行输入了这个参数名的时候,就会将true赋给它。

如果你需要设置一个boolean型参数,默认值为true(不在命令中指明该参数时),则需要定义为附加参数个数为1的参数,用户需要额外说明这个参数的值才能正确调用:

 @Parameter(names = "-debug", description = "Debug mode", arity = 1)
 private boolean debug = true;

调用格式:

 $ programe -debug true
 $ programe -debug false

当Parameter注解被加在String,Integer,int,long,Long等类型上,JCommander会解析后面跟上的参数值,并且会尝试将它放入所属类型之中。

比如:

 @Parameter(names = "-log", description = "Level of verbosity")
 private Integer verbose = 1;

调用:

 $ java Main -log 3

会正确的接受到3这个数值,放入verbose这个字段之中。但是如果用下面的调用:

$ java Main -log test

就会抛出异常。

2. Lists

当一个List类型字段被Parameter注解了,JCommander会解析为一个能多次出现的选项。

例子:

@Parameter(names = "-host", description = "The host")
private List<String> hosts = new ArrayList<>();

命令行调用:

$ java Main -host host1 -verbose -host host2

3. Password

当你的参数需要设置为密码,即其内容不希望被看到的时候,可以采用以下的定义:

public class ArgsPaswword {
	@Parameter(names = "-password", description = "Connection password", password = true)
	private String password;
}

当你运行你的程序后,你会得到下面的提示:

Value for -password (Connection password):

4. 显示输入

在JAVA 6中,默认情况下你是无法看到你输入的密码的(JAVA5以前是必定显示),但是你也可以通过设置echoInput为true来手动设置显示信息

public class ArgsPassword {
	@Parameter(names = "-password", description = "Connection password", password = true, echoInput = true)
	private String password;
}

自定义类型(转换器和分割器)

为了绑定参数到自定义类型上,或者改变JCommander划分参数的默认方式(按照逗号分割),JCommander提供了两种接口:

IStringConverter和IParameterSplitter

1. 自定义类型:单一数值

在Parameter注解中将converter设置为对应的转换器,或者实现IStringConverterFactory接口

1.1 方式一:通过注解

默认情况下,JCommander只会将命令行参数转换为基本类型(string,boolean,integer,long),但是很多情况下,我们的应用需要使用更多的复杂类型,比如文件,主机名,列表等等。

为了达到这样的效果,可以通过实现下面的接口,来写一个类型转换器。

public interface IStringConverter<T> {
	T convert(String value);
}

下面是一个将字符串转换为文件的例子:

public class FileConverter implements IStringConverter<File> {
	@Override
	public File convert(String value) {
		return new File(value);
	}
}

定义完转换类之后,你所需要做的就是将你的字段上用正确的注解来标识它了:

@Parameter(names = "-file", converter = FileConverter.class)
File file;
  • JCommander也帮你封装了一些常用的转换器(详见IStringConverter的实现类)

注意:如果前台调用-file file1 file2 file3,并不会将三个字符串转换一个File数组,而是针对每个字符串,分别转换为一个File,如果要转换为数组,请看后文。

1.2 方式二:通过工厂

如果有一个自定义类型多次出现在你的应用里,每次都在字段上注明转换器就显得有些傻了,这时候我们就可以使用工厂:IStringConverterFactory:

public interface IStringConverterFactory {
	<T> Class<? extends IStringConverter<T>> getConverter(Class<T> forType);
}

这个泛型方法返回一个类型类,该类型实现了IStringConverter接口,参数为这个转换器所转换的类型。

也就是说,我传入一个String类型,该方法就会返回一个String类型的转换器。

举个例子,比如你现在需要将一个字符串转换为主机名和端口号:

$ java App -target example.com:8080

下面这个是自定义类型,存储的是主机名和对应的端口号:

public class HostPort {
	public HostPost(String host, String post) {
		this.host = host;
		this.port = port;
	}
	
	final String host;
	final Integer port;
}

然后实现他的转换器:

class HostPortConverter implements IStringConverter<HostPort> {
	@Override
	public HostPort convert(String value) {
		String[] s = value.split(":");
		return new HostPort(s[0], Integer.parseInt(s[1]))
	}
}

然后是它对应的工厂类:

public class Factory implements IStringConverterFactory {
	public Class<? extends IStringConverter<?>> getConverter(Class forType) {
		if (forType.equals(HostPost.Class)) return HostPortConverter.class;
		else return null;
	}
}

现在,你可以使用HostPort类作为一个参数,而不需要指定任何转换器。

public class ArgsConverterFactory {
	@Parameter(names = "-hostport")
	private HostPort hostPort;
}

当然,仅仅这样JCommander是识别不出来的,你还需要完成最后一步,那就是将你的工厂类添加到JCommander对象之中:

ArgsConverterFactory a = new ArgsConverterFactory();
JCommander jc = JCommander.newBuilder()
		.addObject(a)
		.addConvertFactory(new Factory())
		.build()
		.parse("-hostport", "example.com:8080");
		
Assert.assertEquals(a.hostPort.host, "example.com");
Assert.assertEquals(a.hostPort.port.intValue(), 8080);

2. 自定义类型:列表

使用Parameter注解中的listConverter属性来指定转换器,该转换器需要实现IStringConverter接口。

通过注解

public interface IStringConverter<T> {
	T convert(String value);
}

这里的T是一个List。

下面是例子:

public class FileListConverter implements IStringConverter<List<File>> {
	@Override
	public List<File> convert(String files) {
		String[] paths = files.split(",");
		List<File> fileList = new ArrayList<>();
		for (String path : paths) {
			fileList.add(new File(path));
		}
		return fileList;
	}
}

然后需要的做的就是在字段上正确地定义转换器。

@Parameter(names = "-files", listConverter = FileListConverter.class)
List<File> file;

现在在命令行里输入:

$ java App -files file1,file2,file3

JCommander还封装了一些默认的转换器:

3. 分割器

通过注解

默认情况下,JCommander会按照逗号来截取参数封装List。

当然我们也可以通过实现下面的接口,来自己写分割器实现其他不同的分割方式:

public interface IParameterSplitter {
	List<String> split(String value);
}

在下面的例子中,我们实现一个分割器,它通过分号来切割字符串。

public static class SemiColonSplitter implements IParameterSplitter {
	public List<String> split(String value) {
		return Arrays.asList(value.split(";"));
  }
}
@Parameter(names = "-files", converter = FileConverter.class, splitter = SemiColonSplitter.class)
List<File> files;

JCommander会将字符串:file1;file2;file3转换为file1,file2,file3然后将其一个个地传入转换器。

参数验证

1. 单个参数的验证

可以通过实现下面的接口,来让JCommander对参数进行验证。

public interface IParameterValidator {
	void validate(String name, String value) throws ParameterException;
}
  • name: 参数的名字,比如:"-host"。

  • value: 参数的值,是我们需要验证的对象。

  • ParameterException: 如果验证失败,则抛出该异常。

eg:

public class PositiveInteger implements IParameterValidator {
	public void validate(String name, String value) throws ParameterException {
		int n = Integer.parseInt(value);
    if (n < 0) {
      	throw new ParameterException("Parameter " + name + " should be positive(found " + value + ")");
    }
	}
}
@Parameter(names = "-age", validateWith = PositiveInteger.class)
private Integer age;

尝试传入一个负数的时候,系统会抛出一个异常。

如果需要指定多个验证器的时候:

@Parameter(names = "-count", validateWith = {PositiveInteger.class, CustomOddNumberValidater.class})
private Integer value;

2. 全局参数验证

在使用JCommander分析了你的参数后,你可能会想要在多个参数之间附加一些其他的验证,像是确保两个互斥的参数不能同时被赋予值这样的约束。但是由于JAVA注解的特性限制,JCommander并不提供这些类型的验证支持。如果你需要实现的话,只能自己写JAVA代码来实现了。

主要参数

大多数@Parameter注解里都会第一个声明的names属性,但你可以定义一个(且最多一个)没有任何names属性的参数注解。

这个参数既可以一个List<String>,也可以是一个单独的字段(像是一个String或者任意一个带有转换器的类型),而这种参数就被称为主要参数。

@Parameter(description = "Files")
private List<String> files = new ArrayList<>();

@Parameter(names = "-debug", description = "Debugging level")
private Integer debug = 1;

调用:

$ java Main -debug file1 file2

调用上面的命令,files字段会接受到file1和file2两个字符串

私有属性

参数可以是私有的。

public class ArgsPrivate {
	@Parameter(names = "-verbose")
	private Integer verbose = 1;
	
	public Integer getVerbose() {
		return verbose;
	}
}
ArgsPrivate args = new ArgsPrivate();
JCommander.newBuilder()
	.addObject(args)
	.build()
	.parse("-verbose", "3");
Assert.assertEquals(args.getVerbose().intValue(), 3);

参数分隔符

默认情况下,参数之间是通过空格分隔的,但也可以通过更改设置来支持不同的分隔符:

$ java Main -log:3
$ java Main -level=42

下面是实现办法:

@Parameters(separators = "=")
public class SeparatorEqual {
	@Parameter(names = "-level")
	private Integer level = 2;
}

可以看到,如果需要自定义分隔符,需要将@Parameters注解在参数类上。(注意末尾的s)

多重描述

你可以用多个类来描述你的参数,即可以同时为JCommander指定多个参数类:

public class ArgsMaster {
  @Parameter(names = "-master")
  private String master;
}
public class ArgsSlave {
  @Parameter(names = "-slave")
  private String slave;
}

然后我们将这两个类传递给JCommander对象:

ArgsMaster m = new ArgsMaster();
ArgsSlave s = new ArgsSlave();
String[] argv = {"-master", "master", "-slave", "slave"}
JCommander.newBuilder()
  .addObject(new Object[] {m, s})
  .build()
  .parse(argv);
Assert.assertEquals(m.master, "master");
Assert.assertEquals(s.slave, "slave");

@语法

JCommander支持@语法,你可以将你的选项参数放入一个文件中,然后将这个文件当做参数传入系统加以解析。

比如/tmp/parameters文件的内容是:

-verbose
file1
file2
file3

则我们可以在命令行中调用:

$ java Main @/tmp/parameters

多值参数

1. 固定个数的多值参数

如果你需要为一个参数指定多个值,就像下面调用的这样:

$ java Main -pairs slave master

那么你就需要使用arity属性来对你的参数进行额外的说明,并且将参数类型更改为一个List<String>

@Parameter(names = "-pairs", arity = 2, description = "Pairs")
private List<String> pairs;

你不需要为boolean类型的参数定义arity(默认有值为0的arity),也不需要为int,Integer,long,Long类型定义arity(默认有值为1的arity)

注意,只有List<String>类型才支持arity的自定义,当然,如果你想要一个其他类型的List参数,你需要自己写转换器。

2. 可变个数的多值参数

可变参数的调用:

program -foo a1 a2 a3
program -foo a1

下面使用两种方式来解析这种类型的参数。

2.1 通过数组

@Parameter(names = "-foo", variableArity = true)
public List<String> foo = new ArrayList<>();

2.2 通过类

或者,你可以定义一个类来存储这些参数,不过每个参数需要按照顺序来声明。

static class MvParameters {
	@SubParameter(order = 0)
  String from;
  
  @SubParameter(order = 1)
  String to;
}

@Test
public void arity() {
  class Parameters {
    @Parameter(names = {"--mv"}, arity = 2)
    private MvParameters mvParameters;
  }
  
  Parameters args = new Parameters();
  JCommander.newBuilder()
    .addObject(args)
    .args(new String[]{"--mv", "from", "to"})
    .build();
  Assert.assertNotNull(args.mvParameters);
  Assert.assertEquals(args.mvParameters.from, "from");
  Assert.assertEquals(args.mvParameters.to, "to");
}

多重选项名称

你可以为参数选项定义多个名称:

@Parameter(names = {"-d", "--outputDirectory"}, description = "Directory")
private String outputDirectory;

然后就可以通过下面的方式调用:

$ java Main -d /tmp
$ java Main --outputDirectory /tmp

其他参数设置

下面两种设置,是用来决定JCommander应该如何分析参数的:

  • JCommander#setCaseSensitiveOptions(boolean):大小写是否敏感,设置true时,大小写敏感。

  • JCommander#setAllowAbbreviatedOptions(boolean):用户是否可以使用简称,设置true时,用户可以用"-par"来表示参数"-param",当然,如果用户输入的值存在歧义的时候,会抛出ParameterException异常

必选/可选参数

@Parameter(names = "-host", required = true)
private String host;

不声明required时,为可选参数(即required = false)。

默认值

最常见的定义默认值方法:

private Integer logLevel = 3;

对于一些更复杂的情况,比如你想要在不同的类中复用定义好的默认值,或者在.properties,xml文件中定义默认值的话,就可以使用IDefaultProvider来实现。

public interface IDefaultProvider {
	String getDefaultValueFor(String optionName);
}
  • optionName就是你定义的参数名(names属性)

  • 返回值是这个属性的默认值。

使用方式也和前面介绍的其他接口相似。先是要实现这个接口,然后将实现类的实例传给JCommander对象,需要注意的是这个默认值提供器返回的String会传给converter转换器(如果你定义了的话):

private static final IDefaultProvider DEFAULT_PROVIDER = new IDefaultProvider() {
  @Override
  public String getDefaultValueFor(String optionName) {
    return "-debug".equals(optionName) ? "false" : "42";
  }
};

//...

JCommander jc = JCommander.newBuilder()
  .addObject(new Args())
  .defaultProvider(DEFAULT_PROVIDER)
  .build();

帮助参数

如果某个选项是用来展示帮助信息的,你可以使用help属性来定义:

@Parameter(names = "--help", help = true)
private boolean help;

注意必须设置为boolean类型。

更多复杂语法

像是git或者svn这些复杂工具,都是能够理解一整套命令的,而且每一个命令都有自己不同的语法:

$ git commit --amend -m "Bug fix"

像commit这样的词,在JCommander中被称为“命令”,你可以为每个命令定义一个参数类:

@Parameters(separators = "=", commandDescription = "Record changes to the respository")
private class CommandCommit {
	@Parameter(description = "The list of files to commit")
	private List<String> files;
	
	@Parameter(names = "--amend", description = "Amend")
	private Boolean amend = false;
	
	@Paramter(names = "--author")
	private String author;
}

@Parameters(commandDescription = "Add file contents to the index")
public class CommandAdd {
  @Parameter(description = "File patterns to add to the index")
  private List<String> patterns;
  
  @Parameter(names = "-i")
  private Boolean interactive = false;
}

然后你可以使用JCommander对象来注册你的这些命令。在分析阶段之后,你可以调用getParsedCommand()方法,然后根据返回的命令,去对应的检查参数对象。

CommandMain cm = new CommandMain();
CommandAdd add = new CommandAdd();
CommandCommit commit = new CommandCommit();
JCommander jc = JCommander.newBuilder()
    .addObject(cm)
    .addCommand("add", add);
    .addCommand("commit", commit);
    .build();

jc.parse("-v", "commit", "--amend", "--author=cbeust", "A.java", "B.java");

Assert.assertTrue(cm.verbose);
Assert.assertEquals(jc.getParsedCommand(), "commit");
Assert.assertTrue(commit.amend);
Assert.assertEquals(commit.author, "cbeust");
Assert.assertEquals(commit.files, Arrays.asList("A.java", "B.java"));

异常

当JCommander发生错误时,会抛出ParameterException,该异常中包含JCommander实例。

Usage用法

你可以调用JCommander实例的usage()方法来生成一个参数选项概述。

Usage: <main class> [options]
  Options:
    -debug          Debug mode (default: false)
    -groups         Comma-separated list of group names to be run
  * -log, -verbose  Level of verbosity (default: 1)
    -long           A long number (default: 0)

其中有星号(*)的参数为必选项。

你可以通过调用setProgramName()方法来为程序定制一个想要的名字。

你也可以为调用usage()方法时显示的信息指定一个顺序,通过order属性,就可以为每个参数指定优先级

class Parameters {
    @Parameter(names = "--importantOption", order = 0)
    private boolean a;

    @Parameter(names = "--lessImportantOption", order = 3)
    private boolean b;
}

隐藏参数

如果你不想让某个参数出现在usage里,你可以给他们标记上"hidden",如:

@Parameter(names = "-debug", description = "Debug mode", hidden = true)
private boolean debug = false;

国际化

首先,使用@Parameters注解你的类,然后定义resourceBundle属性,接着你可以使用descriptionKey属性来取代之前的description。

当然,descriptionKey是定义在你刚才指定的resourceBundle里的。

@Parameters(resourceBundle = "MessageBundle")
private class ArgsI18N2 {
  @Parameter(names = "-host", description = "Host", descriptionKey = "host")
  String hostName;
}

下面是MessageBundle.properties文件的内容:

host: Hôte

参数代理

如果你在同一个项目里编写了多个CLI工具,那么你可能会发现,有时候大多数工具都会公用一套配置。

你可以使用继承,来避免重复代码,但是JAVA的单继承限制会让配置的复用变得很麻烦。为了解决这个问题,JCommander提供了“参数代理”的方式。

当JCommander解析到一个添加了@ParameterDelegate注解的对象的时候,它会用这个对象本身来描述这个参数:

class Delegate {
  @Parameter(names = "-port")
  private int port;
}

class MainParams {
  @Parameter(names = "-v")
  private boolean verbose;

  @ParametersDelegate
  private Delegate delegate = new Delegate();
}

也就是说,我们只需要将MainParams添加到JCommander中,就可以顺带添加了Delegate的参数信息。

MainParams p = new MainParams();
JCommander.newBuilder().addObject(p).build().parse("-v", "-port", "1234");
Assert.assertTrue(p.isVerbose);
Assert.assertEquals(p.delegate.port, 1234);

动态参数

JCommander支持你定义一个动态参数,即编译时不确定的参数,类似“-Da=b -Dc=d”

这种参数需要用@DynamicParameter注解标明,并且类型定义为Map<String, String>,动态参数在CLI中出现多次。

@DynamicParameter(names = "-D", description = "Dynamic parameters go here")
private Map<String, String> params = new HashMap<>();

使用:

Args args = new Args();
String[] argv = {"-Da=b", "-Dc=e"};
JCommander.newBuilder().addObject(args)
    .build()
    .parse(argv);
System.out.println(args.getParams());
System.out.println(args.getParams().size());

自定义用法格式

JCommander允许你为Jcommander的usage()方法定制自己的输出样式。

你可以实现IUsageFormatter接口,然后将这个实例通过JCommander.setUsageFormatter()来传入系统。

class ParameterNamesUsageFormatter implements IUsageFormatter {

    // Extend other required methods as seen in DefaultUsageFormatter

    // This is the method which does the actual output formatting
    public void usage(StringBuilder out, String indent) {
        if (commander.getDescriptions() == null) {
            commander.createDescriptions();
        }

        // Create a list of the parameters
        List<ParameterDescription> params = Lists.newArrayList();
        params.addAll(commander.getFields().values());

        // Append all the parameter names
        if (params.size() > 0) {
            out.append("Options:\n");

            for (ParameterDescription pd : params) {
                out.append(pd.getNames()).append("\n");
            }
        }
    }
}
 类似资料: