☰
JMeter函数场景实战:接口测试与性能测试高频用法详解
2026/10/10 8:05:49 网站建设 项目流程

先聊一个场景:你刚接手一个下单接口的压测任务,脚本里把手机号写死成13800138000,并发跑到 80 线程之后,数据库里全是同一条用户记录在刷订单,压测结果是好看,但团队根本不敢拿它当参考。这时候你才意识到,JMeter 里那堆带双下划线的函数,才是把接口测试和性能测试做出价值的关键。

这篇东西我拖了很久,标题叫"最全解析",但真要把 JMeter 官方几十个函数挨个讲一遍,没几个人能看完。所以这篇只做一件事:把接口测试和性能测试里真正高频、真正解决过问题的函数,按场景分类拆透,每个都讲清楚"为什么这么用"和"在哪些地方容易翻车"。无论你是刚开始写 JMeter 脚本,还是已经跑过几轮压测但一直靠死数据硬撑,都应该能从里面找到直接抄作业的写法。

1. 先把函数当参数用:函数语法与变量/属性的边界

1.1 函数表达式的基本结构

很多新手第一次接触 JMeter 函数的反应是"这玩意和变量有啥区别"。严格讲,变量是被动的,函数是主动的——你说"给我一个随机数",它当场生成一个;你说"读文件下一行",它立刻把游标往下挪一格再返回内容。这就是函数的价值:它让脚本在运行过程中自己产生变化的数据。

函数的固定长相是这样:

${__函数名(参数1,参数2,...)}

第一个参数通常是函数的主要输入,后面的参数有的是可选配置,有的是用来指定"把结果存到哪个变量里"。比如:

${__time(yyyy-MM-dd HH:mm:ss,currentTime)}

这里yyyy-MM-dd HH:mm:ss是格式,currentTime是变量名。调用之后,脚本里所有写${currentTime}的地方都能拿到这个格式化时间。

琢磨函数时先分清三种东西,会少走一半弯路:

  • 函数调用:${__xxx(...)},运行时动态生成值;
  • 变量引用:${varName},从当前线程的变量池取值;
  • 属性读取:${__P(propName)}或${__property(...)},从全局属性池取值。

它们长得像,但生命周期和作用域完全不同。

1.2 变量和属性:两套存储体系别混淆

我见过最典型的问题:在"线程组 A"里用vars.put("token", "...")存了个变量,跑到"线程组 B"里用${token}拿,结果永远是空。原因很简单:JMeter 变量是线程私有的,每个线程各有一份,线程结束直接销毁。如果你想在多个线程组之间传递登录状态,必须用属性。属性挂在测试计划级别,整场测试期间所有线程都能读到,跨线程组、跨负载机都没问题。

所以你先记住两个判断:

  • 只想在当前线程、当前请求里复用数据,用变量;
  • 需要在不同线程组之间、甚至分布式节点之间共享数据,用属性。

函数群里对应这两条线:__Random、__time、__CSVRead这类负责生成数据,读写变量;__setProperty、__P、__property负责读写属性。把它们混用是 JMeter 脚本最容易埋雷的地方。

1.3 函数能在哪些输入框生效

几乎只要是个文本输入框,函数就能生效。HTTP 请求的路径、参数值、请求体、HTTP Header、断言表达式、正则提取的模板、前置/后置处理器的字段,全都可以写${__函数(...)}。你甚至可以在一段 JSON 请求体里直接塞函数:

{ "timestamp": "${__time(/1000,)}", "orderId": "${__UUID}", "mobile": "${__Random(13000000000,13999999999,)}" }

运行时它会被替换成具体值。这也是函数和"手动填死数据"最核心的区别——脚本是活的。

提示:函数助手对话框在"选项"菜单里,Windows/Linux 下快捷键是 Ctrl+Shift+F1。里面能看到每个参数的解释,是排查函数参数顺序问题最靠谱的工具。不要硬背参数顺序,尤其不同版本 JMeter 对个别函数有差异。

下面给一张我用得最频繁的场景对照表,写脚本前先对着它选函数:

