JMeter JSR223预处理程序高级用法:解锁自动化测试的灵活性上限

在JMeter自动化测试中,面对复杂场景(如动态数据生成、加密签名、文件操作、接口依赖处理),基础组件往往难以满足需求。而**JSR223预处理程序**作为JMeter的"万能工具",支持Groovy、Python、JavaScript等多种脚本语言,凭借强大的编程能力,能轻松解决各类复杂问题。

本文将聚焦JSR223预处理程序的**高级用法**,通过实战案例讲解动态参数生成、加密签名、接口依赖处理、文件操作等核心场景,帮你突破JMeter基础功能的局限,实现更灵活、更强大的自动化测试。

一、JSR223预处理程序核心优势

相比JMeter内置的BeanShell预处理程序,JSR223的优势更为突出:

  1. 多语言支持:默认支持Groovy(推荐)、Python、JavaScript等,可根据需求选择熟悉的语言;

  2. 性能更优:Groovy基于JVM,执行速度远超BeanShell,高并发场景下更稳定;

  3. 功能强大:可调用Java类库、第三方Jar包,支持复杂逻辑编写(如循环、条件判断、正则表达式);

  4. 无缝集成:脚本中可直接读取JMeter变量、修改请求参数、设置全局变量,与JMeter生态深度融合。

推荐语言:Groovy(JMeter默认推荐,兼容性最好、性能最优,语法接近Java,学习成本低)。

二、基础准备:JSR223核心API与环境配置

1. 核心API(Groovy环境)

在JSR223脚本中,可通过以下内置对象操作JMeter数据:

  • vars:操作JMeter本地变量(线程内有效)

    • vars.get("变量名"):获取变量值;

    • vars.put("变量名", "值"):设置变量值;

    • vars.putObject("变量名", 对象):设置对象类型变量(如List、Map)。

  • ctx:获取JMeter上下文信息

    • ctx.getThreadNum():获取当前线程号;

    • ctx.getVariables():获取全局变量;

    • ctx.getCurrentSampler():获取当前取样器。

  • log:日志输出(调试必备)

    • log.info("日志信息"):输出普通日志;

    • log.error("错误信息"):输出错误日志(视图结果树中可查看)。

  • props:操作JMeter系统属性(全局有效,跨线程组)

    • props.get("user.properties中的键"):读取系统配置;

    • props.put("全局变量名", "值"):设置全局变量。

2. 环境配置(推荐Groovy)

  • JMeter默认已集成Groovy引擎,无需额外安装;

  • 若需使用Python/JavaScript,需确保JMeter环境已配置对应脚本引擎(Groovy性能最优,优先推荐);

  • 脚本中可直接导入Java类库(如java.util.Randomjava.security.MessageDigest),无需额外依赖。

三、高级用法实战案例

以下案例均基于Groovy语言,覆盖自动化测试中最常见的复杂场景,直接复制到JMeter中即可使用。

案例1:动态生成复杂参数(含随机数、时间戳、正则替换)

场景:接口需要传递"唯一订单号""当前时间戳""随机手机号"等动态参数,且订单号需满足特定格式(如ORD-20240520-123456)。

Groovy 复制代码
import java.text.SimpleDateFormat
import java.util.Random

// 1. 生成当前日期(格式:yyyyMMdd)
def dateFormat = new SimpleDateFormat("yyyyMMdd")
def currentDate = dateFormat.format(new Date())

// 2. 生成时间戳(毫秒级)
def timestamp = System.currentTimeMillis()

// 3. 生成6位随机数
def random = new Random()
def randomNum = String.format("%06d", random.nextInt(1000000))

// 4. 生成唯一订单号(格式:ORD-日期-时间戳-随机数)
def orderNo = "ORD-${currentDate}-${timestamp}-${randomNum}"

// 5. 生成随机手机号(13开头,11位)
def phonePrefix = ["130", "131", "132", "133", "134"]
def prefix = phonePrefix[random.nextInt(phonePrefix.size())]
def phoneNum = prefix + String.format("%08d", random.nextInt(100000000))

