1. 需求
在之前的Jmeter在Java中压测并使用Java对压测结果进行收集文章中,写了通过文件替换的方式在进行内容配置替换,然后使用JMeter Engine进行压测,但是有个问题就是做文件替换操作效率太低,非常影响压测服务器的性能。
所以,我只有另寻他路,最后找到一种方式,通过JMeter中的HashTree对压测内容进行组装,最后通过Summariser来收集结果。
最后需要完成的目标如图
- 线程组配置如下
- 请求信息如下
- Header信息如下
- 吞吐量定时器信息如下
- 查看请求是否成功
2. 准备工作
2.1. Maven引用
<!-- jMeter-core -->
<dependency>
<groupId>org.apache.jmeter</groupId>
<artifactId>ApacheJMeter_core</artifactId>
<version>${jmeter.version}</version>
</dependency>
<!--jMeter component-->
<dependency>
<groupId>org.apache.jmeter</groupId>
<artifactId>ApacheJMeter_components</artifactId>
<version>${jmeter.version}</version>
</dependency>
<!--jMeter Http包-->
<dependency>
<groupId>org.apache.jmeter</groupId>
<artifactId>ApacheJMeter_http</artifactId>
<version>${jmeter.version}</version>
</dependency>
<!-- 工具类包 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.11</version>
</dependency>
2.2. jmeter.properties和saveservice.properties
- jmeter.properties jmeter的核心配置类,可以通过该文件配置jmeter的核心参数
- saveservice.properties jmeter的常量映射文件
需要先安装JMeter到本地,之后找到JMeter安装目录中的jmeter.properties和saveservice.properties文件。比如我的安装目录是/Users/wushuaiping/apache-jmeter-5.4.1/
,这两个文件就在安装目录的/bin
目录中
找到文件后,将文件拷贝到项目中的resource
目录
如果对JMeter定制化需求不是很高,saveservice.properties
可以不用拷贝到项目中。
3. HashTree组装
3.1. 初始化JMeter
StandardJMeterEngine jMeterEngine = new StandardJMeterEngine();
// 加载jmeter.properties文件
JMeterUtils.loadJMeterProperties(JMeterUtil.getJMeterPropertiesFile());
// 设置非GUI模式
System.setProperty(JMeter.JMETER_NON_GUI, "true");
// 初始化本地配置
JMeterUtils.initLocale();
新建JMeterUtil
,用于统一jmeter参数初始化,文件加载等操作,避免代码重复。
/**
* JMeter自定义工具类
*
* @author wushuaiping
* @date 2021/8/23 9:48 上午
*/
public class JMeterUtil {
private static final String separator = File.separator;
/**
* 获取jmeter配置文件
*
* @return
* @throws FileNotFoundException
*/
public static File getJMeterPropertiesFile() throws IOException {
return getResourceFile("jmeter", "properties");
}
/**
* 获取jmeter常量类配置文件
*
* @return
* @throws FileNotFoundException
*/
public static File getSaveservicePropertiesFile() throws IOException {
return getResourceFile("saveservice", "properties");
}
/**
* 获取resource目录下文件
*
* @param prefix
* @param suffix
* @return
* @throws IOException
*/
public static File getResourceFile(String prefix, String suffix) throws IOException {
ClassPathResource classPathResource = new ClassPathResource(prefix + "." + suffix);
// 由于线上是jar包方式来运行项目的,因此通过文件读取获取流的方式行不通,因为无法直接读取压缩包中的文件,读取只能通过流的方式读取。
// 所以需要将读取到的resource信息copy到临时文件中,这样可以避免线上报错FileNotFound的问题
byte[] keywordsData = FileCopyUtils.copyToByteArray(classPathResource.getInputStream());
File tempFile = File.createTempFile(prefix, suffix);
FileUtils.writeByteArrayToFile(tempFile, keywordsData);
return tempFile;
}
}
3.2. 初始化JMeter HTTP请求
3.2.1. 压测请求相关类
- PressureTestRequest
/**
* 压测请求
*
* @author wushuaiping
* @date 2021/9/1 10:40 上午
*/
@Data
public class PressureTestRequest {
private String apiName;
private String url;
private String body;
private String method;
private List<PressureTestRequestHeader> headers;
}
- PressureTestRequestHeader
/**
* @author wushuaiping
* @date 2021/9/1 10:41 上午
*/
@Data
public class PressureTestRequestHeader {
private String headerKey;
private String headerValue;
}
- 转换压测请求为HashTree
// 在压测时,我们可能需要多个请求压测的情况。所以我们使用一个List来保存请求信息
List<HashTree> httpRequestTrees = new LinkedList<>();
// 初始化JMeter HTTP请求,将HTTP请求转为HashTree
for (PressureTestRequest request : requests) {
try {
HashTree hashTree = JMeterUtil.httpToHashTree(request);
httpRequestTrees.add(hashTree);
} catch (Exception e) {
log.error("发生错误",e);
return false;
}
}
- 在JMeterUtil中新增httpToHashTree方法
/**
* 将压测请求转为JMeter HashTree
*
* @param pressureTestRequest
* @return
* @throws MalformedURLException
* @throws UnsupportedEncodingException
*/
public static HashTree httpToHashTree(PressureTestRequest pressureTestRequest) throws IOException {
// 创建一个标准取样器对象sampler
HTTPSamplerProxy sampler = new HTTPSamplerProxy();
// 设置sampler的属性(sampler属性部分都会转成xml标签的属性值,和文本值)
sampler.setEnabled(true);
// 设置请求参数,jmeter貌似不支持body和param一起传的情况。这里就如果是body默认使用PostBody模式
// query param也需要放到arguments中
URL url = new URL(pressureTestRequest.getUrl());
String urlPath = url.getPath();
if (pressureTestRequest.getBody() != null) {
sampler.setPostBodyRaw(true);
sampler.setArguments(addHttpBodyArguments(pressureTestRequest.getBody()));
if (StrUtil.isNotBlank(urlPath)) {
String queryParamsPath = UrlUtils.truncateUrlPage(pressureTestRequest.getUrl());
if (StrUtil.isNotBlank(queryParamsPath)) {
// body传参时使用path来拼接queryParam参数
urlPath = urlPath + "?" + queryParamsPath;
}
}
} else {
// 当没有body参数时,使用arguments的方式来添加queryParam参数
// 获取url path中的query param参数
Map<String, String> queryParams = UrlUtils.urlSplit(pressureTestRequest.getUrl());
if (!queryParams.isEmpty()) {
sampler.setArguments(addHttpQueryParamsArguments(queryParams));
}
}
sampler.setName(pressureTestRequest.getApiName());
sampler.setProperty(TestElement.TEST_CLASS, HTTPSamplerProxy.class.getName());
sampler.setProperty(TestElement.GUI_CLASS, HttpTestSampleGui.class.getName());
sampler.setProperty(TestPlan.COMMENTS, "");
sampler.setContentEncoding("UTF-8");
sampler.setFollowRedirects(true);
sampler.setAutoRedirects(false);
sampler.setUseKeepAlive(true);
// 这些选项都可以做成自定义的
sampler.setConnectTimeout("5000");
sampler.setResponseTimeout("5000");
sampler.setEmbeddedUrlRE("");
sampler.setMethod(pressureTestRequest.getMethod());
// 设置请求信息
sampler.setDomain(URLDecoder.decode(url.getHost(), "UTF-8"));
sampler.setPath(URLDecoder.decode(urlPath, "UTF-8"));
sampler.setProtocol(URLDecoder.decode(url.getProtocol(), "UTF-8"));
if (url.getPort() == -1 && StrUtil.equals("http", url.getProtocol())) {
sampler.setPort(80);
} else if (url.getPort() == -1 && StrUtil.equals("https", url.getProtocol())) {
sampler.setPort(443);
} else {
sampler.setPort(url.getPort());
}
sampler.setPort(url.getPort());
HashTree httpTree = new ListedHashTree();
httpTree.put(sampler, new ListedHashTree());
//在sampler的树结构添加同一级别的请求头(等同于HTTP信息头管理器)
addHeaderManagerToHashTree(httpTree, pressureTestRequest.getHeaders());
return httpTree;
}
- 在JMeterUtil中新增addHttpQueryParamsArguments方法
/**
* 将queryParam转为JMeter Arguments
*
* @param queryParams
* @return
*/
public static Arguments addHttpQueryParamsArguments(Map<String, String> queryParams) {
Arguments arguments = new Arguments();
queryParams.forEach((key, value) -> {
HTTPArgument httpArgument = new HTTPArgument(key, value);
httpArgument.setAlwaysEncoded(false);
arguments.addArgument(httpArgument);
});
return arguments;
}
- 在JMeterUtil中新增addHeaderManagerToHashTree方法
/**
* 给HashTree添加请求头
*
* @param hashTree
* @param pressureTestAPIHeaders
*/
public static void addHeaderManagerToHashTree(HashTree hashTree, List<PressureTestRequestHeader> pressureTestAPIHeaders) {
if (CollectionUtils.isEmpty(pressureTestAPIHeaders)) {
return;
}
HeaderManager headerManager = new HeaderManager();
headerManager.setEnabled(true);
headerManager.setName("HTTP信息头管理器");
headerManager.setProperty(TestElement.GUI_CLASS, HeaderPanel.class.getName());
headerManager.setProperty(TestElement.TEST_CLASS, HeaderManager.class.getName());
headerManager.setProperty(TestPlan.COMMENTS, "");
pressureTestAPIHeaders.forEach(header -> headerManager.add(new Header(header.getHeaderKey(), header.getHeaderValue())));
HashTree headerTree = new ListedHashTree();
headerTree.put(headerManager, new ListedHashTree());
hashTree.add(headerTree);
}
在Jmeter GUI中的界面为:
3.2.2. 将转换好的请求HashTree添加到线程组中
// 将请求的HashTree添加进线程组
HashTree threadGroupTree = new ListedHashTree();
ThreadGroup threadGroup = JMeterUtil.initThreadGroup(60*10, -1, 1);
httpRequestTrees.forEach(httpRequestTree -> threadGroupTree.put(threadGroup, httpRequestTree));
- 在JMeterUtil中新增initThreadGroup方法
/**
* 初始化JMeter线程组
* 请求次数=loops * threadNumber
*
* @param duration 持续执行时间,单位秒
* @param loops 循环次数 -1为无限循环
* @param threadNumber 线程数
* @return
*/
public static ThreadGroup initThreadGroup(long duration, int loops, int threadNumber) {
LoopController loopController = new LoopController();
loopController.setName("循环控制器");
loopController.setEnabled(true);
loopController.setLoops(loops);
loopController.setContinueForever(false);
loopController.setProperty(TestElement.TEST_CLASS, LoopController.class.getName());
loopController.setProperty(TestElement.GUI_CLASS, LoopControlPanel.class.getName());
ThreadGroup group = new ThreadGroup();
group.setEnabled(true);
group.setName("线程组");
group.setProperty(TestElement.TEST_CLASS, ThreadGroup.class.getName());
group.setProperty(TestElement.GUI_CLASS, ThreadGroupGui.class.getName());
group.setProperty(ThreadGroup.IS_SAME_USER_ON_NEXT_ITERATION, true);
group.setProperty(TestElement.COMMENTS, "");
group.setNumThreads(threadNumber);
group.setDuration(duration);
group.setRampUp(0);
group.setDelay(0);
group.setProperty(ThreadGroup.ON_SAMPLE_ERROR, ThreadGroup.ON_SAMPLE_ERROR_CONTINUE);
group.setScheduler(true);
group.setSamplerController(loopController);
return group;
}
对应的JMeter GUI界面为:
3.3. 定义吞吐量定时器
// 定义吞吐量定时器,并添加到线程组中
HashTree throughputTimerHashTree = new ListedHashTree();
// limitQps * 60L为限制每分钟最大请求量
BigDecimal limitQpsDecimal = new BigDecimal(limitQps * 60L).setScale(0, RoundingMode.UP);
ConstantThroughputTimer throughputTimer = JMeterUtil.initConstantThroughputTimer(limitQpsDecimal.longValue());
throughputTimerHashTree.put(throughputTimer, new ListedHashTree());
threadGroupTree.put(threadGroup, throughputTimerHashTree);
- 在JMeterUtil中新增initConstantThroughputTimer方法
/**
* 初始化常数吞吐量定时器
*
* @param tps 每分钟吞吐量
* @return
*/
public static ConstantThroughputTimer initConstantThroughputTimer(Long tps) {
ConstantThroughputTimer throughputTimer = new ConstantThroughputTimer();
// 基于计算吞吐量:所有此线程
throughputTimer.setProperty("calcMode", 1);
// 限制每分钟最大吞吐量
throughputTimer.setProperty("throughput", tps);
throughputTimer.setProperty(TestElement.TEST_CLASS, ConstantThroughputTimer.class.getName());
throughputTimer.setProperty(TestElement.GUI_CLASS, TestBeanGUI.class.getName());
throughputTimer.setEnabled(true);
throughputTimer.setName("常数吞吐量定时器");
return throughputTimer;
}
对应JMeter GUI界面:
3.4. 添加测试计划
HashTree testPlanHashTree = new ListedHashTree();
TestPlan testPlan = JMeterUtil.initTestPlan("测试计划-" + 业务id);
testPlanHashTree.put(testPlan, threadGroupTree);
- 在JMeterUtil中新增initTestPlan方法
/**
* 初始化测试计划
*
* @param testPlanName
* @return
* @throws IOException
*/
public static TestPlan initTestPlan(String testPlanName) {
TestPlan plan = new TestPlan();
//设置测试计划属性及内容,最后都会转为xml标签的属性及内容
plan.setProperty(TestElement.NAME, testPlanName);
plan.setProperty(TestElement.TEST_CLASS, TestPlan.class.getName());
plan.setProperty(TestElement.GUI_CLASS, TestPlanGui.class.getName());
plan.setEnabled(true);
plan.setComment("");
plan.setFunctionalMode(false);
plan.setTearDownOnShutdown(true);
plan.setSerialized(false);
plan.setProperty("TestPlan.user_define_classpath", "");
plan.setProperty("TestPlan.user_defined_variables", "");
plan.setUserDefinedVariables(new Arguments());
return plan;
}
对应JMeter GUI界面:
3.5. 准备收集结果集
Summariser summer = null;
String summariserName = JMeterUtils.getPropDefault("summariser.name", "summary");
if (summariserName.length() > 0) {
summer = new Summariser(summariserName);
}
ResultCollector viewResultsFullVisualizer = new ResultCollector(summer);
viewResultsFullVisualizer.setProperty(TestElement.TEST_CLASS, ResultCollector.class.getName());
viewResultsFullVisualizer.setProperty(TestElement.GUI_CLASS, ViewResultsFullVisualizer.class.getName());
viewResultsFullVisualizer.setName("查看结果树");
viewResultsFullVisualizer.setEnabled(true);
testPlanHashTree.add(testPlanHashTree.getArray(), viewResultsFullVisualizer);
3.6. 结果集收集
// 将组装好的HashTree配置到JMeterEngine中
jMeterEngine.configure(testPlanHashTree);
long startPressureTestMillis = System.currentTimeMillis();
jMeterEngine.run();
long endPressureTestMillis = System.currentTimeMillis();
3.6.1. 定制Summariser类
在jmeter的api中是没有提供直接获取压测结果的方法的,所以我们需要找到其他方法来完成该需求。
通过查看jmeter源码,发现收集结果都在Summariser进行,所以我们需要对Summariser进行定制。
找到org.apache.jmeter.reporters.Summariser类
-
- 将
private transient Totals myTotals = null;
改为public transient Totals myTotals = null;
- 将
-
- 将org.apache.jmeter.reporters.Summariser.Totals#total也改为
public
- 将org.apache.jmeter.reporters.Summariser.Totals#total也改为
-
- 将
org.apache.jmeter.reporters.SummariserRunningSample
类改为public修饰
- 将
-
- 在
org.apache.jmeter.reporters.Summariser
末尾新增方法
- 在
public org.apache.jmeter.reporters.SummariserRunningSample getTotal() {
return myTotals == null ? null : myTotals.total;
}
如图,直接将源码拷贝到项目中进行修改,只是包名一定要和源码包名保持一致。
3.6.2. 收集结果
SummariserRunningSample summariserRunningSample = summer.getTotal();
// 本次压测总请求量
summariserRunningSample.getCounter();
// 本次压测总失败请求量
summariserRunningSample.getErrorCount();
// 最小响应时间
summariserRunningSample.getMin();
// 最大响应时间
summariserRunningSample.getMax();
// tps 吞吐量
summariserRunningSample.getRate();
// 平均响应时间
summariserRunningSample.getAverage();
欢迎评论,转发
转发需注明原作者,谢谢~