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

源码分析与使用:args4j库(解析参数)

耿运浩
2023-12-01

源码分析:args4j

前言

在使用Java实现程序时,有时因为功能简单或者特殊,往往是不需要配置文件的。所以程序需要的配置主要通过启动参数。

因此利用public static void main(String[] args)中的args的获取启动参数实现相关功能;

如何解析args成了主要的问题,正常情况下,有以下选择:

  • 硬编码获取启动参数,并进行解析;解析规则虽然可以自定,但为了减少使用成本,一般会与其它程序对齐

如下:

对于 java -jar <程序名> x1 x2

  • 可以强制要求x1,x2分别代表什么变量含义,但如果使用者混淆变量的设置值,这会是灾难性的
  • 也相对于前者更进一步,使用x2表示变量x1的值,则需要编写大量的规则去适配,如x1是个列表对象时,x2应该用什么符号进行分隔表示等等问题
  • 借助常用框架实现,如args4j等框架,能够极大减少实现时间,以及因为考虑到用户使用习惯的问题,所以也能够降低使用成本

为了解决这些问题,选择了args4j库解决这一问题,同时也将完成以下目的:

目的

  • 分析args4j主要类的源码
  • 使用args4j实现常见开发需求
  • 解决一些常见问题
    • 如何关闭参数根据字典排序,或者如何自定义排序器
    • 如何使用辅助对象,如某个参数项是列表类型
    • 如何完善使用,如控制输出说明的文本内容长度
    • 如何将固定配置配置到文件中

链接

args4j库地址

Maven Repository: args4j » args4j (mvnrepository.com)

说明

对于args4j名称,不难发现名称包含主要元素args4j。其中args是主要表达参数的意思,而4j和log4j的4j表达意思一样的,就是

for(four) java的意思。

使用

使用args4j时主要有2个步骤,如下:

定义一个参数类

里面包含程序相关参数选项,如下:

@Setter
@Getter
@ToStrin
public class ArgsConfig {
    @Option(name = "-i", aliases = {"--ints"}, metaVar = "type of int", usage = "整型类型参数")
    public int ints = 10;

    @Option(name = "-s", aliases = {"--str"}, metaVar = "type of string", usage = "字符串类型参数")
    public String str;
}

使用

使用程序启动参数构造参数类,如下:

ArgsConfig argsConfig = new ArgsConfig();
CmdLineParser parser = new CmdLineParser(argsConfig);
public static void main(String[] args) throws CmdLineException {
        ArgsConfig argsConfig = new ArgsConfig();
        CmdLineParser parser = new CmdLineParser(argsConfig);
        parser.parseArgument(args);
    	// 使用
}

参数类构造完成后,就可以将参数解析到对应类属性中,拿来使用即可

由于在源码分析时,需要对源码进行调试,所以在源码分析前,简单实现

实现示例

@Setter
@Getter
@ToString
public class ArgsConfig {

    private static final int USAGE_WIDTH = 160;

    @Option(name = "-i", aliases = {"--ints"}, metaVar = "type of int", usage = "整型类型参数")
    public int ints = 10;

    @Option(name = "-s", aliases = {"--str"}, metaVar = "type of string", usage = "字符串类型参数")
    public String str;

    @Option(name = "-l", aliases = {"--lists"}, metaVar = "type of lists", usage = "列表元素", handler = StringArrayOptionHandler.class)
    public List<String> lists;

    @Option(name = "-v", aliases = {"--version"}, metaVar = "show version", usage = "输出产品版本并退出", handler = BooleanOptionHandler.class)
    public boolean version;

    @Option (name="-h", aliases = {"-?","--help"}, metaVar = "show help info", usage="帮助信息", handler = BooleanOptionHandler.class, help = true)
    public boolean help;

    @Option(name = "-k", aliases = {"--key"}, metaVar = "encrypt/decrypt key", usage = "加解密密钥")
    public String key;

    @Option(name = "-e", aliases = {"--encrypt"}, metaVar = "to encrypt", usage = "加密", depends = {"-k", "--key"}, forbids = {"-d", "--decrypt"})
    public boolean encrypt=false;

    @Option(name = "-d", aliases = {"--decrypt"}, metaVar = "to decrypt", usage = "解密", depends = {"-k", "--key"}, forbids = {"-e", "--encrypt"})
    public boolean decrypt=false;


    public void showHelp(CmdLineParser parser){
        System.out.println("参数说明 [options ...] [arguments...]");
        parser.printUsage(System.out);
    }


