在JMeter性能测试中,面对动态签名、参数加密、数据格式转换等复杂业务场景,内置组件往往难以满足需求。而**JSR223预处理程序**(JSR223 PreProcessor)作为JMeter的"万能工具",支持通过Groovy、Java等脚本语言编写自定义逻辑,能轻松解决各类复杂参数处理问题。本文将从基础配置、核心用法到实战案例,全方位解析JSR223预处理程序,帮你彻底掌握这一高级技能。
一、JSR223预处理程序是什么?
JSR223是Java平台的脚本语言规范,JMeter基于该规范提供了JSR223系列组件(预处理程序、后置处理器、采样器等),其中**JSR223预处理程序**的核心作用是:在**取样器(如HTTP请求)执行前**,运行自定义脚本逻辑,完成参数生成、数据处理、请求组装等操作。
核心优势:
-
灵活性极强:支持Groovy、Python、JavaScript等多种脚本语言(JMeter 3.1+推荐优先使用Groovy,性能最优且兼容Java类库);
-
功能无上限:可调用Java类库、第三方Jar包,实现加密、签名、JSON/XML处理、数据库查询等任意复杂逻辑;
-
无缝集成JMeter :脚本中可直接操作JMeter变量(
vars)、全局属性(props)、取样器配置等,与JMeter原生组件深度联动。
适用场景:
-
接口请求参数需要动态加密(如MD5、SHA256、AES);
-
生成复杂业务签名(如时间戳+密钥+参数拼接后加密);
-
动态组装JSON/XML请求体(如根据条件添加不同字段);
-
从数据库/缓存中查询测试数据并注入请求;
-
批量修改请求头或参数(如统一添加Token、设备ID)。
二、基础配置:JSR223预处理程序入门
1. 组件添加与基本设置
-
在需要预处理的取样器(如HTTP请求)上右键 → 添加 → 前置处理器 → JSR223 PreProcessor;
-
核心配置项说明:
-
Script Language :脚本语言,默认选择
groovy(推荐,性能远超BeanShell、Python); -
Script File:外部脚本文件路径(适合复杂脚本,避免在界面直接编写);
-
Script:脚本编辑区(简单脚本可直接在此编写);
-
Parameters :传递给脚本的参数(脚本中通过
args数组获取); -
Cache compiled script if available:缓存编译后的脚本(默认勾选,重复执行时提升性能,避免每次重新编译)。
-
2. 核心内置对象(必掌握)
JSR223脚本中可直接使用JMeter提供的内置对象,无需额外定义,核心对象如下:
|-----------|-----------------|---------------------------|------------------------------------------------------------|
| 对象名 | 类型 | 作用 | 示例 |
| vars | JMeterVariables | 操作当前线程组的局部变量(仅当前线程可见) | vars.put("key", "value")(存值)、vars.get("key")(取值) |
| props | Properties | 操作JMeter全局属性(所有线程组、线程共享) | props.put("globalKey", "value")、props.get("globalKey") |
| ctx | JMeterContext | 获取JMeter上下文信息(如线程数、取样器信息) | ctx.getThreadNum()(获取当前线程号) |
| sampler | Sampler | 获取当前取样器配置(如HTTP请求的URL、参数) | sampler.getUrlString()(获取请求URL) |
| log | Logger | 输出日志到JMeter控制台/日志文件(调试用) | log.info("调试信息:" + vars.get("key")) |
3. 第一个Groovy脚本:生成动态参数
示例:生成随机手机号并存入JMeter变量,供HTTP请求使用。
脚本代码:
Groovy
// 生成11位随机手机号(以138开头)
def prefix = "138"
def suffix = new Random().nextInt(900000000) + 100000000 // 生成9位随机数
def phone = prefix + suffix.toString()
// 存入JMeter局部变量,取样器中可通过${phone}引用
vars.put("phone", phone)
// 日志输出调试(JMeter控制台可查看)
log.info("生成的随机手机号:" + phone)
使用方式:在HTTP请求参数中直接引用${phone},即可自动带入生成的手机号。
三、核心实战场景:JSR223预处理程序经典用法
场景1:接口签名生成(MD5/SHA256)
多数接口为防止请求篡改,会要求在请求参数中添加签名(如sign字段),签名规则通常为"参数排序+密钥+时间戳"拼接后加密。以下以"MD5签名"为例,演示完整实现。
签名规则:
-
请求参数包含
appId、timestamp、data; -
按参数名ASCII升序排序(
appId→data→timestamp); -
拼接格式:
appId=xxx&data=xxx×tamp=xxx&secret=xxx(secret为接口密钥); -
对拼接字符串做MD5加密(小写),作为
sign参数。
JSR223脚本实现:
Groovy
import java.security.MessageDigest
import java.util.TreeMap
// 1. 定义常量和基础参数
def appId = "test123"
def secret = "abcdefg123456"
def data = '{"userId":"1001","goodsId":"2002"}' // 请求数据
def timestamp = System.currentTimeMillis() / 1000 // 秒级时间戳
// 2. 按参数名ASCII升序排序(TreeMap自动排序)
def params = new TreeMap()
params.put("appId", appId)
params.put("data", data)
params.put("timestamp", timestamp.toString())
// 3. 拼接参数字符串
def signSource = new StringBuilder()
params.each { key, value ->
signSource.append(key).append("=").append(value).append("&")
}
signSource.append("secret=").append(secret) // 拼接密钥
log.info("签名源字符串:" + signSource.toString())
// 4. MD5加密(小写)
def md5 = MessageDigest.getInstance("MD5")
byte[] md5Bytes = md5.digest(signSource.toString().getBytes("UTF-8"))
def sign = new BigInteger(1, md5Bytes).toString(16).padStart(32, '0') // 补全32位
// 5. 存入JMeter变量,供取样器引用
vars.put("appId", appId)
vars.put("timestamp", timestamp.toString())
vars.put("data", data)
vars.put("sign", sign)
log.info("生成的签名:" + sign)
取样器引用:
在HTTP请求的"参数"或"请求体"中引用变量:
-
appId=${appId} -
timestamp=${timestamp} -
data=${data} -
sign=${sign}
场景2:AES加密请求体(敏感数据加密)
对于手机号、身份证号等敏感数据,接口可能要求用AES加密后传输。以下示例将JSON请求体用AES-ECB模式加密(PKCS5填充)。
前置准备:
确保JMeter/lib目录下添加commons-codec-1.15.jar(用于Base64编码,可从Maven仓库下载)。
JSR223脚本实现:
Groovy
import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec
import org.apache.commons.codec.binary.Base64
// 1. 加密配置(与接口协商一致)
def secretKey = "1234567890abcdef" // AES密钥(16位,ECB模式要求密钥长度16/24/32位)
def algorithm = "AES/ECB/PKCS5Padding" // 算法/模式/填充方式
// 2. 原始请求体(敏感数据)
def originalBody = '{"phone":"13800138000","idCard":"110101199001011234"}'
log.info("原始请求体:" + originalBody)
// 3. AES加密实现
def encryptAES(String content, String key) {
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES")
Cipher cipher = Cipher.getInstance(algorithm)
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec)
byte[] encryptedBytes = cipher.doFinal(content.getBytes("UTF-8"))
return Base64.encodeBase64String(encryptedBytes) // 加密后Base64编码(方便传输)
}
// 4. 执行加密并存入变量
def encryptedBody = encryptAES(originalBody, secretKey)
vars.put("encryptedBody", encryptedBody)
log.info("AES加密后请求体(Base64):" + encryptedBody)
取样器引用:
在HTTP请求的"请求体数据"中直接引用${encryptedBody},接口接收后解密即可。
场景3:动态组装JSON请求体(根据条件添加字段)
部分接口的请求体字段需根据业务条件动态增减(如VIP用户显示vipLevel字段,普通用户不显示),可通过JSR223脚本灵活组装。
JSR223脚本实现:
Groovy
import groovy.json.JsonBuilder
// 1. 定义基础字段
def userId = vars.get("userId") // 从CSV或其他变量获取用户ID
def userName = vars.get("userName")
def userType = vars.get("userType") // 用户类型:VIP/COMMON
// 2. 动态构建JSON(Groovy的JsonBuilder简化JSON组装)
def json = new JsonBuilder()
json {
userId(userId)
userName(userName)
createTime(new Date().format("yyyy-MM-dd HH:mm:ss")) // 当前时间
// 条件字段:仅VIP用户添加vipLevel
if (userType == "VIP") {
vipLevel(3) // VIP等级
discount(0.8) // 折扣率
} else {
discount(1.0) // 普通用户无折扣
}
}
// 3. 转为JSON字符串并存入变量
def requestBody = json.toPrettyString() // 格式化JSON(可选)
vars.put("requestBody", requestBody)
log.info("动态组装的请求体:" + requestBody)
取样器引用:
HTTP请求体直接填入${requestBody},最终效果:
-
VIP用户:包含
vipLevel和discount=0.8; -
普通用户:无
vipLevel,discount=1.0。
场景4:从数据库查询测试数据(JDBC联动)
压测时需从数据库获取真实数据(如已存在的用户ID),可通过JSR223预处理程序调用JDBC查询数据并注入请求。
前置准备:
-
添加"JDBC Connection Configuration"组件,配置数据库连接(URL、用户名、密码、驱动类);
-
确保JMeter/lib目录下添加对应数据库驱动Jar包(如MySQL的
mysql-connector-java-8.0.30.jar)。
JSR223脚本实现:
Groovy
import org.apache.jmeter.protocol.jdbc.config.DataSourceElement
import java.sql.Connection
import java.sql.Statement
import java.sql.ResultSet
// 1. 获取JDBC连接池(与JDBC Connection Configuration的Variable Name一致)
def dataSource = DataSourceElement.getDataSource("db_conn") // Variable Name=db_conn
Connection conn = dataSource.getConnection()
try {
// 2. 执行SQL查询(查询前100个用户ID,随机取一个)
Statement stmt = conn.createStatement()
String sql = "SELECT user_id FROM t_user LIMIT 100"
ResultSet rs = stmt.executeQuery(sql)
// 3. 收集查询结果
def userIds = []
while (rs.next()) {
userIds.add(rs.getString("user_id"))
}
// 4. 随机取一个用户ID存入变量
if (userIds.size() > 0) {
def randomUserId = userIds.get(new Random().nextInt(userIds.size()))
vars.put("randomUserId", randomUserId)
log.info("从数据库获取的随机用户ID:" + randomUserId)
} else {
log.error("未查询到用户ID")
vars.put("randomUserId", "default_1001") // 默认值
}
} catch (Exception e) {
log.error("数据库查询异常:" + e.getMessage())
vars.put("randomUserId", "default_1001")
} finally {
// 5. 关闭连接(释放资源)
if (conn != null) {
conn.close()
}
}
取样器引用:
在HTTP请求中引用${randomUserId},即可动态注入从数据库查询的用户ID。
四、性能优化与避坑指南
1. 性能优化技巧
JSR223脚本的性能直接影响压测结果的准确性,尤其是高并发场景,需注意以下优化点:
-
优先使用Groovy语言:Groovy是JMeter优化后的首选脚本语言,性能比BeanShell快10倍以上,且兼容Java类库;
-
勾选"Cache compiled script if available":缓存编译后的脚本,避免每次执行都重新编译(高并发场景必备);
-
避免在脚本中创建大量对象 :如循环中重复创建
Random、MessageDigest等对象,可提前初始化复用; -
复杂脚本用外部文件 :将大型脚本保存为
.groovy文件,通过"Script File"导入(便于维护和复用,且编译效率更高); -
禁用不必要的 日志 :高并发时
log.info()会消耗大量资源,调试完成后注释或删除日志输出。
2. 常见问题与解决方案
问题1:脚本报错"ClassNotFoundException"(如找不到MessageDigest、Base64)
-
原因:缺少相关类库或Jar包;
-
解决方案:
-
确认脚本中导入的类是否正确(如
java.security.MessageDigest是Java内置类,无需额外Jar包); -
第三方类库(如
Base64)需将Jar包放入JMeter/lib目录,重启JMeter生效。
-
问题2:变量引用失败(${key}无法获取脚本中存入的值)
-
原因:
-
脚本中变量名拼写错误;
-
用
props存储全局变量,但线程组执行顺序导致变量未初始化; -
脚本执行异常,变量未成功存入;
-
-
解决方案:
-
用
log.info(vars.get("key"))调试变量是否存入; -
局部变量用
vars,跨线程组变量用props,且确保存储变量的线程组先执行; -
捕获脚本异常,避免因异常导致变量未赋值。
-
问题3:高并发场景下脚本执行缓慢,导致压测结果失真
-
原因:脚本逻辑复杂(如循环次数过多、频繁数据库查询)或未优化;
-
解决方案:
-
简化脚本逻辑,将非必要操作移出预处理程序;
-
数据库查询等耗时操作改为提前批量导入CSV,脚本从CSV读取数据;
-
调整JMeter堆内存(
jmeter.bat/jmeter中的HEAP参数),避免内存溢出。
-
问题4:Groovy脚本语法报错(如"Unexpected token")
-
原因:Groovy语法与Java略有差异(如字符串拼接、闭包用法);
-
解决方案:
-
遵循Groovy语法规范(如字符串可使用单引号/双引号,多行字符串用
'''); -
复杂逻辑可分步骤编写,用
log.info()调试每一步结果; -
参考Groovy官方文档或JMeter官方示例脚本。
-
五、总结:JSR223预处理程序的核心价值
JSR223预处理程序的核心价值在于**打破JMeter内置组件的功能限制**,通过脚本语言实现任意复杂的参数处理逻辑。无论是动态签名、数据加密、JSON组装,还是数据库联动,它都能轻松应对,是性能测试工程师处理复杂业务场景的"必备工具"。
使用时需注意:
-
优先选择Groovy语言,兼顾性能和兼容性;
-
熟练掌握内置对象(
vars、props、log),实现与JMeter的深度联动; -
高并发场景下需优化脚本性能,避免成为压测瓶颈;
-
脚本中添加异常捕获和日志输出,便于调试和问题定位。
通过本文的基础配置、实战场景和避坑指南,