JMeter 的 JSON 提取器设置¶
JSON Extractor 是 JMeter 里最容易被“看起来配了”骗过去的元件:界面填满了,变量却没生成;变量生成了,值却带着引号;值对了,数字又被改成了科学计数法。下面按“现象 → 根因 → 配置 → 验证 → 坑位”的顺序拆开讲,环境为 JMeter 5.6.3 + JDK 17,5.4/5.5 的差异单列说明。
一、先看三个真实故障¶
故障 1:登录 200,下单全 401¶
登录响应本身是正常的:
HTTP/1.1 200 OK
Content-Type: application/json
{"code":0,"msg":"ok","data":{"token":"eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjEwMDg2fQ.x8Kq","user":{"id":10086}}}
下单请求的头却是:
服务端原样返回:
同时 jmeter.log 里有一行:
2026-09-08 10:41:07,218 ERROR o.a.j.e.j.j.JSONPostProcessor: Error processing JSON content in JSON Extractor - login, message: No results for path: $['data']['token']
三条信息拼起来只有一个结论:access_token 这个变量根本没被创建。JMeter 对未定义的变量不做替换,会把 ${access_token} 这 15 个字符原样发出去,所以不是“取到了空值”,而是“压根没取”。
故障 2:token 取到了,但带着一对引号¶
Debug Sampler 输出:
请求体于是变成:
服务端签名校验报 invalid signature。这不是提取器“多加了引号”,而是表达式写成了 $.data——它返回的是一个 JSON 对象,JMeter 存的是对象的 JSON 文本,引号是对象自带的。
故障 3:金额 10000000.00 变成 1.0E7¶
Debug Sampler 输出 amount=1.0E7,断言 amount == 10000000.00 失败,对账接口拒收。根因在 Jayway JsonPath 的数值处理:带小数点的数字被解析成 Double,JMeter 用 String.valueOf() 落成字符串,Java 对 >= 10^7 的 double 输出科学计数法。
二、根因:提取器只负责取值,不负责报错¶
先把职责边界钉死,后面所有现象都能推出来:
| 行为 | 真实表现 |
|---|---|
| 没匹配到 | 写入 Default Values;没填默认值则变量不创建(不是空串) |
| 没匹配到是否失败 | 不失败,采样器仍是 200/success,只有日志里有 ERROR |
| 类型处理 | 一律 toString(),不做金额、时间、整型格式化 |
| 转义处理 | 不做 URL 编码、不做 JSON 转义 |
| 作用范围 | 线程级变量,跨线程组读不到 |
| 执行时机 | 在该采样器响应返回之后、下一个采样器之前 |
所以正确姿势是:提取器配默认值兜底,用断言判空,绝不能让下游请求带着 ${var} 字面量出去。
三、五个字段逐字解释¶
| 字段 | 真实语义 | 高频踩坑 |
|---|---|---|
| Names of created variables | 变量名列表,英文分号 ; 分隔,与表达式按位置一一对应 |
写成 ${token};多写空格;数量与表达式错位 |
| JSON Path expressions | Jayway JsonPath 表达式列表,分号分隔 | 前后带空格;中文引号;$data.token 少点 |
| Match No. | 0=随机一个,-1=全部,n=第 n 个,空=1 | 用 0 导致结果不可复现;-1 后仍引用 ${var} |
| Default Values | 仅“某条表达式无匹配”时使用,分号分隔,可为空 | 填了默认值就以为不会失败;值里含分号 |
| Compute concatenation var | 勾选后额外生成 <varName>_ALL,逗号连接所有匹配 |
与 -1 混用后拿 _ALL 当 JSON 数组用 |
3.1 Names of created variables¶
- 多个名字用分号分隔:
access_token;uid;orderNo。 - 某个表达式暂时不需要,必须留空占位:
access_token;;orderNo,直接少写一个会导致后面全部错位。 - 名字里不能出现
${}、空格、中文括号;建议只用字母、数字、下划线。 - 同一线程内重名会静默覆盖,登录提取器和下单提取器都叫
token时,排查会非常痛苦,建议加前缀login_token、order_token。 - 5.x 变量名大小写敏感,
Token和token是两个变量。
3.2 Match No. 的 0 / -1 / 1¶
| 取值 | 行为 | 适用场景 |
|---|---|---|
| 空 | 等价于 1 | 绝大多数单值场景 |
| 1 | 取第一个匹配,写入 ${var} |
登录 token、单个订单号 |
| 2 / n | 取第 n 个匹配,写入 ${var} |
列表页取第二条做详情 |
| 0 | 随机取一个匹配 | 造随机数据;压测中禁用,结果不可复现 |
| -1 | 全部匹配,写入 ${var_1}…${var_N} |
批量 sku、批量 id |
-1 时基础变量 ${var} 不再是值,JMeter 还会写一个 ${var_matchNr} 表示匹配数量(5.5/5.6 实测有,5.4 建议用 Debug Sampler 确认一次)。取第 N 个之外的越界索引,变量不存在,直接引用会得到字面量。
3.3 Default Values¶
- 只在“没匹配到”时生效;匹配到空字符串
""不算没匹配,此时变量是空串。 - 默认值建议统一写
NOT_FOUND,这样后续断言!${var}.equals("NOT_FOUND")能立刻定位问题;写空会让“没取到”和“取到空”无法区分。 - 默认值不参与
-1的数组编号,只影响你直接引用基础变量的场合。 - 默认值里不要出现分号,分号是分隔符。
3.4 Compute concatenation var¶
勾选后额外生成 <varName>_ALL,把全部匹配用英文逗号拼起来,例如 sku_ALL=1001,1002,1003,同时仍有 sku_1…sku_3 和 sku_matchNr=3。它只在 Match No. = -1 时有意义,分隔符固定为逗号,值本身含逗号就无法还原,所以不要拿 _ALL 去拼 JSON 数组,需要数组时自己用 JSR223 拼。
四、JSONPath 语法:够用的一组¶
| 需求 | 表达式 | 说明 |
|---|---|---|
| 固定层级 | $.data.token |
最稳,优先用这种 |
| 键名带点或中划线 | $['data']['user-id'] |
点号会被当成分隔符 |
| 数组第 1 个 | $.data.items[0].skuId |
下标从 0 开始 |
| 数组最后一个 | $.data.items[-1].skuId |
负下标从末尾数 |
| 数组全部 | $.data.items[*].skuId |
配合 -1 使用 |
| 递归所有同名键 | $..id |
慎用,命中层级不可控 |
| 字符串过滤 | $.data.orders[?(@.status=='PAID')].orderNo |
单引号,不是双引号 |
| 数字过滤 | $.data.orders[?(@.amount>100)].orderNo |
不要给数字加引号 |
| 多条件 | $.data.orders[?(@.status=='PAID' && @.amount>100)].orderNo |
&&,不是 and |
| 字段存在 | $.data.orders[?(@.coupon)].orderNo |
存在性判断 |
| 数组长度 | $.data.items.length() |
5.x 可用 |
| 第一层所有键 | $.data.* |
返回数组,需 -1 |
几条实测结论:
- 过滤条件的字符串必须用英文单引号。粘贴中文引号
‘PAID’会抛InvalidPathException: Could not parse token starting at position 18。 - 表达式首尾不要留空格,
$.data.token在部分版本会被当作路径的一部分,直接 No results。 @.status字段缺失时该元素被静默跳过,不抛异常。所以“过滤结果为空”和“路径写错”在日志上都表现为 No results,要分别验证。$..id会遍历整棵 JSON 树,响应 5MB 时的耗时是$.data.id的十倍以上,只在结构不确定时用。- JsonPath 没有 XPath 的
text()、//,/data/token这种写法直接报错。
五、和正则提取器怎么选¶
| 维度 | JSON Extractor | Regular Expression Extractor |
|---|---|---|
| 结构依赖 | 依赖层级,字段移动要改表达式 | 依赖字符序列,换行/压缩都会失配 |
| 数组支持 | 原生 [*]、-1 取全部 |
要写 (.*?) 多次匹配,易贪婪 |
| 转义 | 不用关心 JSON 转义 | 值里出现 \"、\\ 就断 |
| 报错表现 | No results,静默走默认值 | 同样静默走默认值 |
| 性能 | 需解析整个 JSON,MB 级响应有明显开销 | 字符串扫描,通常更快 |
| 适用 | 响应是合法 JSON | HTML、纯文本、JSON 中夹带非 JSON 片段 |
用 LB="token":" + RB=" 这种正则,一改字段顺序就失配,值里出现转义引号 \" 还会提前截断。JSON 接口就用 JSON 提取器,别赌响应格式永远不变。
六、作用域与执行顺序¶
- 后置处理器只作用于同级的采样器。放在“登录请求”下面,就只处理登录响应;放在线程组根下,会对组内每一个采样器执行,包括静态资源和下单请求,白白多解析十几次。
- Apply to 三个选项:
Main sample only(默认)、Main sample and sub-samples、Sub-samples only。开启“Retrieve All Embedded Resources”后,HTML 页面里的 XHR 是子采样器,默认设置取不到它的响应体。 - 放在 Transaction Controller 内、与多个采样器同级时,会对每一个子采样器执行一次,不是只对合并后的事务样本执行。
- 放在 Once Only Controller 里,只在第一轮迭代提取,后续迭代复用旧 token。token 有效期 30 分钟、场景跑 2 小时的时候,401 会集中出现在第 30 分钟。
- 变量是线程私有的。跨线程组取值必须走属性:
- 执行顺序按 jmx 树中的节点顺序,不按你点击添加的先后。同一个采样器挂两个提取器时,后者能覆盖前者的同名变量。
七、四类高频报错逐条拆¶
| 报错/现象 | 触发条件 | 处理 |
|---|---|---|
No results for path: $['data']['token'] |
路径不存在、大小写不符、多包了一层 result |
先用 JSON Path Tester 验证,再改表达式 |
Could not parse token starting at position 0 |
写成 $data.token、中文引号、末尾多空格 |
改成 $.data.token,全角符号全部替换 |
变量值是 ${token} 字面量 |
变量未创建:作用域错、名字拼错、提取器在请求之后 | 加 Debug Sampler,确认变量列表里有没有这个名字 |
| 值带引号或带花括号 | 表达式取到对象/数组而非标量 | 路径下钻到具体字段,或 JSR223 里 asText() |
值变成 1.0E7 |
小数被解析成 Double | 服务端返回字符串,或 JSR223 格式化 |
| 只拿到第一个匹配 | Match No. 设了 -1,却引用 ${var} |
改用 ${var_1} 或 ${var_ALL} |
| 提取到了但下游收不到 | 跨线程组 | 用 __setProperty / __P,或放进同一个线程组 |
一个容易忽略的点:No results 在 GUI 的“查看结果树”里看不到红字,因为采样器本身成功了。只有 jmeter.log 里有 ERROR,非 GUI 压测时必须显式去 grep 日志,否则整场压测都在发无效请求。
八、怎么验证:三条路都要走一遍¶
8.1 Debug Sampler + 查看结果树¶
在线程组里加 Debug Sampler,只把 JMeter Variables 设为 true,其余两项 false。运行一次后选中它,响应数据里能看到:
看不到名字就说明变量没创建,别再往下查请求头了。
8.2 JSON Path Tester¶
查看结果树选中登录请求 → 切到 JSON Path Tester 标签 → 粘贴表达式。它和提取器用的是同一套 Jayway 实现,能复现表达式层面的所有问题。注意它只验证“能不能取到”,不验证 Default Values 和作用域。
8.3 非 GUI 压测时落盘验证¶
jmeter -n -t login_order.jmx -l run.jtl `
-Jjmeter.save.saveservice.response_data=true `
-Jjmeter.save.saveservice.samplerData=true
Select-String -Path .\run.jtl -Pattern "login_token=" -Context 0,1
Select-String -Path .\jmeter.log -Pattern "JSONPostProcessor"
response_data 默认是 false,不打开的话 jtl 里没有 Debug Sampler 的输出。压测机上跑 500 线程时不要长期开着它,jtl 会膨胀到几十 GB,只在调试轮次打开。
九、登录→下单的完整关联¶
9.1 元件清单¶
| 元件 | 关键配置 |
|---|---|
| HTTP Cookie Manager | 每个线程独立,不清空 Cookie |
| CSV Data Set Config | users.csv,变量 user,pwd,Sharing mode = All threads |
| HTTP 登录请求 | POST /api/login,Body 里引用 ${user} ${pwd} |
| JSON Extractor(登录) | 见下方 jmx 片段 |
| JSON 断言 | $.code 等于 0 |
| HTTP 下单请求 | Header Authorization: Bearer ${login_token} |
| 响应断言 | 状态码 200 且 $.code 等于 0 |
| 事务控制器 | 勾选 Generate parent sample,统计“下单”整体耗时 |
9.2 提取器的 jmx 片段¶
<JSONPostProcessor guiclass="JSONPostProcessorGui"
testclass="JSONPostProcessor"
testname="JSON Extractor - login" enabled="true">
<stringProp name="JSONPostProcessor.referenceNames">login_token;login_uid</stringProp>
<stringProp name="JSONPostProcessor.jsonPathExprs">$.data.token;$.data.user.id</stringProp>
<stringProp name="JSONPostProcessor.match_numbers">1;1</stringProp>
<stringProp name="JSONPostProcessor.defaultValues">NOT_FOUND;0</stringProp>
<boolProp name="JSONPostProcessor.computeConcat">false</boolProp>
</JSONPostProcessor>
界面上的五项与这五个属性一一对应:referenceNames、jsonPathExprs、match_numbers、defaultValues、computeConcat。改脚本时直接改这里比点界面可靠。
9.3 批量 sku 的写法¶
商品列表返回 {"data":{"items":[{"skuId":1001},{"skuId":1002}]}},配置 $.data.items[*].skuId、Match No. = -1。下单时随机取一个:
__V 把 sku_1 这样的字符串当变量名解析。上界用 ${sku_matchNr} 而不是写死 3,否则商品数量变化时会取到不存在的变量。
9.4 反向验证¶
把表达式故意改成 $.data.tokens,跑 1 线程 1 循环:预期 login_token=NOT_FOUND、下单断言失败、错误率 100%。如果不失败,说明断言没覆盖到,先补断言再压测。这比正向跑通更重要,能验证“关联失败会不会被静默吞掉”。
十、提取开销与优化¶
实测环境 JMeter 5.6.3 / JDK 17 / i7-12700,仅算解析耗时,不含网络:
| 响应体大小 | 单个 $.data.x |
单个 $..id |
6 个独立提取器 |
|---|---|---|---|
| 200 KB | 0.6–0.9 ms | 2–3 ms | 4–6 ms |
| 1 MB | 3–5 ms | 11–18 ms | 18–30 ms |
| 5 MB | 18–26 ms | 60–90 ms | 110–160 ms |
500 TPS、1MB 响应、6 个提取器,光解析就吃掉 9–15 个核·秒/秒,压测机的 CPU 会先于服务端打满。优化顺序:
- 合并字段:能用一次
$.data下钻的,不要写 6 条$..递归。 - 能用
$.data.token就不用$..token,递归是整树遍历。 - 大响应 + 多字段,直接上 JSR223 一次解析:
import com.fasterxml.jackson.databind.ObjectMapper
def body = prev.getResponseDataAsString()
def node = new ObjectMapper().readTree(body)
vars.put('login_token', node.path('data').path('token').asText())
vars.put('login_uid', node.path('data').path('user').path('id').asText())
vars.put('amount_raw', node.path('data').path('amount').asText()) // 保留原始字符串
- 不要在生产压测里开查看结果树、Debug Sampler;提取器也不要挂在根节点,避免静态资源采样器被解析。
十一、坑位表¶
| 坑 | 现象 | 规避 |
|---|---|---|
变量名写成 ${token} |
变量不存在,请求带字面量 | 只写 token |
| 名字与表达式数量不一致 | 后面全部错位 | 空位用 ; 占位,改完数一遍分号 |
| Match No. 用 0 | 每次结果不同,缺陷无法复现 | 固定 1 或 -1 |
| -1 后引用基础变量 | 拿到的是 matchNr 或空 | 用 _1…_N |
| 默认值填空 | 分不清“没取到”和“取到空” | 统一 NOT_FOUND |
| 提取对象而非字段 | 值里带 {} 和引号 |
路径下钻到标量 |
| 金额用 JSON 提取 | 1.0E7、12.3 |
服务端返回字符串或 JSR223 asText() |
| 提取器放在 Once Only | 后续迭代用过期 token | 放在每次登录请求下 |
| 只看报告不看日志 | 全程 401 但采样器 success | 压测后 grep JSONPostProcessor |
十二、上线前检查清单¶
- 每个提取器都填了
NOT_FOUND类默认值,且后面有断言 - 做过“故意写错路径”的反向验证,错误率能打到 100%
- Names 与 Expressions 数量一致,Match No. 没有使用 0
- 非 GUI 跑一轮,grep
jmeter.log里的JSONPostProcessor无 ERROR - 大响应场景测过压测机 CPU,提取开销未超过单核 30%
- 变量名带业务前缀,跨线程组的值走属性,jmx 无残留 Debug Sampler