跳转至

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}}}

下单请求的头却是:

Authorization: Bearer ${access_token}

服务端原样返回:

HTTP/1.1 401 Unauthorized
{"code":401,"msg":"token invalid"}

同时 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 输出:

token={"token":"eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjEwMDg2fQ.x8Kq","expires":7200}

请求体于是变成:

{"token":"{\"token\":\"eyJhbGciOiJIUzI1NiJ9...\",\"expires\":7200}"}

服务端签名校验报 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 分钟。
  • 变量是线程私有的。跨线程组取值必须走属性:
${__setProperty(global_token,${login_token},)}
${__P(global_token)}
  • 执行顺序按 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。运行一次后选中它,响应数据里能看到:

login_token=eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjEwMDg2fQ.x8Kq
login_uid=10086

看不到名字就说明变量没创建,别再往下查请求头了。

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_${__Random(1,${sku_matchNr},)},)}

__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 会先于服务端打满。优化顺序:

  1. 合并字段:能用一次 $.data 下钻的,不要写 6 条 $.. 递归。
  2. 能用 $.data.token 就不用 $..token,递归是整树遍历。
  3. 大响应 + 多字段,直接上 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())  // 保留原始字符串
  1. 不要在生产压测里开查看结果树、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

相关阅读