1. 二次开发到底改的是什么:先给三种路线定个性
网上聊Jmeter二次开发,很容易被带偏到"改源码"这条路上去。我刚入行时也犯过这个错,下了源码工程,配了半天的Ant环境,最后只是把某个输出日志的中文改成了英文,纯属浪费了一个下午。后来做过的项目多了才明白,Jmeter的"二次开发"其实应该按需求和难度拆成三档:脚本级(BeanShell/JSR223)、函数级(Java扩展函数)、插件级(自定义Sampler/ConfigElement等GUI插件)。三者的代码量、可维护性、复杂度完全不是一个量级。
先说结论,后续章节再逐条展开。
- 脚本级开发:不碰Jmeter本身的编译环境和GUI代码,只在测试计划里写BeanShell或JSR223脚本完成断言、参数加工、数据生成。门槛最低,适合90%的接口测试和性能测试场景。
- 函数级开发:写一个Java类,继承AbstractFunction,打成jar包丢进lib/ext,就能在Jmeter里当内置函数一样用
${__myFunc(...)}。适合需要被大量脚本复用、但Jmeter默认函数覆盖不到的工具能力。 - 插件级开发:继承Sampler、ConfigElement、PreProcessor等抽象类,做配套的GUI类,在Jmeter里新增一个可视化的测试元件。这是真正意义的深度改造,适合把公司的加解密算法、RPC调用协议、私有报文协议封装成拖拽即用的元件。
这三档在社区里经常被混着讲,搜索"jmeter beanshell断言"出来的帖子大多是脚本级别,搜索"二次开发"出来的又全是写Java类改源码的教程。你如果只是想在压测时加个签名参数,不需要也不可能从改源码开始。但如果你想在公司内封装一个公司自研协议的Sampler,那不仅仅是写Java类的问题,还要搞懂Jmeter的实例化机制和GUI组件生命周期。
这篇内容我按"脚本级 → 函数级 → 插件级 → 踩坑记录"的顺序来拆,每一部分都会给完整可跑的方案,并且会把那些"文档里不会写、但实际做必然会撞上"的坑提前说出来。
2. 脚本级开发:BeanShell与JSR223的实战边界
2.1 先别急着写Java类,BeanShell能覆盖大部分日常需求
很多人一听到"二次开发"四个字就默认必须打开IDEA写Java代码,这其实是认知偏差。Jmeter的BeanShell组件(断言、后置处理器、前置处理器)本质上是让Jmeter在运行时直接执行一段Java风格脚本,你可以在脚本里访问vars、prev、props、log这些内置对象,不必重新编译工程。
举个例子,假设压测接口的入参需要一个MD5加密后的sign,正常做法可能是用__digest函数。但如果业务规则复杂一点,比如sign = md5(时间戳 + 固定salt + 请求体里的两个字段),需要先取上一个请求的响应值再拼接,那用内置函数就比较麻烦。这时候一个BeanShell前置处理器加六行脚本就解决了。
import java.security.MessageDigest; String timestamp = String.valueOf(System.currentTimeMillis()); String rawStr = "stamp=" + timestamp + "&salt=mysalt&orderId=" + vars.get("orderId"); MessageDigest md = MessageDigest.getInstance("MD5"); byte[] bytes = md.digest(rawStr.getBytes("UTF-8")); StringBuilder sb = new StringBuilder(); for (byte b : bytes) { sb.append(Integer.toHexString((b & 0xFF) | 0x100).substring(1, 3)); } vars.put("sign", sb.toString()); vars.put("timestamp", timestamp);这段代码直接写在"前置处理器 → BeanShell PreProcessor"里,脚本执行时vars对象就是Jmeter运行时变量的句柄,vars.put往全局变量池里塞数据,后续HTTP请求里直接${sign}引用。
这里有一个非常关键的性能前提:BeanShell脚本引擎本身解释执行较慢,单脚本几百行或压测线程数一多,会拖累压测自身的TPS上限。Java开发常用的优化方案是:高频逻辑务必迁移到JSR223 + Groovy引擎。JSR223是Java标准脚本接口(java.script包),Groovy的运行时性能比BeanShell高一个数量级,代码风格几乎完全兼容,切过来成本很低。
2.2 断言场景里,JSR223 Groovy才是主流选择
我在真实压测项目中已经把BeanShell断言全部迁到JSR223了。原因是BeanShell断言在1万并发下会出现脚本执行耗时增加、甚至GC压力变大的问题。JSR223 + Groovy的编译缓存机制能显著提升重复执行场景的稳定性。
一个常见需求:响应是JSON数组,要求数组里每个元素的status字段都不能是failed,同时每个元素的耗时不能超过800毫秒。很多人第一反应是引入JSON库做解析,实际上Groovy可以直接调用Jmeter脚本区里的JsonSlurper工具类。
import groovy.json.JsonSlurper def response = prev.getResponseDataAsString() def list = new JsonSlurper().parseText(response) assert list.size() > 0 : "返回列表为空" list.each { item -> assert item.status != "failed" : "存在失败状态: " + item assert item.durationTime < 800 : "响应超时: " + item.durationTime }注意这段脚本里的prev对象,它是SampleResult的实例,prev.getResponseDataAsString()拿到的就是当前取样器返回的报文体。脚本执行失败(断言失败)时,Jmeter会把失败信息挂到当前取样器的断言结果里,压测报告的失败率统计用的就是这个结果。
用JSR223除了性能,还有一个隐藏优势:你可以直接引入项目组自研的jar包里的构造类。比如公司的加解密库放在公司私服,你在lib/ext下丢一个封装jar,再在JSR223脚本里import com.company.xx.EncryptUtil,就能复用公司的算法逻辑。这比在BeanShell里手动复制粘贴200行AES代码靠谱得多。
2.3 脚本级开发的"内容组织"问题:复用逻辑别散落到每个线程组
当脚本量增多,只依赖"每个元件里贴一份脚本"的方式会变得很难维护。我之前遇到过一个项目,四个线程组需要相同的签名逻辑,我在四个地方各粘贴了一份。等算法加了一个version字段后,修改的地方有四处,而且有一处漏改了,压测到一半才发现签名的version有旧值。
后来我把公共逻辑抽成单独的Groovy脚本文件,放在bin/scripts/目录下,每个JSR223元件只保留调用代码:
def helper = new SignHelper(vars, props, log) helper.applySign("orderList")SignHelper是一个Groovy类文件,放在Jmeter的bin/scripts目录中,通过Groovy的源码路径机制被加载。这样签名逻辑只有一份,改一次,全测试计划生效。这是从"会写脚本"到"脚本工程化"的转折点。再往后,如果公共逻辑还需要被多个Jmeter测试计划共用,我更建议直接把这类逻辑下沉到函数级或自定义Java jar里。
3. 函数级开发:写一个真正意义的自定义JMeter函数
3.1 为什么需要自定义函数
Jmeter内置函数虽然多,但覆盖面有限。比如我需要一个"读取指定数据库表当前最大orderId,并自增1"的函数,用于压测时生成唯一业务号。类似逻辑也可以放JSR223里做,但问题在于脚本里连接数据库必须每次执行都初始化连接,逻辑重复且效率低。更合理的做法是写一个Java函数元件,名称叫__nextOrderId,在测试计划里可以直接${__nextOrderId()}引用,内部做长连接池复用。
函数级开发就是这条路。整条链路清晰简单:
- 新建Maven工程,引入
org.apache.jmeter:ApacheJMeter_core依赖 - 写一个类继承
org.apache.jmeter.functions.AbstractFunction - 实现
execute、setParameters、getReferenceKey、getArgumentDesc四个方法 - 打成jar包放到
lib/ext目录,重启Jmeter - 在函数助手对话框里能看到新增的函数,测试计划里也能直接引用
3.2 代码骨架与核心方法说明
package com.example.jmeter.functions; import org.apache.jmeter.engine.util.CompoundVariable; import org.apache.jmeter.functions.AbstractFunction; import org.apache.jmeter.functions.InvalidVariableException; import org.apache.jmeter.samplers.SampleResult; import org.apache.jmeter.samplers.Sampler; import java.util.List; public class NextOrderId extends AbstractFunction { private static final String KEY = "__nextOrderId"; private static final List<String> DESC = java.util.Arrays.asList("prefix(optional)"); private String prefix = ""; @Override public String execute(SampleResult previousResult, Sampler currentSampler) throws InvalidVariableException { long current = System.currentTimeMillis(); // 这里实际业务可以去查库或走连接池,配合自增序列确保唯一 return prefix + current; } @Override public void setParameters(List<CompoundVariable> parameters) throws InvalidVariableException { if (parameters.size() >= 1) { prefix = parameters.get(0).execute(); } } @Override public String getReferenceKey() { return KEY; } @Override public List<String> getArgumentDesc() { return DESC; } }几个核心方法的含义:
execute是函数真正执行的逻辑,previousResult是当前取样器的前一个结果对象,currentSampler是当前取样器实例。这里只是演示,你可以在execute里读取previousResult.getResponseDataAsString()作输入,也能用JMeterContextService.getContext()获取全局变量,用法非常灵活。setParameters负责解析调用方传入的参数。Jmeter会把${__nextOrderId(abc)}中的abc封装成CompoundVariable对象,这里可以做一个或多个参数的解析。getReferenceKey返回的函数名必须与__开头一致,这也是函数助手对话框里显示的名字。getArgumentDesc用来描述参数含义,辅助用户在函数助手对话框里填写参数。
3.3 部署验证的完整流程
- 在pom.xml里加入核心依赖:
<dependency> <groupId>org.apache.jmeter</groupId> <artifactId>ApacheJMeter_core</artifactId> <version>5.5</version> <scope>provided</scope> </dependency>mvn clean package打包,将target下的jar放到$JMETER_HOME/lib/ext/目录。注意这里有一个坑:我后面会在踩坑部分专门讲。重启Jmeter。点击
选项 → 函数助手对话框,左侧下拉框如果多出了__nextOrderId,说明函数加载成功。测试计划任意参数位置引用
${__nextOrderId(ORD)}跑一次请求,查看发送报文中是否带上了ORD + 时间戳拼接的结果。
一个补充经验:execute里如果只依赖系统时间和随机数,压测线程并发时会存在极小概率生成重复值。真实业务号建议在函数内部维护一个线程安全的AtomicLong计数器,用前缀 + 日期 + AtomicLong自增值拼业务号,这样才能保证压测采集的数据不互相干扰。
3.4 函数级开发与脚本级开发的取舍
函数级开发的成本是在Java工程里改动后需要重新打包重启Jmeter,周期长。但只要这个函数是"通用能力、多处调用",这个代价是值得的。我的判断标准是:如果一段逻辑在三个以上JSR223脚本里重复出现,而它的实现还涉及连接池、缓存这类状态管理,就先抽函数。如果只是单一场景的一次性逻辑,留在脚本里更灵活。
4. 插件级开发:做一个带GUI的自定义Sampler
4.1 理解Jmeter的组件模型
如果函数级开发是"穿针引线",插件级开发就是"重新打造一个工具"。具体到自定义请求类插件,核心要搞懂Jmeter元件的双类结构:一个类负责业务逻辑(继承AbstractSampler),一个类负责GUI展示(继承AbstractSamplerGui)。这两个类通过TestBean或setProperty机制进行字段交互。
很多初学二次开发的人搞不明白"为什么GUI类的字段修改后,Sampler运行时能拿到"——因为Jmeter的存储机制很特殊:测试计划保存的其实是JavaBean的property属性,不是GUI对象的字段。当你点击一个Sampler时,GUI类从TestElement里加载属性值渲染到界面上;点应用时,GUI类把界面值写回TestElement;真正跑请求时,采样器类从TestElement拿属性去执行。
这个模型如果不理解,写插件时最典型的问题就是:界面填的参数值永远传不进请求逻辑。
4.2 从零写一个发送TCP协议的Sampler
假设公司内部有很多设备走私有TCP长连接协议,单位置监听端口。用Jmeter压测这种服务,默认的TCP Sampler不够灵活,所以需要一个自定义TCP Sampler来支持加密逻辑。
先写业务逻辑类PrivateTcpSampler extends AbstractSampler。AbstractSampler要求实现sample()方法,返回SampleResult。
package com.example.jmeter.sampler; import org.apache.jmeter.samplers.AbstractSampler; import org.apache.jmeter.samplers.Entry; import org.apache.jmeter.samplers.SampleResult; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; public class PrivateTcpSampler extends AbstractSampler { public static final String HOST = "privateTcp.host"; public static final String PORT = "privateTcp.port"; public static final String PAYLOAD = "privateTcp.payload"; @Override public SampleResult sample(Entry entry) { SampleResult result = new SampleResult(); result.setSampleLabel(getName()); result.sampleStart(); String host = getPropertyAsString(HOST); int port = getPropertyAsInt(PORT); String payload = getPropertyAsString(PAYLOAD); try (Socket socket = new Socket(host, port)) { socket.setSoTimeout(5000); OutputStream os = socket.getOutputStream(); InputStream is = socket.getInputStream(); // 企业私有协议封装:这里可以根据公司加解密算法改造字节流 byte[] requestBytes = encode(payload); result.setSentBytes(requestBytes.length); os.write(requestBytes); os.flush(); // 读取响应的简化处理:第一轮4字节为报文长度 byte[] lenBuf = new byte[4]; is.read(lenBuf); int bodyLen = ((lenBuf[0] & 0xFF) << 24) | ((lenBuf[1] & 0xFF) << 16) | ((lenBuf[2] & 0xFF) << 8) | (lenBuf[3] & 0xFF); ByteArrayOutputStream bos = new ByteArrayOutputStream(); byte[] buf = new byte[1024]; int readTotal = 0; while (readTotal < bodyLen) { int n = is.read(buf, 0, Math.min(buf.length, bodyLen - readTotal)); if (n == -1) break; bos.write(buf, 0, n); readTotal += n; } byte[] responseBytes = bos.toByteArray(); result.setResponseData(responseBytes); result.setResponseCodeOK(); result.setResponseMessage("OK"); result.setSuccessful(true); } catch (Exception e) { result.setResponseCode("500"); result.setResponseMessage(e.toString()); result.setSuccessful(false); } finally { result.sampleEnd(); } return result; } private byte[] encode(String payload) { // 示例:可在此接入公司AES加密、长度字段拼装等逻辑 byte[] body = payload.getBytes(java.nio.charset.StandardCharsets.UTF_8); int length = body.length; byte[] header = new byte[4]; header[0] = (byte) ((length >> 24) & 0xFF); header[1] = (byte) ((length >> 16) & 0xFF); header[2] = (byte) ((length >> 8) & 0xFF); header[3] = (byte) (length & 0xFF); java.io.ByteArrayOutputStream bos = new java.io.ByteArrayOutputStream(); bos.write(header, 0, 4); bos.write(body, 0, body.length); return bos.toByteArray(); } }这里面有几个细节值得说:
sampleStart()和sampleEnd()之间包裹的是真正的耗时,Jmeter报告和聚合图里的时间统计都依赖这两个时间戳。getPropertyAsString系列是从TestElement读取配置,存储值来自GUI类写回的属性。- 变量持有关的要注意:不要用静态变量存连接。压测是并发模型,每个线程都有自己独立的Sampler实例调用
sample(),但如果你把Socket缓存到静态Map里,就必须自己管理线程安全和连接失效问题,否则压测一半就报连接超时。 SampleResult的setResponseCodeOK()和setSuccessful(true)配合使用,会让这个请求在聚合报告里计入成功;如果异常,setSuccessful(false)则计入失败。
4.3 配套GUI类与插件注册机制
有了业务类还差一半,接下来写GUI类PrivateTcpSamplerGui extends AbstractSamplerGui。
package com.example.jmeter.sampler; import org.apache.jmeter.samplers.gui.AbstractSamplerGui; import org.apache.jmeter.testelement.TestElement; import javax.swing.*; import java.awt.*; public class PrivateTcpSamplerGui extends AbstractSamplerGui { private JTextField hostField; private JTextField portField; private JTextArea payloadArea; public PrivateTcpSamplerGui() { init(); } private void init() { setLayout(new BorderLayout()); JPanel mainPanel = new JPanel(new GridBagLayout()); GridBagConstraints gbc = new GridBagConstraints(); gbc.insets = new Insets(5, 5, 5, 5); gbc.fill = GridBagConstraints.HORIZONTAL; hostField = new JTextField(20); portField = new JTextField(10); payloadArea = new JTextArea(6, 30); gbc.gridx = 0; gbc.gridy = 0; mainPanel.add(new JLabel("Host:"), gbc); gbc.gridx = 1; gbc.gridy = 0; mainPanel.add(hostField, gbc); gbc.gridx = 0; gbc.gridy = 1; mainPanel.add(new JLabel("Port:"), gbc); gbc.gridx = 1; gbc.gridy = 1; mainPanel.add(portField, gbc); gbc.gridx = 0; gbc.gridy = 2; mainPanel.add(new JLabel("Payload:"), gbc); gbc.gridx = 1; gbc.gridy = 2; mainPanel.add(new JScrollPane(payloadArea), gbc); add(mainPanel, BorderLayout.CENTER); } @Override public String getStaticLabel() { return "Private TCP Sampler"; } @Override public String getLabelResource() { return getClass().getSimpleName(); } @Override public TestElement createTestElement() { PrivateTcpSampler sampler = new PrivateTcpSampler(); modifyTestElement(sampler); return sampler; } @Override public void modifyTestElement(TestElement element) { super.configureTestElement(element); if (element instanceof PrivateTcpSampler) { PrivateTcpSampler sampler = (PrivateTcpSampler) element; sampler.setProperty(PrivateTcpSampler.HOST, hostField.getText()); sampler.setProperty(PrivateTcpSampler.PORT, portField.getText()); sampler.setProperty(PrivateTcpSampler.PAYLOAD, payloadArea.getText()); } } @Override public void configure(TestElement element) { super.configure(element); PrivateTcpSampler sampler = (PrivateTcpSampler) element; hostField.setText(sampler.getPropertyAsString(PrivateTcpSampler.HOST)); portField.setText(String.valueOf(sampler.getPropertyAsInt(PrivateTcpSampler.PORT))); payloadArea.setText(sampler.getPropertyAsString(PrivateTcpSampler.PAYLOAD)); } }GUI类里的关键方法:
createTestElement():当用户在测试计划里手动新增这个元件时调用,负责生成一个默认的Sampler实例。modifyTestElement:用户点击面板上的"Apply"按钮时把界面值写入TestElement。注意回调函数顺序是:test plan的监控器修改 → GUI修改 → 运行态取值。configure:打开一个既有Sampler时,Jmeter会把已存属性回填到界面控件。如果configure不实现或字段不对应,你会发现:新建的Sampler没问题,但是重新打开保存好的测试计划时,界面上的Host、Port值全部丢失。getStaticLabel():返回组件在测试计划树和"添加元件"菜单里显示的名称。
打包方式类似函数级开发:mvn clean package,生成jar放到lib/ext目录,重启Jmeter后,右键线程组 → 添加 → Sampler,就能看到"Private TCP Sampler"选项。
4.4 附加面板:如果Sampler还依赖ConfigElement配置
很多私有协议不是单纯的Host/Port/Payload三个字段,而是一整套参数:密钥版本、加密算法类型、报文超时重传次数等。把这些塞进PrivateTcpSamplerGui会让面板非常拥挤,更优雅的做法是做一个配套的ConfigElement(配置元件),集中管理这些参数,Sampler运行时通过getPropertyAsString从同名TestElement的属性里读取。
ConfigElement的最小实现也是继承AbstractConfigElement,配套一个GUI类。配置元件的GUI类布局和Sampler GUI几乎一致,唯一的区别在createTestElement返回值是配置元件实例。在测试计划中使用时,同一个线程组下放置配置元件和Sampler,Sampler便可在运行时通过getContext().getCurrentSampler()或线程上下文的配置片段拿到这些属性。实际开发里,我就是把加密算法、私钥索引、重试次数全放在配置元件里,Sampler GUI只保留基础连接参数,这样测试人员只需要关注连接和报文体,不需要理解加密内部细节。
5. 热加载、调试和版本兼容:二次开发中的关键工程化细节
5.1 关于"每次重启Jmeter才能生效"这件事
函数级和插件级开发,最讨厌的就是每次改完代码都要杀掉Jmeter进程再重启。加载路径一旦装机确认后,常见的协作流程是把jar包放到lib/ext,然后开发机调整jmeter.properties里的plugin.not.ignored等属性。但我的实际建议是:写一个专门的调试启动脚本,直接调用JMeter的启动类和CLI参数。这样每次改完代码执行一次脚本,十几秒就能重启进入命令行模式,通过-t指定测试计划、-n指定非GUI模式、-j输出日志文件,再配合-e生成报告即可快速回归。GUI模式因为要等整个Swing界面加载,通常要慢得多。
但这里有个比重启本身更大的坑:Jmeter进程启动时扫描类,同一个类有多个版本时会加载哪个?
5.2 版本矩阵:JDK、Maven依赖版本与Jmeter的对应关系
我把几个稳定组合列出来,这套是我在不同项目里实跑过的,不是网上抄来的:
| Jmeter版本 | 推荐JDK | ApacheJMeter_core依赖版本 | 特别注意 |
|---|---|---|---|
| Jmeter 5.4.x | JDK 8 / 11 | 5.4.1 | 最稳妥的老牌组合,脚本API兼容性好 |
| Jmeter 5.5.x | JDK 8 / 11 | 5.5 | 函数签名无变化,插件开发可复用 |
| Jmeter 5.6.2 | JDK 11 / 17 | 5.6.2 | 包结构有调整,老插件需重新编译 |
有次我在Jmeter 5.6.2环境里直接复用5.4时代打好的自定义Sampler jar,启动时报NoSuchMethodError。排查后原因是5.6版本把某些内部API改了包路径,老jar引用的是旧包名。这类问题不会在编译期暴露,只会在运行期爆出来,所以生产环境里换Jmeter版本一定要重新编译一遍所有自定义jar包。
5.3 jar依赖冲突:这是插件开发里最磨人的问题
lib/ext下放了自定义jar,里面带了第三方依赖包。如果这个第三方包版本和Jmeter主程序自带的冲突,就会出现两个比较典型的症状:
- Jmeter启动卡在Downloading或扫描插件阶段,日志里反复刷
ClassNotFoundException - 某个Sampler执行时报
NoClassDefFoundError或ClassCastException
我踩过最狠的一次是给自定义函数引入了公司内部的httpclient版本(4.5.x),而Jmeter自带的是4.2.x。函数一调用就抛方法不存在异常,查了一个多小时才定位到是jar里面的旧版本覆盖了Jmeter类路径里的新版本。
解决办法就一条纪律:自定义jar内部使用到的依赖,优先选用provided scope或shade重命名,避免往Jmeter亲类加载器里塞冲突类。如果你必须携带一个高版本依赖,建议用Maven shade插件把依赖包relocation迁移到另一个包名下,这样既不会被Jmeter的类稀释,也不会干扰其他插件。
5.4 UI插件刷不出图标的调试技巧
某些自定义Sampler在测试计划树里正常显示,但图标是空白方块。原因是Jmeter GUI组件有独立接口getIcon,如果你没实现,Jmeter会尝试从类路径下查找和getStaticLabel同名的png资源。两个调试方向:
- 检查Maven打包时是否把
src/main/resources里的图标文件带进了jar。默认Java打包插件如果不改配置,不会单独把resources目录打进classpath根路径。 - 如果组件实现了
getIcon接口,确认返回的ImageIcon是独立新建实例,不要用共享静态引用。Jmeter在多个GUI界面切换时会重新拉取图标资源,静态共享容易导致区域绘图异常。
5.5 中文乱码问题
插件面板和日志里出现乱码,多数是文件编码问题。开发工程在pom.xml里强制指定UTF-8,同时源码文件本身必须是UTF-8编码。很多IDE默认GBK编码,一旦中文注释或中文字符串写入源码,打包后的class文件里就是乱码,Jmeter GUI里显示全成问号。
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties>另外,Jmeter本身的bin/jmeter启动参数里有可能被默认添加了-Dfile.encoding。如果出现压测时发送的中文报文体乱码而函数插件里显示正常,就要看是不是Jmeter配置文件的编码推导覆盖了你jar里的设置。解决方案是在user.properties里显式设置file.encoding=UTF-8,保证JVM进程层面的统一。
6. 从二次开发回到测试业务:我的最后三点经验
做过几个完整的Jmeter二次开发项目之后,我发现决定二次开发成功与否的,往往不是代码能力,而是边界的把握和前期沟通。
第一,先搞清楚要封装的是业务逻辑、协议逻辑还是纯粹的测试工具逻辑。如果业务规则频繁变化,比如签名算法每三个月换一次算法,那么做成插件级组件会让每次改动都变成一次版本发布,反而拖慢使用节奏。这种更适合JSR223脚本加配置文件的形式。只有当规则稳定且多团队共用时,才值得下沉成Sampler级插件。
第二,压测工具的开发有一个和业务开发完全不同的思维差异:性能测试工具自身不能成为性能瓶颈。脚本引擎选Groovy而非BeanShell、连接池要复用、内存不能随意缓存过多响应数据、图表标签不能过度刷新UI线程,这些都是在做插件设计阶段就要考虑的问题。一个执行一次需要50毫秒的Sampler,压测200并发时自身CPU占用就可能率先打满,目标系统的瓶颈数据就完全失真了。
第三,也是最实际的一点:二次开发产出的东西一定要让一线测试同学能用起来。我见过有人做了很完整的自定义Sampler,但没配使用文档,一线同事在JMeter UI里根本不知道怎么填报文,最后只能回到老办法。后来我给插件GUI加了示例按钮,用于一键填充测试报文,并配套了一个demo.jmx文件放到bin/scripts/examples,新同学第一次上手五分钟就能跑通。这个投入比写几百行代码的价值还要大。
Jmeter的二次开发,说到底不是炫技,是让工具更贴合你的业务场景。把脚本熟练用起来解决日常70%的问题,再用函数级开发解决复用问题,等确确实实需要可视化组件时再进入插件级开发,这条路走下来不会太痛苦,也不会走太多弯路。