场景推荐函数典型用途
动态时间戳__time/__timeShift签名、请求时间、报告标记
随机整数__Random随机手机号段、随机 ID
随机字符串__RandomString随机昵称、随机密钥、流水号
随机日期__RandomDate订单日期、优惠券有效期
全局唯一标识__UUID幂等键、消息 ID
读文件参数化__CSVRead/__StringFromFile批量账号、批量订单号
计数器__counter/__threadNum唯一编号、线程区分
跨线程传参__setProperty+__P登录 token 传递
签名摘要__digestMD5 / SHA 签名
执行脚本__groovy/__BeanShell复杂逻辑、动态处理

2. 构造动态数据:接口测试每天都在用的时间与随机函数

2.1 时间函数:__time与__timeShift的区别和配合

接口测试里时间函数几乎是万金油。最常见的是签名场景:后端要求请求里带一个时间戳,用来防止重放攻击。这时候直接写:

${__time(/1000,)}

/1000表示把毫秒时间戳转成秒级时间戳,这也是绝大多数后端接口的时间戳格式。如果你需要带格式的时间,比如生成一个2026-07-20 14:30:00这样的字符串,就写成:

${__time(yyyy-MM-dd HH:mm:ss,)}

注意,__time的第二个参数可以直接留空。留空的意思是"只要这个函数的返回值,不存变量",如果后面还要反复用同一时间,就给它一个变量名,避免多次调用导致时间不一致。

__timeShift则是做时间偏移用的。压测的时候你可能需要一个"今天晚上 8 点的优惠券生效时间",或者"三个月前的注册日期",这时候不用在脚本里写死,直接让函数算:

${__timeShift(yyyy-MM-dd,,P1D,,)}

这段的意思是新版 JMeter 函数助手里的参数顺序:第一个是目标格式yyyy-MM-dd,第二个是基准时间(留空表示当前时间),第三个是偏移量P1D(表示增加一天),后面两个分别是区域和变量名。如果你在 JMeter 界面里填,会看到参数名提示,按提示填就行。

注意:P1D是 ISO 8601 的周期写法,P1D是一天,PT2H是两小时,-P1D是往前推一天。老版本 JMeter 的参数顺序和现在相反,网上很多教程都是老的,你发现结果不对时,第一反应应该是打开函数助手对话框核对参数排列,而不是怀疑函数坏了。

2.2 随机函数三兄弟:__Random、__RandomString、__RandomDate

__Random最简单,生成区间内的随机整数:

${__Random(1,100,)} ${__Random(13000000000,13999999999,mobile)}

接口测试里用来模拟用户 ID、商品 ID、年龄等非常方便。但有两件事得留意:第一,JMeter 的随机数是允许重复的,并发跑起来同一个值可能被抽到多次;第二,如果业务上要求"手机号不能重复注册",光靠__Random拼出来的号码,在百万级数据量下依然会有碰撞风险,这时候就要上 UUID 或者数据库现成的账号池。

__RandomString用来生成指定长度的随机字符串:

${__RandomString(8,abcdef0123456789,nonce)}

第二个参数是字符池,你给什么字符,它就从中抽。留空时代默认字符集很大,包含大小写字母和数字,但为了可预期,我一般都会显式指定。这个函数适合生成随机 nonce、随机密钥、随机订单号后缀。

__RandomDate在日期区间里随机挑一天:

${__RandomDate(yyyy-MM-dd,2020-01-01,2030-12-31,)}

它生成的日期可能落到周末或者节假日,如果业务上有"工作日才允许下单"之类的限制,后面最好加一个 if 控制器做二次过滤,或者直接用 Groovy 脚本生成更符合规则的日期。

2.3__UUID:并发场景下的唯一标识

__UUID不需要参数,每次调用生成一个标准 36 位 UUID:

${__UUID}

它几乎是并发场景下保证"全局唯一"的最廉价方案。比如提交订单时后端要一个幂等键,你用${__UUID}生成,理论碰撞概率可以忽略。压测时我习惯把 UUID 和业务语义拼在一起,比如:

{ "traceId": "perf_${__time(/1000,)}_${__UUID}" }

