☰
Jmeter接口测试关联攻略:正则提取器、JSON提取器与跨线程组传参
2026/10/21 18:41:16 网站建设 项目流程

1. 搞懂测试关联:为什么接口测试绕不开它

如果你是最近才上手 Jmeter 做接口测试,八成会在某一个时刻碰到同一个问题:登录接口返回了一个 token,下单接口必须带上这个 token,不然服务器就甩给你一个“未认证”。或者是创建订单接口返回了一个订单号,紧接着支付接口要用这个订单号,你不把它取出来,下一个请求就只能写死一个假数据,一跑就挂。这个把上一个接口的响应数据,动态地交给下一个接口去用的过程,就是测试关联。

说实话,测试关联不算什么高级概念,但它属于“不会就卡死、会了就顺手”的典型知识点。很多初学者一开始会用最笨的办法:跑一遍登录,从结果里手抄 token,再填到下一个请求里。一次两次行,但当你需要模拟几十个并发用户、每个人都带各自不同的 token 时,手抄这条路完全走不通。Jmeter 里所有请求都是并发执行的,每个虚拟用户的登录响应都不一样,你不提前把关联写好,性能测试就无从谈起。

这篇文章我就从实际项目里用出来的经验出发,把 Jmeter 里实现测试关联的几个主流方法掰开揉碎讲一遍:正则提取器、JSON 提取器、跨线程组传参的套路,以及我在真实项目中踩过的一堆坑。不管你是纯新手还是已经写过几个脚本的测试老手,这篇文章都值得你花几分钟认真看一遍,因为里面有些坑是我翻了半天文档才找到答案的。

先说声,这篇文章里涉及的操作实例,我会用一套模拟的接口项目来讲解:某平台(以下称“模拟项目X”)提供登录、用户信息查询、创建订单、支付订单四个接口。接口字段都是模拟的,但逻辑和真实项目完全一致,你照着同样的思路去套自己手头的接口就行。

2. 关联方案选型:正向思路与逆向避坑

2.1 三种主流提取方式的适用边界

Jmeter 实现关联,归根结底就两步:第一步,把上一个请求的响应保存到一个变量里;第二步,在下一个请求里引用这个变量。不过“把响应保存到变量”这件事,Jmeter 给了你好几种工具,选错了工具,后面就是无尽的烦恼。

我先给一张总览表格,你以后选方案时可以快速对照:

提取方式适用场景解析速度学习成本稳定性
正则表达式提取器任意文本格式,响应为 HTML、XML、纯文本、JSON 字符串等中等需要懂正则中
JSON 提取器响应格式明确为 JSON快需要懂 JSONPath高
边界提取器左右边界明确,中间内容动态快低高
CSS/JQuery 提取器响应为 HTML,需要提取标签属性或内容快需要懂选择器中

说句大实话,我平时 90% 的场景只用到 JSON 提取器和正则提取器。边界提取器听起来很简单,但遇到左右边界在文档中出现多次时就容易抓瞎,反而不如正则灵活。CSS 提取器用的人比较少,只处理 HTML 响应时可以考虑。

选型有个核心原则:JSON 响应优先用 JSON 提取器,非 JSON 响应才用正则提取器。这个原则不是我定的,而是实操教训换来的。JSON 解析是结构化的,提取器会严格按照 JSONPath 语法取数据,天然不会出现正则那种贪婪匹配、转义错误等一箩筐问题。而且 JSON 提取器的性能通常比正则好,遇到响应体巨大时优势更明显。

2.2 关联数据可能藏在哪些位置

很多新手容易忽略一个问题:关联数据不一定只存在于响应体中。我在项目里见过的几种常见位置是:

  • 响应体(Response Body):最常见的 token、订单号、用户 ID 都在这里。
  • 响应头(Response Headers):有些项目喜欢把 token 放在响应头的 Authorization 或 Set-Cookie 字段里。曾经有一个 mock 项目,sessionId 只在 Set-Cookie 里返回,body 里压根没有。你只盯着 Body 看,怎么都提取不到。
  • 响应状态行(Response Status Line):极少数时候,接口会用状态码或状态行里的内容来传参。比如某个旧系统用 201 表示成功,同时在响应头 Location 里返回新资源的 URL,你要的 ID 可能就是 URL 最后一段。
  • URL 重定向地址:如果接口是重定向响应(3xx),Location 字段里的参数经常被忽略。