// 6. 正则替换:将固定字符串中的占位符替换为动态值
def rawStr = "订单号:{orderNo},手机号:{phoneNum}"
def replacedStr = rawStr.replaceAll("\\{orderNo\\}", orderNo).replaceAll("\\{phoneNum\\}", phoneNum)

// 7. 存入JMeter变量(后续接口可通过${变量名}引用)
vars.put("orderNo", orderNo)
vars.put("phoneNum", phoneNum)
vars.put("replacedStr", replacedStr)
vars.put("timestamp", timestamp.toString())

// 日志输出(调试用)
log.info("生成的订单号:" + orderNo)
log.info("生成的手机号:" + phoneNum)

使用方式

  • 在JSR223预处理程序中粘贴脚本;

  • 后续HTTP请求的参数值直接引用${orderNo}${phoneNum}即可。

案例2:接口依赖处理(调用前置接口获取动态Token)

场景:测试需要登录的接口时,需先调用登录接口获取Token,再将Token作为请求头参数传递给后续接口(无需添加额外取样器,直接在预处理程序中完成)。

Groovy 复制代码
import org.apache.http.client.methods.HttpPost
import org.apache.http.entity.StringEntity
import org.apache.http.impl.client.CloseableHttpClient
import org.apache.http.impl.client.HttpClients
import org.apache.http.util.EntityUtils
import groovy.json.JsonSlurper

// 1. 登录接口信息
def loginUrl = "https://api.test.com/login"
def username = "test_user"
def password = "test_pass123"

// 2. 构造登录请求参数(JSON格式)
def loginParams = [
    "username": username,
    "password": password
]
def jsonParams = new groovy.json.JsonBuilder(loginParams).toPrettyString()

// 3. 发送HTTP POST请求(调用登录接口)
CloseableHttpClient httpClient = HttpClients.createDefault()
HttpPost httpPost = new HttpPost(loginUrl)

// 设置请求头
httpPost.setHeader("Content-Type", "application/json;charset=UTF-8")
httpPost.setEntity(new StringEntity(jsonParams, "UTF-8"))

// 执行请求并获取响应
def response = httpClient.execute(httpPost)
def responseBody = EntityUtils.toString(response.getEntity(), "UTF-8")
httpClient.close()

// 4. 解析JSON响应,提取Token
def jsonSlurper = new JsonSlurper()
def responseJson = jsonSlurper.parseText(responseBody)
def token = responseJson.data.token  // 假设响应格式为 {"code":200, "data":{"token":"xxx"}}

// 5. 存入变量(后续接口请求头引用${token})
vars.put("token", token)
log.info("获取到的登录Token:" + token)

// 6. 设置全局变量(跨线程组可用)
props.put("global_token", token)

使用方式

  • 脚本中替换登录接口URL、用户名密码和响应解析逻辑;

  • 后续HTTP请求的请求头添加Authorization: Bearer ${token}(根据接口认证格式调整)。

优势:无需在测试计划中添加额外的登录取样器,简化测试结构,且Token获取逻辑与主接口紧密结合。

案例3:加密签名(MD5、SHA256、HMAC-SHA256)

场景:接口要求参数按特定规则加密签名(如将参数按ASCII排序后拼接密钥,再进行MD5加密),确保请求合法性。

Groovy 复制代码
import java.security.MessageDigest
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import java.util.TreeMap

// 1. 待加密的参数(模拟接口请求参数)
def params = [
    "appId": "test_app",
    "timestamp": System.currentTimeMillis().toString(),
    "nonce": String.format("%08d", new Random().nextInt(100000000)),
    "data": "test_data"
]

// 2. 加密密钥(由接口文档提供)
def secretKey = "test_secret_123"

// 3. 步骤1:参数按ASCII排序(TreeMap自动排序)
def sortedMap = new TreeMap<>(params)

// 4. 步骤2:拼接参数(key=value&key=value)
def sb = new StringBuilder()
sortedMap.each { key, value ->
    sb.append(key).append("=").append(value).append("&")
}
def signSource = sb.substring(0, sb.length() - 1) + secretKey  // 拼接密钥
log.info("签名原始字符串:" + signSource)

