JMeter二次封裝之原始碼解析初步
近日,正在做一個壓測平臺,主要功能是想整合壓力機資源,通過一個統一的web平臺,建立和管理壓測任務,並展示包括實時壓測結果、壓力機效能資料、應用伺服器效能資料等資料的效能測試報告。
本系統的核心是對JMeter進行二次封裝,從而實現web版的JMeter平臺。針對其中的實現原理,會逐一通過文章記錄。本文主要是對JMeter的原始碼進行初步解析。
我們以非GUI模式執行JMeter為例,瞭解下JMeter的執行機制。首先我們找到入口類NewDriver,程式碼如下。
public static void main(String[] args) {
if(!EXCEPTIONS_IN_INIT.isEmpty()) {
System.err.println ("Configuration error during init, see exceptions:"+exceptionsToString(EXCEPTIONS_IN_INIT));
} else {
Thread.currentThread().setContextClassLoader(loader);
setLoggingProperties(args);
try {
Class<?> initialClass = loader.loadClass("org.apache.jmeter.JMeter" );// $NON-NLS-1$
Object instance = initialClass.newInstance();
Method startup = initialClass.getMethod("start", new Class[] { new String[0].getClass() });// $NON-NLS-1$
startup.invoke(instance, new Object[] { args });
} catch(Throwable e){ // NOSONAR We want to log home directory in case of exception
e.printStackTrace(); // NOSONAR No logger at this step
System.err.println("JMeter home directory was detected as: "+JMETER_INSTALLATION_DIRECTORY);
}
}
}
很明顯,這裡是通過反射呼叫JMeter類的start方法。接下來我們看下start方法,如下。
public void start(String[] args) {
CLArgsParser parser = new CLArgsParser(args, options);
···
initializeProperties(parser);
···
startNonGui(testFile, jtlFile, rem, reportAtEndOpt != null);
···
}
private void startNonGui(String testFile, String logFile, CLOption remoteStart, boolean generateReportDashboard)
throws IllegalUserActionException, ConfigurationException {
···
driver.runNonGui(testFile, logFile, remoteStart != null, remoteHostsString, generateReportDashboard);
}
private void runNonGui(String testFile, String logFile, boolean remoteStart, String remoteHostsString, boolean generateReportDashboard) {
···
File f = new File(testFile);
···
HashTree tree = SaveService.loadTree(f);
···
JMeterEngine engine = new StandardJMeterEngine();
engine.configure(tree);
engine.runTest();
···
}
再來看下JMeter類,拋開GUI和Remote test相關的程式碼,簡單說,JMeter做的事情主要有
解析命令列引數,載入配置檔案;
將測試檔案(.jmx檔案)解析成HashTree;
建立一個StandardJMeterEngine,並把測試的工作交給JMeterEngine;
當然,JMeter類還有其他重要的職責,比如監聽所有的JMeterEngine,當接收到GUI的StopTestNow/Shutdown等命令時候來呼叫JMeterEngine相應的方法。
接下來研究下JMeterEngine,看下這個介面都提供哪些方法:
public interface JMeterEngine {
/**
* 配置引擎
*/
void configure(HashTree testPlan);
/**
* 執行測試
*/
void runTest() throws JMeterEngineException;
/**
* 停止測試,立即打斷當前取樣器
*/
default void stopTest() {
stopTest(true);
}
/**
* 停止測試,根據引數是否立即打斷當前取樣器
*/
void stopTest(boolean now);
/**
* 停止測試執行
*/
void reset();
/**
* 設定引擎屬性
*/
void setProperties(Properties p);
/**
* 退出引擎
*/
void exit();
/**
* 引擎是否活躍
*/
boolean isActive();
}
JMeterEngine依賴於HashTree,而HashTree是由jmx檔案解析而來,熟悉JMeter的同學都知道,每一個JMeter測試計劃都會對應一個jmx檔案。所以我們只要生成合理的jmx檔案,就可以通過JMeterEngine去執行測試任務。
具體jmx檔案的生成方式,我們可以借鑑JMeter GUI模式下jmx檔案生成方式。在這裡我們的處理方式是,先定義每個元件的生成方式,然後再按一定結構組裝各個元件,示意程式碼如下。
public class HashTreeMaker {
public static TestPlan testPlan(TestPlanDTO testPlanDTO) {
TestPlan tp = new TestPlan();
tp.setName(testPlanDTO.getCaseName());
tp.setProperty(TestElement.TEST_CLASS, "TestPlan");
tp.setProperty(TestElement.GUI_CLASS, "TestPlanGui");
tp.setProperty(TestElement.ENABLED, true);
tp.setComment("");
tp.setFunctionalMode(false);
tp.setSerialized(false);
Arguments arguments = new Arguments();
arguments.setName("使用者定義的變數");
arguments.setProperty(TestElement.TEST_CLASS, "Arguments");
arguments.setProperty(TestElement.GUI_CLASS, "ArgumentsPanel");
arguments.setProperty(TestElement.ENABLED, true);
tp.setUserDefinedVariables(arguments);
return tp;
}
···
}
private HashTree createHashTree(LoadTestCaseDTO loadTestCaseDTO) {
TestPlan testPlan = HashTreeMaker.testPlan(loadTestCaseDTO.getTestPlanDTO());
ListedHashTree tree = new ListedHashTree();
tree.add(testPlan);
for (ThreadGroupDTO threadGroupDTO : loadTestCaseDTO.getThreadGroupDTOList()) {
HashTree groupTree = new HashTree();
ThreadGroup threadGroup = HashTreeMaker.threadGroup(threadGroupDTO);
groupTree.add(threadGroup);
for (HttpSamplerDTO httpSamplerDTO : threadGroupDTO.getHttpSamplerDTOList()) {
HashTree samplerTree = new HashTree();
HTTPSamplerProxy sampler = HashTreeMaker.httpSamplerProxy(httpSamplerDTO);
samplerTree.add(sampler);
HashTree resultTree = new HashTree();
ResultCollector resultCollector = HashTreeMaker.resultCollector();
resultTree.add(resultCollector);
samplerTree.add(samplerTree.getArray()[0], resultTree);
groupTree.add(groupTree.getArray()[0], samplerTree);
}
HashTree resultTree = new HashTree();
ResultCollector resultCollector = HashTreeMaker.resultCollector();
resultTree.add(resultCollector);
groupTree.add(groupTree.getArray()[0], resultTree);
HashTree backendListenerTree = new HashTree();
BackendListener backendListener = HashTreeMaker.backendListener(influxdbUrl, loadTestCaseDTO.getCaseId());
backendListenerTree.add(backendListener);
groupTree.add(groupTree.getArray()[0], backendListenerTree);
tree.add(tree.getArray()[0], groupTree);
}
return tree;
}
知道以上原理,我們就可以對JMeter進行二次封裝,不過如果要執行成功,除了引入JMeter的jar外,還需要載入jmeter.properties和saveservice.properties兩個配置檔案,如下。
JMeterUtils.loadJMeterProperties(new ClassPathResource("jmeter/jmeter.properties").getURL().getPath());
JMeterUtils.setProperty("saveservice_properties", new ClassPathResource("jmeter/saveservice.properties").getURL().getPath());
JMeterUtils.setJMeterHome("");
JMeterUtils.initLocale();
水平有限,JMeter原始碼的初步解析到此結束。