一、前言
这篇文章详细介绍JMeter engine里面的HashTree结构具体用来做什么。
大家看到下面是JMeter控制台配置截图,是一个标准的菜单形式;菜单形式其实就类似于“树型”的数据结构,而HashTree其实就是一个树型数据结构。
我们在JMeter控制台导出的jmx文件,是一个xml结构的数据,他其实就是由HashTree生成的,后面我们会讲到。
二、HashTree的用法
首先通过HashTree类介绍,它一个集合类;具备Map结构的功能,而且是一种树型结构。
/** * This class is used to create a tree structure of objects. Each element in the * tree is also a key to the next node down in the tree. It provides many ways * to add objects and branches, as well as many ways to retrieve. * <p> * HashTree implements the Map interface for convenience reasons. The main * difference between a Map and a HashTree is that the HashTree organizes the * data into a recursive tree structure, and provides the means to manipulate * that structure. * <p> * Of special interest is the {@link #traverse(HashTreeTraverser)} method, which * provides an expedient way to traverse any HashTree by implementing the * {@link HashTreeTraverser} interface in order to perform some operation on the * tree, or to extract information from the tree. * * @see HashTreeTraverser * @see SearchByClass */ public class HashTree implements Serializable, Map<Object, HashTree>, Cloneable { } |
JMeter常用的HashTree方法(以下图配置为例):
//ListedHashTree是HashTree的继承类,可以保证HashTree的顺序性 HashTree tree = new ListedHashTree(); //TestPlan对象,测试计划 TestPlan plan = new TestPlan(); //ThreadGroup对象,线程组 ThreadGroup group = new ThreadGroup(); //创建线程组数结构的对象groupTree HashTree groupTree = new ListedHashTree(); //表示取样器中的HTTP请求 HTTPSamplerProxy sampler = new HTTPSamplerProxy(); //创建HTTP请求的数结构对象samplerTree //调用put方法相当于在plan(测试计划)菜单对象下添加group(线程组)子菜单,这样就形成了一种树型结构 HashTree samplerTree = new ListedHashTree(); samplerTree.put(sampler,new ListedHashTree()) //groupTree树结构添加子树samplerTree groupTree.put(group,samplerTree) //tree树结构为测试计划对象,添加子树groupTree,这样就形成了上图的层级形式 tree.put(plan, groupTree) //调用add方法相当于在tree菜单对象下添加同级菜单 tree.add(Object key) |
三、JMeter源码导出jmx脚本文件介绍
1.首先在JMeter控制台所有点击事件,都会被ActionRouter中performaAction方法进行监听执行,点击导出按钮,会进入到如图方法通过反射由Save类执行。
2.在Save类中执行doAction主要是获取到配置的HashTree。
3.当你点击保存的时候,它会创建一个空文件,此时文件没有任何内容。
4.Save类的doAction方法最后会调用backupAndSave(e, subTree, fullSave, updateFile)这个是来将创建的空文件写入xml内容的。
在SaveService中saveTree方法,其中JMXSAVER是XStream对象,对应的maven坐标如下:
<!-- https://mvnrepository.com/artifact/com.thoughtworks.xstream/xstream --> <dependency> <groupId>com.thoughtworks.xstream</groupId> <artifactId>xstream</artifactId> <version>1.4.15</version> </dependency> |
四、自定义HashTree生成JMeter脚本
1.首先maven引入以下几个坐标:
<jmeter.version>5.3</jmeter.version> <dependency> <groupId>org.apache.jmeter</groupId> <artifactId>ApacheJMeter_http</artifactId> <version>${jmeter.version}</version> <exclusions> <exclusion> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.apache.jmeter</groupId> <artifactId>ApacheJMeter_functions</artifactId> <version>${jmeter.version}</version> </dependency> <dependency> <groupId>org.apache.jmeter</groupId> <artifactId>ApacheJMeter_jdbc</artifactId> <version>${jmeter.version}</version> </dependency> <dependency> <groupId>org.apache.jmeter</groupId> <artifactId>ApacheJMeter_tcp</artifactId> <version>${jmeter.version}</version> </dependency> |
2.先创建一个取样器,然后写成HashTree的数据结构。
public static HashTree httpToHashTree(HttpRequestConfig httpRequest){ //创建一个标准取样器对象sampler HTTPSamplerProxy sampler = new HTTPSamplerProxy(); //设置sampler的属性(sampler属性部分都会转成xml标签的属性值,和文本值) sampler.setEnabled(true); sampler.setName(httpRequest.getLabel()); sampler.setProperty(TestElement.TEST_CLASS, HTTPSamplerProxy.class.getName()); sampler.setProperty(TestElement.GUI_CLASS, "HttpTestSampleGui"); sampler.setProperty(TestPlan.COMMENTS,""); sampler.setContentEncoding("UTF-8"); sampler.setFollowRedirects(true); sampler.setAutoRedirects(false); sampler.setUseKeepAlive(true); sampler.setDoMultipartPost(false); sampler.setConnectTimeout(""); sampler.setResponseTimeout(""); sampler.setEmbeddedUrlRE(""); //设置请求参数 sampler.setArguments(addHttpArguments(httpRequest.getRequestBody())); sampler.setMethod(httpRequest.getMethod()); String requestUrl = httpRequest.getUrl(); if (!requestUrl.startsWith("http://") && !requestUrl.startsWith("https://")) { requestUrl = "http://" + requestUrl; } URL url = null; try { url = new URL(requestUrl); sampler.setDomain(URLDecoder.decode(url.getHost(),"UTF-8")); sampler.setPath(URLDecoder.decode(url.getPath(), "UTF-8")); sampler.setProtocol(URLDecoder.decode(url.getProtocol(),"UTF-8")); if (url.getPort() == -1 && url.getProtocol().equals("http")){ sampler.setPort(80); }else if (url.getPort() == -1 && url.getProtocol().equals("https")){ sampler.setPort(443); }else{ sampler.setPort(url.getPort()); } } catch (Exception e) { e.printStackTrace(); } HashTree httpTree = new ListedHashTree(); httpTree.put(sampler,new ListedHashTree()); //在sampler的树结构添加同一级别的请求头(等同于HPPT信息头管理器) addHeaderManagerToHashTree(httpTree,httpRequest.getRequestHeader()); return httpTree; } //将键值对请求参数存入Arguments对象中 private static Arguments addHttpArguments(List<KeyValueConfig> requestBody){ Arguments arguments = new Arguments(); requestBody.stream().filter(KeyValueConfig::isValid) .forEach(keyValueConfig -> { HTTPArgument httpArgument = new HTTPArgument(keyValueConfig.getKey(),keyValueConfig.getValue()); httpArgument.setAlwaysEncoded(true); arguments.addArgument(httpArgument); }); return arguments; } //将请求头参数存入HeaderManager对象中 private static void addHeaderManagerToHashTree(HashTree hashTree, List<KeyValueConfig> requestHeader){ if (CollUtil.isEmpty(requestHeader)){ return; } HeaderManager headerManager = new HeaderManager(); headerManager.setEnabled(true); headerManager.setName("headers"); headerManager.setProperty(TestElement.GUI_CLASS, JMeterUtil.readSaveProperties("HeaderPanel")); headerManager.setProperty(TestElement.TEST_CLASS,JMeterUtil.readSaveProperties("HeaderManager")); headerManager.setProperty(TestPlan.COMMENTS,""); requestHeader.stream().filter(KeyValueConfig::isValid) .forEach(keyValueConfig -> { headerManager.add(new Header(keyValueConfig.getKey(),keyValueConfig.getValue())); }); HashTree headerTree = new ListedHashTree(); headerTree.put(headerManager,new ListedHashTree()); hashTree.add(headerTree); } |
3.创建一个标准的线程组
public static ThreadGroup threadGroup; //创建一个标准的线程组 private static void initThreadGroup(){ LoopController loopController = new LoopController(); loopController.setName("LoopController"); loopController.setProperty(TestElement.TEST_CLASS, LoopController.class.getName()); loopController.setProperty(TestElement.GUI_CLASS, JMeterUtil.readSaveProperties("LoopControlPanel")); loopController.setEnabled(true); loopController.setLoops(1); ThreadGroup group = new ThreadGroup(); group.setEnabled(true); group.setName("ThreadGroup"); group.setProperty(TestElement.TEST_CLASS, JMeterUtil.readSaveProperties("ThreadGroup")); group.setProperty(TestElement.GUI_CLASS, JMeterUtil.readSaveProperties("ThreadGroupGui")); group.setProperty(ThreadGroup.ON_SAMPLE_ERROR,"continue"); group.setProperty(ThreadGroup.IS_SAME_USER_ON_NEXT_ITERATION,true); group.setProperty(TestElement.COMMENTS,""); group.setNumThreads(1); group.setRampUp(1); group.setDelay(0); group.setDuration(0); group.setProperty(ThreadGroup.ON_SAMPLE_ERROR, ThreadGroup.ON_SAMPLE_ERROR_CONTINUE); group.setScheduler(false); group.setSamplerController(loopController); threadGroup = group; } |
4.创建一个标准的测试计划
public static TestPlan testPlan; //创建一个标准的测试计划 private static void initTestPlan() { TestPlan plan = new TestPlan(); //设置测试计划属性及内容,最后都会转为xml标签的属性及内容 plan.setProperty(TestElement.NAME, "测试计划"); plan.setProperty(TestElement.TEST_CLASS, JMeterUtil.readSaveProperties("TestPlan")); plan.setProperty(TestElement.GUI_CLASS, JMeterUtil.readSaveProperties("TestPlanGui")); 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()); testPlan = plan; } |
5.开始封装成一个HashTree的配置
//先创建一个测试计划hashtree对象 HashTree hashTree = new ListedHashTree(); //在创建一个线程组threaddGroupTree对象 HashTree threadGroupTree = new ListedHashTree(); //HttpRequestConfig为HTTP对应的请求头、请求体等信息数据,传入httpToHashTree静态方法获取到取样器的HashTree数据结构,源码上图已分享 HashTree httpConfigTree = XXClass.httpToHashTree(HttpRequestConfig httpRequestData) //threadGroupTree添加子菜单httpConfigTree对象 threadGroupTree.put(group, httpConfigTree); //测试计划hashTree添加子菜单threadGroupTree对象 hashTree.put(JMeterTestPlanConfigService.testPlan, threadGroupTree); |
6.HashTree写好后,调用JMeter原生方法SaveService.saveTree(hashTree,outStream);生成对应的xml
如果直接调用的话生成的xml格式会形成如下图所示,而非JMeter原生导出jmx形式,这种文件结构JMeter控制台读取会报错,识别不了。
7.后面阅读SaveService源码才明白,生成xml文件之前会先初始化静态代码块内容,初始化属性。
8.过程中会调用JMeterUtils中的findFile方法来寻找saveservice.properties文件。
9.由于SaveService 中都是静态方法无法重写,所以根据最后调用JMeterUtils中的findFile方法来寻找。saveservice.properties有两种解决方案。
10.方案一 :不推荐,在项目根目录下存放saveservice.properties,这样findFile方法就能拿到,但是这样不好,因为maven打包的时候该文件会打不进去,至少我springboot项目是遇到这样的问题。
11.方案二:推荐,创建一个临时文件命名为saveservice.properties,然后提前将saveservice.properties配置读取到临时文件中,这样在调用JMeterUtils中的findFile方法同样能够找到配置,成功解决SaveService初始化属性导致的问题,具体代码如下:
private void hashTreeToXML(HashTree hashTree,PressureConfigInfo configInfo){ FileOutputStream outStream = null; File file = new File("temp.jmx"); File tempFile = null; try { //创建一个临时的saveservice.properties文件 tempFile = new File("saveservice.properties"); InputStream is = JMeterUtil.class.getResource("/jmeter/saveservice.properties").openStream(); //将配置文件写入临时文件中 FileUtil.writeFromStream(is,tempFile); outStream = new FileOutputStream(file); //调用saveTree成功转为xml SaveService.saveTree(hashTree,outStream); String xmlContent = FileUtil.readUtf8String(file); configInfo.setFile(xmlContent.getBytes()); } catch (IOException e) { e.printStackTrace(); }finally { try { FileUtils.forceDelete(file); FileUtils.forceDelete(tempFile); } catch (IOException e) { e.printStackTrace(); } } } |
最后生成的xml文件结构如下图,通过JMeter控制台也能成功打开识别:
本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理