Java 命令行参数解析方式探索(一):原始实现
- 2023-07-24 北京
本文字数:4073 字
阅读完需:约 13 分钟
1. 背景
最近开发个工具,根据用户输入的接口地址、并发数、调用次数和调用时长统计接口的 TPS,当用户输入 -h 时输出帮助信息:
使用帮助: java -jar api-test.jar [options...]
-h, --help 输出帮助信息
-t, --thread <value> 并发数
-c, --count <value> 调用次数
-s, --second <value> 调用时长:单位秒
-p, --property <key=value> 自定义扩展属性
-o, --output <file> 结果输出到文件中
该工具的主要处理流程:
识别用户输入的参数,对输入的参数进行合法校验;
根据参数构建线程池、创建每个线程的上下文用于存储调用次数和调用时间便于后续统计;
启动线程池对测试接口进行测试;
汇总每个线程上下文存储信息,计算接口的 TPS。
为了更好的解析命令行参数,探索了多种实现方式,后续会对比每种实现方式的优缺点。如果你在工作中遇到 java 命令行解析的工作,希望本系列文章对你会有所帮助。
2. 设计
识别用户输入的参数后需要映射为参数实体类,为了方便后续扩展定义参数 Parameter 接口:
package com.ice;
import java.util.Map;
public interface Parameter {
int getThread();
int getCount();
int getSecond();
Map<String, String> getProperty();
String getOutput();
boolean isHelp();
}
接收用户输入参数、执行测试主要工作定义入口类 Starter,该类主要功能:解析参数、执行初始化工作、执行测试任务和统计输出结果:
package com.ice;
public abstract class Starter implements Runnable {
protected final String[] args;
public Starter(String[] args) {
this.args = args;
}
public void run() {
Parameter parameter = parse();
if(parameter.isHelp()){
printHelp();
return;
}
init(parameter);
innerRun(parameter);
output(parameter.getOutput());
}
protected abstract Parameter parse();
private void init(Parameter parameter) {
}
private void innerRun(Parameter parameter) {
}
private void output(String output) {
}
private void printHelp() {
String message = "使用帮助: java -jar api-test.jar [options...]\n" +
" -h, --help 输出帮助信息\n" +
" -t, --thread <value> 并发数\n" +
" -c, --count <value> 调用次数\n" +
" -s, --second <value> 调用时长:单位秒\n" +
" -p, --property <key=value> 自定义扩展属性\n" +
" -o, --output <file> 结果输出到文件中";
System.out.println(message);
}
}
验证各种解析结果的正确性,设计单元测试用例:
package com.ice;
import org.junit.Assert;
import org.junit.Before;
public abstract class ParameterTest {
private static final String CONNECT_TIMEOUT = "connectTimeout";
private static final String READ_TIMEOUT = "readTimeout";
protected String[] args;
private int thread;
private int count;
private int second;
private int connectTimeout;
private int readTimeout;
private String output;
@Before
public void before() {
thread = 10;
count = 20;
second = 30;
connectTimeout = 3;
readTimeout = 10;
output = "result.txt";
args = new String[]{
"-t", Integer.toString(thread),
"-c", Integer.toString(count),
"-s", Integer.toString(second),
"-p", CONNECT_TIMEOUT + "=" + connectTimeout,
"-p", READ_TIMEOUT + "=" + readTimeout,
"-o", output
};
}
protected abstract void startTest();
protected void validate(Parameter parameter) {
Assert.assertEquals(thread, parameter.getThread());
Assert.assertEquals(count, parameter.getCount());
Assert.assertEquals(Integer.toString(connectTimeout), parameter.getProperty().get(CONNECT_TIMEOUT));
Assert.assertEquals(Integer.toString(readTimeout), parameter.getProperty().get(READ_TIMEOUT));
Assert.assertEquals(output, parameter.getOutput());
}
}
一切准备好之后,让我们来探索各种实现方式。
3. 原始实现
首先定义 Parameter 接口的实现类,用于存储实际的命令行参数:
import com.ice.Parameter;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.util.Map;
@Getter
@Setter
@ToString
public class PlanParameter implements Parameter {
private int thread;
private int count;
private int second;
private Map<String, String> property;
private String output;
private boolean isHelp;
}
命令行实际的参数会存储在字符串数组 args 中:
可以发现数组 args 的偶数下标存储参数的标识符,奇数下标存储实际参数值,那么基本思路就有了:
从 0 开始遍历偶数下标;
获取偶数下标判断是否为参数标识符,例如预先设定的简写 -t 或全写 --thread 等;
如果匹配成功,获取偶数下标 + 1 对应的值,将该值设置为匹配参数实际的值;
如果匹配失败,直接抛出异常或忽略即可。
偶数下标遍历完毕参数的解析工作完成。
这里面解析的难点在于标识符解析以及解析实际参数值的设置,为了避免使用大量的 if-else 来判断标识符,可以借助于策略模式使用 HashMap:使用标识符作为键,设置参数值的回调方法作为值。
package com.ice.impl;
import com.ice.Parameter;
import com.ice.Starter;
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiConsumer;
public class PlanStarter extends Starter {
public PlanStarter(String[] args) {
super(args);
}
public Parameter parse() {
Map<String, BiConsumer<PlanParameter, String>> functions = createFunctions();
PlanParameter parameter = new PlanParameter();
for (int i = 0; i < args.length; i += 2) {
BiConsumer<PlanParameter, String> function = functions.get(args[i]);
if (function != null) {
function.accept(parameter, args[i + 1]);
}
}
return parameter;
}
private Map<String, BiConsumer<PlanParameter, String>> createFunctions() {
Map<String, BiConsumer<PlanParameter, String>> functions = new HashMap<>();
// 1. 设置输出帮助信息参数
BiConsumer<PlanParameter, String> helpFunc = (parameter, value) -> parameter.setHelp(true);
functions.put("-h", helpFunc);
functions.put("--help", helpFunc);
// 2. 设置并发数
BiConsumer<PlanParameter, String> threadFunc = (parameter, value) -> parameter.setThread(Integer.parseInt(value));
functions.put("-t", threadFunc);
functions.put("--thread", threadFunc);
// 3. 设置调用次数
BiConsumer<PlanParameter, String> countFunc = (parameter, value) -> parameter.setCount(Integer.parseInt(value));
functions.put("-c", countFunc);
functions.put("--count", countFunc);
// 4. 设置调用时长
BiConsumer<PlanParameter, String> secondFunc = (parameter, value) -> parameter.setSecond(Integer.parseInt(value));
functions.put("-s", secondFunc);
functions.put("--second", secondFunc);
// 5. 设置自定义参数信息
BiConsumer<PlanParameter, String> propertyFunc = (parameter, value) -> {
// key1=value1
Map<String, String> property = parameter.getProperty();
if (property == null) {
property = new HashMap<>();
parameter.setProperty(property);
}
int index = value.indexOf("=");
if (index > 0) {
property.put(value.substring(0, index), value.substring(index + 1));
}
};
functions.put("-p", propertyFunc);
functions.put("--property", propertyFunc);
// 6. 设置输出文件路径
BiConsumer<PlanParameter, String> outputFunc = (parameter, value) -> parameter.setOutput(value);
functions.put("-o", outputFunc);
functions.put("--output", outputFunc);
return functions;
}
public static void main(String[] args) {
Starter planStarter = new PlanStarter(args);
planStarter.run();
}
}
代码实现后,编写单元测试进行验证:
package com.ice;
import com.ice.impl.PlanStarter;
import org.junit.Test;
public class PlanParameterTest extends ParameterTest{
@Test
@Override
public void startTest() {
Starter starter = new PlanStarter(args);
Parameter parameter = starter.parse();
validate(parameter);
}
}
单元测试通过,代码实现没有问题。
版权声明: 本文为 InfoQ 作者【冰心的小屋】的原创文章。
原文链接:【http://xie.infoq.cn/article/2b6959f8a78a909c7d529d0b5】。文章转载请联系作者。
冰心的小屋
分享技术上的点点滴滴! 2013-08-06 加入
一杯咖啡,一首老歌,一段代码,欢迎做客冰屋。
评论