在JMeter自动化测试中,面对复杂场景(如动态数据生成、加密签名、文件操作、接口依赖处理),基础组件往往难以满足需求。而**JSR223预处理程序**作为JMeter的"万能工具",支持Groovy、Python、JavaScript等多种脚本语言,凭借强大的编程能力,能轻松解决各类复杂问题。
本文将聚焦JSR223预处理程序的**高级用法**,通过实战案例讲解动态参数生成、加密签名、接口依赖处理、文件操作等核心场景,帮你突破JMeter基础功能的局限,实现更灵活、更强大的自动化测试。
一、JSR223预处理程序核心优势
相比JMeter内置的BeanShell预处理程序,JSR223的优势更为突出:
-
多语言支持:默认支持Groovy(推荐)、Python、JavaScript等,可根据需求选择熟悉的语言;
-
性能更优:Groovy基于JVM,执行速度远超BeanShell,高并发场景下更稳定;
-
功能强大:可调用Java类库、第三方Jar包,支持复杂逻辑编写(如循环、条件判断、正则表达式);
-
无缝集成:脚本中可直接读取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.Random、java.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的数倍,高并发场景下更稳定;
-
避免重复创建对象:如
SimpleDateFormat、JsonSlurper可复用,减少对象创建开销; -
关闭资源:文件操作、HTTP请求后,需关闭流和客户端(如
httpClient.close()),避免资源泄露; -
减少日志输出:
log.info()过多会影响性能,仅在调试时保留关键日志。
2. 最佳实践
-
脚本模块化:将通用逻辑(如加密、文件操作)提取为独立的Groovy脚本文件,通过
source("路径/脚本.groovy")引入,提高复用性; -
调试技巧:
-
用
log.info()输出中间变量,在"视图结果树"的"JSR223预处理程序"中查看日志; -
复杂脚本可先在本地Groovy环境中测试,再移植到JMeter;
-
-
依赖管理:若需使用第三方Jar包(如特殊加密库),将Jar包放入
JMETER_HOME/lib目录,重启JMeter即可引用; -
变量作用域:明确
vars