所以拿到一个新接口,我建议你先用 Jmeter 跑一次,打开“查看结果树”,把响应体、响应头全展开看一眼。别急着写提取器,先搞清楚数据到底在哪。这一步比选什么提取器都重要。

3. 正则提取器实战:从头到尾读透每一步

3.1 正则提取器的界面配置拆解

我们还是先看最通用的正则表达式提取器。它的配置面板长这样,从“添加 → 后置处理器 → 正则表达式提取器”可以找到。看似格子不多,但每个都有讲究:

  • Apply to:默认是 Main sample only。如果这个请求下面挂了子请求(比如页面加载了多个静态资源),你需要根据实际情况决定是只管主样本,还是管所有样本。接口测试一般都用默认值。
  • 要检查的响应字段(Field to check):核心选项。默认是 Body,但前面说了,如果数据在响应头,就要选“响应头”或“信息头”。Jmeter 里英文界面是 Response Headers,中文界面是信息头,名字叫法不一样但指同一个东西。
  • 引用名称(Reference Name):这是你自己起的变量名,后面用${变量名}来引用。注意:这个名字不要和线程组里其他变量重名,不然你会拿到一个被覆盖过的值,排查到怀疑人生。
  • 正则表达式(Regular Expression):这是整个提取器的心脏,后面单独讲。
  • 模板(Template):新建变量时如何从正则匹配结果里取值。配合正则里的捕获组使用,$1$ 表示取第一个括号里的内容,$2$ 取第二个。如果只想用匹配到的当前值,不分组,就直接写 $0$。
  • 匹配数字(Match No.):0 表示随机取一个匹配值,1 表示取第一个,2 表示取第二个。负数比较特殊,表示使用所有匹配到的值。这个属性我们后面单独细说。
  • 默认值(Default Value):如果正则没有匹配到任何东西,这个变量会被赋予默认值。我强烈建议这个值永远不要留空。

最后一点要展开说。如果你不填默认值,提取失败时 Jmeter 就会给变量一个空字符串。空字符串在后续请求里会导致什么?不会直接报错,而是发出一个字段缺失的请求,服务器返回“参数错误”或“未认证”,然后你又得回头查。但如果你填了默认值,比如NOT_FOUND,作用有两个:第一,你一眼就能在调试结果里看到这个变量没被匹配上;第二,部分校验场景下,你可以直接用NOT_FOUND去断言,判断这个接口是不是提取失败了。这个习惯帮我省了无数次排查时间。

3.2 正则表达式的三个典型写法

写正则表达式是很多人觉得难的地方,但接口测试里需要的正则其实就那么几类。我总结了三个我在项目里反复用到的模板:

第一类:提取 JSON 中的某个字符串值。

响应:{"token":"ABC123XYZ789","user_id":1001}

你要拿 token,正则这么写:

"token":"([^"]+)"

