在使用Java实现程序时,有时因为功能简单或者特殊,往往是不需要配置文件的。所以程序需要的配置主要通过启动参数。
因此利用public static void main(String[] args)
中的args的获取启动参数实现相关功能;
如何解析args成了主要的问题,正常情况下,有以下选择:
如下:
对于 java -jar <程序名> x1 x2
- 可以强制要求x1,x2分别代表什么变量含义,但如果使用者混淆变量的设置值,这会是灾难性的
- 也相对于前者更进一步,使用x2表示变量x1的值,则需要编写大量的规则去适配,如x1是个列表对象时,x2应该用什么符号进行分隔表示等等问题
为了解决这些问题,选择了args4j库解决这一问题,同时也将完成以下目的:
args4j库地址
Maven Repository: args4j » args4j (mvnrepository.com)
对于args4j名称,不难发现名称包含主要元素args
和4j
。其中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中有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) |
/**
* 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;
参数使用说明输出长度,默认值是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)
实现的。也因此,CmdLineParser
的setUsageWidth(int usageWidth)
也被定义为废弃方法
是否排序输出参数项说明,默认是根据配置项的字符串排序
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)
此时与属性的定义是一致的(具体见实现示例中的参数类示例)。
源码中注释说明
/**
* 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
ArgsConfig(ints=10, str=@config/config.properties, lists=null, version=false, help=false, key=null, encrypt=false, decrypt=false)
可以看到参数项str被赋值为@config/config.properties
ArgsConfig(ints=10, str=123, lists=null, version=false, help=false, key=null, encrypt=false, decrypt=false)
可以看到str被赋值为123
当然,初看注释时,对这个参数还是有点迷糊,后面通过解析类CmdLineParser对于这个参数的处理就明白了(见解析类CmdLineParser
-parseArgument()
)
剩余的两个属性比较简单,说明如下:
设置分隔符,默认是" ";一般这个不修改
是否打印选项默认值
类中大部分方法主要是对上面类属性值的设置
类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;
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 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的处理
除了上面两个主要方法,剩余方法主要有以下分类:
主要完成了以下内容:
仿照实现示例,即可很好的使用args4j实现开发需求
实现的代码不多,主要类也较少,但却把基本的需求都实现了
在分析源码过程中,也对一些常见问题进行解决
如何关闭参数根据字典排序,或者如何自定义排序器
如何完善使用,如控制输出说明的文本内容长度
两个问题都可以在解析时,根据需求配置好ParserProperties后,再与参数实例作为参数,构造解析器CmdLineParser
ParserProperties prop = ParserProperties.defaults()
.withUsageWidth(160).withOptionSorter(null);
ArgsConfig argsConfig = new ArgsConfig();
CmdLineParser parser = new CmdLineParser(argsConfig, prop);
parser.parseArgument(args);
在注解类Option分析中有涉及,即使用辅助类OptionHandler
对于固定的启动参数,可以使用文件进行配置。启动程序时,指定即可。如下:
配置文件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)
当然,还有其它内容,如主要类的主要逻辑等等