    public static void main(String[] args) throws CmdLineException {
        ArgsConfig argsConfig = new ArgsConfig();
        CmdLineParser parser = new CmdLineParser(argsConfig);
        parser.setUsageWidth(USAGE_WIDTH);
        parser.parseArgument(args);
        // 没有参数
        if(args.length == 0){
            argsConfig.showHelp(parser);
        }
        // 如果启动参数中包含 帮助参数项, 则打印参数信息
        if(argsConfig.isHelp()){
            argsConfig.showHelp(parser);
        }
        System.out.println(argsConfig.toString());
    }

}

帮助信息

# -h,-?,--help
参数说明 [options ...] [arguments...]
 -d (--decrypt) to decrypt      : 解密 (default: false)
 -e (--encrypt) to encrypt      : 加密 (default: false)
 -h (-?, --help) show help info : 帮助信息 (default: true)
 -i (--ints) type of int        : 整型类型参数 (default: 10)
 -k (--key) encrypt/decrypt key : 加解密密钥
 -l (--lists) type of lists     : 列表元素
 -s (--str) type of string      : 字符串类型参数
 -v (--version) show version    : 输出产品版本并退出 (default: false)
 
ArgsConfig(ints=10, str=null, lists=null, version=false, help=true, key=null, encrypt=false, decrypt=false)

普通参数

# -i 10 -s test -l x1 x2 x3

ArgsConfig(ints=10, str=test, lists=[x1, x2, x3], version=false, help=false, key=null, encrypt=false, decrypt=false)

依赖参数

# -e
Exception in thread "main" org.kohsuke.args4j.CmdLineException: option "-e (--encrypt)" requires the option(s) [-k, --key]
	at org.kohsuke.args4j.CmdLineParser.checkRequiredOptionsAndArguments(CmdLineParser.java:602)
	at org.kohsuke.args4j.CmdLineParser.parseArgument(CmdLineParser.java:534)
	at zhj.core.args4j.bean.ArgsConfig.main(ArgsConfig.java:64)

# -e -k 123456
ArgsConfig(ints=10, str=null, lists=null, version=false, help=false, key=123456, encrypt=true, decrypt=false)

# -d -k 123456
ArgsConfig(ints=10, str=null, lists=null, version=false, help=false, key=123456, encrypt=false, decrypt=true)

互斥参数

# -e -d -k 123456
Exception in thread "main" org.kohsuke.args4j.CmdLineException: option "-d (--decrypt)" cannot be used with the option(s) [-e, --encrypt]
	at org.kohsuke.args4j.CmdLineParser.checkRequiredOptionsAndArguments(CmdLineParser.java:610)
	at org.kohsuke.args4j.CmdLineParser.parseArgument(CmdLineParser.java:534)
	at zhj.core.args4j.bean.ArgsConfig.main(ArgsConfig.java:64)

源码分析

注解类Option分析

在注解类Option中有9个参数,如下:

@Retention(RUNTIME)
@Target({FIELD,METHOD,PARAMETER})
public @interface Option {

    String name();
    String[] aliases() default { };
    String usage() default "";
    String metaVar() default "";
    boolean required() default false;
    boolean help() default false;
    boolean hidden() default false;
    Class<? extends OptionHandler> handler() default OptionHandler.class;
    String[] depends() default { };
    String[] forbids() default { };    
}

对于上面参数的说明,源码中的注释非常详细,如对于参数name

    /**
     * Name of the option, such as <code>-foo</code> or <code>-bar</code>.
     */
    String name();

name为参数名称,如对于name="foo",使用时只要指定-foo即可。

上面参数总结如下:

参数说明
name参数名称,通常设置为单破折号,只能指定一个,如示例中指定name=“-foo”,则启动时指定-foo即可
aliases参数别名,通常设置为双破折号,可以指定多个,如aliases = {“–foo1”,“–foo2”},其中指定--foo1或者-foo2都可以
usage参数使用说明,通常用于描述该参数的用法
metaVar元信息
required是否必选,如果设置了required=true,在启动时没有指定,会报错
help该参数是否是帮助选项,设置为help=true后,当启动时存在这个参数时,则会打印帮助信息
hidden是否隐藏,设置为hidden=true后,帮助信息中将不会有这个参数的说明;通常用于隐藏敏感参数
handler类型帮助类,如当参数类型为List时,可以在这个参数中属性中设置为handler = StringArrayOptionHandler.class
depends参数依赖类,如参数-a(–a1)和-b(–b1)时,当启动参数有-a(–a1)时,同时也需要指定参数-b(–b1)
forbids参数互斥类,如参数-a(–a1)和-b(–b1)时,当启动参数有-a(–a1)时,同时不能存在参数-b(–b1)

