先聊一个场景:你刚接手一个下单接口的压测任务,脚本里把手机号写死成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 传递 |
| 签名摘要 | __digest | MD5 / 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% 的日常需求。