背景
在进行上传文件的性能测试时,需要大批量的上传文件,测试人员不可能为每一次请求都准备一个不同的文件。所以需要支持只选择一个本地文件,但是动态修改JMeter 传给服务器的 Content-Disposition 里的 filename名称,使之每次都不唯一。
解决方法
由于JMeter上传文件参数的filename使固定从文件路径里面取的,不支持修改。那就只能通过修改jmeter ApacheJMeter_http.jar源码或者直接构造 HTTP上传 请求。本文采用第二种JSR223 脚本直接构造 HTTP上传 请求。
具体实现
setUp线程组(JSR223 Sampler)------ 文件缓存脚本
作用:读取本地文件,将文件字节缓存到JMeter全局属性(props)中,仅执行一次,供所有普通线程组复用,避免多线程重复读取本地文件,提升脚本执行效率。
groovy
def filePath = vars.get("filePath")
// 全局缓存文件内容(只加载一次,避免多线程重复读盘)
def cacheKey = "UPLOAD_FILE_BYTES_${filePath}"
if (props.get(cacheKey) == null) {
def file = new File(filePath)
// 校验文件是否存在,避免脚本报错
if (!file.exists()) {
log.error("文件不存在:" + filePath)
return
}
def fileBytes = file.bytes
props.put(cacheKey, fileBytes)
log.info("文件已缓存:" + filePath + ",大小:" + fileBytes.length + " 字节")
}
普通线程组(JSR223 Sampler)------ 动态修改 Content-Disposition 的 filename
groovy
import java.io.ByteArrayOutputStream
import java.io.DataOutputStream
import java.net.HttpURLConnection
import java.net.URL
import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
import java.security.cert.X509Certificate
// ==================== 忽略SSL证书验证 ====================
TrustManager[] trustAllCerts = [
new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() { return null }
public void checkClientTrusted(X509Certificate[] certs, String authType) { }
public void checkServerTrusted(X509Certificate[] certs, String authType) { }
}
]
def sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, trustAllCerts, new java.security.SecureRandom())
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory())
HttpsURLConnection.setDefaultHostnameVerifier { hostname, session -> true }
// ===================================================================
// ==================== 你的实际URL ====================
def uploadUrl = "https://xxxxxxxxxxxxxxxxxx/file/uploadBatch"
// ==================== 请求头配置 ====================
def authorization = vars.get("authorization") ?: ""
def cookie = vars.get("cookie") ?: ""
// ==================================================
// 1. 表单参数
def params = [
"isDeal": "0",
"download": "1",
"visibleScope": "",
"knowledgeSourceList": "2",
"themeCategoryId": "1924658089953329153",
"organization": "1950445468057321474",
"knowledgeId": "2054451841533865986",
"sharePermissionMainLi": "",
"foldersId": "",
"graphRag": "false",
"ruleId": "2011260510020169729",
"targetKnowledgeId": "2054452332598784002",
"sharePermission": "1",
"targetFoldersId": "2054452333601222658",
"targetFoldersName": "测试一 > 测试二 > 测试三",
"isSelf": "0",
"faqFile": "false"
]
// 2. 文件配置
//字段名配置
def paramName = "files"
//filename动态配置:获取用户自定义变量中的时间戳加计数器组合,也可以使用uuid
def customFileName = "test0513_${time1}_${num}.pdf"
//获取用户自定义变量中真实文件路径
def filePath = vars.get("filePath")
//配置MIME类型
def mime = "application/pdf"
//读取全局变量中存储的文件
def cacheKey = "UPLOAD_FILE_BYTES_${filePath}"
def fileBytes = props.get(cacheKey)
// 4. 构建 multipart 请求体
def boundary = "----WebKitFormBoundary" + System.currentTimeMillis() + Thread.currentThread().getId()
def lineEnd = "\r\n"
def twoHyphens = "--"
def outputStream = new ByteArrayOutputStream()
def writer = new DataOutputStream(outputStream)
// 4.1 添加所有表单参数
params.each { key, value ->
if (value != null && value.toString().trim().length() > 0) {
writer.writeBytes(twoHyphens + boundary + lineEnd)
writer.writeBytes("Content-Disposition: form-data; name=\"${key}\"" + lineEnd)
writer.writeBytes(lineEnd)
writer.writeBytes(value.toString())
writer.writeBytes(lineEnd)
}
}
// 4.2 添加文件部分
writer.writeBytes(twoHyphens + boundary + lineEnd)
writer.writeBytes("Content-Disposition: form-data; name=\"${paramName}\"; filename=\"${customFileName}\"" + lineEnd)
writer.writeBytes("Content-Type: ${mime}" + lineEnd)
writer.writeBytes(lineEnd)
writer.write(fileBytes)
writer.writeBytes(lineEnd)
// 4.3 结束标记
writer.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd)
writer.flush()
// 5. 发送请求
def url = new URL(uploadUrl)
def connection = url.openConnection() as HttpURLConnection
connection.setRequestMethod("POST")
connection.setDoOutput(true)
connection.setDoInput(true)
connection.setUseCaches(false)
// 5.1 设置请求头
connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary)
if (authorization != null && authorization.trim().length() > 0) {
connection.setRequestProperty("Authorization", authorization)
}
if (cookie != null && cookie.trim().length() > 0) {
connection.setRequestProperty("Cookie", cookie)
}
connection.setRequestProperty("Accept", "application/json, text/plain, */*")
connection.setRequestProperty("Accept-Language", "zh-CN,zh;q=0.9")
connection.setRequestProperty("User-Agent", "Apache JMeter")
connection.setRequestProperty("Connection", "keep-alive")
// 5.2 设置超时时间
connection.setConnectTimeout(30000)
connection.setReadTimeout(60000)
// 5.3 发送请求体
connection.outputStream.write(outputStream.toByteArray())
connection.outputStream.flush()
connection.outputStream.close()
// 6. 获取响应
def responseCode = connection.getResponseCode()
def responseMessage = connection.getResponseMessage()
def responseBody = ""
def reader = null
try {
if (responseCode >= 200 && responseCode < 300) {
reader = new BufferedReader(new InputStreamReader(connection.inputStream, "UTF-8"))
} else if (connection.errorStream != null) {
reader = new BufferedReader(new InputStreamReader(connection.errorStream, "UTF-8"))
}
if (reader != null) {
def response = new StringBuilder()
String line
while ((line = reader.readLine()) != null) {
response.append(line)
}
responseBody = response.toString()
}
SampleResult.setSuccessful(responseCode >= 200 && responseCode < 300)
} catch (Exception e) {
SampleResult.setSuccessful(false)
responseBody = "读取响应失败: ${e.message}"
} finally {
if (reader != null) {
try { reader.close() } catch (Exception e) { }
}
}
//讲结果给到jmeter,以便在查看结果数据查看
SampleResult.setResponseCode(String.valueOf(responseCode))
SampleResult.setResponseMessage(responseMessage)
SampleResult.setResponseData(responseBody, "UTF-8")
//关闭
connection.disconnect()