// 5. 方式1:MD5加密(32位大写)
def md5Digest = MessageDigest.getInstance("MD5")
def md5Bytes = md5Digest.digest(signSource.getBytes("UTF-8"))
def md5Sign = md5Bytes.collect { String.format("%02x", it) }.join().toUpperCase()

// 6. 方式2:SHA256加密
def sha256Digest = MessageDigest.getInstance("SHA-256")
def sha256Bytes = sha256Digest.digest(signSource.getBytes("UTF-8"))
def sha256Sign = sha256Bytes.collect { String.format("%02x", it) }.join().toUpperCase()

// 7. 方式3:HMAC-SHA256加密(需密钥)
def hmacSha256 = Mac.getInstance("HmacSHA256")
def secretKeySpec = new SecretKeySpec(secretKey.getBytes("UTF-8"), "HmacSHA256")
hmacSha256.init(secretKeySpec)
def hmacBytes = hmacSha256.doFinal(signSource.getBytes("UTF-8"))
def hmacSign = hmacBytes.collect { String.format("%02x", it) }.join().toUpperCase()

// 8. 存入变量(接口参数引用对应的签名)
vars.put("md5Sign", md5Sign)
vars.put("sha256Sign", sha256Sign)
vars.put("hmacSign", hmacSign)
vars.put("timestamp", params.timestamp)
vars.put("nonce", params.nonce)

log.info("MD5签名:" + md5Sign)
log.info("SHA256签名:" + sha256Sign)
log.info("HMAC-SHA256签名:" + hmacSign)

使用方式

  • 替换params为接口实际请求参数,secretKey为接口提供的密钥;

  • 接口参数中添加sign=${md5Sign}(或对应加密方式的签名变量)。

案例4:文件操作(动态生成文件并读取内容)

场景:接口需要上传动态生成的文件(如CSV数据文件、JSON配置文件),需在预处理程序中创建文件、写入数据,并将文件路径或内容传递给接口。

Groovy 复制代码
import java.io.BufferedWriter
import java.io.FileWriter
import java.text.SimpleDateFormat

// 1. 配置文件路径和名称
def baseDir = "D:/jmeter_test/files"  // 基础目录
def dateDir = new SimpleDateFormat("yyyyMMdd").format(new Date())  // 日期文件夹
def fileName = "data_${System.currentTimeMillis()}.csv"  // 唯一文件名
def fullFilePath = "${baseDir}/${dateDir}/${fileName}"

// 2. 创建文件夹(递归创建,若不存在)
def fileDir = new File("${baseDir}/${dateDir}")
if (!fileDir.exists()) {
    fileDir.mkdirs()
    log.info("创建文件夹:" + fileDir.getAbsolutePath())
}

// 3. 写入CSV数据(表头+10条随机数据)
def file = new File(fullFilePath)
BufferedWriter bw = new BufferedWriter(new FileWriter(file))
// 写入表头
bw.write("id,name,age,email\n")
// 写入10条随机数据
def random = new Random()
for (int i = 0; i < 10; i++) {
    def id = i + 1
    def name = "user_${id}"
    def age = 18 + random.nextInt(30)  // 18-47岁
    def email = "${name}@test.com"
    bw.write("${id},${name},${age},${email}\n")
}
bw.close()
log.info("文件生成成功:" + fullFilePath)

// 4. 读取文件内容(若接口需要传递文件内容)
def fileContent = new File(fullFilePath).text
vars.put("fileContent", fileContent)

// 5. 存入文件路径(若接口需要传递文件路径)
vars.put("fullFilePath", fullFilePath)
vars.put("fileName", fileName)

使用方式

  • 后续HTTP请求中,若接口为文件上传,可通过"文件上传"组件引用${fullFilePath}

  • 若接口需要传递文件内容,直接引用${fileContent}作为参数值。

案例5:复杂逻辑处理(条件判断、循环、数据转换)