属性配置类ParserProperties

/**
 * Set of properties that controls {@link CmdLineParser} behaviours.
 *
 * @see CmdLineParser#CmdLineParser(Object, ParserProperties)
 */
public class ParserProperties {
    
}

类属性

属性配置类主要控制CmdLineParser的属性,具体的属性及其说明如下:

private static final int DEFAULT_USAGE_WIDTH = 80;
private int usageWidth = DEFAULT_USAGE_WIDTH;
private Comparator<OptionHandler> optionSorter = DEFAULT_COMPARATOR;
private String optionValueDelimiter=" ";
private boolean atSyntax = true;
private boolean showDefaults = true;
usageWidth

参数使用说明输出长度,默认值是80

参数说明 [options ...] [arguments...]
 -d (--decrypt) to decrypt      : 解密 (default: false)
 -e (--encrypt) to encrypt      : 加密 (default: false)
 -h (-?, --help) show help info : 帮助信息 (default: true)
 -i (--ints) type of int        : 整型类型参数 (default: 10)
 -k (--key) encrypt/decrypt key : 加解密密钥
 -l (--lists) type of lists     : 列表元素
 -s (--str) type of string      : 字符串类型参数
 -v (--version) show version    : 输出产品版本并退出 (default: false)

如果设置为30,则如下:

ParserProperties prop = ParserProperties.defaults()
    .withUsageWidth(30);
ArgsConfig argsConfig = new ArgsConfig();
CmdLineParser parser = new CmdLineParser(argsConfig, prop);

则打印内容如下:

参数说明 [options ...] [arguments...]
 -d (--decrypt : 解密 (default:
 ) to decrypt    false)
 -e (--encrypt : 加密 (default:
 ) to encrypt    false)
 -h (-?, --hel : 帮助信息 (default
 p) show help    : true)
 info             
 -i (--ints)   : 整型类型参数 (defau
 type of int     lt: 10)
 -k (--key)    : 加解密密钥
 encrypt/decry    
 pt key           
 -l (--lists)  : 列表元素
 type of lists    
 -s (--str)    : 字符串类型参数
 type of strin    
 g                
 -v (--version : 输出产品版本并退出
 ) show versio   (default:
 n               false)

另外,CmdLineParser有一个方法是setUsageWidth(int usageWidth),也是通过ParserProperties的方法withUsageWidth(int usageWidth)实现的。也因此,CmdLineParsersetUsageWidth(int usageWidth)也被定义为废弃方法

optionSorter

是否排序输出参数项说明,默认是根据配置项的字符串排序

static final Comparator<OptionHandler> DEFAULT_COMPARATOR = new Comparator<OptionHandler>() {
    public int compare(OptionHandler o1, OptionHandler o2) {
        return o1.option.toString().compareTo(o2.option.toString());
    }
};

因此,可以自定义排序器进行排序,也可以设置为null,通过参数类实例的属性定义属性。使用默认值时,如下:

参数说明 [options ...] [arguments...]
 -d (--decrypt) to decrypt      : 解密 (default: false)
 -e (--encrypt) to encrypt      : 加密 (default: false)
 -h (-?, --help) show help info : 帮助信息 (default: true)
 -i (--ints) type of int        : 整型类型参数 (default: 10)
 -k (--key) encrypt/decrypt key : 加解密密钥
 -l (--lists) type of lists     : 列表元素
 -s (--str) type of string      : 字符串类型参数
 -v (--version) show version    : 输出产品版本并退出 (default: false)

可以看到是按照字典顺序输出

如果设置ParserProperties.defaults().withOptionSorter(null)

参数说明 [options ...] [arguments...]
 -i (--ints) type of int        : 整型类型参数 (default: 10)
 -s (--str) type of string      : 字符串类型参数
 -l (--lists) type of lists     : 列表元素
 -v (--version) show version    : 输出产品版本并退出 (default: false)
 -h (-?, --help) show help info : 帮助信息 (default: true)
 -k (--key) encrypt/decrypt key : 加解密密钥
 -e (--encrypt) to encrypt      : 加密 (default: false)
 -d (--decrypt) to decrypt      : 解密 (default: false)

