写点什么

Java 命令行参数解析方式探索(一):原始实现

作者:冰心的小屋
  • 2023-07-24
    北京
  • 本文字数:4073 字

    阅读完需:约 13 分钟

Java 命令行参数解析方式探索(一):原始实现

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>            结果输出到文件中
复制代码


该工具的主要处理流程:

  1. 识别用户输入的参数,对输入的参数进行合法校验;

  2. 根据参数构建线程池、创建每个线程的上下文用于存储调用次数和调用时间便于后续统计;

  3. 启动线程池对测试接口进行测试;

  4. 汇总每个线程上下文存储信息,计算接口的 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@ToStringpublic 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 的偶数下标存储参数的标识符,奇数下标存储实际参数值,那么基本思路就有了:

  1. 从 0 开始遍历偶数下标;

  2. 获取偶数下标判断是否为参数标识符,例如预先设定的简写 -t 或全写 --thread 等;

  3. 如果匹配成功,获取偶数下标 + 1 对应的值,将该值设置为匹配参数实际的值;

  4. 如果匹配失败,直接抛出异常或忽略即可。

  5. 偶数下标遍历完毕参数的解析工作完成。


这里面解析的难点在于标识符解析以及解析实际参数值的设置,为了避免使用大量的 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); }}
复制代码


单元测试通过,代码实现没有问题。


发布于: 16 小时前阅读数: 24
用户头像

分享技术上的点点滴滴! 2013-08-06 加入

一杯咖啡,一首老歌,一段代码,欢迎做客冰屋。

评论

发布
暂无评论
Java 命令行参数解析方式探索(一):原始实现_Java_冰心的小屋_InfoQ写作社区