文章目录
一、前言
最近要做一个系统的性能测试,菜单多,相对接口应该也比较多,想一想能否通过har格式转换成一个脚本呢?网上有不少案例,找了几个发现都没有成功,后面想了想还是自己搞定。
二、什么是HAR文件?
HAR(HTTP Archive format),是一种或 JSON 格式的存档格式文件,通用扩展名为 .har。Web 浏览器可以使用该格式导出有关其加载的网页的详细性能数据。
三、参考代码
代码主要实现以下功能:
- 配置JMeter路径
- 读取HAR文件
- 解析HAR文件
- 构建JMX文件
- 输出JMX 脚本
java
// 设置jmeterHome路径
// 主要是读取了几个配置文件,jmeter.properties,user.properties,system.properties。
// 设置一下的本地的Locale环境。
// 其实到这里,是可以仅将这3个配置文件抽离出来,即不需要整个Jmeter的home目录,仅要这3个配置文件就能运行Jmeter脚本。
// 甚至仅在代码中写要的配置,都不需要实体的配置文件即可。
// 当然随着功能越来越多,平台跟Jmeter的耦合也越来越多,这个Jmeter_home目录还是越来越必要了。
public static void main(String[] args) {
// jmeter 路径
File jmeterHome = new File("D:\\Program Files\\apache-jmeter-5.6.3");
// 分隔符
String slash = System.getProperty("file.separator");
//判断jmeterHome
if (jmeterHome.exists()) {
File jmeterProperties = new File(jmeterHome.getPath() + slash + "bin" + slash + "jmeter.properties");
if (jmeterProperties.exists()) {
// 初始化压测引擎
StandardJMeterEngine jmeter = new StandardJMeterEngine();
// JMeter初始化(属性、日志级别、区域设置等)
JMeterUtils.setJMeterHome(jmeterHome.getPath());
JMeterUtils.loadJMeterProperties(jmeterProperties.getPath());
// 可以注释这一行,查看额外的日志,例如DEBUG级别
JMeterUtils.initLogging();
JMeterUtils.initLocale();
// JMeter测试计划,基本上是JOrphan HashTree
HashTree testPlanTree = new HashTree();
String Name = "baidu-jmx";
// har 路径
FileReader fileReader = new FileReader("aa".har");
String result = fileReader.readString();
JSONObject harJson = JSONObject.parseObject(result);
// 获取 HAR 文件中的 entries
JSONObject log = harJson.getJSONObject("log");
JSONArray entries = log.getJSONArray("entries");
// Loop Controller 循环控制
LoopController loopController = getLoopController();
// Thread Group 线程组
ThreadGroup threadGroup = getThreadGroup(Name, loopController);
// Test Plan 测试计划
TestPlan testPlan = getTestPlan();
// 从以上初始化的元素构造测试计划
testPlanTree.add(testPlan);
HashTree threadGroupHashTree = testPlanTree.add(testPlan, threadGroup);
// 构建事务
TransactionController transactionController = getTransactionController();
HashTree transactionControllerHashTree = threadGroupHashTree.add(transactionController);
entries.forEach(item -> {
JSONObject json = (JSONObject) item;
JSONObject request = json.getJSONObject("request");
String method = request.getString("method");
String url = request.getString("url");
HTTPSamplerProxy Sampler = new HTTPSamplerProxy();
buildTheUnderlyingData(url, Sampler, method);
Sampler.setProperty(TestElement.TEST_CLASS, HTTPSamplerProxy.class.getName());
Sampler.setProperty(TestElement.GUI_CLASS, HttpTestSampleGui.class.getName());
// 头信息
HeaderManager headerManager = new HeaderManager();
JSONArray headersList = request.getJSONArray("headers");
headersList.forEach(header -> {
JSONObject jsonObject = (JSONObject) header;
Header headers = new Header();
headers.setName(jsonObject.getString("name"));
headers.setValue(jsonObject.getString("value"));
headerManager.add(headers);
});
headerManager.setComment("");
headerManager.setName("header Manager");
headerManager.setProperty(TestElement.TEST_CLASS, HeaderManager.class.getName());
headerManager.setProperty(TestElement.GUI_CLASS, HeaderPanel.class.getName());
// 构建参数
buildParameters(method, headersList, Sampler, request);
// 处理断言
ResponseAssertion assertion = getResponseAssertion();
// 构造新的树
transactionControllerHashTree.add(Sampler, headerManager);
transactionControllerHashTree.add(Sampler, assertion);
});
try {
// 将生成的测试计划保存为JMeter的.jmx文件格式
String jmxFilePath = "D:\\work\\jmx\\aaa.jmx";
SaveService.saveTree(testPlanTree, Files.newOutputStream(Paths.get(jmxFilePath)));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
/**
* 根据给定的URL配置 HTTPSamplerProxy 对象
* 该方法用于解析URL并配置HTTPSamplerProxy对象的相关参数,以便进行HTTP请求
* buildTheUnderlyingData
* @param url 要请求的URL地址
* @param Sampler HTTPSamplerProxy对象,用于发送HTTP请求
* @param method HTTP请求方法,如GET、POST等
* @throws RuntimeException 如果URL格式错误,则通过抛出运行时异常来处理
*/
private static void buildTheUnderlyingData(String url, HTTPSamplerProxy Sampler, String method) {
String path;
int port = 80;
String domain = "";
try {
// 解析URL
URL urlPath = new URL(url);
// 获取URL的路径
path = urlPath.getPath();
// 获取URL的域名
domain = urlPath.getHost();
// 获取URL的端口,如果未指定则默认为-1
port = urlPath.getPort();
// 获取并设置URL的协议,如http、https等
String protocol = urlPath.getProtocol();
Sampler.setProtocol(protocol);
// 根据URL的端口设置Sampler的端口,默认为80
if (port == -1) {
Sampler.setPort(port);
} else {
Sampler.setPort(80);
}
// 设置Sampler的域名、路径和请求方法
Sampler.setDomain(domain);
Sampler.setPath(path);
Sampler.setMethod(method);
} catch (MalformedURLException e) {
// 如果URL格式不正确,抛出运行时异常
throw new RuntimeException(e);
}
// 设置Sampler的名称为URL的路径
Sampler.setName(path);
}
/**
* 构建HTTP请求参数
* 根据请求方法、请求头和请求体的不同情况,设置HTTP请求的参数
* 目前支持GET、POST请求方法,DELETE和PUT方法留作未来扩展
*
* @param method 请求方法,如"GET"、"POST"、"DELETE"、"PUT"
* @param headersList 请求头的JSONArray,用于判断内容类型
* @param Sampler HTTPSamplerProxy对象,用于添加请求参数
* @param request 请求的JSONObject,包含请求数据
*/
private static void buildParameters(String method, JSONArray headersList, HTTPSamplerProxy Sampler, JSONObject request) {
// 处理POST请求
if (method.equals("POST")) {
// 过滤出内容类型为application/json的请求头
List<JSONObject> filteredObjects = filterJsonObjectsByValue(headersList, "application/json");
// 如果存在application/json类型的请求头,设置请求体为原始格式,并设置请求体内容
if (!filteredObjects.isEmpty()) {
Sampler.setPostBodyRaw(true);
JSONObject postData = request.getJSONObject("postData");
Sampler.addArgument("", postData.getString("text"), "=");
}
// 过滤出内容类型为application/x-www-form-urlencoded的请求头
filteredObjects = filterJsonObjectsByValue(headersList, "application/x-www-form-urlencoded");
// 如果存在application/x-www-form-urlencoded类型的请求头,设置请求体为非原始格式,并添加查询字符串参数
if (!filteredObjects.isEmpty()) {
Sampler.setPostBodyRaw(false);
JSONArray queryString = request.getJSONArray("queryString");
queryString.forEach(item1 -> {
JSONObject jsonObject = (JSONObject) item1;
Sampler.addArgument(jsonObject.getString("name"), jsonObject.getString("value"), "=");
});
}
}
// 处理GET请求
else if (method.equals("GET")) {
// 设置请求体为非原始格式,并添加查询字符串参数
Sampler.setPostBodyRaw(false);
JSONArray queryString = request.getJSONArray("queryString");
queryString.forEach(item -> {
JSONObject jsonObject = (JSONObject) item;
Sampler.addArgument(jsonObject.getString("name"), jsonObject.getString("value"), "=");
});
}
// 处理DELETE请求,目前未实现具体逻辑
else if (method.equals("DELETE")) {
// todo 根据实际情况处理
}
// 处理PUT请求,目前未实现具体逻辑
else if (method.equals("PUT")) {
// todo 根据实际情况处理
}
}
/**
* 获取响应断言对象
* <p>
* 此方法用于创建并配置一个ResponseAssertion对象,主要用于测试过程中对响应内容进行断言
* 它设置了断言的基本属性,并配置它以检查响应数据中是否存在特定的内容
*
* @return ResponseAssertion 返回配置好的ResponseAssertion对象,用于后续的HTTP响应断言
*/
private static @NotNull ResponseAssertion getResponseAssertion() {
// 创建一个响应断言实例
ResponseAssertion assertion = new ResponseAssertion();
// 设置测试字段的响应数据
assertion.setTestFieldResponseData();
// 设置不默认假设成功
assertion.setAssumeSuccess(false);
// 定义断言内容
String assertContent = "success\":";
// 设置测试类和GUI类的属性
assertion.setProperty(TestElement.TEST_CLASS, ResponseAssertion.class.getName());
assertion.setProperty(TestElement.GUI_CLASS, AssertionGui.class.getName());
// 设置断言的名称
assertion.setName("Response Assertion");
// 如果断言内容非空,添加到断言中
if (Objects.nonNull(assertContent)) {
assertion.addTestString(assertContent.trim());
}
// 设置断言类型为包含测试
assertion.setToContainsType();
// 返回配置好的断言对象
return assertion;
}
/**
* 获取一个配置好的TransactionController实例
*
* 该方法负责创建并配置一个TransactionController对象,用于后续的事务控制
* TransactionController是用于在性能测试中管理事务的重要类,它可以控制事务的开始和结束,
* 以及是否包含计时器等属性的设置
*
* @return TransactionController 返回一个配置好的TransactionController实例,用于在测试中控制事务
*/
private static @NotNull TransactionController getTransactionController() {
// 创建一个新的TransactionController实例
TransactionController transactionController = new TransactionController();
// 设置TransactionController的名称
transactionController.setName("Transaction Controller");
// 设置测试类名为TransactionController类的名称
transactionController.setProperty(TestElement.TEST_CLASS, TransactionController.class.getName());
// 设置GUI类名为TransactionControllerGui类的名称
transactionController.setProperty(TestElement.GUI_CLASS, TransactionControllerGui.class.getName());
// 设置生成父样本的标志,以便在采样时包含父事务的信息
transactionController.setGenerateParentSample(true);
// 设置不包含计时器,即事务中不包含计时操作
transactionController.setIncludeTimers(false);
// 返回配置好的TransactionController实例
return transactionController;
}
/**
* 创建并返回一个JMeter测试计划
* 测试计划用于组织和管理JMeter测试的所有方面
*
* @return 返回一个已经初始化的TestPlan实例,从未返回null
*/
private static @NotNull TestPlan getTestPlan() {
// 创建一个新的测试计划,命名为"创建JMeter脚本"
TestPlan testPlan = new TestPlan("创建JMeter脚本");
// 设置测试元素的测试类属性,为其提供类名
// 这是JMeter内部用来识别和分类测试元素的一种机制
testPlan.setProperty(TestElement.TEST_CLASS, TestPlan.class.getName());
// 设置测试元素的GUI类属性,为其提供对应的GUI类名
// 这影响了在图形界面模式下测试元素如何被呈现
testPlan.setProperty(TestElement.GUI_CLASS, TestPlanGui.class.getName());
// 设置用户定义的变量
// 这里使用ArgumentsPanel创建一个TestElement,并将其转换为Arguments类型,以便设置用户变量
// Arguments是JMeter中用于管理用户定义变量的类
testPlan.setUserDefinedVariables((Arguments) new ArgumentsPanel().createTestElement());
// 返回初始化完成的测试计划对象
return testPlan;
}
/**
* 根据给定的线程名称和循环控制器创建并返回一个新的线程组
* 此方法用于初始化一个线程组实例,设置其基本属性,如名称、线程数、预热步骤和采样控制器
*
* @param Name 线程组的名称,用于标识该线程组
* @param loopController 控制线程循环的控制器,决定线程如何循环执行任务
* @return 返回一个初始化完毕的、不可为空的线程组实例
*/
private static @NotNull ThreadGroup getThreadGroup(String Name, LoopController loopController) {
// 创建一个新的线程组实例
ThreadGroup threadGroup = new ThreadGroup();
// 设置线程组的名称
threadGroup.setName(Name);
// 设置线程组中的线程数量为1
threadGroup.setNumThreads(1);
// 设置预热步骤为1,通常用于性能测试前的预热
threadGroup.setRampUp(1);
// 设置采样控制器,控制线程的循环行为
threadGroup.setSamplerController(loopController);
// 设置线程组的类名属性,用于测试元素的分类
threadGroup.setProperty(TestElement.TEST_CLASS, ThreadGroup.class.getName());
// 设置线程组的GUI类名属性,用于图形用户界面的关联
threadGroup.setProperty(TestElement.GUI_CLASS, ThreadGroupGui.class.getName());
// 返回配置完毕的线程组实例
return threadGroup;
}
/**
* 获取循环控制器实例
*
* 该方法用于创建并初始化一个LoopController实例,用于控制测试循环的行为
* 循环控制器在性能测试中扮演重要角色,它定义了测试用例如何循环执行
*
* @return 返回一个已经初始化的LoopController实例,保证不会返回null
*/
private static @NotNull LoopController getLoopController() {
// 创建一个新的循环控制器实例
LoopController loopController = new LoopController();
// 设置循环次数为1,意味着测试用例将被执行一次
loopController.setLoops(1);
// 将第一个循环标志设置为true
// 这个标志用于指示当前的循环是否是第一次循环,在某些逻辑中可能会用到这个信息
loopController.setFirst(true);
// 设置循环控制器的测试类属性为其自身的类名
// 这是为了在JMeter的GUI中正确识别该元素的类型
loopController.setProperty(TestElement.TEST_CLASS, LoopController.class.getName());
// 设置循环控制器的GUI类属性为其对应的GUI组件类名
// 这是为了在JMeter的GUI中显示相应的配置面板
loopController.setProperty(TestElement.GUI_CLASS, LoopControlPanel.class.getName());
// 初始化循环控制器
// 初始化步骤是必要的,确保循环控制器能够正确地开始工作
loopController.initialize();
// 返回已经配置和初始化的循环控制器实例
return loopController;
}
/**
* 过滤JSON对象数组中的值
*
* 该方法通过搜索字符串来过滤一个JSON对象数组,返回值中包含搜索字符串的所有JSON对象
*
* @param jsonArray JSON对象数组,其中每个对象都包含一个"key"-"value"对
* @param searchString 搜索字符串,用于过滤JSON对象
* @return 包含搜索字符串的所有JSON对象的列表
*/
public static List<JSONObject> filterJsonObjectsByValue(JSONArray jsonArray, String searchString) {
// 创建一个列表,用于存储过滤后的JSON对象
List<JSONObject> filteredObjects = new ArrayList<>();
// 遍历JSON对象数组
for (int i = 0; i < jsonArray.size(); i++) {
// 获取当前迭代的JSON对象
JSONObject jsonObject = jsonArray.getJSONObject(i);
// 获取JSON对象中的"value"字段字符串
String value = jsonObject.getString("value");
// 如果"value"字段字符串包含搜索字符串,则将该JSON对象添加到过滤列表中
if (value.contains(searchString)) {
filteredObjects.add(jsonObject);
}
}
// 返回过滤后的JSON对象列表
return filteredObjects;
}
四、操作步骤
上面代码写完毕后,打开"浏览器开启开发者模式",点击<Fetch/XHR>:
保存上面数据:
执行上面脚本:
修改执行保存路径:
打开存储路,查看文件:
打开JMeter:
添加结果查看树,并且执行该脚本:
执行正确,为什么会是红色显示呢,大家看上面代码在看看断言就知道是上面原因呢?
通过上面步骤就能构建一个完整的 JMX 文件。
如果想通过内置StandardJMeterEngine 执行该脚本。
可以参考如下: