Jmeter源码系列(1)-NewDriver类详解-Jmeter 的启动器

写在前面的话

Jmeter 全称(Apache JMeter)是一个开源的、功能强大的性能测试工具,用于对各种应用程序和协议进行功能、负载、压力和性能测试。它被广泛应用于软件开发和计划阶段,以确保应用程序在各种负载情况下的稳定性和可靠性。
本系列将从 Jmeter 代码层面陆续剖析其实现原理,包括但不限于 Jmeter 设计思路,Jmeter 核心对象/接口/方法。如有错误,敬请指正!

NewDriver

NewDriver 是 org.apache.jmeter 包下的一个类,如下是 NewDriver 源码中的类说明

1
2
3
/**
* Main class for JMeter - sets up initial classpath and the loader.
*/

从这个说明中,我们可以知道,这个类提供了 2 个主要功能:

  • 初始化 classpath
  • 初始化一个 loader, 这个 loader 其实就是一个动态类加载器

以下内容摘抄自 NewDriver 源码,在源码中会使用注释来说明关键代码的作用,最后也会做总结,让我们开始吧

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
public final class NewDriver {

/**
* 定义一堆常量,会在 static 代码块中使用
*/
private static final String CLASSPATH_SEPARATOR = File.pathSeparator;
private static final String OS_NAME = System.getProperty("os.name");
private static final String OS_NAME_LC = OS_NAME.toLowerCase(java.util.Locale.ENGLISH);
private static final String JAVA_CLASS_PATH = "java.class.path";
private static final String JMETER_LOGFILE_SYSTEM_PROPERTY = "jmeter.logfile";
private static final String HEADLESS_MODE_PROPERTY = "java.awt.headless";
/**
* 动态类加载器,继承自 URLClassLoader,提供了一个静态方法 updateLoader(URL [] urls) 实现了动态加载 jar
* 的功能。
*/
private static final DynamicClassLoader loader;
private static final String JMETER_INSTALLATION_DIRECTORY;
private static final List<Exception> EXCEPTIONS_IN_INIT = new ArrayList<>();

static {
final List<URL> jars = new ArrayList<>();
/**
* 启动时从 jvm 获取 classpath
*/
final String initiaClasspath = System.getProperty(JAVA_CLASS_PATH);
String tmpDir;
/**
* 按照指定标记符来分割给定的字符串,但是 StringTokenizer 是一个遗留类,出于兼容性原因而保留,建议使用 String 的拆分方法或 java.util.regex 包。
* 顺便说一下,Jmeter 源码中会包含非常多的过时的方法或者写法,有些是因为 Jmeter 本身开发较早,当时的 jdk 版本没有我们常用的新方法,
* 有些则是因为当时 jdk 早期版本存在 bug,jmeter 会使用另一种写法来规避这些 bug,当然,现在这些 bug 可能已经修复了,不过 jmeter 的源码中
* 任然会保留这部分注释
*/
StringTokenizer tok = new StringTokenizer(initiaClasspath, File.pathSeparator);
/**
* 对 mac 系统做了单独的判断,我也没有深究为啥要单独处理,不晓得现在还需不需要这么写
*/
if (tok.countTokens() == 1|| (tok.countTokens() == 2 && OS_NAME_LC.startsWith("mac os x"))) {
File jar = new File(tok.nextToken());
try {
tmpDir = jar.getCanonicalFile().getParentFile().getParent();
} catch (IOException e) {
tmpDir = null;
}
} else {
/**
* 从 jvm 获取 jmeter.home 属性,没有的话就默认从环境变量 JMETER_HOME 取值,当然这个值也不一定有,因为不是所有人都会配置 JMETER_HOME 这个环境变量
* 其实从这边开始,大家就会发现,Jmeter 会经常使用 System.getProperty 来获取一些属性,在后面的代码中我们也会经常见到这样的代码
*/
tmpDir = System.getProperty("jmeter.home", System.getenv("JMETER_HOME"));
if (tmpDir == null || tmpDir.length() == 0) {
File userDir = new File(System.getProperty("user.dir"));
tmpDir = userDir.getAbsoluteFile().getParent();
}
}
if (tmpDir == null) {
tmpDir = System.getenv("JMETER_HOME");
}
JMETER_INSTALLATION_DIRECTORY = tmpDir;
boolean usesUNC = OS_NAME_LC.startsWith("windows");
StringBuilder classpath = new StringBuilder();
/**
* 下面的几个目录大家就很眼熟了,就是 Jmeter 解压后,主目录下的文件夹,里面都是 Jmeter 可能用到的一些 jar 包
*/
File[] libDirs = new File[] { new File(JMETER_INSTALLATION_DIRECTORY + File.separator + "lib"),
new File(JMETER_INSTALLATION_DIRECTORY + File.separator + "lib" + File.separator + "ext"),
new File(JMETER_INSTALLATION_DIRECTORY + File.separator + "lib" + File.separator + "junit")};
for (File libDir : libDirs) {
File[] libJars = libDir.listFiles((dir, name) -> name.endsWith(".jar"));
if (libJars == null) {
new Throwable("Could not access " + libDir).printStackTrace();
continue;
}
/**
* 不晓得为啥要排个序
*/
Arrays.sort(libJars);
for (File libJar : libJars) {
try {
String s = libJar.getPath();
if (usesUNC) {
if (s.startsWith("\\\\") && !s.startsWith("\\\\\\")) {
s = "\\\\" + s;
} else if (s.startsWith("//") && !s.startsWith("///")) {
s = "//" + s;
}
}
jars.add(new File(s).toURI().toURL());
classpath.append(CLASSPATH_SEPARATOR);
classpath.append(s);
} catch (MalformedURLException e) {
EXCEPTIONS_IN_INIT.add(new Exception("Error adding jar:"+libJar.getAbsolutePath(), e));
}
}
}
System.setProperty(JAVA_CLASS_PATH, initiaClasspath + classpath.toString());
/**
* 类加载器会加载扫描到的这些 jar 包,为 Jmeter 真正启动做好准备
*/
loader = AccessController.doPrivileged(
(PrivilegedAction<DynamicClassLoader>) () ->
new DynamicClassLoader(jars.toArray(new URL[jars.size()]))
);
}
}

从上面的代码中,我们可以看到,NewDriver 在实例化时,会执行一个静态代码块,主要作用就是加载 Jmeter 安装目录下的 jar 包。

Main方法介绍

下面介绍 NewDriver 的 main 方法,这个方法就是整个 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
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 {
/**
* 设置当前线程的类加载器,也就是 Jmeter 自己写的那个动态类加载器
*/
Thread.currentThread().setContextClassLoader(loader);
/**
* 配置一些日志属性,不重要
*/
setLoggingProperties(args);
try {
/**
* 判断要不要用 GUI 模式启动,默认 true,也可以通过 Jmeter 命令行参数 -n 来指定使用非 GUI 模式启动
*/
if(System.getProperty(HEADLESS_MODE_PROPERTY) == null && shouldBeHeadless(args)) {
System.setProperty(HEADLESS_MODE_PROPERTY, "true");
}
/**
* 获取 Jmeter 类,作用类似于 Class.forName(String clazzName)
*/
Class<?> initialClass = loader.loadClass("org.apache.jmeter.JMeter");
/**
* 获取 Jmeter 实例
*/
Object instance = initialClass.getDeclaredConstructor().newInstance();
/**
* 获取 Jmeter.start方法,并调用
*/
Method startup = initialClass.getMethod("start", new Class[] { new String[0].getClass() });
startup.invoke(instance, new Object[] { args });
} catch(Throwable e){
e.printStackTrace();
System.err.println("JMeter home directory was detected as: "+JMETER_INSTALLATION_DIRECTORY);
}
}
}

main 方法其实很简单直接,就是看下是不是要启动 GUI,然后就是通过反射调用 Jmeter 的 start 方法,来开始测试。
综上,NewDriver 其实就是一个启动器,正如其所在源码模块 launcher 一样,他的作用就是为 Jmeter 真正启动做好准备。
好了,NewDriver 就介绍完了,下一章将介绍 Jmeter 这个核心类,以及调用其 start(String[] args) 之后会发生什么…