目录
说明
为了简单,我们使用metersphere快速调试功能的接口( /run/debug)作为分析对象,因为底层调用逻辑应该是类似的,如下操作
在Servcie中的核心代码就是ApiExecuteService的debug中如下两个方法。请求转为Jmeter可以执行的dto对象。然后执行。
//转为jemter可执行的对象。主要是里面的hashtree
JmeterRunRequestDTO runRequest = this.initRunRequest(request, bodyFiles);
// 开始执行
jMeterService.run(runRequest);
request转为JmeterRunRequestDTO 的核心
其核心就是将request转为org.apache.jorphan.collections.HashTree。这个是jmeter实际执行的对象的api。我们可以在ApiExecuteService的initRunRequest方法中看到。
HashTree hashTree = request.getTestElement().generateHashTree(config);
.......
JmeterRunRequestDTO runRequest = new JmeterRunRequestDTO(testId, request.getId(), runMode, hashTree);
核心就在HashTree。这个HashTree 一定是在jmeter中可以运行的hashtree。
request.getTestElement() 从源码看。返回的是一个MsTestElement。但实际是其子类MsTestPlan。
只是子类没有重写父类的generateHashTree方法所以调用的依然是父类的generateHashTree方法。但是调用的是子类实现toHashTree是子类实现,也就是MsTestPlan的toHashTree方法
public HashTree generateHashTree(MsParameter config) {
HashTree jmeterTestPlanHashTree = new ListedHashTree();
//MsTestPlan的toHashTree方法,并且调用MsTestPlan的hashTree这个字段,看下面json结构
this.toHashTree(jmeterTestPlanHashTree, this.hashTree, config);
return jmeterTestPlanHashTree;
}
那么为啥request.getTestElement() 是子类实现呢?回到请求。似乎在传入转为request对象时候就已经转为子类了
{
"id": "93438da6",
"testElement": {
"id": "01c812da-60ea-6cfb-b8cd-0291615cd435",
"type": "TestPlan",
"name": "TestPlan",
"enabled": true,
"hashTree": [{
"id": "c62c7828-7ea5-ffad-a07c-b44c525f566f",
"type": "ThreadGroup",
"name": "ThreadGroup",
"enabled": true,
"onSampleError": true,
"clazzName": "io.metersphere.api.dto.definition.request.MsThreadGroup",
"hashTree": [{
"id": "3db381ae-be7f-fe14-b7ba-57599a9dc626",
"type": "HTTPSamplerProxy",
"name": "75b7dbd5",
"enabled": true,
"$type": "Sampler",
"protocol": "HTTP",
"method": "GET",
"autoRedirects": false,
"followRedirects": true,
"useKeepalive": true,
"doMultipartPost": false,
"connectTimeout": 60000,
"responseTimeout": 60000,
"body": {
"type": "KeyValue",
"kvs": [],
"binary": []
},
"arguments": [{
"value": null,
"type": "text",
"files": null,
"enable": true,
"uuid": "28c76",
"contentType": "text/plain",
"description": null,
"file": false,
"isEdit": false,
"max": null,
"min": null,
"required": false,
"urlEncode": false,
"valid": false
}],
"rest": [],
"files": [],
"headers": [{
"name": "",
"value": "",
"enable": true
}],
"hashTree": [{
"resourceId": "ffbb1004-8c36-462b-8702-65844b976da6",
"type": "Assertions",
"text": [],
"regex": [],
"jsonPath": [],
"jsr223": [],
"xpath2": [],
"duration": {
"type": "Duration"
},
"enable": true,
"document": {
"type": "JSON",
"data": {
"xmlFollowAPI": false,
"jsonFollowAPI": false,
"json": [],
"xml": []
},
"enable": true
},
"id": "731c1739-6381-1a45-cd3c-f33c9f5a3871",
"clazzName": "io.metersphere.api.dto.definition.request.assertions.MsAssertions"
}],
"preSize": 0,
"postSize": 0,
"ruleSize": 0,
"url": "www.baidu.com",
"projectId": "1e0a6c4c-b274-11ed-ba08-00155da5ea2b",
"clazzName": "io.metersphere.api.dto.definition.request.sampler.MsHTTPSamplerProxy"
}]
}],
"clazzName": "io.metersphere.api.dto.definition.request.MsTestPlan"
},
"projectId": "1e0a6c4c-b274-11ed-ba08-00155da5ea2b",
"environmentMap": null,
"bodyUploadIds": [],
"requestId": "3db381ae-be7f-fe14-b7ba-57599a9dc626",
"editCaseRequest": false,
"debug": true,
"name": "www.baidu.com",
"reportId": "93438da6"
}
这里应该是通过MsTestElement上的@JsonTypeInfo进行了反序列化
@JsonTypeInfo(
use = Id.CLASS,
include = As.EXISTING_PROPERTY,
property = "clazzName"
)
@JsonIgnoreProperties(
ignoreUnknown = true
)
public abstract class MsTestElement {
}
回到this.toHashTree这一行,我们看下mstestPaln是如何处理的。又调每个子类的toHashTree。包括MsThreadGroup等。
@Override
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, MsParameter msParameter) {
ParameterConfig config = (ParameterConfig) msParameter;
final HashTree testPlanTree = tree.add(getPlan());
if (CollectionUtils.isNotEmpty(hashTree)) {
hashTree.forEach(el -> {
el.toHashTree(testPlanTree, el.getHashTree(), config);
});
}
}
如MsThreadGroup的toHashTree
@Override
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, MsParameter msParameter) {
ParameterConfig config = (ParameterConfig) msParameter;
final HashTree groupTree = tree.add(getThreadGroup());
if ((config != null && config.isEnableCookieShare()) || enableCookieShare) {
CookieManager cookieManager = new CookieManager();
cookieManager.setProperty(TestElement.TEST_CLASS, CookieManager.class.getName());
cookieManager.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("CookiePanel"));
cookieManager.setEnabled(true);
cookieManager.setName("CookieManager");
cookieManager.setClearEachIteration(false);
cookieManager.setControlledByThread(false);
groupTree.add(cookieManager);
}
if (CollectionUtils.isNotEmpty(hashTree)) {
for (MsTestElement el : hashTree) {
el.toHashTree(groupTree, el.getHashTree(), config);
}
if (!config.isOperating()) {
MsDebugSampler el = new MsDebugSampler();
el.setName(RunningParamKeys.RUNNING_DEBUG_SAMPLER_NAME);
el.toHashTree(groupTree, el.getHashTree(), config);
}
}
}
所以从这里我们大概理解了,request如何转hashtree的基本逻辑。就是通过子类的toHashTree去处理。怎么处理呢。每个子类不太相同。但是有个基本的两个操作就是tree.add,上面第4行的操作。此处的tree。实际是父节点的hashree对象。然后添加了新的Jmeter中的测试对象。作为key。存入到一个map中。
public HashTree add(Object key) {
if (!data.containsKey(key)) {
HashTree newTree = createNewTree();
data.put(key, newTree);
return newTree;
}
return getTree(key);
}
我们来分析下hashtree到底是一个什么样子的结构
我们可以看下hashtree实际是实现了map接口的。
总结
如果我们想要将自己的对象转换成Hashtree对象。实际就是参考MsTestElement所有实现子类如何实现toHashTree这个方法的,这里应该都对应了Jmeter的组件对象的转换。
如何运行的
当前接口走的是else if分支,通过通读,应是调用的jmeter的原生api执行测试的。runNode有待下次解读
public void run(JmeterRunRequestDTO request) {
if (request.getPool().isPool() && StringUtils.isNotBlank(request.getRunMode())) {
this.runNode(request);
} else if (request.getHashTree() != null) {
//大概就是替换变量
ElementUtil.coverArguments(request.getHashTree());
//解析hashTree,是否含有文件库文件?有待研究
HashTreeUtil.initRepositoryFiles(request);
execThreadPoolExecutor.addTask(request);
}
}
public void addTask(JmeterRunRequestDTO requestDTO) {
ExecTask task = new ExecTask(requestDTO);
threadPool.execute(task);
outApiThreadPoolExecutorLogger("报告:[" + requestDTO.getReportId() + "] 资源:[" + requestDTO.getTestId() + "] 加入执行队列");
}
public class ExecTask implements Runnable {
private JmeterRunRequestDTO request;
public ExecTask(JmeterRunRequestDTO request) {
this.request = request;
}
public JmeterRunRequestDTO getRequest() {
return this.request;
}
@Override
public void run() {
CommonBeanFactory.getBean(JMeterService.class).addQueue(request);
Object res = PoolExecBlockingQueueUtil.take(request.getReportId());
if (res == null && !JMeterThreadUtils.isRunning(request.getReportId(), request.getTestId())) {
LoggerUtil.info("任务执行超时", request.getReportId());
}
}
}
public void addQueue(JmeterRunRequestDTO request) {
this.runLocal(request);
}
private void runLocal(JmeterRunRequestDTO request) {
init();
// 接口用例集成报告/测试计划报告日志记录
if (StringUtils.isNotEmpty(request.getTestPlanReportId())
&& StringUtils.equals(request.getReportType(), RunModeConstants.SET_REPORT.toString())) {
FixedCapacityUtil.put(request.getTestPlanReportId(), new StringBuffer(""));
} else {
// 报告日志记录
FixedCapacityUtil.put(request.getReportId(), new StringBuffer(""));
}
LoggerUtil.debug("监听MessageCache.tasks当前容量:" + FixedCapacityUtil.size());
if (request.isDebug() && !StringUtils.equalsAny(request.getRunMode(), ApiRunMode.DEFINITION.name())) {
LoggerUtil.debug("为请求 [ " + request.getReportId() + " ] 添加同步接收结果 Listener");
JMeterBase.addBackendListener(request, request.getHashTree(), MsApiBackendListener.class.getCanonicalName());
}
if (MapUtils.isNotEmpty(request.getExtendedParameters())
&& request.getExtendedParameters().containsKey(ExtendedParameter.SYNC_STATUS)
&& (Boolean) request.getExtendedParameters().get(ExtendedParameter.SYNC_STATUS)) {
LoggerUtil.debug("为请求 [ " + request.getReportId() + " ] 添加Debug Listener");
addDebugListener(request.getReportId(), request.getHashTree(), request.getRunMode());
}
if (request.isDebug()) {
LoggerUtil.debug("为请求 [ " + request.getReportId() + " ] 添加Debug Listener");
addDebugListener(request.getReportId(), request.getHashTree(), request.getRunMode());
} else {
LoggerUtil.debug("为请求 [ " + request.getReportId() + " ] 添加同步接收结果 Listener");
JMeterBase.addBackendListener(request, request.getHashTree(), MsApiBackendListener.class.getCanonicalName());
}
LoggerUtil.info("资源:[" + request.getTestId() + "] 加入JMETER中开始执行", request.getReportId());
LocalRunner runner = new LocalRunner(request.getHashTree());
runner.run(request.getReportId());
}
核心应该在 runner.run(request.getReportId())。大概就是启动了一个本地Runner启动。
public void run(String report) {
//应该就是调用了jmeter自身的api启动。
StandardJMeterEngine engine = new StandardJMeterEngine();
engine.configure(jmxTree);
try {
LoggerUtil.info("LocalRunner 开始执行报告",report);
engine.runTest();
} catch (JMeterEngineException e) {
engine.stopTest(true);
}
}
其核心有ms-jemter-core提供。可以看出改包应该从jmeter源码的core抽取了engine等相关代码