此时与属性的定义是一致的(具体见实现示例中的参数类示例)。

atSyntax

源码中注释说明

/**
     * Toggles the parsing of @-prefixes in values.
     * If a command line value starts with @, it is interpreted
     * as being a file, loaded, and interpreted as if
     * the file content would have been passed to the command line.
     * @param atSyntax {@code true} if at sign is being parsed, {@code false}
     * if it is to be ignored. Defaults to {@code true}.
     * @see #getAtSyntax() 
     */
public ParserProperties withAtSyntax(boolean atSyntax) {
    this.atSyntax = atSyntax;
    return this;
}

翻译结果如下:

切换值中@前缀的解析。如果命令行值以@开头,它将被解释为一个文件,被加载,并被解释为文件内容将被传递到命令行@param atSyntax{@code true}如果正在解析at符号,则返回{@code false}如果要忽略它。默认为{@code true}@请参见getAtSyntax()

简单来说,就是当atSyntax=true时,在参数值指定为@开头时,将会被解析为文件,并且会将文件内容作为参数值。如下:

对于同一启动命令-s @config/configs,即指定参数项str为@config/configs

其中文件config/configs的内容如下:

123

  • atSyntax=false
ArgsConfig(ints=10, str=@config/config.properties, lists=null, version=false, help=false, key=null, encrypt=false, decrypt=false)

可以看到参数项str被赋值为@config/config.properties

  • atSyntax=true
ArgsConfig(ints=10, str=123, lists=null, version=false, help=false, key=null, encrypt=false, decrypt=false)

可以看到str被赋值为123

