一、使用 Jmeter GUI 進行測試配置
我們在使用Jmeter做性能測試時,通常需要使用 GUI 來配置腳本,生成 jmx 文件,然后使用命令來執行。腳本的配置通常需要有以下幾個步驟:
測試計划 → 線程組 → 循環控制器 → Java請求 → 結果統計
通過以上配置生成 jmx 文件后,再使用命令行執行,生成結果文件,如:jmeter -n -t testscript\test.jmx -l testresult\01-reslut.jtl
二、運行機制 && 源碼分析
- NewDriver 是 Jmeter 程序的入口類,通過反射調用 JMeter 類的 start() 方法;
- JMeter 類的 start() 方法根據不同的命令來執行不同的啟動方法,startGui() 和 startNonGui();
- startNonGui() 方法又調用了 runNonGui() 方法來執行腳本;
- HashTree 是 JMeter 執行測試依賴的數據結構,在執行測試之前進行配置測試數據,HashTree將數據組織到一個遞歸樹結構中,並提供了操作該結構的方法;
- StandardJMeterEngine 執行JMeter 測試 ,直接用於本地 GUI 和非 GUI 調用,或者在服務器模式下運行時由 RemoteJMeterEngineImpl 啟動;
- JMeterEngine 接口被運行 JMeter的測試類實現,此接口共8個方法,JMeterEngine本質就是一個線程;
源碼分析

