一文搞懂JMeter engine中的HashTree配置

发表于:2021-5-17 09:32

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:HenryXiao8080    来源:CSDN

  一、前言
  这篇文章详细介绍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),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号