Jmeter二次开发实战:自定义Sampler与Groovy脚本解决接口压测难题
但凡用Jmeter做接口压测做过一段时间基本都会撞上同一个瓶颈官方功能再好到了真实业务场景总有那么几个需求卡着你。比如要动态控制QPS、要拿实时token做签名、要解析嵌套JSON做断言、要对接内部加密算法。网上搜一圈全是碎片官方文档又写得跟天书似的东拼西凑折腾一周才算出师。这篇东西我就围绕Jmeter二次开发把“能不能改、怎么改、改哪里、踩过什么坑”一次性讲透。适合已经会用Jmeter做基础压测、但想往测试开发方向走的同学。我会从扩展点分类、自定义Java Sampler完整实现、groovy脚本实战、动态QPS调整到高频问题排查尽量用代码和现场案例说话少讲空理论。1. 先搞清楚一件事Jmeter到底能“二次开发”什么先说结论Jmeter的二次开发核心不是改源码而是围绕它暴露的扩展接口把官方没做好的那部分能力补上。搞清楚这一点你就不会一上来就想着下载源码改Bug那样改完还怎么维护。1.1 官方扩展点全览Jmeter的每个GUI节点类型背后都有一组扩展接口你要知道门在哪Sampler采样器最核心的扩展点自定义协议请求、自定义加解密逻辑都在这做对应JavaSamplerClient和AbstractJavaSamplerClient接口写完后通过“Java请求”采样器调用。Function函数实现AbstractFunction可以造出${__myfunc(param)}这种自定义函数常用于参数化场景。Assertion断言官方断言搞不定复杂校验时用JSR223 Assertion配合groovy脚本做动态断言或者实现Assertion接口做自定义断言组件。Pre Processor / Post Processor前置/后置处理器请求前拼参数、请求后提取数据这块用JSR223 Sampler或BeanShell就能搞定大部分需求。Listener监听器实现AbstractVisualizer或SampleListener把结果落库或对接内部监控平台。Config Element配置元件如果是全局变量、数据源之类的扩展实现ConfigElement接口。你观察一下就能发现大多数二次开发需求其实都集中在“Sampler实现”和“脚本化实现”这两条路上。前者适合生产级、可复用的功能后者适合快速验证、临时救火。1.2 二次开发的价值边界不是所有问题都值得写一个插件。我吃过这个亏有一次就想给报文加个时间戳结果磨蹭半天写了个Sampler后来发现官方__time()函数加拼接表达式十秒钟就能解决。二次开发适合做这几类事公司内部加密协议、私有TCP/UDP协议压测、自定义签名算法、动态生成海量测试数据、需要输出到特定报表平台。一句话概括官方组件解决不了或解决起来太别扭的才值得开发。另外要明确一个边界性能测试工具本身的职责是压测不是业务逻辑容器。你把复杂的业务计算全塞进Jmeter脚本里不仅跑不快还会掩盖被测系统的真实瓶颈。我见过有人在JSR223里写递归遍历树形结构的场景压测机CPU直接打满被压服务反而闲得很。脚本越轻越好重逻辑放服务端或测试工程里。2. 二次开发前的环境准备与目录结构解剖环境准备这块看着简单但出错率极高。很多人在网上找Demo编译通过一丢进Jmeter就报类找不到最后发现是依赖包和JDK版本的问题。2.1 JDK、Maven、IDE的配置要点Jmeter 5.x系列要求JDK 8以上我自己习惯用JDK 8或者JDK 11来做插件编译目标编译版本一定要低于等于运行Jmeter的JDK版本。你本机用JDK 17写代码编译target设成1.8否则放到别人JDK 8的机器上直接报UnsupportedClassVersionError。Maven工程里注意一点不是所有依赖都要打包进去。Jmeter运行时已经带了一大堆依赖你只需要把ApacheJMeter_core和ApacheJMeter_java设为provided作用域编译的时候用打包的时候别带进去。XStream、commons-lang3、log4j-api这些Jmeter自带的东西也不要重复打包否则会出现两个版本冲突行为非常诡异。我建议的pom依赖配置简化就是这样dependency groupIdorg.apache.jmeter/groupId artifactIdApacheJMeter_core/artifactId version5.6.3/version scopeprovided/scope /dependency dependency groupIdorg.apache.jmeter/groupId artifactIdApacheJMeter_java/artifactId version5.6.3/version scopeprovided/scope /dependencyIDE方面没什么可纠结的IntelliJ IDEA或Eclipse都行。关键是调试方式我强烈建议装一个JSR223 Sampler来验证逻辑而不是每次改完代码就重启Jmeter看效果。JSR223里用的groovy脚本可以和Java代码互相印证逻辑跑通了再固化成Sampler效率高得多。2.2 lib与lib/ext目录的职责划分这个目录问题值得单独说因为90%的“插件不生效”都跟它有关。Jmeter根目录下有lib和lib/ext两个目录职责完全不同。lib目录放的是Jmeter运行时的公共依赖库包括commons系列、httpclient、log4j等。lib/ext目录放的是Jmeter的扩展实现包括官方自带的各种Sampler组件以及你自己编译出来的jar包。注意区分第三方公共依赖放lib你写的扩展jar放lib/ext。还有一种情况你的插件依赖了某个第三方库而这个库Jmeter没带。那你需要把第三方库jar也扔进lib目录而不是lib/ext。扔错位置不会让你立刻报错但可能在某个类加载的关键时刻才炸出来那时候排查成本就高了。启动时如果想确认插件被正确加载用jmeter -v看启动日志或者直接在JMeter启动输出里搜Adding ...关键词。我每次部署完新插件都会先在GUI里看一眼Java请求采样器下拉框里有没有出现新的类名没有就说明类加载失败直接按下面的排查步骤走。3. 核心战场自定义Java Sampler的完整实战这是二次开发最硬核的部分也是面试官最爱问的点。我直接用一个“带MD5签名的HTTP请求Sampler”作为案例走一遍完整开发流程。3.1 接口与生命周期分析JavaSamplerClient接口有四个关键方法对应Sampler的完整生命周期getDefaultParameters()定义这个Sampler在GUI里展示哪些输入参数返回Arguments对象每个参数有名称、默认值、描述。setupTest(JavaSamplerContext ctx)初始化阶段每个线程在测试开始前调用一次。适合做连接池创建、配置读取这种一次性的操作。runTest(JavaSamplerContext ctx)核心方法每个请求都会调用。要在这里构造请求、执行请求、生成SampleResult并返回。返回的SampleResult会被监听器统计。teardownTest(JavaSamplerContext ctx)收尾阶段线程结束时调用一次释放资源。需要特别说明的一点setupTest和teardownTest是按线程执行的不是全局一次。如果你的Sampler要维护全局共享资源比如一个线程安全的数据缓存要用static修饰或借助JMeterContextService来管理。SampleResult是结果载体它有几个关键操作设置采样器名称、标注成功或失败、记录请求数据、记录响应数据、设置响应时间。监听器界面上的“样本数”“异常数”“平均响应时间”都是从这些数据统计出来的。如果你要自定义输出报告正确处理SampleResult的字段就是第一步。3.2 实现代码从参数读取到采样结果下面先看一个最简单但完整的Java Sampler实现这个Demo能跑通你就理解整个链路了package com.example.jmeter.sampler; import org.apache.jmeter.config.Arguments; import org.apache.jmeter.protocol.java.sampler.AbstractJavaSamplerClient; import org.apache.jmeter.protocol.java.sampler.JavaSamplerContext; import org.apache.jmeter.samplers.SampleResult; public class DemoSignSampler extends AbstractJavaSamplerClient { private String baseUrl; private String appId; private String secretKey; Override public Arguments getDefaultParameters() { Arguments arguments new Arguments(); arguments.addArgument(baseUrl, http://127.0.0.1:8080); arguments.addArgument(appId, 10001); arguments.addArgument(secretKey, your-secret-key); return arguments; } Override public void setupTest(JavaSamplerContext context) { baseUrl context.getParameter(baseUrl); appId context.getParameter(appId); secretKey context.getParameter(secretKey); } Override public SampleResult runTest(JavaSamplerContext context) { SampleResult result new SampleResult(); result.setSampleLabel(DemoSignSampler); result.setDataType(SampleResult.TEXT); try { String timestamp String.valueOf(System.currentTimeMillis()); String signSource appId timestamp secretKey; String sign Md5Util.md5(signSource); result.sampleStart(); String response HttpClientUtil.get(baseUrl /api/demo?appId appId timestamp timestamp sign sign); result.sampleEnd(); result.setResponseData(response, UTF-8); result.setSuccessful(true); } catch (Exception e) { result.sampleEnd(); result.setResponseData(ERROR: e.getMessage(), UTF-8); result.setSuccessful(false); result.setResponseMessage(e.getMessage()); } return result; } Override public void teardownTest(JavaSamplerContext context) { // 释放连接池等资源 } }这里有几个关键细节。第一sampleStart()和sampleEnd()之间的时间会被记录为SampleResult的响应时间一定要把真正的请求执行代码放在这两个方法之间不要把签名计算时间也计入响应时间否则压测数据会失真。第二setSuccessful(true)必须显式调用别指望默认值因为很多奇怪的现象都来自于状态没设对监听器那边看到的全是灰色或红色的奇怪条目。签名算法往往有个坑时间戳对齐。客户端和服务端的时间如果不同步签名会验不过。实际项目里我们一般从服务端取时间接口拿标准时间再参与签名而不是取本机时间。这个逻辑在setupTest里先做一次后面每个请求可以直接用不需要重复请求时间接口。接口调用这块我建议直接用java.net.http.HttpClient或者Apache HttpClient不要用Jmeter内置的HTTP Sampler来发请求。为什么因为你自己写Sampler的目的是构造特殊签名报头内置HTTP Sampler没法精确控制每一个字节。并且内置HTTP Sampler会和Jmeter代理配置纠缠不清容易混入代理设置。3.3 打包、部署与调试流程写完代码后打包之前建议做两件检查一是确认没有把provided依赖打进去二是确认没有用maven-shade-plugin把所有依赖打进同一个jar。除非你的第三方库Jmeter确实没有否则尽量不要搞fat jar因为jar包之间的同名类冲突非常难查。打包用标准的mvn clean package在target目录找到xxx.jar复制到Jmeter安装目录的lib/ext下重启Jmeter。注意如果Jmeter正在运行先关闭再拷贝JVM不会热加载新jar。重启后在测试计划里添加“Java请求”采样器下拉框选择com.example.jmeter.sampler.DemoSignSampler参数面板里会显示getDefaultParameters定义的那些参数。填好参数加上监听器直接运行。如果能看到采样器绿色通过就说明部署成功。如果运行报错最常见的两类一是在GUI的日志面板看到ClassNotFoundException那就是jar没放对位置或类路径不对二是有NoSuchMethodError或NoClassDefFoundError那就是依赖冲突。顺着这个思路查比盲改代码高效得多。4. 轻量级二次开发JSR223脚本与BeanShell的深度用法写Java Sampler是重武器但实际项目里80%的场景用脚本就能解决。JSR223和BeanShell是Jmeter里两种脚本化方案很多人把这两个概念混着用其实差距不小。4.1 BeanShell与JSR223怎么选这里先给一个对比表格你在选型的时候可以直接参考对比项BeanShellJSR223groovy启动速度慢每次执行都要解释快groovy脚本编译后有缓存性能开销高压测线程多时明显拖慢低高并发下更稳定语法支持简化版Java语法完整groovy语法兼容Java内置变量访问vars、props、ctx、prevvars、props、ctx、prev、log、OUT官方推荐度官方文档已明确不推荐新脚本使用官方推荐JMeter 3.2之后默认支持我在实际压测里的体会同一段逻辑BeanShell写出来跑1000个用户时线程耗时明显偏高换groovy脚本后相同场景下CPU占用直接降了一截。原因就是groovy脚本引擎有编译缓存重复执行时不用反复解析脚本源文本这也是官方把JSR223作为首选的根本原因。JSR223里的内置对象要记牢vars可以读写Jmeter变量props可以读写全局属性ctx能拿到当前采样器上下文和线程组信息prev能拿到上一个采样结果log能直接输出日志到Jmeter日志文件。你在脚本里写vars.put(token, abc)下游采样器就能用${token}引用它。这个读写模型是脚本化二次开发的核心比直接用Java代码改内部变量优雅得多。4.2 动态QPS调整从线程数控制到定时器思路热搜词里有个jmeter 动态调整QPS bshclient这个场景我当年也折腾过。通常压测时QPS上不去或上太快都需要动态调整并发线程数。直接改测试计划里线程组的线程数是做不到的因为线程组参数在启动时就固化了。但JSR223脚本里可以通过ctx.getThreadGroup()拿到当前线程组对象再用JMeterUtils.getJMeterEngine()拿到引擎来动态调整。一个简单合法的思路是在JSR223 Sampler里根据当前压测阶段动态修改线程数。比如规定压测0到5分钟跑100线程5到10分钟跑200线程脚本可以这么写long now System.currentTimeMillis(); long elapsedMinutes (now - startTime) / 60000; int targetThreads elapsedMinutes 5 ? 100 : 200; JMeterUtils.setProperty(THREADS, String.valueOf(targetThreads));实际上更稳的方式是结合Constant Throughput Timer或者Throughput Shaping Timer插件来做精确的RPS控制。毕竟线程数只是影响QPS的一个因素还有思考时间、响应时间、业务逻辑复杂度等。你调整了线程数QPS也不一定线性变化。如果追求确定的QPS曲线建议用Throughput Shaping Timer配Concurrency Thread Group直接按时间表设定目标吞吐量Jmeter会自动加减并发来逼近目标值。还有一类需求是“动态开关某些线程组”。比如压测过程中想临时加一个混合场景不能重启Jmeter。通过JMeterUtils.getJMeterEngine().stop()可以停止整个测试stopTestNow()可以立即中断。如果你只想暂停或恢复某个线程组可以通过ctx.getEngine()拿到引擎再对线程组做调度不过这块比较绕我自己更倾向于在压力机上控制场景切换用脚本启动不同jmx文件方便排查。4.3 断言与数据处理脚本技巧另一个高频需求是复杂断言。官方响应断言只能做文本包含、正则匹配这类基础操作一旦要校验JSON里的数字范围、数组长度、嵌套字段关系就得用脚本化断言。JSR223断言里有个方便之处能直接拿到当前采样结果和响应内容。下面这段groovy脚本可以用来解析响应并做多层断言import groovy.json.JsonSlurper def response prev.getResponseDataAsString(); def json new JsonSlurper().parseText(response); assert json.code 200 : 业务code不是200; assert json.data.list.size() 10 : 列表数量不足10条; assert json.data.total 0 : total字段必须大于0;如果断言失败直接用AssertionResult.setFailure(true)或者让groovy脚本抛出异常。JSR223断言失败后采样结果会标记为失败监听器里能看到红点。这里有个细节脚本里不能用return退出因为JSR223引擎执行的是脚本体不是方法体。抛异常是最直观的失败方式。还有一种场景是加密参数生成。比如接口需要AES加密报文官方前置处理器做不了。我在JSR223 PreProcessor里用groovy写了完整的AES加解密逻辑加密完写入vars下游HTTP采样器用${encryptedData}引用。这样既不用编译Java又能灵活调整算法压力机的启动成本也不高。不过要提醒一句如果算法复杂度高、执行次数多groovy代码的性能也会影响压测机资源。建议把公用的加密逻辑写成一个groovy脚本文件放到bin目录下用GroovyShell加载比每个采样器复制一份脚本好维护得多。5. 常见问题与排查技巧实录二次开发踩坑是常态我把这几年遇到最多的问题整理成一个速查表你以后遇到类似问题可以直接对照排查。现象可能原因排查方向Jmeter启动就报ClassNotFoundException自定义jar没放对位置检查jar是否在lib/ext目录类名是否拼写正确采样器下拉框看不到自定义类类没实现JavaSamplerClient接口或接口没写全确认继承的是AbstractJavaSamplerClient运行时报NoSuchMethodError依赖版本冲突检查Jmeter自带依赖版本与你的编译版本是否一致尽量用provided脚本跑得慢或CPU飙升用了BeanShell或脚本里有重逻辑换成JSR223groovy把重计算挪到服务端响应数据中文乱码响应编码没设置result.setResponseData(bytes, UTF-8)显式指定编码压测机时间与服务端不同导致签名失败客户端时间不准从服务端纳时后再签名或放宽时间戳窗口脚本里vars取值为null变量作用域不对或脚本执行顺序问题确认参数在脚本之前已被赋值检查采样器执行顺序动态调整线程数不生效线程组配置不允许运行时修改结合Throughput Shaping Timer或使用并发线程组插件5.1 代码层面的高发问题第一高发问题就是打包了重复依赖。有同事把ApacheHttpClient打进了fat jar结果压测时旧版本类被加载报了一堆莫名其妙的AbstractMethodError。排查了一整天最后发现是maven-shade-plugin把所有依赖都打进去了里面有两份org.apache.http的类。解决办法很简单把依赖设为provided或者排除Jmeter自带的那些库。第二高发问题是把setupTest当成全局初始化用。前面说过它是按线程执行的如果你在setupTest里创建连接池每个线程都会创建一份很容易把服务端连接数打爆。我见过一个案例测试目标服务本身只支持500个连接自定义Sampler在每个线程里都开了50个连接的池子20个线程就把服务端打崩溃了。如果你的连接池需要跨线程共享用static加锁或者放到JmeterSearchByClass机制里去管理。第三高发问题是脚本里对响应断言时忘了解析异常。groovy的JsonSlurper解析非JSON响应会抛异常异常没被捕获的话整个测试线程会中断。正确写法是包一层try-catch解析失败时明确标记断言失败而不是让异常扩散到线程外面。我在压测中遇到过“突然所有线程停止”的情况最后查到是一个非法JSON响应把脚本线程干掉了。5.2 环境与部署层面的排查关于Jmeter本身的启动参数二次开发后遇到内存溢出是常事。Jmeter默认堆内存不大你要在jmeter.bat或jmeter.sh里调整HEAP参数比如设置-Xms2g -Xmx4g。如果脚本里创建了大量对象还不释放GC压力会直接拖垮压测结果。我一般习惯在运行压测时加-XX:PrintGCDetails观察GC频率如果GC时间占比超过10%就要反思脚本逻辑了。部署插件时还有个细节容易被忽略Jmeter启动时会扫描lib/ext目录下的所有jar如果你的jar包里有同名类谁先被加载是不确定的。为了避免这种不确定性我建议自定义类名加公司域名前缀比如com.yourcompany.jmeter.sampler.xxx。这样既能避免类名冲突也方便团队里其他人一眼看出哪些是自定义组件。另外Jmeter 5.6系列更新后部分API有点变化。你在网上找的旧Demo可能编译不过比如setResponseData的传参类型、JMeterUtils的包名调整。遇到编译报错时先看Jmeter lib目录下jar包的源码版本再去对应源码里查方法签名。我的习惯是直接用IDEA反编译Jmeter源码方法签名一目了然比翻旧博客快得多。5.3 性能与资源优化的实战心得二次开发组件本身也要有性能意识。Java Sampler里我踩过一个大坑每跑一个请求就new一个HttpClient压测到400并发时压力机出现大量TIME_WAIT连接目标服务端响应时间飙升。后来改成连接池复用问题才消失。压测工具自己性能差实验结果完全不可信。线程数越高脚本化的性能差距越明显。我的经验门槛是并发超过200时尽可能避免BeanShell能用JSR223就用JSR223。如果并发超过1000连groovy都要慎用关键路径上的逻辑应该挪到Java Sampler里或者干脆不放在脚本中。Jmeter的扩展性终究有限不要在脚本层做应用层该做的事。日志输出也要克制。groovy脚本里写log.info(xxx)在低并发时没什么感觉高并发时会疯狂刷日志I/O开销直接体现在响应时间上。压测过程中真正需要看的日志用采样器计数器控制打印频率比如每1000次请求打一条摘要日志而不是每条都打。6. 从二次开发到测试框架的演进路线当你把自定义Sampler和脚本跑顺之后往往会发现纯Jmeter的工程化能力偏弱。版本管理靠jmx文件本身参数管理靠GUI手填断言逻辑散落在各个脚本节点里。这时候就要开始做框架层的东西了。6.1 基于Maven管理jmx与脚本我现在的做法是把jmx文件、groovy脚本、jar包统一放在Maven工程里管理用src/main/resources存放脚本资源自定义插件放在独立模块jmx文件用src/test/jmx统一维护。这样代码变动、脚本变动、压测场景变动都能纳入版本控制回退任何一个历史版本都容易。压测参数不要写死在jmx里。用Jmeter的-J参数从命令行注入比如jmeter -n -t xxx.jmx -Jthreads200 -Jduration300。jmx文件里的线程数位置写成${__P(threads,100)}这个语法是读取Jmeter属性并带默认值。这样同一个jmx文件可以在不同环境、不同压力等级下复用不用维护三份差不多的文件。6.2 测试结果与监控平台的对接自定义Listener是把压测结果往监控平台推送的正规路子。我做过一个场景压测任务跑完后结果要汇总到内部性能看板。直接在监听器界面上看肯定不行因为分布式压测时多台压力机的结果是分散的。一个简单方案是JSR223 Listener配合HTTP接口上报另一个方案是写一个自定义SampleListener实现类在sampleOccurred事件里把SampleResult序列化后发给看板后端。需要注意的是如果要压测结束后统一汇总再上报可以用testEnded回调来触发避免每条样本都发一次HTTP请求把监控后端打挂。这个设计思路我强烈建议在动手前先画清数据流向否则很容易写出“压测没问题监控挂了”的尴尬代码。7. 一次完整的二次开发实战复盘说了这么多我想用一次真实经历把这个过程串起来你照着这个思路就能少走弯路。项目背景是某内部交易系统要做一次峰值压测核心接口要求请求头携带动态签名签名由appId timestamp nonce bodyHash拼接后做HMAC-SHA256。官方HTTP Sampler无法动态计算bodyHash再重置请求数据用BeanShell写吧又担心高并发下脚本解释开销太大。最后我选型为自定义Java Sampler加JSR223前置处理器的组合方案。第一步先把签名逻辑抽成纯Java工具类在Sampler里复用第二步实现getDefaultParameters把appId、secretKey、服务地址全部参数化方便不同环境切换第三步在setupTest阶段初始化连接池和nonce生成器避免运行时频繁创建第四步写JUnit单元测试验证签名工具类确保本机算出的hash和服务端验签结果一致第五步打包丢进lib/ext用一个含100个参数的压测场景跑小并发验证第六步确认功能正常后把jmx里的线程组设计成阶梯模式用Throughput Shaping Timer控制RPS曲线跑完整压测。这个过程中最大的教训是签名工具类千万别在工程里改完就忘加字段或改算法后要立刻回归验证不然后面压测报告出来数据无法解释。我那次就是改了一个排序逻辑后忘了本地验签结果前半小时的压测数据全是签名失败等于白跑。你可以在teardownTest里打印当前线程成功请求数和失败数做一份粗粒度的自检日志压测完先看自检日志再决定是否信任整体报告。如果不用Java Sampler我其实也有替代方案所有逻辑写在JSR223前置处理器里用import引入自带的javax.crypto.Mac类做HMAC逻辑也不复杂。但相比自定义Sampler脚本方案的问题是每次执行都有groovy编译缓存命中率的问题并且在GUI里不易调试。各有利弊按团队维护成本选就行。8. 给新手的一句话总结与扩展思路写到最后我不打算再堆一堆所谓的“精华总结”就说两件我个人最深的体会。第一件二次开发的价值不在于你会写Java代码或会写groovy脚本而在于你知道哪个扩展点该做什么、哪些事不该交给Jmeter做。边界感比coding能力重要得多。你花两周写一个复杂Sampler可能不如把业务逻辑前置到服务端、再用标准HTTP Sampler压测来得可靠。第二件先从JSR223groovy入手练手不要一上来就啃源码、写插件。groovy脚本能解决大多数动态签名、断言、参数处理的问题等你把Jmeter的执行模型、变量作用域、扩展点机制都摸透了再动手写Java Sampler你会发现很多之前“看不懂”的东西其实就那么回事。至于后续扩展方向我建议你可以沿着这条路线走先熟悉JMeterContext和vars/props/ctx这几个核心对象再研究自定义函数AbstractFunction然后尝试开发自己的可视化Listener插件最后把整套东西用Maven工程串起来统一构建、统一部署。真到了那个阶段你就不需要看任何Jmeter二次开发教程了因为官方源码就是最好的老师。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →