
先聊一个场景你刚接手一个下单接口的压测任务脚本里把手机号写死成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 下快捷键是 CtrlShiftF1。里面能看到每个参数的解释是排查函数参数顺序问题最靠谱的工具。不要硬背参数顺序尤其不同版本 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 keyyour_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 -Jthreads200 -Jduration600 -Jrampup30脚本里写成${__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 提取器与函数配合读取结果接口返回基本都是 JSONJSON 提取器几乎是后置处理器的标配。配置好了之后后续请求就能用${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% 的日常需求。