当然,初看注释时,对这个参数还是有点迷糊,后面通过解析类CmdLineParser对于这个参数的处理就明白了(见解析类CmdLineParser-parseArgument()

剩余的两个属性比较简单,说明如下:

optionValueDelimiter

设置分隔符,默认是" ";一般这个不修改

showDefaults

是否打印选项默认值

方法

类中大部分方法主要是对上面类属性值的设置

解析类CmdLineParser

构造方法

类CmdLineParser有2个构造方法,如下:

public CmdLineParser(Object bean) {
    // 最终构造方法也是另一个构造方法CmdLineParser(Object bean, ParserProperties parserProperties)
    this(bean, ParserProperties.defaults());
}
public CmdLineParser(Object bean, ParserProperties parserProperties) {
    this.parserProperties = parserProperties;
    // A 'return' in the constructor just skips the rest of the implementation
    // and returns the new object directly.
    // 判断类实例bean是否合法
    if (bean==null) return;
    
    // Parse the metadata and create the setters
    new ClassParser().parse(bean,this);
    
	// 对参数是否排序输出
    if (parserProperties.getOptionSorter()!=null) {
        Collections.sort(options, parserProperties.getOptionSorter());
    }
}

其中参数Object bean表示:参数类实例,每一个参数都应该对应一个类属性,并且使用注解Option控制

ParserProperties parserProperties表示:CmdLineParser的属性配置,比如控制usage的宽度,参数输出是否排序等;(详细见

属性配置类ParserProperties的类属性解析)

在处理逻辑块中,new ClassParser().parse(bean,this);

public class ClassParser {
    public void parse(Object bean, CmdLineParser parser) {
        // recursively process all the methods/fields.
        for( Class c=bean.getClass(); c!=null; c=c.getSuperclass()) {
            for( Method m : c.getDeclaredMethods() ) {
                Option o = m.getAnnotation(Option.class);
                if(o!=null) {
                	parser.addOption(new MethodSetter(parser,bean,m), o);
                }
                Argument a = m.getAnnotation(Argument.class);
                if(a!=null) {
                    parser.addArgument(new MethodSetter(parser,bean,m), a);
                }
            }

            for( Field f : c.getDeclaredFields() ) {
                Option o = f.getAnnotation(Option.class);
                if(o!=null) {
                	parser.addOption(Setters.create(f,bean),o);
                }
                Argument a = f.getAnnotation(Argument.class);
                if(a!=null) {
                	parser.addArgument(Setters.create(f,bean), a);
                }
            }
        }
	}
}

主要是利用反射递归解析参数实例类到CmdLineParser parser中,所以new ClassParser().parse(bean,this);中使用了this;

解析方法parseArgument()

CmdLineParser类中存在使用过程中对参数args的解析方法parseArgument()

public void parseArgument(Collection<String> args) throws CmdLineException {
    parseArgument(args.toArray(new String[args.size()]));
}
public void parseArgument(final String... args) throws CmdLineException {}

两个方法中,其实有效的方法是parseArgument(final String... args)

对于方法parseArgument(final String... args),主要的逻辑如下:

  • 检查方法参数是否有效,如果为空,则返回空指针异常
checkNonNull(args, "args");

// 方法内容如下:
static void checkNonNull(Object obj, String name) {
    if (obj == null) {
        throw new NullPointerException(name+" is null");
    }
}   
  • 根据属性atSyntax的配置,判断是否需要解析文件内容到参数中
String expandedArgs[] = args;
if (parserProperties.getAtSyntax()) {
    expandedArgs = expandAtFiles(args);
}

如果atSyntax=true,则走以下逻辑

private String[] expandAtFiles(String args[]) throws CmdLineException {
    List<String> result = new ArrayList<String>();
    for (String arg : args) {
        if (arg.startsWith("@")) {
            File file = new File(arg.substring(1));
            if (!file.exists())
                throw new CmdLineException(this,Messages.NO_SUCH_FILE,file.getPath());
            try {
                result.addAll(readAllLines(file));
            } catch (IOException ex) {
                throw new CmdLineException(this, "Failed to parse "+file,ex);
            }
        } else {
            result.add(arg);
        }
    }
    return result.toArray(new String[result.size()]);
}

逻辑如下:

  • 遍历参数数组的每一个元素
  • 如果元素是以符号@开头,则会将@后面的内容作为文件路径,并解析
    • 如果文件不存在,则抛出文件不存在异常
    • 否则,逐行读取文件内容,放入新的参数数组result
  • 否则,则将参数放到新的参数数组result

注意:其中读取文件内容方法 readAllLines(file)返回的是列表对象

这一段处理,可以将部分固定启动参数,如连接信息(用户名,密码)等参数定义在文件中;启动程序时,增加参数@配置文件路径,可以有效减少启动参数内容

  • 使用内部类CmdLineImpl处理参数
CmdLineImpl cmdLine = new CmdLineImpl(expandedArgs);

类CmdLineImpl的实现逻辑比较简单,主要有以下方法

CmdLineImpl( String[] args );
protected boolean hasMore(); // 判断是否下一位置是否有值
protected String getCurrentToken();// 获取当前位置的参数
private void proceed( int n );// 当前位置前进一
public String getParameter(int idx) throws CmdLineException;// 根据位置获取参数
public int size(); // 当前指针
void splitToken();// 分离特殊参数,比如将-s=1这种参数为:-s 1 

可以理解为将一个数组对象转换为链表对象,剩余逻辑主要是对CmdLineImpl的处理

其它方法

除了上面两个主要方法,剩余方法主要有以下分类:

  • 获取参数属性,如判断启动参数是否存在,以及获取参数值
  • 根据ParserProperties的属性进行判断处理
  • 检查启动参数指定是否合法,比如两个互斥参数是否同时出现,或者必传参数是否包含其中

总结

主要完成了以下内容:

args4j的常见开发使用指南

仿照实现示例,即可很好的使用args4j实现开发需求

分析了args4j的主要源码

实现的代码不多,主要类也较少,但却把基本的需求都实现了

常见问题与解决

在分析源码过程中,也对一些常见问题进行解决

  1. 帮助信息输出内容格式
  • 如何关闭参数根据字典排序,或者如何自定义排序器

  • 如何完善使用,如控制输出说明的文本内容长度

两个问题都可以在解析时,根据需求配置好ParserProperties后,再与参数实例作为参数,构造解析器CmdLineParser

ParserProperties prop = ParserProperties.defaults()
        .withUsageWidth(160).withOptionSorter(null);
ArgsConfig argsConfig = new ArgsConfig();
CmdLineParser parser = new CmdLineParser(argsConfig, prop);
parser.parseArgument(args);
  1. 复杂数据结构
  • 如何使用辅助对象,如某个参数项是列表类型

在注解类Option分析中有涉及,即使用辅助类OptionHandler

  1. 固定的启动参数

对于固定的启动参数,可以使用文件进行配置。启动程序时,指定即可。如下:

配置文件configs

-i
20
-s
zhj1121
-l
1 2 3

启动参数指定@config/configs,输出内容如下:

ArgsConfig(ints=20, str=zhj1121, lists=[1, 2, 3], version=false, help=false, key=null, encrypt=false, decrypt=false)

当然,还有其它内容,如主要类的主要逻辑等等

 类似资料: