Jmeter的插件开发
一、Jmeter的启动流程
在说启动流程之前我们先来看看Jmeter源码的各个重要的包:
- components—包含与协议无关的组件,如可视化、断言等等。
- core —JMeter的核心代码,包括所有的核心接口和抽象类。
- examples —演示采样器如何使用新bean框架的例子(开发插件前可以好好看看该包下的样例代码)。
- functions —所使用的组件的标准功能。
- jorphan—提供常见实用功能的实用工具类
- protocol—包含了JMeter支持的不同协议(ftp http、tcp—socket协议,没有webservice)
我们先看jmeter.bat这个脚本文件
%JM_START% "%JM_LAUNCH%" %ARGS% %JVM_ARGS% -jar "%JMETER_BIN%ApacheJMeter.jar" %JMETER_CMD_LINE_ARGS%
反正就是定义了一些JVM参数和java路径,重点看加载的jar包ApacheJMeter.jar
org.apache.jmeter.NewDriver.class里面有一个main方法,我们可以看到它使用自定义的ClassLoader加载JMeter.class,并且调用start方法
public static void main(String[] args) {
if(!EXCEPTIONS_IN_INIT.isEmpty()) {
System.err.println("Configuration error during init, see exceptions:"+exceptionsToString(EXCEPTIONS_IN_INIT)); // NOSONAR Intentional System.err use
} else {
Thread.currentThread().setContextClassLoader(loader);
setLoggingProperties(args);
try {
// Only set property if it has not been set explicitely
if(System.getProperty(HEADLESS_MODE_PROPERTY) == null && shouldBeHeadless(args)) {
System.setProperty(HEADLESS_MODE_PROPERTY, "true");
}
Class<?> initialClass = loader.loadClass("org.apache.jmeter.JMeter");// $NON-NLS-1$
Object instance = initialClass.getDeclaredConstructor().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); // NOSONAR Intentional System.err use
}
}
}
从JMeter.start方法我们可以得知JMeter支持两种启动方式——startGui(图形化界面启动)和startNonGui(使用命令行启动),以下便是start方法的主要流程:
https://www.processon.com/view/link/6005018af346fb566eb6f835
插件的加载
插件加载的基本原理:通过各种类别的工厂类加载约定路径/lib/ext下的jar包,这里以SamplerCreatorFactory为例
SamplerCreatorFactory#init()
private void init() { // WARNING: called from ctor so must not be overridden (i.e. must be private or final)
try {
List<String> listClasses = ClassFinder.findClassesThatExtend(
JMeterUtils.getSearchPaths(),
new Class[] {SamplerCreator.class });
//以下代码略.....
与此类似的工厂类还有MenuFactory#initializeMenus()
private static void initializeMenus(
Map<String, List<MenuInfo>> menus, Set<String> elementsToSkip) {
try {
List<String> guiClasses = ClassFinder
.findClassesThatExtend(
JMeterUtils.getSearchPaths(),
new Class[] {JMeterGUIComponent.class, TestBean.class})
.stream()
// JMeterTreeNode and TestBeanGUI are special GUI classes,
// and aren't intended to be added to menus
.filter(name -> !name.endsWith("JMeterTreeNode"))
.filter(name -> !name.endsWith("TestBeanGUI"))
.filter(name -> !name.equals("org.apache.jmeter.gui.menu.StaticJMeterGUIComponent"))
.filter(name -> !elementsToSkip.contains(name))
.distinct()
.map(String::trim)
.collect(Collectors.toList());
//以下代码略.....
那还有一个问题,我们看到的GUI界面里面,插件跟图标是怎么对应上的呢?这里我们可以看到JMeter.getIconMappings()方法
@Override
@SuppressWarnings("JdkObsolete")
public String[][] getIconMappings() {
//图标与类映射关系的加载路径
final String defaultIconProp = "org/apache/jmeter/images/icon.properties";
final String iconSize = JMeterUtils.getPropDefault(TREE_ICON_SIZE, DEFAULT_TREE_ICON_SIZE);
String iconProp = JMeterUtils.getPropDefault("jmeter.icons", defaultIconProp);//$NON-NLS-1$
//图片文件的加载方法
Properties p = JMeterUtils.loadProperties(iconProp);
if (p == null && !iconProp.equals(defaultIconProp)) {
log.info("{} not found - using {}", iconProp, defaultIconProp);
iconProp = defaultIconProp;
p = JMeterUtils.loadProperties(iconProp);
}
if (p == null) {
log.info("{} not found - using inbuilt icon set", iconProp);
return DEFAULT_ICONS;
}
log.info("Loaded icon properties from {}", iconProp);
String[][] iconlist = new String[p.size()][3];
Enumeration<?> pe = p.keys();
int i = 0;
while (pe.hasMoreElements()) {
String key = (String) pe.nextElement();
String[] icons = JOrphanUtils.split(p.getProperty(key), " ");//$NON-NLS-1$
iconlist[i][0] = key;
iconlist[i][1] = icons[0].replace(KEY_SIZE, iconSize);
if (icons.length > 1) {
iconlist[i][2] = icons[1].replace(KEY_SIZE, iconSize);
}
i++;
}
return iconlist;
}
我们先来看看“org/apache/jmeter/images/icon.properties”这个文件到底是什么
与此同时JMeter定义了一个二维数组作为默认值。
我们可以看到只要是插件类的GUI部分继承了以上数组中的GUI类,JMeter框架便会自动将其映射为所对应的组件类型和图标。这些组件分别是:TestPlanGui、AbstractTimerGui、ThreadGroupGui、AbstractListenerGui、AbstractConfigGui、AbstractPreProcessorGui、AbstractPostProcessorGui、AbstractControllerGui、WorkBenchGui、AbstractSamplerGui、AbstractAssertionGui。
二、Jmeter各组件介绍
组件的类型
对于JMeter的基本组件,我们可以将其简单的划分为GUI和非GUI两大类,分别对应JMeter的GUI启动和命令行启动,也就是说如果我们要开发一款插件同时支持这两种启动的话,需要实现其对应的GUI和非GUI接口,而TestElement是所有组件的最基本单元,组件类都是TestElement类的子类。
GUI组件
即可以通过JMeter图形管理控制器在测试计划Tree中进行添加的组件,主要包括ThreadGroup(线程组)、Config(配置元件)、Timer(定时器)、Modifier(前置处理器)、Extractor(后置处理器)、Controller(逻辑控制器)、Sampler(测试抽样器)、Assertion(断言)和Listener(监听器)
非GUI组件
典型的代表是Function(函数)和某些子测试抽样器,如JavaSamplerClient。
对于组件一般有两种实现方法:
- GUI与逻辑控制分离:GUI部分通过继承各种组件GUI抽象类,逻辑控制部分通过继承组件逻辑抽象类和实现各种接口方式从而实现不同组件的内部逻辑控制;
- GUI与逻辑控制不分离:与分离方法的区别在于不单独实现GUI部分,在逻辑控制部分通过实现TestBean接口方法从而实现对GUI界面的配置。
各个类别介绍
ThreadGroup(线程组)组件
ThreadGroup(线程组)组件继承AbstractThreadGroup抽象类,通过重写各类控制方法,如void scheduleThread(JMeterThread thread) 、stopThread(String threadName, boolean now) 、threadFinished(JMeterThread thread)等,来达到控制和协调各线程(虚拟用户)的行为,线程组是构建一个性能测试模型的最基本组件。
Config(配置元件)组件
Config(配置元件)组件相对其他组件比较特殊,通过继承ConfigTestElement类或只需要GUI部分的实现即可完成本体任务,而对于一个需要配置的组件类则需要实现ConfigMergabilityIndicator接口的public boolean applies(ConfigTestElement configElement)方法,用来指明哪些Config组件可以用来对其进行配置,这里参考TCPSampler的源代码如下:
private static final Set<String> APPLIABLE_CONFIG_CLASSES = new HashSet<String>(
Arrays.asList(new String[]{
"org.apache.jmeter.config.gui.LoginConfigGui",
"org.apache.jmeter.protocol.tcp.config.gui.TCPConfigGui",
"org.apache.jmeter.config.gui.SimpleConfigGui"}));
@Override
public boolean applies(ConfigTestElement configElement) {
String guiClass = configElement.getProperty(TestElement.GUI_CLASS).getStringValue();
return APPLIABLE_CONFIG_CLASSES.contains(guiClass);
}
以上代码指明LoginConfigGui、SimpleConfigGui和TCPConfigGui这三个配置元件可以对TCPSampler组件进行配置。
Timer(定时器)组件
Timer(定时器)组件通过继承AbstractTestElement抽象类,实现Timer接口的delay()方法来实现对时间的控制,主要的控制内容如下:
控制线程延时,即用来模仿思考时间(ThinkTime)或键盘时间(KeyTime);
控制线程行为,如SyncTimer(同步计时器),就是内部利用CyclicBarrier来控制阻塞和释放全部运行线程的逻辑行为,从而达到“集合点”的目的。
Modifier(前置处理器)组件
Modifier(前置处理器)组件通过继承AbstractTestElement抽象类,实现PreProcessor接口的process ()方法控制逻辑,常常需要对线程上下文中的当前Sampler和前一个SampleResult进行识别和判断,以做出正确的处理,一般的行为是通过取出SampleResult的某些值或直接在当前Sampler启动sample方法之前对其某些属性进行修饰。
Extractor(后置处理器)组件
Extractor(后置处理器)组件通过继承AbstractTestElement抽象类,实现PostProcessor接口的process ()方法控制逻辑,常常需要对线程上下文中的前一个SampleResult进行识别和判断,以做出正确的处理。
Controller(控制器)组件
Controller(控制器)组件通过继承GenericController类,通过重写Sampler next()、void setDone(boolean done)、int getIterCount()、void reInitialize()等方法来控制Sampler的测试行为。
Sampler(测试抽样器)组件
Sampler(测试抽样器)组件继承AbstractSampler抽象类,通过重写SampleResult sample(Entry e)方法,实现测试过程以及测试结果的采集功能。
Assertion(断言)组件
Assertion(断言)组件通过继承AbstractTestElement抽象类,实现Assertion接口的getResult(SampleResult result)方法对结果内容进行判断,从而实现断言方法,用于对Sampler组件所产生的抽样采集结果内容进行断言。
Listener(监听器)主要有两种方案:
- 直接继承AbstractTestElement,实现sampleListener或Visualizer等接口方法
- 实现ResultCollector和Runnable等接口方法
三、如何开发一个插件(以Sampler组件为例)
1.创建一个项目,并添加JMeter的核心包
2.实现AbstractSampler抽象类
public class DubboSampler extends AbstractSampler {
public final static String FUNCTION = "function";
@Override
public SampleResult sample(Entry entry) {
SampleResult res = new SampleResult();
res.sampleStart();
//输出GUI界面所输入的函数方法返回结果
System.out.println(this.getProperty(FUNCTION));
res.sampleEnd();
res.setSuccessful(true);
return res;
}
}
3.实现AbstractSamplerGui抽象类
public class DubboSamplerGUI extends AbstractSamplerGui {
private JTextField functionTextField = null;
public DubboSamplerGUI() { init(); }
@Override
public void configure(TestElement element) {
super.configure(element);
functionTextField.setText(element.getPropertyAsString(DubboSampler.FUNCTION));
}
private void init() {
JPanel mainPanel = new JPanel(new GridBagLayout());
functionTextField = new JTextField(20);
mainPanel.add(functionTextField);
add(mainPanel);
}
@Override
public TestElement createTestElement() {
//创建所对应的Sampler
TestElement sampler = new DubboSampler();
modifyTestElement(sampler);
return sampler;
}
@Override
public String getLabelResource() {
return this.getClass().getSimpleName();
}
@Override
public void modifyTestElement(TestElement sampler) {
super.configureTestElement(sampler);
if (sampler instanceof DubboSampler) {
DubboSampler dubboSampler = (DubboSampler) sampler;
dubboSampler.setProperty(DubboSampler.FUNCTION, functionTextField.getText());
}
}
@Override
public String getStaticLabel() {
//设置显示名称
return "DubboSampler";
}
private void initFields() {
functionTextField.setText("");
}
@Override
public void clearGui() {
super.clearGui();
initFields();
}
}
4.把工程打成jar包放到\lib\ext之下
四、开源插件DubboSampler的二次开发
需求背景:在自动化回归测试中,使用线上录制好的流量直接在压测环境进行回放,需要DubboSampler支持按照录制流量调用。
源码分析:
DubboSampler
public class DubboSample extends AbstractSampler implements Interruptible {
private static final Logger log = LoggingManager.getLoggerForClass();
private static final long serialVersionUID = -6794913295411458705L;
public static ApplicationConfig application = new ApplicationConfig("DubboSample");
@Override
public SampleResult sample(final Entry entry) {
final SampleResult res = new SampleResult();
res.setSampleLabel(this.getName());
//构造请求数据
res.setSamplerData(this.getSampleData());
//调用dubbo
res.setResponseData(JsonUtils.toJson(this.callDubbo(res)), StandardCharsets.UTF_8.name());
//构造响应数据
res.setDataType(SampleResult.TEXT);
return res;
}
sample方法是取样器的核心方法,返回的SampleResult也就是测试数据的取样结果。
我们再来看看getSampleData方法
/**
* Construct request data
*/
private String getSampleData() {
log.info("sample中的实例id" + this.toString() + ",element名称" + this.getName());
final StringBuilder sb = new StringBuilder();
sb.append("Registry Protocol: ").append(Constants.getRegistryProtocol(this)).append("\n");
sb.append("Address: ").append(Constants.getAddress(this)).append("\n");
sb.append("RPC Protocol: ").append(Constants.getRpcProtocol(this)).append("\n");
sb.append("Timeout: ").append(Constants.getTimeout(this)).append("\n");
sb.append("Version: ").append(Constants.getVersion(this)).append("\n");
sb.append("Retries: ").append(Constants.getRetries(this)).append("\n");
sb.append("Cluster: ").append(Constants.getCluster(this)).append("\n");
sb.append("Group: ").append(Constants.getGroup(this)).append("\n");
sb.append("Connections: ").append(Constants.getConnections(this)).append("\n");
sb.append("LoadBalance: ").append(Constants.getLoadbalance(this)).append("\n");
sb.append("Async: ").append(Constants.getAsync(this)).append("\n");
sb.append("Interface: ").append(Constants.getInterface(this)).append("\n");
sb.append("Method: ").append(Constants.getMethod(this)).append("\n");
sb.append("Method Args: ").append(Constants.getMethodArgs(this).toString());
sb.append("Attachment Args: ").append(Constants.getAttachmentArgs(this).toString());
return sb.toString();
}
这里是解析TestElement对应的参数用作Sampler的请求,这里用一个Constants里的枚举属性作为键名解析。我们可以注意到AbstractSampler extends AbstractTestElement,而AbstractTestElement implements TestElement,因此这里可以直接把this(Sampler类)丢进去解析。
如果我们想控制dubbo调用的入参,可能就得从这个方法切入,把简单的解析参数改成通过http接口或者从数据库查询出来,建议做一个本地缓存,以防频繁请求影响测试性能。
DubboSampleGui
这是DubboSampler的Gui组件,实现了AbstractSamplerGui抽象类的接口,我们可以重点看createTestElement方法,我们发现其实Gui的调用方式一样会调用到DubboSampler的方法 (DubboSample sample = new DubboSample())
public class DubboSampleGui extends AbstractSamplerGui {
private static final Logger log = LoggingManager.getLoggerForClass();
private static final long serialVersionUID = -3248204995359935007L;
private final DubboPanel panel;
public DubboSampleGui() {
super();
panel = new DubboPanel();
init();
}
/**
* Initialize the interface layout and elements
*/
private void init() {
//所有设置panel,垂直布局
JPanel settingPanel = new VerticalPanel(5, 0);
settingPanel.setBorder(makeBorder());
Container container = makeTitlePanel();
settingPanel.add(container);
//所有设置panel
panel.drawPanel(settingPanel);
//全局布局设置
setLayout(new BorderLayout(0, 5));
setBorder(makeBorder());
add(settingPanel,BorderLayout.CENTER);
}
/**
* component title/name
*/
@Override
public String getLabelResource() {
return this.getClass().getSimpleName();
}
/**
* this method sets the Sample's data into the gui
*/
@Override
public void configure(TestElement element) {
super.configure(element);
log.debug("sample赋值给gui");
panel.configure(element);
panel.bundleElement(element);
}
/**
* Create a new sampler. And pass it to the modifyTestElement(TestElement) method.
*/
@Override
public TestElement createTestElement() {
log.debug("创建sample对象");
//创建sample对象
DubboSample sample = new DubboSample();
modifyTestElement(sample);
return sample;
}
/**
* this method sets the Gui's data into the sample
*/
@SuppressWarnings("unchecked")
@Override
public void modifyTestElement(TestElement element) {
log.debug("gui数据赋值给sample");
//给sample赋值
super.configureTestElement(element);
panel.modifyTestElement(element);
panel.bundleElement(element);
}
/**
* sample's name
*/
@Override
public String getStaticLabel() {
return "Dubbo Sample";
}
/**
* clear gui's data
*/
@Override
public void clearGui() {
log.debug("清空gui数据");
super.clearGui();
panel.clearGui();
}
}
这个包的其他几个类是写Gui组件的界面的,这里跟需求无关就不多展开了。