天天看點

Java 指令行參數解析方式探索(一):原始實作

作者:冰心de小屋

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
@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 中:

Java 指令行參數解析方式探索(一):原始實作

可以發現數組 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);
    }
}

           

複制代碼

單元測試通過,代碼實作沒有問題。

Java 指令行參數解析方式探索(一):原始實作

繼續閱讀