这样既能看到发起时间,又能保证不同线程之间区分度足够高。不过要提醒你,UUID 偏长,如果接口对请求体长度敏感或者日志里不好看,可以只截取一部分,但不要自己写截取逻辑去拼——直接在 Groovy 里用UUID.randomUUID().toString().replace("-", "")更干净。

2.4 实战拼包:一个下单接口的请求体怎么用函数凑出来

下面这个例子是典型的接口测试请求体,我把上面几个函数全部组合进去:

{ "service": "createOrder", "timestamp": "${__time(/1000,)}", "mobile": "${__Random(13000000000,13999999999,)}", "goodsId": "100${__Random(1,999,)}", "quantity": "${__Random(1,5,)}", "orderId": "${__UUID}", "source": "perf_test_${__threadNum}" }

跑起来之后,每一条请求都是一个全新的待下单用户,订单号也各不重复。这样的数据才是压测能用的数据——既贴近真实业务,又不会因为主键冲突把系统打挂。

3. 读数据才是性能测试的命门:文件读取与计数函数怎么选

接口测试阶段,函数随机造数据就够用了。但性能测试不一样:压测要求数据要可控、要贴近生产、要有足够的量。生产环境真实用户的手机号、账号、订单号,通常都在文件或数据库里,你得把它们喂给脚本。这就是文件读取函数的主场。

3.1__CSVRead和 CSV Data Set Config,什么时候用函数

__CSVRead算是一个老牌函数了:

${__CSVRead(D:/data/user.csv,0)}

第一个参数是文件路径,第二个参数是列索引,从 0 开始。它按行顺序读,每次调用自动取下一行。听起来很方便,但实际维护成本不低——因为它内部有游标状态,同一文件在多处引用时,位置会互相影响,你还得手动用*和next()去控制。我很少在新脚本里用它,除非只是临时读一次。

