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);
}
}
複制代碼
單元測試通過,代碼實作沒有問題。