[From] https://github.com/Sayi/sayi.github.com/issues/32
我喜欢简单,什么是简单?正如若干字符组成的命令行。
有时候我们用Java开发了一个小工具,希望通过命令行(CLI)或者图形界面直接调用。命令行相较于图形界面,实现迅速,交互更接近于程序员人群,本文主要介绍Java在命令行交互上的应用,我们不妨先看看命令行的两种风格:
- POSIX风格
tar -zxvf foo.tar.gz
- Java风格
java -Djava.awt.headless=true -Djava.net.useSystemProxies=true Foo
JCommander介绍
JCommander是Java解析命令行参数的工具,作者是cbeust,他的开源测试框架testNG相信很多程序员都有耳闻。
根据官方文档,我简单总结了JCommander的几个特点:
-
注解驱动
它的核心功能命令行参数定义是基于注解的,这也是我选择用它的主要原因。我们可以轻松做到命令行参数与属性的映射,属性除了是String类型,还可以是Integer、boolean,甚至是File、集合类型。 -
功能丰富
它同时支持文章开头的两种命令行风格,并且提供了输出帮助文档的能力(usage()
),还提供了国际化的支持。 -
高度扩展
下文会详述。
在看具体应用示例前,我们先读懂核心注解@Parameter
的源码(你大可以跳过下面这段长长的源码,直接看示例),以此来了解它向我们展示了哪些方面的能力:
@Retention(java.lang.annotation.RetentionPolicy.RUNTIME) @Target({ FIELD, METHOD }) public @interface Parameter { /** * An array of allowed command line parameters (e.g. "-d", "--outputdir", etc...). * If this attribute is omitted, the field it's annotating will receive all the * unparsed options. There can only be at most one such annotation. */ String[] names() default {}; /** * A description of this option. */ String description() default ""; /** * Whether this option is required. */ boolean required() default false; /** * The key used to find the string in the message bundle. */ String descriptionKey() default ""; /** * How many parameter values this parameter will consume. For example, * an arity of 2 will allow "-pair value1 value2". */ public static int DEFAULT_ARITY = -1; int arity() default DEFAULT_ARITY; /** * If true, this parameter is a password and it will be prompted on the console * (if available). */ boolean password() default false; /** * The string converter to use for this field. If the field is of type <tt>List</tt> * and not <tt>listConverter</tt> attribute was specified, JCommander will split * the input in individual values and convert each of them separately. */ Class<? extends IStringConverter<?>> converter() default NoConverter.class; /** * The list string converter to use for this field. If it's specified, the * field has to be of type <tt>List</tt> and the converter needs to return * a List that's compatible with that type. */ Class<? extends IStringConverter<?>> listConverter() default NoConverter.class; /** * If true, this parameter won't appear in the usage(). */ boolean hidden() default false; /** * Validate the parameter found on the command line. */ Class<? extends IParameterValidator>[] validateWith() default NoValidator.class; /** * Validate the value for this parameter. */ Class<? extends IValueValidator>[] validateValueWith() default NoValueValidator.class; /** * @return true if this parameter has a variable arity. See @{IVariableArity} */ boolean variableArity() default false; /** * What splitter to use (applicable only on fields of type <tt>List</tt>). By default, * a comma separated splitter will be used. */ Class<? extends IParameterSplitter> splitter() default CommaParameterSplitter.class; /** * If true, console will not echo typed input * Used in conjunction with password = true */ boolean echoInput() default false; /** * If true, this parameter is for help. If such a parameter is specified, * required parameters are no longer checked for their presence. */ boolean help() default false; /** * If true, this parameter can be overwritten through a file or another appearance of the parameter * @return nc */ boolean forceNonOverwritable() default false; /** * If specified, this number will be used to order the description of this parameter when usage() is invoked. * @return */ int order() default -1; }
JCommander 应用示例
在一般应用场景,我们可能只需要设置@Parameter
以下几个属性值:
- names 设置命令行参数,如
-old
- required 设置此参数是否必须
- description 设置参数的描述
- order 设置帮助文档的顺序
- help 设置此参数是否为展示帮助文档或者辅助功能
下面是一个完整的示例,它用来比较两份文档,然后输出差异。源码在https://github.com/Sayi/swagger-diff上。
/** * * @author Sayi * @version */ public class CLI { private static final String OUTPUT_MODE_MARKDOWN = "markdown"; @Parameter(names = "-old", description = "old api-doc location:Json file path or Http url", required = true, order = 0) private String oldSpec; @Parameter(names = "-new", description = "new api-doc location:Json file path or Http url", required = true, order = 1) private String newSpec; @Parameter(names = "-v", description = "swagger version:1.0 or 2.0", validateWith = RegexValidator.class, order = 2) @Regex("(2\\.0|1\\.0)") private String version = SwaggerDiff.SWAGGER_VERSION_V2; @Parameter(names = "-output-mode", description = "render mode: markdown or html", validateWith = RegexValidator.class, order = 3) @Regex("(markdown|html)") private String outputMode = OUTPUT_MODE_MARKDOWN; @Parameter(names = "--help", help = true, order = 5) private boolean help; @Parameter(names = "--version", description = "swagger-diff tool version", help = true, order = 6) private boolean v; public static void main(String[] args) { CLI cli = new CLI(); JCommander jCommander = JCommander.newBuilder().addObject(cli).build(); jCommander.parse(args); cli.run(jCommander); } public void run(JCommander jCommander) { if (help) { jCommander.setProgramName("java -jar swagger-diff.jar"); jCommander.usage(); return; } if (v) { JCommander.getConsole().println("1.2.0"); return; } //SwaggerDiff diff = null; } }
运行命令行查看帮助文档,输出结果如下:
$ java -jar swagger-diff.jar --help Usage: java -jar swagger-diff.jar [options] Options: * -old old api-doc location:Json file path or Http url * -new new api-doc location:Json file path or Http url -v swagger version:1.0 or 2.0 Default: 2.0 -output-mode render mode: markdown or html Default: markdown --help --version swagger-diff tool version
这个示例像我们展示了JCommander注解的强大,我们仅仅使用注解就完成了所有参数的定义。注意,对于boolean为true的参数,我们只需要输入参数名,比如--help
,而不是--help=true
。
示例中使用了usage()
方法即可完美的输出帮助文档。
JCommander扩展:增加正则表达式校验
JCommander是高度扩展的,两个核心接口定义了扩展的能力。
IStringConverter
支持String类型的参数值可以转化为任意其他类型的属性。
/** * An interface that converts strings to any arbitrary type. * * If your class implements a constructor that takes a String, this * constructor will be used to instantiate your converter and the * parameter will receive the name of the option that's being parsed, * which can be useful to issue a more useful error message if the * conversion fails. * * You can also extend BaseConverter to make your life easier. * * @author cbeust */ public interface IStringConverter<T> { /** * @return an object of type <T> created from the parameter value. */ T convert(String value); }
IParameterValidator
支持参数值的校验。
/** * The class used to validate parameters. * * @author Cedric Beust <cedric@beust.com> */ public interface IParameterValidator { /** * Validate the parameter. * * @param name The name of the parameter (e.g. "-host"). * @param value The value of the parameter that we need to validate * * @throws ParameterException Thrown if the value of the parameter is invalid. */ void validate(String name, String value) throws ParameterException; }
在阅读上文示例中,可能会有些许疑问,比如@Regex
是什么注解,JCommander并没有提供正则表达式校验参数值的功能。
对于很多参数,我们都有校验的场景,比如值只能是几个可选值,或者是在一定范围内,IParameterValidator 和IParameterValidator2实现了参数校验了功能,接下来我们将基于接口IParameterValidator2
扩展JCommander,同样,我们只需要使用注解即可。
- 自定义正则注解,这样我们就可以在需要正则校验的属性上,设置表达式,如
@Regex("(2\\.0|1\\.0)")
package com.deepoove.swagger.diff.cli; import static java.lang.annotation.ElementType.FIELD; import java.lang.annotation.Retention; import java.lang.annotation.Target; @Retention(java.lang.annotation.RetentionPolicy.RUNTIME) @Target({ FIELD }) public @interface Regex { String value() default ""; }
- 实现
RegexValidator
,当有Regex注解的时候,解析正则表达式,应用校验规则。注意这段代码使用了反射,可能并不是最优雅的方式,但是在不修改JCommander源码的情况下,可能是最好的方式了。
package com.deepoove.swagger.diff.cli; import java.lang.reflect.Field; import java.util.regex.Pattern; import com.beust.jcommander.IParameterValidator2; import com.beust.jcommander.ParameterDescription; import com.beust.jcommander.ParameterException; import com.beust.jcommander.Parameterized; public class RegexValidator implements IParameterValidator2 { private static final String PARAMETERIZED_FIELD_NAME = "field"; @Override public void validate(String name, String value) throws ParameterException { return; } @Override public void validate(String name, String value, ParameterDescription pd) throws ParameterException { Parameterized parameterized = pd.getParameterized(); Class<? extends Parameterized> clazz = parameterized.getClass(); try { Field declaredField = clazz.getDeclaredField(PARAMETERIZED_FIELD_NAME); declaredField.setAccessible(true); Field paramField = (Field) declaredField.get(parameterized); Regex regex = paramField.getAnnotation(Regex.class); if (null == regex) return; String regexStr = regex.value(); if (!Pattern.matches(regexStr, value)) { throw new ParameterException( "Parameter " + name + " should match " + regexStr + " (found " + value + ")"); } } catch (NoSuchFieldException e) { return; } catch (IllegalArgumentException e) { return; } catch (IllegalAccessException e) { return; } } }
- 使用正则注解和正则校验类
@Parameter(names = "-v", validateWith = RegexValidator.class) @Regex("(2\\.0|1\\.0)") private String version = "2.0";
至此,正则校验已完成。
更多More: Apache Commons CLI
从源码中可以看到,JCommander默认提供了不少转化器。
----IStringConverter \--BaseConverter --\--BigDecimalConverter --\--BooleanConverter --\--DoubleConverter --\--FloatConverter --\--IntegerConverter --\--ISO8601DateConverter --\--LongConverter --\--PathConverter --\--URIConverter --\--URLConverter \--EnumConverter \--InetAddressConverter \--FileConverter
Java在命令行交互的应用,还有很多工具。另一个使用比较广泛的是Apache Commons CLI: http://commons.apache.org/proper/commons-cli/index.html,它比JCommander支持更多的命令行风格,但是扩展能力不够。
https://github.com/Sayi/sayi.github.com/issues/32