/** * The main program which actually runs JMeter. * mian方法 * @param args * the command line arguments */ 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 { // 加載JMeter類 Class<?> initialClass = loader.loadClass("org.apache.jmeter.JMeter"); // 獲取JMeter類實例 Object instance = initialClass.getDeclaredConstructor().newInstance(); // 獲取start方法類型實例 Method startup = initialClass.getMethod("start", new Class[] { new String[0].getClass() }); // 反射調用JMeter類的start方法 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() 方法。我們再來看看 JMeter 類的 start() 方法:

/** * Takes the command line arguments and uses them to determine how to * startup JMeter. * 根據命令行執行不同的操作 * 主要功能為:1)startgui 2)startnogui * * Called reflectively by {@link NewDriver#main(String[])} * @param args The arguments for JMeter */ public void start(String[] args) { // 解析命令號參數的類 CLArgsParser parser = new CLArgsParser(args, options); // 錯誤信息 String error = parser.getErrorString(); if (error == null){// Check option combinations 檢查選項組合 boolean gui = parser.getArgumentById(NONGUI_OPT)==null; boolean nonGuiOnly = parser.getArgumentById(REMOTE_OPT)!=null || parser.getArgumentById(REMOTE_OPT_PARAM)!=null || parser.getArgumentById(REMOTE_STOP)!=null; if (gui && nonGuiOnly) { error = "-r and -R and -X are only valid in non-GUI mode"; } } // 輸出錯誤信息 if (null != error) { System.err.println("Error: " + error);//NOSONAR System.out.println("Usage");//NOSONAR System.out.println(CLUtil.describeOptions(options).toString()); // repeat the error so no need to scroll back past the usage to see it System.out.println("Error: " + error);//NOSONAR return; } try { // 初始化配置,同時初始化JMeter日志 initializeProperties(parser); // Also initialises JMeter logging Thread.setDefaultUncaughtExceptionHandler( (Thread t, Throwable e) -> { if (!(e instanceof ThreadDeath)) { log.error("Uncaught exception: ", e); System.err.println("Uncaught Exception " + e + ". See log file for details."); } }); if (log.isInfoEnabled()) { log.info(JMeterUtils.getJMeterCopyright()); log.info("Version {}", JMeterUtils.getJMeterVersion()); log.info("java.version={}", System.getProperty("java.version"));//$NON-NLS-1$ //$NON-NLS-2$ log.info("java.vm.name={}", System.getProperty("java.vm.name"));//$NON-NLS-1$ //$NON-NLS-2$ log.info("os.name={}", System.getProperty("os.name"));//$NON-NLS-1$ //$NON-NLS-2$ log.info("os.arch={}", System.getProperty("os.arch"));//$NON-NLS-1$ //$NON-NLS-2$ log.info("os.version={}", System.getProperty("os.version"));//$NON-NLS-1$ //$NON-NLS-2$ log.info("file.encoding={}", System.getProperty("file.encoding"));//$NON-NLS-1$ //$NON-NLS-2$ log.info("Max memory ={}", Runtime.getRuntime().maxMemory()); log.info("Available Processors ={}", Runtime.getRuntime().availableProcessors()); log.info("Default Locale={}", Locale.getDefault().getDisplayName()); log.info("JMeter Locale={}", JMeterUtils.getLocale().getDisplayName()); log.info("JMeterHome={}", JMeterUtils.getJMeterHome()); log.info("user.dir ={}", System.getProperty("user.dir"));//$NON-NLS-1$ //$NON-NLS-2$ log.info("PWD ={}", new File(".").getCanonicalPath());//$NON-NLS-1$ log.info("IP: {} Name: {} FullName: {}", JMeterUtils.getLocalHostIP(), JMeterUtils.getLocalHostName(), JMeterUtils.getLocalHostFullName()); } setProxy(parser); updateClassLoader(); if (log.isDebugEnabled()) { String jcp=System.getProperty("java.class.path");// $NON-NLS-1$ String[] bits = jcp.split(File.pathSeparator); log.debug("ClassPath"); for(String bit : bits){ log.debug(bit); } } // Set some (hopefully!) useful properties 設置屬性 long now=System.currentTimeMillis(); JMeterUtils.setProperty("START.MS",Long.toString(now));// $NON-NLS-1$ Date today=new Date(now); // so it agrees with above JMeterUtils.setProperty("START.YMD",new SimpleDateFormat("yyyyMMdd").format(today));// $NON-NLS-1$ $NON-NLS-2$ JMeterUtils.setProperty("START.HMS",new SimpleDateFormat("HHmmss").format(today));// $NON-NLS-1$ $NON-NLS-2$ // 判斷 if (parser.getArgumentById(VERSION_OPT) != null) { displayAsciiArt(); } else if (parser.getArgumentById(HELP_OPT) != null) { displayAsciiArt(); System.out.println(JMeterUtils.getResourceFileAsText("org/apache/jmeter/help.txt"));//NOSONAR $NON-NLS-1$ } else if (parser.getArgumentById(OPTIONS_OPT) != null) { displayAsciiArt(); System.out.println(CLUtil.describeOptions(options).toString());//NOSONAR } else if (parser.getArgumentById(SERVER_OPT) != null) { // Start the server 啟動服務 try { RemoteJMeterEngineImpl.startServer(RmiUtils.getRmiRegistryPort()); // $NON-NLS-1$ startOptionalServers(); } catch (Exception ex) { System.err.println("Server failed to start: "+ex);//NOSONAR log.error("Giving up, as server failed with:", ex); throw ex; } } else { String testFile=null; CLOption testFileOpt = parser.getArgumentById(TESTFILE_OPT); if (testFileOpt != null){ testFile = testFileOpt.getArgument(); if (USE_LAST_JMX.equals(testFile)) { testFile = LoadRecentProject.getRecentFile(0);// most recent } } CLOption testReportOpt = parser.getArgumentById(REPORT_GENERATING_OPT); if (testReportOpt != null) { // generate report from existing file 從現有文件生成報告 String reportFile = testReportOpt.getArgument(); extractAndSetReportOutputFolder(parser, false); ReportGenerator generator = new ReportGenerator(reportFile, null); generator.generate(); } else if (parser.getArgumentById(NONGUI_OPT) == null) { // not non-GUI => GUI // 在GUI模式執行 startGui(testFile); startOptionalServers(); } else { // NON-GUI must be true 必須為無GUI模式 extractAndSetReportOutputFolder(parser, deleteResultFile); CLOption rem = parser.getArgumentById(REMOTE_OPT_PARAM); if (rem == null) { rem = parser.getArgumentById(REMOTE_OPT); } CLOption jtl = parser.getArgumentById(LOGFILE_OPT); String jtlFile = null; if (jtl != null) { jtlFile = processLAST(jtl.getArgument(), ".jtl"); // $NON-NLS-1$ } CLOption reportAtEndOpt = parser.getArgumentById(REPORT_AT_END_OPT); if(reportAtEndOpt != null && jtlFile == null) { throw new IllegalUserActionException( "Option -"+ ((char)REPORT_AT_END_OPT)+" requires -"+((char)LOGFILE_OPT )+ " option"); } // 非GUI模式執行 startNonGui(testFile, jtlFile, rem, reportAtEndOpt != null); startOptionalServers(); } } } catch (IllegalUserActionException e) {// NOSONAR System.out.println("Incorrect Usage:"+e.getMessage());//NOSONAR System.out.println(CLUtil.describeOptions(options).toString());//NOSONAR } catch (Throwable e) { // NOSONAR log.error("An error occurred: ", e); System.out.println("An error occurred: " + e.getMessage());//NOSONAR // FIXME Should we exit here ? If we are called by Maven or Jenkins System.exit(1); } }
JMeter類的 start() 方法根據不同的命令來執行啟動方法,分別為startGui() 和 startNonGui(),我們來看一下非 GUI 啟動方法:

private void startNonGui(String testFile, String logFile, CLOption remoteStart, boolean generateReportDashboard) throws IllegalUserActionException, ConfigurationException { // add a system property so samplers can check to see if JMeter // is running in NonGui mode System.setProperty(JMETER_NON_GUI, "true");// $NON-NLS-1$ JMeter driver = new JMeter();// TODO - why does it create a new instance? driver.remoteProps = this.remoteProps; driver.remoteStop = this.remoteStop; driver.deleteResultFile = this.deleteResultFile; PluginManager.install(this, false); String remoteHostsString = null; if (remoteStart != null) { remoteHostsString = remoteStart.getArgument(); if (remoteHostsString == null) { remoteHostsString = JMeterUtils.getPropDefault( "remote_hosts", //$NON-NLS-1$ "127.0.0.1");//NOSONAR $NON-NLS-1$ } } if (testFile == null) { throw new IllegalUserActionException("Non-GUI runs require a test plan"); } // 運行場景 driver.runNonGui(testFile, logFile, remoteStart != null, remoteHostsString, generateReportDashboard); }
startNonGui() 方法種通過調用 runNonGui() 方法來執行腳本,我們來看一下 runNonGui() 方法:

// run test in batch mode 批處理運行測試 private void runNonGui(String testFile, String logFile, boolean remoteStart, String remoteHostsString, boolean generateReportDashboard) { try { // 獲取腳本文件 File f = new File(testFile); if (!f.exists() || !f.isFile()) { println("Could not open " + testFile); return; } // 設置腳本文件 FileServer.getFileServer().setBaseForScript(f); HashTree tree = SaveService.loadTree(f); @SuppressWarnings("deprecation") // Deliberate use of deprecated ctor JMeterTreeModel treeModel = new JMeterTreeModel(new Object());// NOSONAR Create non-GUI version to avoid headless problems JMeterTreeNode root = (JMeterTreeNode) treeModel.getRoot(); treeModel.addSubTree(tree, root); // Hack to resolve ModuleControllers in non GUI mode SearchByClass<ReplaceableController> replaceableControllers = new SearchByClass<>(ReplaceableController.class); tree.traverse(replaceableControllers); Collection<ReplaceableController> replaceableControllersRes = replaceableControllers.getSearchResults(); for (ReplaceableController replaceableController : replaceableControllersRes) { replaceableController.resolveReplacementSubTree(root); } // Ensure tree is interpreted (ReplaceableControllers are replaced) // For GUI runs this is done in Start.java // 將測試文件(.jmx文件)解析成HashTree HashTree clonedTree = convertSubTree(tree, true); Summariser summariser = null; String summariserName = JMeterUtils.getPropDefault("summariser.name", "");//$NON-NLS-1$ if (summariserName.length() > 0) { log.info("Creating summariser <{}>", summariserName); println("Creating summariser <" + summariserName + ">"); summariser = new Summariser(summariserName); } ResultCollector resultCollector = null; if (logFile != null) { resultCollector = new ResultCollector(summariser); resultCollector.setFilename(logFile); clonedTree.add(clonedTree.getArray()[0], resultCollector); } else { // only add Summariser if it can not be shared with the ResultCollector if (summariser != null) { clonedTree.add(clonedTree.getArray()[0], summariser); } } if (deleteResultFile) { SearchByClass<ResultCollector> resultListeners = new SearchByClass<>(ResultCollector.class); clonedTree.traverse(resultListeners); Iterator<ResultCollector> irc = resultListeners.getSearchResults().iterator(); while (irc.hasNext()) { ResultCollector rc = irc.next(); File resultFile = new File(rc.getFilename()); if (resultFile.exists() && !resultFile.delete()) { throw new IllegalStateException("Could not delete results file " + resultFile.getAbsolutePath() + "(canRead:"+resultFile.canRead()+", canWrite:"+resultFile.canWrite()+")"); } } } ReportGenerator reportGenerator = null; if (logFile != null && generateReportDashboard) { reportGenerator = new ReportGenerator(logFile, resultCollector); } // Used for remote notification of threads start/stop,see BUG 54152 // Summariser uses this feature to compute correctly number of threads // when NON GUI mode is used clonedTree.add(clonedTree.getArray()[0], new RemoteThreadsListenerTestElement()); List<JMeterEngine> engines = new LinkedList<>(); clonedTree.add(clonedTree.getArray()[0], new ListenToTest(remoteStart && remoteStop ? engines : null, reportGenerator)); println("Created the tree successfully using "+testFile); if (!remoteStart) { // 實例化一個JMeterEngine來對付腳本,JMeterEngine本質就是一個線程 JMeterEngine engine = new StandardJMeterEngine(); engine.configure(clonedTree); long now=System.currentTimeMillis(); println("Starting the test @ "+new Date(now)+" ("+now+")"); // 調用runTest方法 engine.runTest(); engines.add(engine); } else { java.util.StringTokenizer st = new java.util.StringTokenizer(remoteHostsString, ",");//$NON-NLS-1$ List<String> hosts = new LinkedList<>(); while (st.hasMoreElements()) { hosts.add((String) st.nextElement()); } DistributedRunner distributedRunner=new DistributedRunner(this.remoteProps); distributedRunner.setStdout(System.out); // NOSONAR distributedRunner.setStdErr(System.err); // NOSONAR distributedRunner.init(hosts, clonedTree); engines.addAll(distributedRunner.getEngines()); distributedRunner.start(); } startUdpDdaemon(engines); } catch (Exception e) { System.out.println("Error in NonGUIDriver " + e.toString());//NOSONAR log.error("Error in NonGUIDriver", e); } }
runNonGui() 方法是用來執行腳本,它的主要邏輯包括:
- 獲取腳本文件;
- 配置腳本文件;
- 將腳本文件(jmx文件)解析成 HashTree;
- 實例化一個JMeterEngine來配置轉化后的 HashTree;
- 調用 runTest() 方法最終運行測試;
簡單來說,Jmeter的執行步驟包括以下幾步:
- 解析命令行參數,加載配置文件;
- 將 jmx 文件解析為 HashTree;
- 實例化一個 StandardJMeterEngine對象 JMeterEngine,並把測試的工作交給 JMeterEngine;
- JMeterEngine 調用 runTest() 方法來執行測試;
當然,JMeter類還有其他重要的職責,比如監聽所有的 JMeterEngine ,當接收到 GUI 的 StopTestNow / Shutdown 等命令時候來調用JMeterEngine接口相應的方法。接下來看下這個接口提供了哪些方法:

/** * This interface is implemented by classes that can run JMeter tests. */ public interface JMeterEngine { /** * Configure engine * 配置引擎 * @param testPlan the test plan */ void configure(HashTree testPlan); /** * Runs the test * 執行測試 * @throws JMeterEngineException if an error occurs */ void runTest() throws JMeterEngineException; /** * Stop test immediately interrupting current samplers * 停止測試,立即打斷當前samplers */ default void stopTest() { stopTest(true); } /** * 停止測試,根據參數是否立即打斷當前samplers * @param now boolean that tell wether stop is immediate (interrupt) or not (wait for current sample end) */ void stopTest(boolean now); /** * Stop test if running * 停止測試運行 */ void reset(); /** * set Properties on engine * 設置引擎屬性 * @param p the properties to set */ void setProperties(Properties p); /** * Exit engine * 退出引擎 */ void exit(); /** * 引擎是否活躍 * @return boolean Flag to show whether engine is active (true when test is running). Set to false at end of test */ boolean isActive(); }
三、Jmeter擴展實現
從上面的分析我們發現:JMeterEngine 依賴於 HashTree,而 HashTree 是由 jmx 文件解析而來,每一個 JMeter 測試計划都會對應一個 jmx 文件。所以我們只要生成合理的 jmx 文件,就可以通過 JMeterEngine 壓測引擎去執行測試任務。但是仔細一想發現,我們通過 Jmeter GUI生成一個 jmx 文件,然后 Jmeter 內部又把 jmx 文件解析成了一個 HashTree,再通過 JMeterEngine 來執行,那我們應該能夠直接來創建這個 HashTree,並把這個 HashTree需要的內容添加進去,最后就能通過 JMeterEngine 來執行,這樣我們就不必去生成 jmx 文件了,可以全部用代碼來實現了,如下:
public class JmxCodeDemo { /** * 使用GUI的方式,做完各種配置之后, 會生成一個jmx文件 * * 實際上這個Jmx文件在給到jmeter進程去執行時,JMeter類去負責將jmx文件解析成hashTree * * 這個hashTree的內容就跟我們在GUI中配置的關系是一致的, 各種層次的包裹關系 * * 最終去調StandardJMeterEngine,將配置關系設置進去(jmeterEngine.configure(tree)) * * 最后啟動(執行) */ public static void main(String[] args) { // 1、啟動初始化配置 URL resourceUrl = getResource("jmeter.properties"); JMeterUtils.loadJMeterProperties(resourceUrl.getPath()); JMeterUtils.setLocale(Locale.ENGLISH); // 2、創建一個測試計划 TestPlan testPlan = new TestPlan(); testPlan.setName("local perf test plan"); // 3、創建一個javaSampler JavaSampler javaSampler = new JavaSampler(); javaSampler.setName("local perf java sampler"); javaSampler.setClassname(CaseDemo.class.getName()); // 4、創建一個循環控制器 LoopController controller = new LoopController(); controller.setName("local perf loop controller"); controller.setLoops(5); controller.addTestElement(javaSampler); controller.initialize(); // 5、創建threadGroup ThreadGroup threadGroup = new ThreadGroup(); threadGroup.setName("local perf thread group"); threadGroup.setNumThreads(3); threadGroup.setDelay(100); threadGroup.setSamplerController(controller); threadGroup.initialize(); // 6、創建結果收集器 ResultCollector resultCollector = new ResultCollector(); resultCollector.setName("local perf result collector"); // 7、構建同級tree HashTree subTree = new HashTree(); subTree.add(javaSampler); subTree.add(controller); subTree.add(threadGroup); subTree.add(resultCollector); // 8、構建層次tree HashTree tree = new HashTree(); tree.add(testPlan,subTree); // 9、創建Jmeter引擎並將配置的tree賦值進去 StandardJMeterEngine jmeterEngine = new StandardJMeterEngine(); jmeterEngine.configure(tree); // 10、執行引擎 try { jmeterEngine.runTest(); } catch (JMeterEngineException e) { e.printStackTrace(); } } }
值得注意的是 javaSample 中需要傳入我們編寫的case類,一般我們編寫的case中應該包括:構造數據,發送請求,判斷返回結果等操作,例如:

public class CaseDemo extends AbstractJavaSamplerClient { /** * order_id, * * insert order_id=123 5*50*600 = x, timestamp - 10000 0000001 - 10000 100000 * * select order_id=123 */ @Override public SampleResult runTest(JavaSamplerContext javaSamplerContext) { SampleResult sampleResult = new SampleResult(); sampleResult.setContentType("hahaha"); boolean resultFlag = false; try { sampleResult.sampleStart(); // send [http|dubbo|thrift] response, Http code 200, errorCode=1000,errorMsg=success // response = sendHttpRequest // parse response, 拿到errorCode resultFlag = true; System.out.println("CaseDemo.runTest=" + Thread.currentThread().getId()); } catch (Exception e) { // ignore resultFlag = false; } finally { sampleResult.setSuccessful(resultFlag); sampleResult.sampleEnd(); } return sampleResult; } }
它需要繼承 AbstractJavaSamplerClient 類,重寫 runTest() 方法,然后我們需要邏輯都寫在這個 runTest() 方法中。
后面我們介紹性能測試平台時將會使用這種方式來實現。