这里([^"]+)表示捕获一个不含双引号的连续字符串。[^"]的意思是非双引号字符集,+表示一个或多个。这样写比"token":"(.*?)"更严谨,因为它强制要求取到的是非引号内容,避免贪婪匹配引发的越界。

第二类:提取两段动态内容之间的数据。

响应:<order_id>20250113001</order_id>

正则:

<order_id>(.*?)</order_id>

.*?是非贪婪匹配,表示尽可能少地匹配任意字符。如果你写成.*,在页面存在两个<order_id>标签时就会出问题:第一个匹配会把中间所有内容全部吞进去,直到最后一个结束标签。这个坑我确实踩过,排查了半天才发现是贪婪匹配把两个标签之间的东西全吃掉了。

第三类:提取请求路径中的动态片段。

响应地址:http://api.example-sim.com/order/88234/pay

你要拿订单号 88234:

/order/(\d+)/pay

\d+表示一位或多位数字,专门用来提取纯数字内容。这里同样用到了捕获组,模板填$1$。

3.3 Match No. 和模板的配合逻辑

正则的匹配结果可能不止一个。比如响应里有三个<id>标签,你的正则需要能提取到指定的那一个。默认情况下(Match No. = 0),Jmeter 会随机挑选其中一个结果。听起来好像没什么问题,但随机意味着不可控。假如你后续断言期望先处理第一个 ID,结果随机到了第三个,测试就会产生偶发性的失败,特别难排查。

所以我的建议是:需要取第几个,就显式填写数字 1、2、3。如果接口返回的是一个列表,你希望后续循环分别处理每一条数据,那就把 Match No. 填为 -1。这时 Jmeter 会创建一个后缀_matchNr的变量来记录匹配总数,同时生成多个变量,比如var_1、var_2、var_3。你可以在循环控制器里配合使用。

模板的作用也不止一种。假设你的正则写了两个捕获组,模板填$1$-p0$2$这种组合也行,Jmeter 会按模板拼接出一个新字符串。这个特性在做 URL 拼接时挺实用,两段数据合并成一个变量,省一次手动拼接。

3.4 正则提取器效率优化与防坑

正则写多了,性能也是要关注的。虽然接口测试脚本通常不需要极致的解析速度,但压测场景很多人直接就拿 Jmeter 脚本去跑性能测试。这时候响应体可能是个几千行的大 JSON,一条写得很烂的正则(比如大量使用.*)会显着拖慢吞吐量。

我自己总结了几条正则优化建议:

  • 尽量用字符集代替点号。[^"]+比.*?更快、更安全。
  • 不要用.*开头,除非确实不知道前缀是什么。正则引擎的贪婪回溯会消耗大量 CPU。固定前缀 +.*?+ 固定后缀这种组合最好。
  • 如果一条正则特别复杂,拆成多条后置处理器依次提取,反而容易维护。

另外提醒一句:在调试正则时,建议直接先把正则表达式提取器加上,跑一次请求,然后用“调试取样器”(Debug Sampler)配合“查看结果树”,在响应里能看到所有变量的值。这个调试器比我手写 BeanShell 打印变量方便得多,强烈安利。

4. JSON 提取器与更高阶的数据处理

4.1 JSONPath 的核心语法与记忆方法

JSON 提取器的面板比正则提取器简单多了:引用名称、JSONPath 表达式、默认值、匹配数字。真正花功夫的是 JSONPath 语法。不过别怕,JSONPath 和 XPath 很像,核心就几条规则。

假设响应长这样:

{ "code": 0, "data": { "token": "EYJ123", "user_info": { "id": 1001, "name": "张三" }, "order_list": [ {"order_id": "A001", "amount": 99.5}, {"order_id": "A002", "amount": 150.0} ] } }

JSONPath 取值的几个写法:

  • $.data.token取到"EYJ123",$表示根对象,后面的点号表示一层层往下。
  • $.data.user_info.id取到数字1001。
  • $.data.order_list[0].order_id取到"A001",注意下标从 0 开始。
  • $.data.order_list[*].order_id取到所有订单号列表。
  • $.data.order_list[?(@.amount > 100)].order_id取到金额大于 100 的订单的 ID,这是带过滤条件的写法,虽然用得少,但掌握后很省事。

记不住 JSONPath 怎么办?我的经验是:在 JMeter 界面里,用“JSON 提取器”面板旁边那个“测试”按钮,写完表达式直接点一下,就能看匹配结果,不用反复跑请求。这个按钮在 Jmeter 5.x 版本里一直都有,很多人没注意到,实测很顶。

4.2 JSON 提取器与正则提取器的运行时机

使用 JSON 提取器有一个隐性前提:请求的响应必须是合法 JSON。如果你的响应其实是 JSON 字符串嵌套在某个变量里(比如有些封装会把 JSON 当字符串返回),那么 JSON 提取器会报错或取不到值。

另外要注意后置处理器的运行顺序。Jmeter 的后置处理器默认是顺序执行的。如果你在一个取样器下面挂了多个提取器,它们会按从上到下的顺序跑。假设你用第一个正则提取了原始 JSON,再用 JSON 提取器从这个 JSON 字符串里取内层字段,那第一个提取器的变量必须定义在第二个提取器使用之前。不少同事栽过这个跟头:在同一个取样器下加了两个提取器,顺序反了,结果第二个永远是空值。

4.3 用 BeanShell 与 JSR223 补充复杂逻辑

如果只是单纯提取一个字段,JSON 提取器足够。但有些项目比较变态,需要处理时间戳、拼接签名或者做多次数据转换,这时候纯配置方式就捉襟见肘了。我常用 JSR223 取样器配合 Groovy 语言处理这些复杂场景。

我举个典型场景:登录接口返回的 token 可能是 JWT 格式,中间段的 playload 是 Base64 编码的 JSON,你需要解析出里面的用户角色,再传给下一个接口。这种逻辑用提取器完全做不了,只能在 JSR223 里写 Groovy 脚本来解码。

写这类脚本时,注意 Jmeter 提供了两个内置变量:vars用于读写当前线程组的变量,prev用于访问前一个取样器的结果。常见的一段代码长这样:

// 从上一个取样器的响应中获取数据并保存为新变量 String response = prev.getResponseDataAsString(); // 这里可以解析 JSON 或者截取字符串 def json = new groovy.json.JsonSlurper().parseText(response); String userId = json.data.user_id; vars.put("userIdFromGroovy", userId);

关键在于:Groovy 脚本的执行效率远高于 BeanShell。Jmeter 官方文档也明确推荐在高并发场景下用 JSR223 + Groovy,而不是 BeanShell。所以我自己的项目里,能不用 BeanShell 就不用。

5. 从登录到支付:完整串联一个接口关联实战案例

5.1 接口背景与关联点分析

纸上谈兵半天,来做一个完整的模拟项目实战。模拟项目X提供四个接口:

  • 登录接口:POST /auth/login,请求体为用户名和密码,响应返回token和refresh_token。
  • 用户信息接口:GET /user/info,请求头需要携带登录拿到的token。
  • 创建订单接口:POST /order/create,请求体需要用户 ID,响应返回order_id。
  • 支付订单接口:POST /order/pay,请求体需要order_id,响应返回支付结果。

正常情况下,四个接口在业务上是一条链:需要先登录拿到 token,然后查用户信息拿到 user_id,再创建订单拿到 order_id,最后用 order_id 去支付。

我在 Jmeter 里会新建一个线程组,四个 HTTP 请求依次排列,每个请求下面挂对应的提取器。这里最容易踩的坑是线程组策略:默认情况下,线程组里的多个取样器是按顺序执行的,这没问题。但如果你开了“独立运行每个线程组”或者用了插件控制执行顺序,关联就会断掉,因为下一个请求的变量还没生成。

5.2 第一步:登录接口与 token 提取

登录请求配置完成后,给登录请求添加一个“正则表达式提取器”或“JSON 提取器”。因为登录返回的是 JSON 格式,我选择 JSON 提取器:

  • 引用名称:loginToken
  • JSONPath 表达式:$.data.token
  • 默认值:NOT_FOUND

跑一次登录请求,打开“查看结果树”,确认响应里存在data.token字段。然后在下一个请求的 HTTP Header 管理器里,添加一个请求头:

Authorization: Bearer ${loginToken}

注意,这里的${loginToken}是 Jmeter 的变量引用语法。Jmeter 在运行时会把整个字符串中的${变量名}替换成变量的实际值。我见过新手在这里犯错误:把${loginToken}写进了请求体,请求体渲染时变成了字面量,服务器就返回 token 无效。这部分其实很隐秘,因为你点“查看结果树”时,看到发送的报文里变量没有展开,但有时候 Jmeter 的变量显示是有延迟的,建议直接开“调试取样器”确认。

5.3 第二步:跨线程组传递数据的思路

真实的项目中,登录往往是一个线程组里的公共操作,业务操作在另一个线程组。比如你压测时,一个线程组专门负责登录并发,另一个线程组负责下单业务并发,这两个线程组之间的变量是不互通的。

Jmeter 的变量作用域是这样的:同一个线程组内,后置处理器创建的变量默认只能在该线程组内可见。不同线程组之间,默认是不可见的。那怎么让登录线程组里拿到的 token 传给业务线程组呢?我有三种常用方案,按推荐程度排序:

方案一:使用属性(props)传递。Jmeter 的 props 是全局性的,所有线程组都能访问。加一个 BeanShell 后置处理器或 JSR223 后置处理器:

props.put("globalToken", vars.get("loginToken"));

然后在另一个线程组里,通过 JSR223 前置处理器读取:

String gToken = props.get("globalToken"); vars.put("loginToken", gToken);

或者直接在请求头里使用${__P(globalToken)}引用属性。这里__P是 Jmeter 内置的函数,专门读取全局属性。

方案二:使用 CSV 数据文件。登录线程组将 token 写入一个临时 CSV 文件,业务线程组用 CSV 数据文件配置元件读取。这种方式适合需要大量数据、且要保留每个虚拟用户独立 token 的场景。缺点是文件 IO 会影响性能,压测建议慎用。

方案三:把请求全部放在一个线程组里,用逻辑控制器控制顺序。这最简单,但灵活性差。

最稳的个人建议:能用单线程组解决的,就在单线程组里完成;主线程组业务线程组之间要传公共数据,用 props 方案。

5.4 第三步:动态请求体与数据验证

创建订单接口需要用户 ID。用户信息接口返回的是一位用户的 ID,在 Jmeter 里你添加一个 JSON 提取器,引用名称userId,表达式:$.data.user_info.id。

然后创建订单请求体可以直接写成:

{ "user_id": ${userId}, "goods_id": 101, "quantity": 2 }

这里不需要双引号,因为 user_id 是数字类型。如果服务端要求字符串,你就写成"user_id": "${userId}",注意引号别漏,不然 JSON 语法就错了。

有些同学会问:如果提取到的 user_id 是在某个范围内,或者要做参数化组合怎么办?这时我会用${__V()}或${__evalVar()}这类函数做变量嵌套。举个例子,你需要把两个变量拼接:

${__V(userId_orderId)}

这个__V函数会用括号里的内容作为变量名,再去读取对应变量的值。嵌套变量的场景虽然不多,但一旦遇到就会特别感谢有这个函数。

响应数据验证方面,我强烈建议给每次提取都做好断言。最常见的是在请求下加“响应断言”,检查响应文本是否包含某个关键标识。比如支付请求返回{"result": "success"},就断言响应文本包含"success"。这个断言能让你在逻辑断链时立刻定位到哪一步出了问题,而不是傻傻地看整个链路黑盒运行。

5.5 顺带说下循环控制器与批量数据关联

在接口测试里,经常需要对一批订单号逐个执行支付操作。这时我会用到“循环控制器”。在循环控制器里放一个支付请求,循环次数填为绑定的变量,比如 ${orderCount}。

关键点:如果订单号是列表,提取器收到多个值后,需要在循环内用变量动态引用。比如 JSON 提取器设置了匹配数字 -1,生成了orderList_1、orderList_2等变量。循环控制器里用${__V(orderList_${i})}来动态取值,其中i是循环计数变量。这段逻辑第一次看可能会绕,建议找一个实际接口练一练,理解了之后就会觉得非常好用。

6. 常见问题与排查技巧实录

6.1 提取不到值:按这个顺序排查

每次听到“我写了提取器,但变量是空的”这种问题,我心里都有一套固定排查顺序,分享给你:

  1. 先用“查看结果树”确认该取样器的响应内容非空。如果响应本来就是空的,那再怎么写提取器也没用。
  2. 确认提取器挂在哪个取样器下面。提取器是后置处理器,必须挂在“产生响应”的那个取样器下面,不能挂在 Timer 或 HTTP Header 管理器下面。这个低级错误其实很常见。
  3. 确认提取器中的“要检查的响应字段”是否选对。响应内容在 Body 里就选 Body,在响应头里就选响应头。
  4. 确认正则或 JSONPath 表达式本身没有问题。把表达式直接在提取器面板的“测试”按钮里跑一遍,能立即看到结果,这是一种非常直观的验证方式。
  5. 检查引用名称是否与已有变量冲突。Jmeter 的变量系统在重复赋值时不会给你任何警告,你只能通过 Debug 取样器看当前所有变量。
  6. 确认该取样器之前是否有断言失败导致响应字段被置为空。有些断言在失败后会让后续取样器中断执行。

6.2 变量在下一个请求里没生效:作用域惹的祸

有一个问题特别有代表性:Session Id 是在登录取样器的响应头里提取的,变量名sessionId。在接下来的 HTTP 请求里,直接写 Cookie:JSESSIONID=${sessionId},但测试结果显示发送出去的请求里,这个 cookie 值是空的。

这种问题十有八九就是作用域。Jmeter 的变量作用域默认是当前线程组当前取样器链,但如果你的 HTTP 请求不在同一个线程组,就看不到之前提取的变量。另一个常见误操作是:两个 HTTP 请求虽然是同一个线程组,但中间夹了一个“并行控制器”或“事务控制器”,导致变量初始化顺序被打乱。解决方式也比较粗暴:要么把提取器和引用提取器的请求放在同一个线性的取样器链里,要么用 props 做全局变量跨线程组共享。

6.3 中文乱码与编码问题

接口返回的中文变成了\u5728\u7ebf\u65e5\u5fd7这种 Unicode 转义,或者直接乱码,通常在两种场合格外明显:一是响应内容编码是 UTF-8 但 Jmeter 默认用 ISO-8859-1 解析,二是接口返回的内容是 Gzip 压缩流,需要先解压再提取。

针对第一种情况,在 HTTP 请求的“内容编码”一栏填上 UTF-8 基本可以解决。如果某些响应是 GB2312 编码,则在 JSR223 里用prev.getResponseDataAsString()配合手动指定字符集读取,更灵活。第二种情况的解法是:在 HTTP 请求下添加一个“BeanShell 后置处理器”或“JSR223 后置处理器”,先判断prev.getContentEncoding(),再解压,然后再做提取。我建议你平时多留意接口的 Content-Type 和 Content-Encoding 头部,很多提取失败其实是编码问题伪装出来的。

6.4 多个匹配时的必要策略

正则提取器在遇到响应里出现多个相同结构时,默认取随机一个,容易导致测试不稳定。我在第 3.3 节提过 Match No. 的作用。这里再补充一个实战场景:创建订单接口一次返回 10 个订单号,后续要做轮询,直到订单变为“已支付”。这种情况不适合在 Jmeter 里用一个变量解决,而是要把订单号列表存下来,配合 BeanShell 或 Groovy 做逻辑判断。

我的做法是:

// 伪代码:遍历订单号列表,检查每个订单状态 String orderList = vars.get("orderList_matchNr"); for (int i = 1; i <= Integer.parseInt(orderList); i++) { String orderId = vars.get("orderList_" + i); // 发起查询请求,判断状态 }

这段代码用 JSR223 实现会很稳,但注意不要在大并发下用过多 Groovy 做复杂 IO,会影响压测结果。

7. 最后聊一点个人经验

做了这么久的接口测试,我最大的感受是:关联这个功能本身不难,难的是对响应数据的敏感度。拿到一个接口,先判断它依赖哪些数据、能提供哪些数据,画一张简单的数据流转表,再动手写脚本,比上来就在 Jmeter 里堆提取器要高效得多。这也是老手和新手之间最明显的区别之一。

如果你刚开始接触,我建议你拿自己手头最常见的接口练手,先跑通“登录 token 提取 → 下一个请求引用”这个闭环,再逐步尝试跨线程组和循环处理。不要一上来就追求用 Groovy 处理一切复杂逻辑,灯下黑的情况我见过太多了。

至于将来,这套思路完全可以直接平移到接口自动化平台或 CI 流水线里——只要你理解了“变量怎么来、变量怎么用、变量作用域有多大”这三个问题,不管 Jmeter 未来怎么更新,或者你切换到其他工具,核心逻辑都不会变。希望这篇文章能让你少走一些弯路。如果实操中遇到问题,欢迎在讨论区把你的请求结构描述出来,大家互相参考一下。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询