场景:根据不同的测试环境(开发/测试/生产),动态切换接口域名、调整参数值,且需要对数据进行格式转换(如JSON字符串转Map、List排序)。

Groovy 复制代码
import groovy.json.JsonSlurper

// 1. 读取环境变量(在JMeter用户定义的变量中配置env=dev/test/prod)
def env = vars.get("env")
log.info("当前环境:" + env)

// 2. 条件判断:切换接口域名
def baseUrl = ""
if (env == "dev") {
    baseUrl = "https://dev-api.test.com"
} else if (env == "test") {
    baseUrl = "https://test-api.test.com"
} else if (env == "prod") {
    baseUrl = "https://api.test.com"
} else {
    baseUrl = "https://test-api.test.com"  // 默认环境
}
vars.put("baseUrl", baseUrl)

// 3. 数据转换:JSON字符串转Map
def jsonStr = '{"name":"张三","age":25,"hobbies":["篮球","跑步","读书"]}'
def jsonSlurper = new JsonSlurper()
def userMap = jsonSlurper.parseText(jsonStr)
log.info("解析后的用户信息:" + userMap)

// 4. 循环处理:List排序并拼接
def hobbies = userMap.hobbies
hobbies.sort()  // 排序
def hobbyStr = hobbies.join("、")  // 拼接为"篮球、跑步、读书"
vars.put("hobbyStr", hobbyStr)

// 5. 动态调整参数:生产环境下年龄+10
if (env == "prod") {
    userMap.age = userMap.age + 10
}
vars.put("userAge", userMap.age.toString())
vars.put("userName", userMap.name)

// 6. 循环生成测试数据(生成5个用户ID)
def userIdList = []
for (int i = 1; i <= 5; i++) {
    userIdList.add("U${System.currentTimeMillis()}_${i}")
}
vars.put("userIdList", userIdList.join(","))  // 存入逗号分隔的字符串
vars.putObject("userIdListObj", userIdList)  // 存入List对象(后续可在其他脚本中读取)

使用方式

  • 后续HTTP请求的接口路径可拼接为${baseUrl}/api/user

  • 引用${userName}${userAge}${hobbyStr}等变量作为参数值。

四、性能优化与最佳实践

1. 性能优化(避免高并发场景下的瓶颈)

  • 优先使用Groovy语言:Groovy执行速度是BeanShell的数倍,高并发场景下更稳定;

  • 避免重复创建对象:如SimpleDateFormatJsonSlurper可复用,减少对象创建开销;

  • 关闭资源:文件操作、HTTP请求后,需关闭流和客户端(如httpClient.close()),避免资源泄露;

  • 减少日志输出:log.info()过多会影响性能,仅在调试时保留关键日志。

2. 最佳实践

  • 脚本模块化:将通用逻辑(如加密、文件操作)提取为独立的Groovy脚本文件,通过source("路径/脚本.groovy")引入,提高复用性;

  • 调试技巧:

    • log.info()输出中间变量,在"视图结果树"的"JSR223预处理程序"中查看日志;

    • 复杂脚本可先在本地Groovy环境中测试,再移植到JMeter;

  • 依赖管理:若需使用第三方Jar包(如特殊加密库),将Jar包放入JMETER_HOME/lib目录,重启JMeter即可引用;

  • 变量作用域:明确vars

相关推荐
星释2 小时前
Rust 练习册 22:映射函数与泛型的威力
开发语言·rust·机器人
云泽8082 小时前
C++ List 容器详解:迭代器失效、排序与高效操作
开发语言·c++·list
云帆小二3 小时前
从开发语言出发如何选择学习考试系统
开发语言·学习
光泽雨3 小时前
python学习基础
开发语言·数据库·python
CesareCheung3 小时前
JMeter 进行 WebSocket 接口压测
python·websocket·jmeter
百***06014 小时前
python爬虫——爬取全年天气数据并做可视化分析
开发语言·爬虫·python
jghhh014 小时前
基于幅度的和差测角程序
开发语言·matlab
fruge4 小时前
自制浏览器插件:实现网页内容高亮、自动整理收藏夹功能
开发语言·前端·javascript