zoukankan      html  css  js  c++  java
  • [转] Java 命令行交互-JCommander

    [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,同样,我们只需要使用注解即可。

    1. 自定义正则注解,这样我们就可以在需要正则校验的属性上,设置表达式,如@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 "";
    
    }
    1. 实现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;
        }
      }
    }
    1. 使用正则注解和正则校验类
    @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

  • 相关阅读:
    [Leetcode] Merge Intervals
    [Leetcode] Sort Colors
    junit
    DBUnit的使用
    xml简介---来自百度百科
    今天开始深入学习XML
    Java 用Myeclipse部署项目基础坏境搭建
    properties配置文件读取方法
    Java web做服务器之间的通信方法
    Java Socket简单的客服端及其服务器端
  • 原文地址:https://www.cnblogs.com/pekkle/p/9680014.html
Copyright © 2011-2022 走看看