如果今年我只能给 JMeter 新手推荐一个取样器,我一定选 JSR223。原因很简单:压测场景里那些“卡脖子”的动态参数、复杂断言、数据加工,最后基本都是靠它兜底的。这套《Jmeter取样器之JSR223取样器详解》系列能写到第 7 篇,也说明这个取样器值得反复啃。
上个月我刚做完一次真实的迁移后压测验证:单节点 k8s 上的若依微服务整套环境,要求准不停服、不丢数据地迁移到阿里云 ECS。迁移完成后,压测同事用一套配套的 JMeter 脚本跑高并发,验证云上环境到底能扛住多少流量。这套脚本里最有含金量的部分,恰恰都落在 JSR223 取样器上。所以这一篇,我结合这次实战,把 JSR223 取样器的用法、内置变量、参数化思路、排查技巧一次讲透。
无论是刚接触 JMeter 的小白,还是已经在做接口测试、性能测试的工程师,这篇文章都能让你少踩几个坑。
1. JSR223取样器到底怎么用才不拖垮压测性能
1.1 为什么压测脚本里JSR223是主角
很多人一开始做压测,习惯用 HTTP 请求默认值加一堆“录制回放”的脚本。这种思路对付纯静态页面还行,一旦涉及真实业务就崩。比如若依这种前后端分离的微服务项目,接口之间有严格的登录态校验、权限控制、动态 Token,有的接口还需要对时间戳和参数做签名,纯录制回放根本走不通。
JSR223 取样器厉害在哪?它允许你在取样器内部执行一段脚本,这段脚本可以是 Groovy、JavaScript、Beanshell(虽然不建议)、Python 等 JVM 上能跑的语言。脚本可以访问 JMeter 的上下文,能动态生成请求参数、修改请求体、读取响应结果、控制逻辑流程、写日志。相当于把一个原本只能发固定请求的“机械臂”,升级成了有逻辑处理能力的“机器人”。
在这次若依迁移压测里,最核心的场景就是登录拿 Token 然后访问业务接口。若依登录成功后会返回加密后的 Token,后续所有请求都要在 Header 里带上这个 Token。如果只是录制脚本,Token 是固定死的,压测跑几分钟就失效,后面的请求全是 401。用 JSR223 取样器,就可以在每轮迭代或每个虚拟用户开始时动态请求登录接口,解析出 Token 并传给后续请求,这样压测才能真正模拟真实用户行为。
1.2 JSR223和BeanShell怎么选
JMeter 里有个老牌的脚本取样器叫 BeanShell,很多老教程都在用。但如果你要压测高并发,我真的劝你尽早放弃 BeanShell,换 JSR223 + Groovy。
我第一次用 BeanShell 写脚本时觉得挺方便,直到有一次压测 200 并发,施压机自己的 CPU 直接打满,报错全是一堆等待超时。后来才发现,BeanShell 每一次执行都要启动脚本解释器,性能开销特别大。JMeter 官方从 3.1 版本开始就明确建议使用 JSR223 + Groovy,并且强调要开启脚本缓存。
JSR223 的优势体现在两个地方:第一,它基于标准 Java Scripting API,与 JMeter 集成更好;第二,Groovy 脚本可以编译缓存,只要脚本内容不变,后续执行直接复用编译结果,性能比 BeanShell 高一个数量级以上。
实际选型时,我的建议很直接:
- 能用 Groovy 别用 Beanshell,除非你维护的旧脚本里全是 Beanshell,短期内不打算重写。
- 只做简单参数赋值,用 JMeter 自带的函数也好,没必要上脚本。
- 一旦涉及循环、条件判断、字符串处理、正则提取、JSON解析,直接上 JSR223。
另外,在 JDK 9 以上的环境里,BeanShell 偶尔会出现奇怪的 ClassNotFound 异常,排查起来非常痛苦。Groovy 虽然在类加载上偶尔也有坑,但整体可靠太多。
2. JSR223取样器核心概念与内置变量解析
2.1 界面参数与常用语言配置
JSR223 取样器的界面看起来简单,但里面有几个配置点很容易被忽略。逐个说:
- 名称:写清楚这个取样器在干什么,比如“JSR223-登录并提取Token”。压测脚本一多,命名清晰能省很多时间。
- 语言:建议选 Groovy。JMeter 插件包里自带 Groovy 支持,不用额外装环境。
- 参数:这里可以传递一些静态或半静态参数给脚本。比如在“参数”框里写
username=admin password=123456,脚本里可以用Parameters这个变量接收,再自行解析。 - 脚本:真正写逻辑的地方。
- 缓存编译脚本:这个选项一定要勾上。勾选后,脚本内容不变时,JMeter 不会重复编译。我在压测时经常发现,没勾缓存时线程数一高,引擎线程全卡在编译上。勾上之后性能立刻稳定下来。
另外,JMeter 5.x 里还有一个隐藏坑:如果你在脚本里调用了外部 jar 包里的类,首次运行大概率会报类找不到。原因不是你的 jar 没放对,而是 Groovy 脚本的类加载器在缓存模式下不会自动刷新。解决办法是去掉缓存编译,或者把 jar 放到 JMeter 的 lib/ext 目录后重启 JMeter,保证类能被正确加载。
2.2 vars、props、ctx、prev四个内置对象怎么用
JSR223 脚本最核心的就是四个内置对象,初学阶段把这四个对象玩明白,脚本就入门了。
vars是当前线程的变量容器,可以把它理解成每个虚拟用户自己的小本子。vars.put("token", "abc")存一个值,vars.get("token")取一个值。注意:这里是字符串,取值出来永远是 String。如果要在脚本里做计算,用vars.get("count").toInteger()之类的转换。
props是全局属性容器,所有线程共享,可以理解为 JMeter 进程级别的公共黑板。常见的用途是统计全局错误数、保存开关状态。举个例子,你可以用props.put("errorCount", count)在多个线程里累加错误,最后在 JSR223 断言或后置处理器里读出来。
ctx是 JMeterContext 对象,能拿到当前线程组、当前取样器、当前变量等完整上下文。这个对象平时用得不多,但在做复杂逻辑控制、动态修改取样器属性时非常有用。
prev是上一个取样器的 SampleResult,也就是取样器的执行结果。你可以通过prev.getResponseCode()拿到 HTTP 状态码,prev.getResponseDataAsString()拿到响应体字符串,prev.getTime()拿到耗时。这个对象是做断言、提取数据的核心入口。
写一个最常见的登录提取代码示例:
import groovy.json.JsonSlurper // 获取前一个HTTP取样器返回的响应体 def response = prev.getResponseDataAsString() def json = new JsonSlurper().parseText(response) // 假设响应体结构是 {"code":200,"token":"xxxx"} if (json.code == 200) { vars.put("token", json.token) log.info("Token获取成功: " + json.token) } else { log.warn("登录失败,响应: " + response) }这段代码就是若依迁移压测脚本里登录逻辑的核心。响应体解析用 JsonSlurper,比正则提取可靠得多,而且性能不错。
2.3 动态参数和加签逻辑怎么落地
压测时最烦的就是动态参数。比如有的接口要求请求体里必须带一个timestamp,还得把 URL 参数按字典序排序后拼接一个sign字段。用 JMeter 自带函数处理这种逻辑非常痛苦,因为你没法在一个表达式里做排序和拼接。但 JSR223 里就是用 Groovy 写一段代码的事。
我在若依的压测脚本里就写过一个签名工具类逻辑,放在 JSR223 预处理脚本里。先通过vars拿到请求参数,然后排序、拼接、MD5 加密,最后把签名结果放回vars,HTTP 请求里直接引用${sign}。整个流程 10 行代码以内搞定。
要注意的是:签名逻辑不要在每个请求里复制粘贴,而是应该封装成一个 Groovy 方法,或者放进 JSR223 的“初始化脚本”里统一加载。JMeter 的 JSR223 取样器支持“初始化脚本”选项,在脚本第一次执行前会执行一次初始化代码。把公共方法放在初始化脚本里,性能更好,代码也更干净。
3. 压测若依微服务:登录态、参数化与数据校验
3.1 登录拿Token的脚本写法
若依微服务的登录流程一般是:前端把用户名和密码提交到/login接口,后端校验成功后返回一个 Token。压测时要模拟大量用户,不能所有用户共用一个账号,否则后端会限流或封禁。我当时用的是 CSV 文件存放测试账号,每个线程读一行,动态登录。
具体实现分三步:
第一步,在测试计划里放一个 CSV Data Set Config,配置好账号文件的路径、变量名username、password。重点是设置“共享模式”为“当前线程组”,确保每个线程拿到的数据不重复,避免压测时并发获取到同一行数据。
第二步,添加一个 HTTP 取样器,发送 POST 请求到/login,请求体用${username}和${password}引用 CSV 里的账号。这里不需要硬编码任何东西。
第三步,在 HTTP 取样器后面加一个 JSR223 后置处理器,脚本里解析响应、提取 Token 并存入vars。后续所有业务请求的 Header 里都用${token}引用。
这里一个关键细节:若依项目如果开启了验证码,压测环境最好通过配置关闭验证码功能,否则脚本里还要处理 OCR,成本太高。如果没法关,可以在 JSR223 脚本里调用后端验证码生成接口,然后通过 OCR 识别,再把验证码值传给登录请求。我一般建议压测前就找开发把验证码关掉或者改成万能验证码,这是最省事的做法。
3.2 数据库查询结果参数化到下一个接口
压测过程中经常需要从数据库查出批量数据,比如一批用户 ID、订单 ID,然后把这些数据作为下一个接口的入参。这是 JDBC Request 参数化的经典场景,也是网上搜“jmeter将jdbc request查询出的数据作为下一个接口的参数”最常见的问题。
我在这次若依压测里,需要构造一批带真实业务数据的流程:先查数据库里已存在的项目 ID,再对每个项目 ID 发起详情查询请求。
推荐的做法有两种:
第一种是用 JDBC Request 配置查询 SQL,比如SELECT id FROM sys_project WHERE del_flag='0' LIMIT 100,在 JDBC Request 的“变量名”里填projectId。查询结果会生成形如projectId_1、projectId_2这样的变量,后面的请求可以用${projectId_1}引用。
这里有个大坑:JDBC Request 默认返回多行结果,但如果你不配置Result variable name(结果变量名),后面的 JSR223 脚本里根本拿不到完整的 List 结果。正确的做法是在 JDBC Request 的高级设置里填一个变量名,比如resultList,然后在 JSR223 脚本里这样取:
def rows = vars.getObject("resultList") rows.eachWithIndex { row, index -> // 每行row的类型是HashMap,可以用row.get("id")取字段 vars.put("projectId_" + index, row.get("id").toString()) }第二种方式,直接在 JSR223 脚本里用 Groovy 连接数据库,查询数据并放入vars。这种方式更灵活,适合查询结果需要二次处理的场景。但要注意,脚本里写的数据库密码会暴露在脚本里,建议通过 JMeter 的props或环境变量读取,避免明文。
我自己更倾向于第一种方式,因为 JDBC Request 的配置可视化,填 SQL 和变量名比较直观,压测脚本其他人接手维护时也容易懂。
3.3 上传文件与唯一文件名处理
若依后台管理项目里的素材上传接口,是压测脚本里容易被忽略的难点。上传接口要求 multipart 请求里带一个文件,同时文件名不能和系统里已存在的重复。用 JMeter 的上传文件功能配合 CSV 参数化,可以准备一批不同的文件名,但问题是如果测试反复跑,第二次跑就会提示文件已存在。
解决思路是在 JSR223 脚本里动态生成一个唯一文件名。常见做法是用时间戳加随机数:
import java.text.SimpleDateFormat def timestamp = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) def random = new Random().nextInt(9999) vars.put("uploadFileName", "压测图片_" + timestamp + "_" + random + ".jpg")然后在 HTTP 请求的文件上传表单里,文件名称栏填写${uploadFileName}。这样每次执行都会生成一个新文件名,不会冲突。
如果你还要校验上传后的文件确实存在,可以在上传请求后面加一个 JSR223 取样器,解析上传接口返回的 URL 或文件 ID,再用 HTTP 请求去访问这个资源,断言响应码是否为 200。这一步很重要,能验证文件是不是真的传上去了,而不只是请求发出去就算成功。
3.4 安全证书和HTTPS脚本录制问题
JSR223 和 HTTPS 本身不冲突,但我发现很多刚上手 JMeter 的同事,第一步就被证书卡住了。报错信息通常是“PKIX path building failed”或者“unable to find valid certification path”。如果你只是想做压测,不想深究证书体系,最简单的办法是把 JMeter 的jmeter.properties里这两行改掉:
server.rmi.ssl.disable=true httpclient.default.ssl.protocol=TLSv1.2或者直接在测试计划里启用“Java”作为 HTTP 采样器实现,并把 HTTPS 证书校验关掉。我在真实验证云上环境时,都是通过 JMeter 选项里的“SSL Manager”导入服务端证书,或者让开发把测试环境的 HTTPS 临时改成 HTTP,压测完成后改回去。这里要注意,改协议会影响响应时间指标,所以压测报告里要注明当时用的是 HTTP 还是 HTTPS。
4. 高并发压测中的常见坑与排错实录
4.1 并发报告的数据怎么读才准
压测完成后,很多人第一件事就是看聚合报告里的 Average 响应时间。但 Average 很容易被长尾请求带偏,90 个请求都很快,10 个请求特别慢,平均下来数据就不好看了。我在这次若依迁移验证里,重点看的是三个指标:Throughput(每秒事务数)、Error%(错误率)、P95/P99(95%和99%的响应时间分位数)。
说个具体的:压测同事第一次跑完报告,Average 显示 800ms,看表面数据好像还能接受,但 P95 已经到了 2800ms,说明系统在长尾请求上性能很差。后来排查发现是应用服务器的数据库连接池太小,高峰并发时线程都在等连接。这个问题如果只看 Average,根本发现不了。
所以我的建议是:聚合报告里不仅要看平均响应时间,还要看 90% Line、95% Line、99% Line。JMeter 自带的聚合报告里有这些字段,直接用就行。如果想更直观,可以装 jp@gc 系列的响应时间百分位监听器。
4.2 常见异常速查表
压测脚本跑起来之后,报错主要集中在几个固定位置。我把自己踩过的坑整理成一张表,方便你直接对照排查。
| 报错信息 | 常见原因 | 排查方法 |
|---|---|---|
| java.io.IOException: error writing to server | 客户端到服务端的连接被断开,通常是服务端主动关闭了 keep-alive 连接 | 在 HTTP 请求里勾选“KeepAlive”,或减少单个线程的请求间隔时间 |
| PKIX path building failed | HTTPS 证书校验失败 | 导入证书,或临时关闭 JMeter 的证书校验 |
| __RequestVerificationToken 未提供必要的防伪标记 | 某些 Web 框架要求请求头携带防伪 Token | 先用 JSR223 脚本 GET 登录页面提取 Token,再在 POST 请求 Header 里带上 |
| 提示文件已经存在 | 上传文件名重复 | 使用 JSR223 生成唯一文件名 |
| 同一个CSV参数化文件里每个线程重复取值 | CSV Data Set Config 的共享模式配置不对 | 将共享模式从“所有线程”改为“当前线程组” |
| 数据库参数化取值取不到 | JDBC Request 没有配置结果变量名 | 在 JDBC Request 高级设置里填结果变量名 |
| JMeter界面字体太小 | 高分辨率屏幕下 JMeter 默认字体不够大 | 修改 jmeter.properties 里的jmeter.hidpi.mode和字体大小参数 |
表格里这几类问题,几乎每一类我都亲自踩过。特别说一下error writing to server,这种报错在压测刚开始不出现,跑到一半突然大量冒出来,通常不是脚本问题,而是后端服务在压力下关闭了空闲连接。这时别急着改脚本,先看服务端的连接池配置和日志。
4.3 “不停服、不丢数据”迁移对压测脚本的要求
这次若依迁移后压测验证,和普通的压测有一点本质不同:业务要求准不停服、不丢数据。这意味着压测脚本不能只跑单个接口,也不能用静态数据把后端某些缓存全部打穿;它要尽量贴近真实用户行为。
所以我在脚本设计上做了三件事:
第一,构造完整业务流。压测路径是登录、查询列表、查看详情、提交表单、上传文件、退出登录,整条链路走完才算一个完整事务。这样做的好处是能验证迁移后各个模块之间的配合,而不是只验证某个接口的吞吐量。
第二,准备基础数据。高并发压测前,先从数据库准备一批真实可用的项目数据、账号数据。否则压测跑到一半,后端返回“数据不存在”,错误率看着很高,但其实是你的测试数据没造够。
第三,阶梯式施压。不要一上来就 100 个用户并发。我先从 10 个并发跑 2 分钟,看服务响应时间和错误率;然后逐步加到 30、50、80、100。每次加压后观察 1 分钟,确认稳定后再继续加。这种做法能帮你在压测过程中尽早发现性能拐点,而不是等全部跑完再分析结果。
还有一个细节:压测机的性能也会影响压测结果。如果单台机器跑不了高并发,一定要用分布式压测,否则漏报大量超时不说,压测结果也不能真实反映服务端承载能力。
4.4 JSR223脚本性能优化与常见写法误区
JSR223 脚本虽然强,但写不好反而会成为压测瓶颈。我见过不少同事在脚本里写了一个复杂正则,每次请求都重新编译一遍,导致施压机 CPU 直接爆掉。
几个核心优化原则:
- 能用 JMeter 内置函数或后置处理器解决的事,不要全部写进 JSR223。比如简单的 JSON 提取,用 JSON Extractor 配置起来更快,性能也不差。
- 脚本里避免创建大量临时对象,能复用就在初始化脚本里提前创建。
- 避免在循环里使用
vars.get()和vars.put()太频繁,因为每一次调用都有上下文切换成本。可以先把值取到局部变量,循环结束后再一次性写回vars。 - 开启脚本缓存编译,这是最简单也最容易忽略的性能提升手段。
写法误区方面,最常见的是把 JSR223 取样器当成万能工具,一个脚本里塞了几百行代码,既不好维护,也拖慢执行。正确的思路是:一个 JSR223 取样器只负责一个明确的小任务,任务多了就拆成多个取样器,通过变量串联。
5. 我个人的经验体会
这套若依微服务从单节点 k8s 迁移到阿里云 ECS 的压测验证,前后跑了三轮才拿到稳定的报告。第一轮脚本写得太重,施压机自己先挂了;第二轮数据准备不足,错误率虚高;第三轮把 JSR223 脚本优化清楚、数据铺好、阶梯加压跑通,才算真正反映了云上环境的承载能力。
回头来看,JSR223 取样器不是万能的,但它在 JMeter 里确实扮演着“万能胶水”的角色。登录态管理靠它、签名逻辑靠它、数据库参数化靠它、复杂断言靠它,几乎每个关键环节都需要它。它也并不是越复杂越好,写得清晰、可维护、能复用,才是它最重要的价值。
如果你现在正准备用 JMeter 做压测,我建议你花一晚上把 JSR223 + Groovy 的内置对象过一遍,尤其是 vars、prev、ctx 的用法。剩下的问题,大多都能在跑一次真实业务压测后自己摸清楚。