Jmeter 的 JSON 提取器JSON Extractor这个后置处理器我在几乎每一个需要接口串联的压测脚本和自动化校验脚本里都会用它。它的活儿说穿了很朴素从上一次请求返回的 JSON 报文里把某个字段抠出来存成 JMeter 变量下一次请求拿着这个变量当参数用。听起来平平无奇但真正落到“提取一个参数的所有值”和“一次提取多个参数”这两个场景上翻车的概率相当高——Match No. 填错一位脚本提取为空JSON Path 多写了一行忘了补变量名直接报参数不足变量名少写一行一堆请求全带着空值跑过去还看不出来哪里错了。这篇东西我打算按我自己的实操顺序来讲先把提取器在脚本里的位置摆正再把每个输入框的作用说透然后分别攻克“单个值”“同一字段的所有值”“多个不同字段”这三类需求最后把我这几年在真实项目里踩过的坑整理成一张排查表。刚接触 Jmeter 接口测试的人可以照着步骤直接抄已经用过一段时间的人建议重点看第 4、5、6 节那几个地方是最容易出隐蔽问题的地方。1. 先想清楚 JSON 提取器在脚本里承担什么角色1.1 一次真实的接口串联场景我先描述一个几乎所有做过接口压测的人都遇到过的场景。业务是这样的用户登录拿 token然后用 token 去查订单列表拿到订单号之后再逐个去查订单详情最后对订单做一次状态变更。整条链路里token、订单号列表、订单详情里的商品 ID全部都要从上一个响应里抠出来否则后面的请求根本没法发。放在 Jmeter 里这几类数据传递全靠后置处理器。JSON 提取器就是其中最常用的一种后置处理器它挂在某个 HTTP 请求下面在一个采样器执行完成、拿到响应体之后立即运行把匹配到的内容写进 JMeter 的变量空间。写进去的变量同一个线程组内的后续采样器就可以用${变量名}的方式引用。它和其他后置处理器的区别在哪里正则表达式提取器Regular Expression Extractor什么响应都能处理但写正则本身有门槛遇到嵌套层级深、字段名重复的 JSON正则写起来又长又容易错XPath 提取器只对 XML/HTML 有效现在主流接口基本都是 JSON用不上。JSON 提取器基于 JSON Path 语法表达力强、可读性好只要你的接口返回是标准 JSON它就是第一选择。注意JSON 提取器只管从 JSON 文本里取值它不做 JSON 合法性校验。响应体本身不是合法 JSON比如返回了 HTML 错误页、或者前面有多余的 BOM 字符提取器不会报错只会把变量设成默认值然后你在后面的请求里看到一堆空参数。这种“静默失败”最坑人。1.2 提取器怎么选JSON、正则、XPath 三种方案对比很多人上来就问“哪个提取器最好”这个问题本身没有标准答案只有适配场景。我把三种常见提取器的特性整理成一张表你可以按这张表快速判断。对比维度JSON 提取器正则表达式提取器XPath 提取器适用响应格式标准 JSON任意文本XML / HTML语法门槛中需懂 JSON Path中高需懂正则中需懂 XPath处理嵌套结构强弱强提取数组全部值原生支持Match No. 填 -1需配合模板和 Match No.需配合循环对格式变化敏感度高字段名一变就失效低但容易误匹配中调试难度低可用在线 JSON Path 工具验证高正则调试费时中我自己的判断逻辑是这样接口返回是 JSON就默认用 JSON 提取器返回是 XML 或者老系统的 SOAP 报文用 XPath返回是纯文本、或者需要从一段非结构化字符串里抠内容比如从重定向 URL 里取参数才动用正则。这个优先级能覆盖九成以上的场景。有一点要提前说明Jmeter 的 JSON 提取器在不同版本上的实现细节有差异。较老的版本可能需要额外装插件新的版本3.0 以后是内置的直接用就行。JSON Path 的解析引擎用的是 Jayway 那一套所以过滤表达式、数组下标这些写法基本和主流 JSON Path 工具一致你可以在浏览器里找在线的 JSON Path 测试页面先把表达式调通再贴进 Jmeter能省下大量反复跑脚本的时间。2. JSON 提取器的每个输入框到底怎么填2.1 Name、Apply to 与作用域打开 JSON 提取器的配置面板第一项是 Name这个纯粹是给你自己看的备注名写“提取token”“提取订单号列表”这种一眼能懂的名字就行。它不影响任何逻辑但脚本跑起来之后在结果树里排查问题时一个清晰的命名能救你不少时间。第二项 Apply to应用范围有四个选项这里是最容易被忽略的地方Main sample and sub-samples主采样器和所有子采样器都处理。Main sample only只处理主采样器这是默认值。Sub-samples only只处理子采样器。JMeter Variable Name to use不处理响应体而是从一个已有的 JMeter 变量里取值再提取。绝大多数情况下默认的 Main sample only 就够了。什么时候需要改当你的请求触发了重定向或者页面里带 iframe、嵌入式资源请求时响应内容可能落在子采样器上。还有一种常见情况是用“JMeter Variable Name to use”做二次提取——比如前任一个正则把整个 JSON 字符串抠到了一个变量里你想用 JSON Path 在这个变量内部再精确取值就填那个变量的名字。作用域的问题更值得单独强调。后置处理器的影响范围是它所在的控制器层级挂在某个 HTTP 请求下面它就只对这个请求生效如果你不小心把它拖到了线程组这一层线程组里所有采样器执行完都会跑一遍这个提取器轻则浪费性能重则把变量覆盖成别的接口的值。我见过最典型的一次事故就是把提取 token 的处理器放错层级结果第二个请求的响应把 token 变量覆盖了后面所有请求全带错凭证。2.2 JSON Path expressions 与 Default Values 的对应关系JSON Path expressions 这个框支持多行输入一行一个表达式每行对应一个变量。默认值Default Values同样是多行输入按顺序和表达式一一对应。这个对应关系是理解“一次提取多个参数”的关键后面第 5 节会详细展开。表达式的基本写法$代表根节点.往下走一层..递归查找[]用于数组下标或过滤条件。几个例子$.data.token 取值 data 下的 token $.data.list[0].orderNo 取列表第一个元素的订单号 $.data.list[*].orderNo 取列表所有元素的订单号 $..orderNo 递归查找任意层级的 orderNo $.data.list[?(.statusPAID)].orderNo 过滤出已支付订单的订单号 $.data.total 取总数 $.data.list.length() 取列表长度默认值这一项我建议永远不要留空。留空的后果是提取失败时变量是空字符串后续请求发出去的是?orderNo服务端报个参数校验失败你还得回头一层层查。填一个有辨识度的默认值比如NOT_FOUND在结果树里一眼就能看出这次提取到底成没成。提示默认值行数少于表达式行数时多出来的表达式会用最后一个默认值兜底反过来默认值给多了不会报错多出来的行被忽略。为了可读性最好还是严格对齐数量。2.3 Match No. 与 Compute concatenation var 的联动Match No.匹配编号是第二个最容易出错的输入框它决定了在一个表达式命中多个结果时取哪一个取值行为0随机取一个匹配项1取第一个匹配项2、3……n取第 n 个匹配项-1取所有匹配项逐个存成带下标的变量这里有个细节必须说清楚不管 Match No. 填多少Jmeter 都会额外生成一个变量名_matchNr变量记录本次一共匹配到多少条。这个数字在调试时非常好用你可以用 Debug Sampler 直接看到它。另外如果 JSON Path 压根没匹配到任何内容_matchNr会是 0。Compute concatenation var 这个勾选项配合 Match No. -1 使用勾上以后所有匹配项会用英文逗号拼成一个字符串存进变量名_ALL里。注意是英文逗号如果你的待提取值本身包含逗号拼出来的结果就没法再拆分了这个坑我在第 6 节会再提一次。还有一个容易被忽略的点Match No. 填 0随机时每次执行的结果都不一样脚本的稳定性会下降。压测场景下我一般不用随机匹配只在做数据多样性测试、故意想让每次请求打在不同数据上时才用并且会在文档里注明。3. 提取单个参数从登录到下单的完整实操3.1 抓包确认响应结构动手写脚本之前先用抓包工具或者浏览器开发者面板把目标接口的响应体完整看一遍。假设登录接口返回的报文长这样{ code: 0, message: success, data: { token: eyJhbGciOiJIUzI1NiJ9.abc123, userId: 10086, expireAt: 1735689600 } }“看一眼”这件事看着废话但我见过太多人直接凭接口文档写 JSON Path结果文档里写的是data.accessToken实际返回的是data.token脚本跑起来一直提取为空白白折腾半小时。以实际响应为准这是铁律。3.2 配置提取器并验证在登录请求上右键添加 - 后置处理器 - JSON 提取器按下面填Name提取登录tokenApply toMain sample onlyJSON Path expressions$.data.tokenMatch No.1Default ValuesNOT_FOUND然后在同一个线程组里加一个 Debug Sampler调试取样器再挂一个察看结果树。跑一次在结果树里点 Debug Sampler 的响应数据你能看到当前线程内所有 JMeter 变量和属性确认变量名和值都对。这一步是我强烈建议固定下来的习惯动作比在后面请求里瞎猜变量有没有取到高效得多。3.3 把变量传给下一个请求变量拿到之后在下个请求的 HTTP 请求头里加一行Authorization: Bearer ${token}或者在请求体里写{userId: ${userId}}。这里要注意两点第一变量引用必须写全${变量名}不要写成$变量名或#{变量名}。Jmeter 只有${}一种写法。第二如果你是在请求体里用 JSON 格式传参数字类型的值直接用${userId}没问题但如果值本身是字符串类型必须自己带上引号写成token: ${token}。漏了引号拼出来的报文就不是合法 JSON服务端会直接拒掉。还有一个实操经验压测脚本里登录这类接口通常只跑一次后面的业务接口跑很多次。这时候要控制登录请求的执行次数别让它跟着循环一起跑。常见的做法是在线程组下加一个仅一次控制器Once Only Controller把登录请求包进去token 提取器也跟着放进去。这样每个线程只登录一次后续循环复用这个 token。4. 提取一个参数的所有值4.1 Match No. 填 -1 之后发生了什么这是标题里点名的第一个高频需求。场景很典型订单列表接口返回了 20 条订单每条都有一个 orderNo你要把这 20 个订单号全拿出来后面逐个去查详情。响应体结构如下{ code: 0, data: { list: [ { orderNo: NO20240001, amount: 99.5 }, { orderNo: NO20240002, amount: 128.0 }, { orderNo: NO20240003, amount: 45.0 } ], total: 3 } }配置 JSON 提取器JSON Path expressions$.data.list[*].orderNoMatch No.-1Default ValuesNOT_FOUND跑完之后变量空间里会出现这些东西变量名值orderNo_1NO20240001orderNo_2NO20240002orderNo_3NO20240003orderNo_matchNr3orderNo_ALLNO20240001,NO20240002,NO20240003注意orderNo_matchNr这个名字是“变量名 下划线 matchNr”不是matchNr。我见过有人直接写${matchNr}结果取不到值就是这个原因。4.2 _matchNr 与 _ALL 的正确用法_matchNr最实用的地方是配合循环控制器做动态次数的迭代。假设你想让后面的详情查询接口跑的次数刚好等于列表长度可以把循环控制器的循环次数写成${__V(orderNo_matchNr)}或者更简单用 ForEach 控制器自动遍历不用自己数。_ALL适合的场景是后端某个接口要求把多个 ID 用逗号拼成一个参数传过去比如批量查询接口GET /order/batch?nosNO20240001,NO20240002,NO20240003。这时候你什么都不用做直接把${orderNo_ALL}塞进请求参数就行。但前提是被提取的值里本身不含逗号否则拼接结果会被服务端拆错。注意_ALL只有在勾选 Compute concatenation var 时才会生成。图片里那个勾选框很多人习惯性跳过等到要用${xxx_ALL}的时候发现变量不存在回头再翻配置。4.3 用 ForEach 控制器把列表跑一遍这是把“提取所有值”真正用起来的核心步骤。在线程组下添加一个 ForEach 控制器逻辑控制器 - ForEach Controller配置如下输入变量前缀orderNo开始索引1结束索引留空输出变量名称currentOrderNoAdd _ before number勾选ForEach 控制器会从 orderNo_1 开始往后找每找到一个就把值赋给${currentOrderNo}循环体内的采样器执行一轮然后继续找下一个。循环体里放订单详情请求路径写/order/detail/${currentOrderNo}就能把列表里的订单逐个跑一遍。这里有个隐藏的坑必须提醒ForEach 遇到第一个不存在的编号就会停止。假如你的变量是 orderNo_1、orderNo_2、orderNo_4中间断了号循环只会跑两次。正常情况下 Jmeter 生成的下标是连续的不会断但如果你在脚本里手动改过变量名、或者同时有多个提取器往同一个前缀写值就可能出现断号。遇到“循环次数比预期少”先查这个。另外orderNo_matchNr和orderNo_ALL这两个变量名虽然是orderNo_开头但后缀不是纯数字ForEach 不会把它们当成有效项去遍历这点可以放心。不过如果你手上有其他工具或插件对这个前缀做字符串匹配就要留意一下了。5. 一次提取多个参数5.1 多行表达式的对齐规则同一个响应里往往不止一个字段有用。比如订单列表的每一条里你既要订单号又要商品 ID还要下单时间。这时候不用配三个提取器一个提取器里写多行表达式就行$.data.list[*].orderNo $.data.list[*].productId $.data.list[*].createTime变量名区域在 Jmeter 的 JSON 提取器面板里叫 Names of created variables用分号分隔写成orderNo;productId;createTime这里的规则是变量名按分号切分按顺序和表达式一一对应。第一行表达式的结果写进 orderNo第二行写进 productId第三行写进 createTime。用分号而不是逗号或换行这一点必须记住写错了整个提取器会失效而且报错信息不直观。三个表达式都设 Match No. -1 的话你会同时得到 orderNo_1..N、productId_1..N、createTime_1..N 三组带下标的变量加上各自的 matchNr 和 ALL 变量。这三组变量的下标是对齐的orderNo_2 对应的就是 productId_2 和 createTime_2可以在 ForEach 循环里同时引用三个变量拼成一条完整记录。5.2 数量不匹配会怎样变量名数量少于表达式数量时多出来的表达式照样会执行但结果无处可放通常会落到最后一个变量名上或者直接被丢弃具体表现依版本略有差异。变量名多于表达式数量时多出来的名字对应的变量会被设成默认值。不管哪种情况都不会抛出显眼的错误脚本照跑只是数据不对。这就是我为什么一直强调配完多个参数之后一定要用 Debug Sampler 过一遍逐个核对变量名和值。三个提取目标结果只出来两个靠肉眼在结果树的海量日志里翻效率极低。5.3 复杂嵌套与过滤表达式的实战写法真实接口的 JSON 结构往往比示例复杂得多。举几个我实际处理过的形态嵌套数组取值。响应里是data.orders[0].items[0].skuId这种结构如果只要第一层第一条的第一件商品直接写$.data.orders[0].items[0].skuId。如果要所有订单下所有商品的 SKU写$.data.orders[*].items[*].skuId递归展开。带条件的过滤。只想要金额大于 100 的订单号$.data.list[?(.amount 100)].orderNo。Jayway 这套语法里字符串比较要用单引号包起来写成[?(.statusPAID)]数字不用引号。这一点和某些在线工具默认的写法不一样贴进 Jmeter 之前最好本地验证一次。取数组长度做断言。$.data.list.length()可以直接拿到列表长度提取出来存在变量里配合响应断言校验“本次返回条数是否为 10”。这种用法在压测时检查分页逻辑有没有出错特别顺手。递归查找兜底。$..token会扫描整个文档里所有叫 token 的字段。当你不确定 token 藏在哪一层或者接口结构经常调整时用递归查找能提高容错率。代价是如果报文里有多个同名 token你会一次拿到一串值得自己判断取第几个。再补充一个多个提取器协同的写法。有些字段的提取依赖上一步的结果比如先提取出所有订单号再从某个嵌套结构里按订单号过滤。JSON Path 本身能做条件过滤但依赖关系复杂时我倾向于拆成两个提取器串联第一个提取列表第二个用JMeter Variable Name to use模式做二次加工。可读性比堆一个超长表达式好得多。6. 常见问题与排查6.1 提取结果为空的排查顺序提取为空是出现频率最高的问题。我整理了一套固定的排查顺序按这个顺序走基本五分钟内能定位确认响应体里真的有这个字段。打开结果树看响应数据原文别只看接口文档。确认响应体是合法 JSON。如果接口在出错时返回 HTML 页面或者返回体前后带了说明文字JSON Path 直接解析失败此时应改用正则提取器。确认 JSON Path 表达式正确。复制响应体到在线 JSON Path 工具里验证能出结果再贴回 Jmeter。确认 Apply to 选对了。重定向、子请求场景下值可能不在主采样器的响应里。确认提取器的作用域。是不是放错层级或者被别的采样器的后置处理器覆盖了。确认变量引用写法。是${orderNo_1}而不是$orderNo_1也不要把下标写错。6.2 编码、转义与特殊字符有几个特别隐蔽的情况值得单独列出来。BOM 头导致解析失败。某些服务端返回的 JSON 文件前面带了不可见的 BOM 字符人眼看着完全正常JSON Path 就是匹配不到。这种情况一般出现在直接返回静态文件的场景处理方法是检查服务端输出编码配置或者改用正则提取器绕过。中文乱码。如果待提取的值包含中文先确认 Jmeter 的默认编码设置和 HTTP 请求里的内容编码配置是否一致全链路编码统一到 UTF-8 是最省心的做法。乱码问题不解决后面拿这个变量去发请求服务端校验必然失败。值里带逗号。前面提到过_ALL变量用逗号拼接过个值如果原始值里自带逗号拼接结果就无法反向拆分。批量查询这类场景建议改用 ForEach 逐个发请求或者用 JSR223 后置处理器自己拼接成分号、竖线等安全分隔符。值里带引号或反斜杠。变量放进请求体时如果值本身含双引号拼出来的 JSON 会断掉。需要在脚本层面做转义处理或者把参数放到请求头、路径里避开 JSON 拼接。6.3 排查速查表现象可能原因处理方式变量值一直是默认值JSON Path 写错、字段不存在用在线工具验证表达式核对响应原文变量名找不到变量名写错、提取器未生效用 Debug Sampler 查看当前所有变量只取到第一个值Match No. 填了 1改为 -1_ALL变量不存在未勾选 Compute concatenation var勾选该项并重跑ForEach 循环次数偏少变量下标断号检查是否有多个提取器写同一前缀多参数提取时部分为空变量名和表达式数量不一致按分号逐行对齐核对跨线程组取不到变量提取的变量是线程局部的用属性传递或在同一线程组内完成链路偶发提取失败Match No. 填了 0 导致随机匹配改为固定编号保证脚本可复现跨线程组传参这个问题值得展开讲两句。JSON 提取器写出来的是线程变量作用范围只在本线程内。如果你有多个线程组想在 A 组提取、B 组使用标准做法是在 A 组里用 JSR223 后置处理器或函数把值写进 Jmeter 属性B 组再用函数读出来。不过这种做法在并发场景下要小心——多个线程同时写同一个属性名会互相覆盖最终读到的是谁的值说不准。真有这种需求我一般改成 setUp 线程组做数据准备或者干脆把整条链路放进同一个线程组里用逻辑控制器组织省掉跨组传递这一层麻烦。7. 踩坑之后的几点经验7.1 先想清楚变量的生命周期变量在 Jmeter 里是线程隔离的。同一个线程组开 10 个线程每个线程都会执行一遍登录、都会提取一次 token各自存各自的副本互不干扰——这是设计上的正确行为不是 bug。但很多人第一次看到“结果树里有一堆不同 token”时会慌以为脚本写错了。理解这一点之后很多设计就有了依据想让每个虚拟用户都用独立身份就让登录在循环里跑想让大家共用一个凭证就用 setUp 线程组提前跑一次把结果写进属性供所有线程读取。选择哪种取决于你要模拟的业务形态而不是哪个写起来简单。7.2 表达式别写得太“聪明”我早期写 JSON Path 有个坏习惯喜欢用$..递归查找来“兼容所有结构”觉得这样接口改版了脚本也不用改。实际用下来发现这是个陷阱一旦报文里出现多处同名字段递归查找会一次返回一堆值Match No. 填 1 就只取第一个运气不好就取错了根本不是你想要的字段而且排查起来毫无头绪。后来我改成写明确的绝对路径$.data.orderList[0].orderNo这种一眼能看出取的是哪一层。接口改版了脚本会立刻报错早报错比晚报错好至少你知道是这里需要改。7.3 提取器和断言配合效果翻倍单纯把值提取出来只是第一步真正让脚本变得可靠的是加断言。我通常在提取之后挂一个响应断言校验code字段是不是 0再挂一个 JSR223 断言检查提取出来的_matchNr是不是大于 0。这样一个接口返回了错误结构、或者列表为空的时候脚本会立刻标红而不是安静地带着空参数继续往下跑最后在某个不相干的接口上暴露出一个莫名其妙的报错。JSR223 断言里可以直接读变量比如检查vars.get(orderNo_matchNr)转换成整数后是否大于 0不满足就AssertionResult.setFailure(true)。这段逻辑写一次可以复用到所有提取场景比每个接口单独写断言省事得多。7.4 把常用表达式记成模板最后分享一个我自己的习惯把高频使用的 JSON Path 表达式和维护要点整理成一个文本文件新项目直接复制修改。里面大概长这样单值 $.data.token 列表全值 $.data.list[*].orderNo Match No.-1 条件过滤 $.data.list[?(.statusPAID)].orderNo 数组长度 $.data.list.length() 递归查找 $..orderNo 谨慎使用 嵌套展开 $.data.orders[*].items[*].skuId配合一份“配置检查清单”——变量名数量与表达式数量是否对齐、Match No. 是否为预期值、默认值是否填写、是否用 Debug Sampler 验证过——几分钟就能配好一个新的提取器比每次从零回忆这些细节快得多也少犯很多重复性的错误。
