搞接口测试和压测的人,几乎都会遇到同一个场景:登录接口返回一个 token,后面十几个业务接口每次请求头里都得带上它。刚开始做的时候,可能都是手动复制 token 粘贴到下一个请求里,一两个接口还好,等接口数量上了两位数,或者并发压测的时候需要每个线程各用各的 token,手工操作就完全不可行了。
解决这个问题的核心组件,就是 Jmeter 里的后置处理器(Post Processors)。它的作用概括成一句话:在取样器执行完之后,对响应内容做二次处理,最常见的就是从响应里提取出动态值,存成变量供后续请求使用。这篇文章我就围绕后置处理器这个主题,把它的原理、常用组件、完整实战和排查思路一次讲透。不管是刚接触 Jmeter 的测试新人,还是已经写过脚本但总在提取变量上翻车的老手,这篇都能给你一些可以直接落地的参考。
1. 后置处理器的定位与设计思路
1.1 为什么需要后置处理器:动态数据的依赖问题
先想一个问题:接口测试里,请求之间往往不是孤立的。A 接口登录后拿到 token,B 接口查询订单必须带这个 token;C 接口创建了一个订单,D 接口要拿订单号去查询状态。这种数据依赖在真实业务里比比皆是。
如果这些返回数据是固定的,那写死在脚本里没有任何问题。但现实中,token 有过期时间,订单号每次创建都不同,验证码是一次性的,session id 也是动态生成的。性能测试就更明显了,模拟 100 个用户并发登录,每个人都有独立的 token,脚本里不可能写 100 个不同的固定值。
这时候就需要一种机制:在请求发出并收到响应后,自动从响应内容里把需要的数据“抠”出来,保存为变量,下一个请求直接引用这个变量。 后置处理器就是干这个的。它在取样器执行结束之后触发,因此天然适合做关联数据的提取。
1.2 执行时机与作用域:这俩概念搞不懂,脚本肯定出问题
后置处理器最容易被忽略但也最关键的特性,就是它的执行时机和作用域。
执行时机方面,后置处理器只会作用于它所在的那个取样器。也就是说,你把一个正则提取器放在某个 HTTP 请求下面,那么只有这个 HTTP 请求执行完,提取器才会执行。其他请求执行完,这个提取器不会插手。
作用域方面,这就是个坑。一个后置处理器如果在某个取样器下,它的作用范围就是“该取样器的响应”。如果你把它放在线程组层级,那么它会作用于线程组下每一个取样器;如果放在某个控制器(比如循环控制器、事务控制器)下面,就会作用于该控制器内所有取样器。
这么说可能还是有点抽象,我直接举一个踩过坑的例子。早期我把一个提取 token 的正则提取器直接放在了线程组下面,想着“反正登录请求会返回 token,放哪里都能提取到”。结果呢?线程组下第一个请求根本就不是登录请求,它的响应里压根没有 token,提取器拿不到匹配值,变量就是空的,后面的请求全部 401。如果你的线程组里第一个请求恰好是登录请求,那可能侥幸能用,但只要调换顺序或新增请求,脚本就废了。所以我的建议很明确:和登录请求强相关的提取器,就放到登录请求下面,不要放在线程组层级。规则请记住:后置处理器放在哪一级,就影响哪一级范围内的取样器响应。
我再用一个表格把常见放置位置和生效范围梳理一下:
| 放置位置 | 生效范围 |
|---|---|
| 取样器子节点(推荐) | 仅对该取样器的响应生效 |
| 控制器下 | 对该控制器内所有取样器响应生效 |
| 线程组下 | 对该线程组内所有取样器响应生效 |
| 测试计划下 | 对所有线程组所有取样器响应生效 |
搞清楚执行时机和作用域之后,后置处理器才能被你掌控在手里。上面这种层级关系,实际调试的时候可以通过“查看结果树”里面的取样器执行顺序来印证。
1.3 后置处理器家族:先认识这些常用组件
Jmeter 的后置处理器不止一种,内置的有:
- 正则表达式提取器(Regular Expression Extractor)
- JSON 提取器(JSON Extractor)
- CSS/JQuery 提取器(CSS Selector Extractor)
- XPath 提取器(XPath Extractor)
- 边界提取器(Boundary Extractor)
- HTML 链接提取器(HTML Link Parser)
- BeanShell 后置处理器(BeanShell PostProcessor)
- JSR223 后置处理器(JSR223 PostProcessor)
- JDBC 后置处理器(JDBC PostProcessor)
- 调试后置处理器(Debug PostProcessor)等
大多数时候,我们用到的其实就是前五个和脚本类。我给它们简单分个类:
- 文本类提取:正则表达式提取器、边界提取器。适用于任意文本响应,包括 JSON、HTML、XML,只要你能用正则描述出特征串。
- 结构化提取:JSON 提取器用于 JSON 响应;XPath 提取器用于 XML/HTML;CSS/JQuery 提取器用于 HTML 页面。它们更语义化,写起来更直观,但要求响应格式是特定的结构化数据。
- 脚本类:BeanShell 和 JSR223。当内置提取器无法满足需求时,比如需要先判断条件再提取、需要做字符串拼接、需要调用外部 jar 包中的方法时,就用脚本类兜底。
我的个人选择顺序是这样的:能用 JSON 提取器就不写正则,能用边界提取器就不写正则,正则留到最后。 原因后面细说。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心细节解析与实操要点
2.1 正则表达式提取器:传参配置和正则写法
正则表达式提取器是后置处理器里的元老级组件,任何文本响应都能用。先看它的面板配置项,每个字段都有自己的含义:
- Apply to:默认是 Main sample only,意思是只用主取样器的响应。如果请求发生了重定向或者有子请求,你可以选 Main sample and sub-samples 或者 Sub-samples only。
- Field to check:对响应的哪个部分做匹配。常用的是 Body(响应正文)、Response Headers(响应头)、URL。比如有些 token 会放在响应头里,那这里就要选 Response Headers。
- Name of created variable:变量名,后面用
${变量名}引用。 - Regular Expression:正则表达式。
- Template:模板,也就是提取匹配结果中的哪一组。
- Match No.:匹配第几个,0 表示随机,-1 表示全部匹配,1 表示第一个匹配。
- Default Value:没匹配到时变量使用的默认值。这里一定要写,否则变量就是空对象,调试时非常难定位。
正则表达式的写法是多数人的痛点。我先说一个核心思路:用括号把需要提取的内容包起来,用 (.*?) 这种非贪婪模式去匹配中间的值,前后用固定文本作为锚点。
举个例子,假设响应体是下面这样:
code复制{"code":0,"data":{"token":"eyJhbGciOiJIUzI1NiJ9.abc123"}}
我要提取 token 的值 eyJhbGciOiJIUzI1NiJ9.abc123,正则可以这样写:
code复制"token":"(.*?)"
解释一下:"token":" 是固定的左锚点,(.*?) 是捕获组," 是右锚点。括号表示我要提取的内容,.*? 表示非贪婪匹配任意字符——这里非贪婪很关键,如果写成 .* 贪婪模式,遇到响应里同时存在多个引号的情况,可能一口气匹配到最后一个引号,把多余的内容也吞进来。
正则里还有一个常见写法:匹配登录后返回的 session 或 token,但响应里可能有多个类似结构的字段,这时你需要定位唯一性。那就把锚点写长一点,比如:
code复制"access_token"\s*:\s*"([^"]+)"
\s* 表示允许存在零或多个空格,[^"]+ 表示匹配一个或多个非引号字符,这样即使 JSON 里 key 和 value 之间有空格也能匹配到。
正则提取器的配置上,我建议配合“查看结果树”右边面板的“正则表达式测试器”来调试正则。这个工具会实时显示匹配内容和组数,不用一遍遍跑脚本验证,省很多时间。
2.2 JSON 提取器:JSONPath 语法和配置
响应是 JSON 格式时,JSON 提取器比正则更推荐。正则写起来像在“绣花”,而 JSONPath 是直接按照 JSON 的结构定位,可读性强多了。
JSON 提取器的核心字段:
- Name of created variables:变量名,多个变量用分号分隔,比如
token;orderId - JSON Path expressions:JSONPath 表达式,多个用分号分隔,和变量名一一对应
- Match No.:0 随机,-1 提取全部,1 提取第一个
- Default Values:默认值,分号分隔
JSONPath 的基本语法需要掌握这几个:
| 表达式 | 含义 |
|---|---|
$.data.token |
取 data 对象下的 token 字段 |
$..token |
递归查找所有 token 字段 |
$.data.items[0] |
取 data 下 items 数组的第一个元素 |
$.data.items[*].id |
取 items 数组中所有元素的 id 字段 |
$[?(@.name == 'test')] |
过滤,取 name 为 test 的元素 |
再看一个实战例子。登录接口响应:
json复制{
"code": 0,
"message": "success",
"data": {
"userInfo": {
"id": 10001,
"username": "zhangsan"
},
"token": "abc123xyz",
"permissions": ["read", "write", "delete"]
}
}
要提取 token,JSONPath 写 $.data.token;要提取用户 id,写 $.data.userInfo.id;要提取第一个权限字段,写 $.data.permissions[0]。配置上,变量名和 JSONPath 表达式是一一对应的,比如变量名写 token;userId,表达式写 $.data.token;$.data.userInfo.id,分号是分隔符,注意不能写中文分号或者多空格,否则会解析出错。
有一点要注意:如果 JSONPath 匹配结果是数组,Match No. 选择很关键。比如 $..id 可能返回多个 id,你 Match No. 填 1 取第一个,填 -1 则取全部,此时变量会变成 id_1、id_2 这样的格式。默认 0 是随机取一个,这在某些场景下会带来不确定性,如果你期望拿到确定的第一个值,一定要把 Match No. 写成 1。
2.3 边界提取器:正则的简化和替代
Jmeter 5.x 之后加入的边界提取器(Boundary Extractor)是个被很多人低估的组件。它不需要写正则,只需要指定左边界和右边界,就能把中间的内容提取出来。
比如响应里有 <input type="hidden" name="csrf_token" value="abc123">,我需要提取 abc123。用边界提取器,左边界填 value=",右边界填 ",搞定。比正则简单直白多了,而且不用担心转义问题。
边界提取器的配置项和正则提取器几乎一样,也有变量名、左右边界、匹配数字和默认值。它比较适合提取那些“前后固定、中间动态”的文本内容,例如 token、验证码、ID。我在实际项目里,很多原本用正则的地方都已经换成边界提取器了,尤其是提取 HTML 里的缓存值、隐藏字段这类,效率高不少。
2.4 CSS/JQuery 提取器和 XPath 提取器
这两个是针对 HTML/XML 的提取器。
CSS/JQuery 提取器用 CSS 选择器来定位元素并提取属性或文本,适合对 HTML 页面做响应断言和数据提取。比如要提取页面上所有 a 标签的 href 属性,表达式写 a@href;要提取 class 为 item 的 div 中的文本,写 div.item。它基于 Jsoup 解析,对 HTML 结构不严格的页面容忍度也不错。
XPath 提取器是用 XPath 语法从 XML/HTML 中提取节点。比如提取 //input[@name='csrf_token']/@value,它能直接按属性定位。不过 XPath 对 HTML 的解析要求相对严格,如果页面结构里标签不闭合、属性值不规范,XPath 可能会提取失败,这时候 CSS 选择器反而更稳。
一个建议:如果接口返回的是标准 HTML 文档,且你熟悉 CSS 选择器,优先用 CSS/JQuery 提取器;如果响应是 XML,特别是 SOAP 协议那种,XPath 提取器是第一选择;如果你两种语法都不熟,那就老实写正则吧,正则至少还能复用你已有的知识。
2.5 脚本类后置处理器:BeanShell 和 JSR223 的使用场景
内置提取器大多数时候够用了,但总有奇怪的业务需求,比如:
- 提取到 token 后,需要把 token 拼成一个特殊的请求头格式,比如
Bearer abc123。 - 需要根据响应内容做判断,如果 code 不等于 0,就标记失败。
- 需要把提取到的多个值拼成一个逗号分隔的字符串。
这时候就需要脚本类后置处理器。JSR223 后置处理器是目前官方强烈推荐的脚本嵌入口,支持 Groovy、Java、JavaScript 等语言。Groovy 因为兼容 Java 语法、性能好、有闭包等特性,是首选。BeanShell 也可以做同样的事情,但它每次执行都会做脚本解析,性能差不少,如果脚本里做了比较重的逻辑,在压测场景下会成为瓶颈。
举个例子,用 JSR223 + Groovy 提取响应数据并处理:
groovy复制def response = prev.getResponseDataAsString()
def json = new groovy.json.JsonSlurper().parseText(response)
def token = json.data.token
vars.put("token", "Bearer " + token)
这段脚本做了三件事:拿到上一个取样器的响应字符串、解析 JSON、提取 token 并拼上前缀 Bearer ,最后通过 vars.put 存入 Jmeter 变量。后续请求头里直接用 ${token} 就能引用到完整值。
使用脚本类后置处理器时,有几个要点:
- 脚本里访问 Jmeter 变量用
vars.get("变量名"),设置变量用vars.put("变量名", 值)。 - 访问上一个取样器结果用
prev对象,它的常用方法有getResponseDataAsString()、getResponseCode()、getResponseHeaders()。 - 脚本里做日志输出用
log.info("debug message"),可以在 Jmeter 日志里看到,推荐多打日志,排查问题快很多。
3. 实操过程与核心环节实现
3.1 实战场景:登录 token 提取与后续接口调用
讲了这么多理论,我拿一个最常见的场景完整走一遍:登录接口返回 token,后续多个业务接口需要这个 token 做鉴权。
先描述接口背景:
- 登录接口:
POST /api/login,请求体是 JSON{ "username": "admin", "password": "123456" }。 - 登录响应:
{ "code": 0, "data": { "token": "eyJhbGciOiJIUzI1NiJ9.xxxxx", "expiresIn": 7200 } } - 查询用户信息接口:
GET /api/user/info,请求头需要携带Authorization: Bearer <token>。 - 查询订单列表接口:
GET /api/orders,同样需要携带该 token。
我下面用两种方式实现 token 提取,一种正则,一种 JSON 提取器,你可以对比着看。
3.2 完整测试计划搭建步骤
在 Jmeter 里,我按下面的结构搭建:
code复制测试计划
└── 线程组
├── HTTP 请求默认值(服务器地址、端口、协议)
├── 登录请求
│ ├── 后置处理器:JSON 提取器 或 正则表达式提取器
│ └── 后置处理器:调试取样器(Debug Sampler,调试用)
├── 查询用户信息请求
└── 查询订单列表请求
具体步骤:
- 添加线程组。线程数根据场景来,验证功能时设 1 即可;压测时再调高。
- 添加 HTTP 请求默认值,填入协议
http、服务器地址example.com、端口8080。这样后续请求只需要填路径,不用重复写主机地址。 - 添加登录请求。方法选 POST,路径填
/api/login,Body Data 填:
json复制{
"username": "admin",
"password": "123456"
}
-
在登录请求上右键,添加后置处理器。如果响应是 JSON,我推荐用 JSON 提取器,配置如下:
- Name of created variables:
loginToken - JSON Path expressions:
$.data.token - Match No.:
1 - Default Values:
NOT_FOUND
- Name of created variables:
-
如果要用正则提取器,配置如下:
- Field to check: Body
- Name of created variable:
loginToken - Regular Expression:
"token":\s*"([^"]+)" - Template:
$1$ - Match No.:
1 - Default Value:
NOT_FOUND
-
添加查询用户信息请求。路径填
/api/user/info,在 HTTP 请求的“HTTP Header Manager”里添加一个请求头,名称Authorization,值Bearer ${loginToken}。 -
添加查询订单列表请求,同样在请求头里引用
${loginToken}。 -
添加“查看结果树”监听器,运行脚本,查看响应。
这一步跑通后,你会发现只要登录请求执行成功,后续两个请求就会自动带上有 token 的请求头。线程数调到 50、100,每个线程也都会使用自己登录返回的 token,互不干扰。这就是关联带来的效果。
3.3 提取结果如何跨线程组传递
前面提到的是同一个线程组内的变量传递,变量默认是线程私有的。但有些测试场景是:线程组 A 负责登录并获取 token,线程组 B 负责用这些 token 执行业务操作。此时不能直接 ${loginToken} 引用,因为不同线程组之间的变量不可见。
最简单的跨线程组传递方式是用 Jmeter 属性(Property)配合 __setProperty 函数。在登录请求的后置处理器里,先提取到 token 变量,然后在 BeanShell/JSR223 后置处理器或直接在请求参数中调用 __setProperty 把它存成属性:
groovy复制props.put("loginToken", vars.get("loginToken"))
然后在另一个线程组中,用 ${__P(loginToken,)} 引用这个属性。
注意:属性是全局的,但如果你同时跑多个线程,每个线程都会往同一个属性里写值,后写的会覆盖先写的。因此如果要求每个线程用各自独立的 token,这种做法就不合适了。更合理的做法是把所有业务请求放在同一个线程组里,靠线程内的变量传递,保证每个线程使用的是自己的 token。跨线程组共享属性只适用于那种“一个登录 token 被所有线程复用”的简单场景,比如登录一次,然后所有线程拿同一个 token 去压测查询接口。
还有一个方案是使用 CSV 数据文件,预先准备多个 token 写入 CSV,通过 CSV 数据文件配置器读取。这种方式适合 token 已经通过外部脚本预先获取好的场景,脚本里不需要再去登录,也就没有跨线程组传递的问题了。
3.4 多个匹配值的处理技巧
接口返回可能不是单条数据,而是一个列表。比如查询订单接口返回了 3 个订单 ID,你需要在后续请求里分别使用这 3 个 ID 做操作。
这时候提取器的 Match No. 就要设成 -1,表示提取全部。正则提取器会生成变量:
orderId_1:第一个匹配值orderId_2:第二个匹配值orderId_3:第三个匹配值orderId_g:正则里总共有多少组,注意这个值不是匹配个数,而是正则的捕获组数量,新手经常搞混。
如果你用了 JSON 提取器,Match No. 设置为 -1 后,同样会生成 orderId_1、orderId_2 这样的变量,但没有 _g 这个组数变量。
那么在循环控制器里怎么逐个使用这些变量呢?一个常见的做法是配合计数器:
- 添加一个循环控制器,循环次数填提取到的值数量,比如写
${orderId_matchNr}——对了,正则提取器在 Match No. 为 -1 时还会生成一个${变量名_matchNr},它表示实际匹配到的总数。JSON 提取器用-1匹配时,同样也有这个matchNr变量。 - 循环控制器下添加一个计数器,起始值 1,递增 1,最大值就是
matchNr,引用名为idx。 - 在请求参数中引用
${orderId_${idx}},Jmeter 支持这种嵌套变量引用。
这种方式处理“不定数量的动态值”非常经典。比如从查询接口响应中拿到一批订单号,然后逐个请求订单详情,脚本就能天然适配不同数量的数据,不用写死。
3.5 用 Debug Sampler 验证提取结果
脚本写完了,怎么确认变量到底提取到没有?不要盲跑。在登录请求后加一个调试取样器(Debug Sampler),运行后打开“查看结果树”,Debug Sampler 的响应里会列出所有 Jmeter 变量和属性,包括你提取的 loginToken。
我调试时习惯先加 Debug Sampler,确认提取值正确后再删除,避免最终脚本里多出无关的取样器影响压测数据。还有一种方式是用“响应断言”断言变量值,但调试阶段 Debug Sampler 更直观。
另外,如果发现提取不到值,不要急着改正则,先打开“查看结果树”,看登录请求的响应正文到底长什么样。有时候你以为返回的是 JSON,实际可能是 {"msg":"用户名或密码错误"},或者响应里有 BOM 头、空格、转义符,正则一匹配就失灵。先确认响应内容,再改提取表达式,这才是有效率的排查顺序。
4. 常见问题与排查技巧实录
4.1 提取到的值总是 null 或显示为默认值
这是最基础也最常见的问题。变量取不到值时,会回落到 Default Value 设定的内容。常见原因有这几个:
- 响应里根本没有你要的字段,先去“查看结果树”确认响应内容。
- 响应有空格、换行、转义字符,正则或边界没匹配上。常见的是 JSON 返回里
"token": "abc"之间多了一个空格,正则"token":"(.*?)"就挂了,需要写成"token"\s*:\s*"([^"]+)"。 - 作用域不对。提取器不在对应的取样器下面,跑的是别的请求的响应内容。
- 提取器的 Field to check 选错。token 在响应头里,你选的 Body,自然提取不到。
排查顺序建议是:先看响应内容,再看作用域,再看匹配表达式和字段选择。
4.2 同一线程组里,后一个请求拿不到前一个请求提取的变量
先检查引用方式是不是 ${变量名},再检查变量名拼写是否一致。如果确认没问题,那么大概率是作用域问题——比如提取器放在线程组层级,但因为没有放在登录请求下,导致先执行的其他取样器把它覆盖成空值了。把它挪到具体的登录请求下就好。
还有一种情况是:后一个请求先于登录请求执行了。如果线程组内请求顺序不是按照你排列的顺序执行,可能是因为 Jmeter 默认是“同一线程组内按添加顺序执行”,但如果你使用了常量吞吐量定时器、调度器之类的组件,或者请求被放到不同控制器里,执行顺序可能会被改变。解决方法是再添加一个逻辑控制器,比如“临界部分控制器”,或直接把两个请求放到同一个“简单控制器”里,保证顺序。
4.3 正则贪婪匹配导致提取到一大串东西
典型的例子:响应是 "name":"aaa" "token":"bbb",你写 "name":"(.*)",结果把后面所有内容都吞进去了。把 .* 改成 .*? 或者 [^"]* 基本能解决。这个我在前面已经强调过,这里再列出来是因为它是正则提取器翻车率最高的一个原因。
4.4 JSON 提取器遇到数组取不到值
有些接口返回的结构是:
json复制{
"data": [
{
"id": 1,
"name": "a"
},
{
"id": 2,
"name": "b"
}
]
}
如果你写 $.data.id,就取不到,因为 data 是数组,不是对象。正确写法是 $.data[0].id 或者 $.data[*].id。前者取第一个,后者取全部,然后结合 Match No. 使用。
4.5 多个用户并发时,token 被覆盖怎么办
如果你把 token 存成了 Jmeter 属性(Property),那么所有线程都会往同一个属性上写,就会出现互相覆盖。如果存成了变量(Variable),变量本身就是线程私有的,不存在覆盖问题。所以并发场景下推荐用变量传递,而不是属性传递。只有你确实希望所有线程共用一个 token 时,才用属性。
4.6 响应内容非 UTF-8 导致的中文乱码和提取失败
有些老系统的接口返回 GBK 编码的 JSON,Jmeter 默认按 UTF-8 解码会出现乱码,也会影响正则匹配。解决办法是在 HTTP 请求里设置 Content Encoding 为 GBK,或者在配置文件 jmeter.properties 里设置 sampleresult.default.encoding=GBK,然后重启 Jmeter。再不行就用 JSR223 后置处理器手动按指定编码读取响应内容:
groovy复制def bytes = prev.getResponseData()
def response = new String(bytes, "GBK")
在脚本里按 GBK 解码后再处理,这样就不会被 Jmeter 默认的解码逻辑干扰了。
4.7 常用组件选型速查表
我整理一个选型建议,方便你对照使用:
| 场景 | 首选组件 | 备选 | 说明 |
|---|---|---|---|
| JSON 响应中提取字段 | JSON 提取器 | 正则提取器 / 边界提取器 | JSONPath 可读性强,修改成本低 |
| HTML 中提取固定前后缀文本 | 边界提取器 | 正则提取器 | 简单直接,不易出错 |
| 按 CSS 选择器提取元素 | CSS/JQuery 提取器 | XPath / 正则 | 对结构不严谨的 HTML 更友好 |
| XML / SOAP 响应 | XPath 提取器 | 正则提取器 | 和 XML 语义匹配 |
| 复杂逻辑处理后再取值 | JSR223 后置处理器 | BeanShell 后置处理器 | Groovy 优先,性能好 |
| URL 中提取参数 | 正则提取器 | 边界提取器 | Field to check 选择 URL |
这个表基本都是我实际使用后得出的结论,不一定所有场景都适用,但拿来当默认选择基本不会出大错。
最后再分享一个心得:后置处理器调试的时候,耐心比技术更重要。提取不到值,90% 的问题都出在“响应内容和你预期的不一样”这个根因上。先看响应,再改提取规则,用 Debug Sampler 逐步验证,这个思路能帮你少走很多弯路。初次接触的话,我建议你先拿一个简单的登录接口,把 JSON 提取器和正则提取器各配一遍,跑通后再去碰复杂场景,基础打牢了,后续写脚本会顺很多。
