jmeter源码学习一

概览

最近在搞jmeter版本升级的事情,此前仅仅对jmeter的使用有大致的了解,并没有完全熟悉jmeter的源码。趁着这次升级的计划,打算细致的看下jmeter源码。

项目目录

使用的是jmeter5.0版本的代码,项目目录如下
avatar
源码目录如下
avatar
运行机制如下
avatar
HashTree 是 JMeter 执行测试依赖的数据结构,在执行测试之前进行配置测试数据,HashTree将数据组织到一个递归树结构中,并提供了操作该结构的方法
StandardJMeterEngine 执行JMeter 测试 ,直接用于本地 GUI 和非 GUI 调用,或者在服务器模式下运行时由 RemoteJMeterEngineImpl 启动
JMeterEngine 接口被运行 JMeter的测试类实现,此接口共8个方法,JMeterEngine本质就是一个线程。

代码分析

  1. NewDriver类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    main方法
    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);
    }
    }
    }
  2. Jmeter类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    1. start方法
    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());//NOSONAR
    // repeat the error so no need to scroll back past the usage to see it
    System.out.println("Error: " + error);//NOSONAR
    return;
    }
    try {
    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.");//NOSONAR
    }
    });

    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
    startGui(testFile);
    startOptionalServers();
    } else { // NON-GUI must be true
    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");
    }
    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);
    }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
2. startGui方法
private void startGui(String testFile) {
System.out.println("================================================================================");//NOSONAR
System.out.println("Don't use GUI mode for load testing !, only for Test creation and Test debugging.");//NOSONAR
System.out.println("For load testing, use NON GUI Mode:");//NOSONAR
System.out.println(" jmeter -n -t [jmx file] -l [results file] -e -o [Path to web report folder]");//NOSONAR
System.out.println("& increase Java Heap to meet your test requirements:");//NOSONAR
System.out.println(" Modify current env variable HEAP=\"-Xms1g -Xmx1g -XX:MaxMetaspaceSize=256m\" in the jmeter batch file");//NOSONAR
System.out.println("Check : https://jmeter.apache.org/usermanual/best-practices.html");//NOSONAR
System.out.println("================================================================================");//NOSONAR

SplashScreen splash = new SplashScreen();
splash.showScreen();
String jMeterLaf = LookAndFeelCommand.getJMeterLaf();
try {
log.info("Setting LAF to: {}", jMeterLaf);
UIManager.setLookAndFeel(jMeterLaf);
} catch (Exception ex) {
log.warn("Could not set LAF to: {}", jMeterLaf, ex);
}
splash.setProgress(10);
JMeterUtils.applyHiDPIOnFonts();
PluginManager.install(this, true);

JMeterTreeModel treeModel = new JMeterTreeModel();
splash.setProgress(30);
JMeterTreeListener treeLis = new JMeterTreeListener(treeModel);
final ActionRouter instance = ActionRouter.getInstance();
instance.populateCommandMap();
splash.setProgress(60);
treeLis.setActionHandler(instance);
GuiPackage.initInstance(treeLis, treeModel);
splash.setProgress(80);
MainFrame main = new MainFrame(treeModel, treeLis);
splash.setProgress(100);
ComponentUtil.centerComponentInWindow(main, 80);
main.setLocationRelativeTo(splash);
main.setVisible(true);
main.toFront();
instance.actionPerformed(new ActionEvent(main, 1, ActionNames.ADD_ALL));
if (testFile != null) {
try {
File f = new File(testFile);
log.info("Loading file: {}", f);
FileServer.getFileServer().setBaseForScript(f);

HashTree tree = SaveService.loadTree(f);

GuiPackage.getInstance().setTestPlanFile(f.getAbsolutePath());

Load.insertLoadedTree(1, tree);
} catch (ConversionException e) {
log.error("Failure loading test file", e);
JMeterUtils.reportErrorToUser(SaveService.CEtoString(e));
} catch (Exception e) {
log.error("Failure loading test file", e);
JMeterUtils.reportErrorToUser(e.toString());
}
} else {
JTree jTree = GuiPackage.getInstance().getMainFrame().getTree();
TreePath path = jTree.getPathForRow(0);
jTree.setSelectionPath(path);
FocusRequester.requestFocus(jTree);
}
splash.setProgress(100);
splash.close();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
3. startNoGui方法
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);
}
// 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());// 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);
}

// Remove the disabled items
// For GUI runs this is done in Start.java
convertSubTree(tree);

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);
tree.add(tree.getArray()[0], resultCollector);
}
else {
// only add Summariser if it can not be shared with the ResultCollector
if (summariser != null) {
tree.add(tree.getArray()[0], summariser);
}
}

if (deleteResultFile) {
SearchByClass<ResultCollector> resultListeners = new SearchByClass<>(ResultCollector.class);
tree.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
tree.add(tree.getArray()[0], new RemoteThreadsListenerTestElement());

List<JMeterEngine> engines = new LinkedList<>();
tree.add(tree.getArray()[0], new ListenToTest(remoteStart && remoteStop ? engines : null, reportGenerator));
println("Created the tree successfully using "+testFile);
if (!remoteStart) {
JMeterEngine engine = new StandardJMeterEngine();
engine.configure(tree);
long now=System.currentTimeMillis();
println("Starting the test @ "+new Date(now)+" ("+now+")");
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, tree);
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);
}
}

1. 解析命令行参数,加载配置文件;
2. 将 .Jmx 文件解析成 HashTree;
3. 实例化一个StandardJMeterEngine,并把测试的工作交给JMeterEngine;
4. 监听所有的 JMeterEngine ,当接收到 GUI 的 StopTestNow / Shutdown 等命令时候来调用JMeterEngine接口相应的方法。
  1. JMeterEngine接口
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    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
    */
    default void stopTest() {
    stopTest(true);
    }
    /**
    *
    * @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();
    }
    JMeterEngine 依赖于 HashTree,而 HashTree 是由 jmx 文件解析而来,每一个 JMeter 测试计划都会对应一个 jmx 文件。所以我们只要生成合理的 jmx 文件,就可以通过 JMeterEngine 压测引擎去执行测试任务。

    具体 jmx 文件的生成方式,我们可以借鉴JMeter GUI模式下 jmx 文件生成方式。在这里我们的演示的处理方式是,先定义每个组件的生成方式,然后再按一定结构组装各个组件,示意代码如下。