真正推荐的是CSV Data Set Config配置元件。它也是顺序读文件,但有"循环、随机、乱序"等模式可选,还支持分隔符、变量名映射、线程间共享策略。你可以在线程组的"添加 -> 配置元件 -> CSV 数据文件设置"里配置,然后在请求里直接写.注意:CSV 函数虽然叫"函数",但实际几乎没有人用函数方式日常参数化,因为__CSVRead` 的可控性太差;CSVDataSet 才是正道。

什么时候只用函数不用元件呢?答:当你要读的文件内容本身不是一个规整表格,而是一个"按行组织的纯文本列表"时,__StringFromFile比 CSVRead 更合适。

3.2__StringFromFile与__FileToString的取舍

${__StringFromFile(D:/data/orderId.txt,,,)}

__StringFromFile每次调用读取文件里的下一行,适合"一批一批地消费数据"的场景。但它有坑:大文件性能一般,且因为是顺序消费,一旦压测过程中断,游标状态可能丢失,下次重跑又从某个不确定位置开始。

__FileToString则是一次性把整个文件读成一个大字符串:

${__FileToString(D:/data/orderTemplate.json,UTF-8,)}

它的典型用途是读取"固定的请求体模板"。比如你有一个特别复杂的 JSON 请求体,中间要替换的字段有十几个,你可以把模板放在文件里,再用 Groovy 或变量替换来拼完整请求。这样做的好处是脚本里不用写一大坨转义之后的 JSON,维护起来舒服得多。

这两个函数的取舍,我的原则是:

  • 逐行消费、有顺序要求的:优先 CSV Data Set Config;
  • 一次性整文件读取、内容较短:__FileToString;
  • 临时快速处理、不想加元件:__StringFromFile。

3.3__counter的计数器与并发边界问题

__counter是压测脚本里出现频率极高的函数,用来生成递增序号:

${__counter(TRUE,)} ${__counter(FALSE,)}

第一个参数TRUE表示"每个线程独立计数"——你有 100 个线程,每个都从 1 开始数;FALSE表示"全局计数"——所有线程共用一个计数器。实际使用中,全局计数模式常被用来生成"流水号"或"文件前缀"。

但注意一个坑:__counter(FALSE,)在多线程并发下并不保证严格无重复。它的自增逻辑在极端并发下可能因为竞态条件出现跳号或重复。如果你需要的是"全局唯一且绝对连续"的编号,不要只依赖它,比较稳的做法是组合起来:

perf_${__time(/1000,)}_${__threadNum}_${__counter(TRUE,)}

线程号加独立计数器加时间戳,唯一性足够高,而且一眼能看出是哪个线程发的。

3.4 大数据量压测下的数据准备策略

压测最怕的不是脚本写错,而是数据量根本撑不住场景。举个例子:你要压一个"用户领券"接口,要求每个用户只能领一次,假设 QPS 目标是 500,跑 10 分钟就需要 30 万个不重复的 user_id。如果只用__Random随机拼,很可能大量撞车。

我的常规做法是:

  • 先用脚本或 SQL 从生产脱敏导出 N 万条真实用户 ID;
  • 把数据按线程数拆分成多个 CSV,或者一个大 CSV 配合 CSV Data Set Config 的"线程共享"模式;
  • 如果数据量大到单文件几十 MB,优先用"乱序模式 + 独立文件"给每台负载机分发,减轻磁盘 IO 压力;
  • 对必须保持会话状态的接口(登录态、token),把 token 提前生成并连带 user_id 一起写入 CSV,压测时直接读,不要在压测过程中做实时登录。

提示:分布式压测时,CSV 文件要确保每台 Agent 机器上都有路径一致的副本。不要只在 Master 上放文件就以为万事大吉,Agent 跑起来会报文件找不到。

4. 需要算一算的场景:eval、intSum、groovy 与摘要函数

4.1__V与__eval:嵌套变量引用的最后一环

先看一个问题:你有一个订单变量叫order_1、order_2、order_3,循环里每次想取其中某一个,直接用${order_${i}}会解析失败——因为 JMeter 不会自动做两层展开。这时候就要用__V:

${__V(order_${i})}

__V的作用就是"把传入的字符串再当作变量名解析一次"。它是嵌套变量引用的通行证,在循环、前置处理器、后置处理器里非常常见。

__eval更泛化一点,它会对传入的表达式做一次完整的变量替换再返回结果。比如有个变量var1的值是hello,写:

${__eval(${var1})}

返回的结果就是hello。但说实话,__eval直接这样写在普通场景里没啥意义,因为${var1}本身就能取出 hello。它的价值在于拼接复杂表达式,尤其在写脚本化断言时,可以把一段混合了变量和字面量的字符串整体求值。只是这个函数可读性一般,我用它的频率比__V低得多。

4.2__intSum/__longSum:循环里的累加与偏移

${__intSum(1,2,3,total)} ${__longSum(${start},${step},)}

__intSum把所有参数相加,最后一个参数可以作为变量名保存结果。它适合做"循环下标偏移"。比如你在循环控制器里每次需要把商品 ID 区间整体往前挪 1000,就可以用:

${__intSum(${startId},1000,)}

作为下一轮请求的起始 ID。

__longSum和它作用一样,只是用 long 类型防止大数溢出。压测请求里如果涉及金额、流水号累加,用__longSum更保险。注意一点:这些计算函数做的都是"运行时计算",每次请求都会重新求值,不要试图用它来维护"上一次的值"这样的状态——那种状态应该交给变量vars.get()和vars.put()。

4.3__groovy与__BeanShell:脚本函数怎么选

脚本函数是 JMeter 函数群里最"重"的,也是讨论最多的。__BeanShell是老牌子了,但现在我强烈建议新脚本直接上__groovy:

${__groovy(1 + 1,)} ${__groovy(vars.get('mobile').substring(0,3),)}

Groovy 和 BeanShell 都能做复杂逻辑,但区别很大。Groovy 在 JMeter 3.1 之后被官方定为推荐脚本语言,它的执行性能远好于 BeanShell,尤其是在高并发压测时,BeanShell 的解释执行会拖慢线程的执行速度,有时候甚至比接口本身还慢。

我自己踩过一个坑:给一个后台管理接口写了 BeanShell 前置脚本做签名,单机跑 200 线程时 CPU 直接打满,压测结果完全失真。换成 JSR223 + Groovy 之后,CPU 掉了一半多。所以如果你的脚本里还有 BeanShell 取样器或者__BeanShell函数,碰上性能瓶颈时优先怀疑它。

不是所有逻辑都适合塞进__groovy。函数里写大段脚本会让整个表达式变得又长又难查。如果逻辑超过几行,我通常的做法是:单独建一个 JSR223 前置处理器或 JSR223 取样器,把 Groovy 代码写在专门的代码框里,而不是堆在函数参数中。函数适合"短、快、一次求值"的场景。

4.4__digest:接口签名计算完整配置

接口签名是接口测试逃不过的坎。常见签名逻辑:把所有参数拼成一个字符串,加盐,再求 MD5 或 SHA-256。JMeter 的__digest函数就是干这个的:

${__digest(MD5,${timestamp}${nonce}${secret},,sign)}

参数里第一个是算法,第二个是要计算的字符串,第三个是盐(salt,可留空),第四个是变量名。上面的例子把时间戳、随机字符串、固定密钥拼在一起做 MD5,结果存到sign变量里。

真实项目里签名规则五花八门,有的是"参数按 key 排序后拼接",有的是"AES 加密后取摘要"。"按 key 排序"这种规则用__digest还是有点勉强,因为函数内部不做排序。这时候最好还是用 JSR223 前置处理器,Groovy 里写:

def params = [a:'1', c:'3', b:'2'] def raw = params.sort().collect { k, v -> "$k=$v" }.join('&') raw += '&key=your_secret' vars.put("sign", raw.md5())

这才是应对复杂签名规则的通用姿势。__digest适合签名规则简单、转发效验的场景;复杂规则一律脚本化。

5. 跨线程组与分布式压测:属性函数与线程函数搭桥

5.1__threadNum:线程号到底能做什么

${__threadNum}

返回当前线程的编号,从 1 开始。压测时它非常有用:

  • 给每个线程分配不同的数据段:比如 100 个线程,每个线程读取 CSV 里"第 threadNum 行";
  • 生成带线程标识的请求 ID,方便定位日志;
  • 分布式压测里配合机器名,生成全局唯一的流标识。

一个典型写法:

user_${__threadNum}_${__time(/1000,)}

这样哪怕同一秒内两个线程并发,线程号也能把两者区分开。

5.2__setProperty与__P:登录态怎么传给下游线程组

接口压测里最常见的跨线程需求是登录态传递。结构一般是这样:

  • setUp 线程组:负责登录,拿到 token;
  • 业务线程组:负责压测业务接口,需要带 token。

因为变量在线程组之间不通,所以要靠属性搭桥。登录后执行:

${__setProperty(loginToken,${token},)}

__setProperty第一个参数是属性名,第二个是属性值,第三个是"是否返回旧值",一般留空就行。然后在业务线程组里读:

${__P(loginToken,)}

__P直接读属性,第二个参数是默认值。如果属性不存在,就返回默认值。这套组合我几乎在每个压测脚本里都用,是跨线程组传参最标准的方式。

5.3-J参数 +__P:一套脚本压不同档位

__P不仅能读__setProperty写进去的值,还能读取命令行的-J参数。这意味着你可以一套脚本应对不同的压测配置。

跑压测时执行:

jmeter -n -t test.jmx -Jthreads=200 -Jduration=600 -Jrampup=30

脚本里写成:

${__P(threads,50)} ${__P(duration,300)}

这样你压测前不用改脚本,只需调整命令行参数即可。配合 Jenkins 或命令行发压工具,可以非常方便地做梯度压测:100 线程跑一轮,200 线程再跑一轮,每轮只变命令参数,脚本零改动。这也是区分"新手脚本"和"工程化脚本"的标志之一。

5.4 分布式压测下函数最容易踩的坑

分布式压测(Master 挂多台 Agent)时,函数的行为有几个容易被忽视的点。

第一,随机函数各自为政。每台 Agent 上的__Random用的独立种子,不同 Agent 之间可能生成相同数据。如果业务要求全集群唯一,建议用${__machineName()}_${__threadNum}_${__time(/1000,)}组合。

第二,时间戳跨节点差异。Agent 机器时钟如果不准,__time生成的时间戳可能不稳定,尤其做签名和去重时,会造成大量失败请求。发压前要确认所有 Agent 时钟同步,这个坑很多人是压到一半才发现的。

第三,属性分节点隔离。__setProperty写的属性在每个 Agent 上是各自维护的,Master 上读不到每个 Agent 的局部属性。想要全局共享,要么通过 CSV 预分片,要么在结果层面做聚合,不要在分布式场景里依赖属性做精准的全局计数。

6. 让函数参与断言和提取:从正则到 Beanshell/Groovy 断言

6.1__regexFunction:低调的正则函数

正则提取器大家应该都用过,但函数层面的__regexFunction用的人不多:

${__regexFunction(订单号:(\d+),,1,,)}

它在函数参数里直接做正则提取,第一个参数是正则表达式,第二个参数是匹配的模板(通常留空),第三个参数是取第几个捕获组。好处是你可以把它嵌在任何文本输入框里,不用专门添加正则提取器。但它没有变量命名的习惯用法,多步骤关联时反而不如提取器好维护。所以日常接口测试我还是推荐"正则提取器 / JSON 提取器 +${变量名}"的组合,__regexFunction更适合一次性小提取。

6.2 JSON 提取器与函数配合读取结果

接口返回基本都是 JSON,JSON 提取器几乎是后置处理器的标配。配置好了之后,后续请求就能用${result_code}、${data_token}这类变量引用上一响应里的值。函数在这里的价值是"对提取结果做二次加工",比如:

${__groovy(vars.get('data_token').substring(0,16),)}

把提取出来的 token 截断后再用。Groovy 里可以访问vars对象,所以几乎所有"提取结果后想再改一改"的诉求都能在这里完成。但注意不要在一个请求里同时塞太长的 Groovy 函数,可读性会迅速恶化。

6.3 Beanshell 断言为什么被劝退

网上到处是"JMeter Beanshell 断言"的教程,老项目里确实也不少。但我每次看到新脚本里还在用 Beanshell,都会劝一句:能不用就别用了。

Beanshell 的问题有两个层面。性能上,它是解释执行,并发一高 CPU 占用就飙升;维护上,很多老教程里的写法依赖Failure、prev、log之类的隐式变量,新人接手时很难一眼看懂到底断的是什么。最关键的是官方早就把 Groovy 当作推荐脚本语言,新的 JMeter 版本对 BeanShell 没有太多投入,遇到 JDK 版本升级时它往往是第一个出问题的。

6.4 JSR223 + Groovy 断言:把断言写成可读的检查器

使用 JSR223 断言时,处理器类型选 JSR223,语言选 Groovy,然后可以写这样的代码:

def body = prev.getResponseDataAsString() if (prev.getResponseCode() != "200") { AssertionResult.setFailure(true) AssertionResult.setFailureMessage("响应码错误: " + prev.getResponseCode()) return } if (!body.contains("\"code\":0")) { AssertionResult.setFailure(true) AssertionResult.setFailureMessage("业务断言失败: " + body) }

prev是上一次请求结果对象,AssertionResult是断言结果对象,直接设置失败原因即可。Groovy 断言的写法比 Beanshell 直观得多,而且你能在脚本里读vars、props、log,几乎可以做任何级别的校验。

如果你需要断言里用上前面函数生成的值,也很自然:

def expect = vars.get("expected_status") def body = prev.getResponseDataAsString() if (!body.contains(expect)) { AssertionResult.setFailure(true) AssertionResult.setFailureMessage("期望包含: " + expect) }

这样一来,断言逻辑和参数化逻辑完全打通,脚本要维护什么、改哪里,全都能一眼定位。

最后再分享一个我自己的习惯:函数这个东西,刚上手时容易贪多,什么都想用函数生成,但真正跑压测时你会发现,一个请求里塞七八个函数,脚本调试起来特别痛苦。比较成熟的做法是——数据能前置准备就前置准备,能放进 CSV 的不要用随机函数,随机函数留给那些"必须动态"的字段,脚本逻辑交给 Groovy,函数只负责提供变量值。这样脚本既经得起并发,又经得起改需求。JMeter 的函数远不止这篇里写的这些,但上面这些,已经覆盖了接口测试和性能测试 90% 的日常需求。

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

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

立即咨询