JMeter 源码解读 [4] - HashTree

HashTree

首先要特别说明一下这里说的HashTree 和我们在数据结构里面谈的 哈希树 完全不是一个概念,一开始我也被它的名字误导,特地去看了一会哈希树相关的技术细节,后来越深人越发现,这个HashTree 完全是挂羊头卖狗肉嘛,不过作为整个JMeter 最核心的一个数据结构,还是很值得认真分析一下的

JMX文件

我们知道jmxJMeter用来描述测试用例的核心文件,它可以用于在GUI 模式下加载整个测试组件,也可以直接运行于Non-GUI的执行模式,它是格式是基于XML 。那么HashTree就是它在内存的一份映射。 我们先来看一份我自定义的JMX 文件,我把一些 xml 节点都 fold 起来了,否则会很长

JMX Test Plan

可以看到这个xml是有一定格式的,他的每一层都只有2个类型的节点

  • Object 节点 - 代表一个 Test Component
  • HashTree - 一个HashTree 的子节点

那我们来看一下org.apache.jorphan.collections.HashTree 这个类的定义

public class HashTree implements Serializable, Map<Object, HashTree>, Cloneable {

private static final long serialVersionUID = 240L;

// Used for the RuntimeException to short-circuit the traversal
private static final String FOUND = "found"; // $NON-NLS-1$

// N.B. The keys can be either JMeterTreeNode or TestElement
protected final Map<Object, HashTree> data; //

/**
* Creates an empty new HashTree.
*/
public HashTree() {
this(null, null);
}
.......

它的核心存储就是一个protected final Map<Object, HashTree> data 的 Map ,然后通过对外提供Map 接口来给上层提供读写的能力。我尝试单步调试了一下,确认ListedHashTree 在内存的结构就是 jmx 文件的映射, ListedHashTreHashTree的子类,实际使用的都是ListedHashMap,它类似于LinkedHashMap ,这里就不展开了

image-debug

Vistor 设计模式

HashTree在数据节点的遍历上还用了一个经典的访问者的设计模式,由于在测试执行中JMeter的Engine需要经常访问 JMX 中的某些节点和子节点,如果把这些方法放在HashTree 本身会使得耦合特别紧密,通过访问者模式可以解耦并提供更好灵活的扩展性, 访问者模式参考这里

HashTreeIterator 定义类访问者的标准接口,类似accept(Object node) 这样的定义,这里主要是addNode的方法,我们看一下HashTreeIterator的定义

public interface HashTreeTraverser {
/**
* The tree traverses itself depth-first, calling addNode for each object it
* encounters as it goes. This is a callback method, and should not be
* called except by a HashTree during traversal.
*
* @param node
* the node currently encountered
* @param subTree
* the HashTree under the node encountered
*/
void addNode(Object node, HashTree subTree);

/**
* Indicates traversal has moved up a step, and the visitor should remove
* the top node from its stack structure. This is a callback method, and
* should not be called except by a HashTree during traversal.
*/
void subtractNode();

/**
* Process path is called when a leaf is reached. If a visitor wishes to
* generate Lists of path elements to each leaf, it should keep a Stack data
* structure of nodes passed to it with addNode, and removing top items for
* every {@link #subtractNode()} call. This is a callback method, and should
* not be called except by a HashTree during traversal.
*/
void processPath();

SearchByClass 是访问者的实现,它主要实现了addNode的方法,并且通过泛型定义了需要访问的Object 的类型,我们看下代码

public class SearchByClass<T> implements HashTreeTraverser {
private final List<T> objectsOfClass = new LinkedList<>();

private final Map<Object, ListedHashTree> subTrees = new HashMap<>();

private final Class<T> searchClass;

/**
* Creates an instance of SearchByClass, and sets the Class to be searched
* for.
*
* @param searchClass
* class to be searched for
*/
public SearchByClass(Class<T> searchClass) {
this.searchClass = searchClass;
}

/**
* After traversing the HashTree, call this method to get a collection of
* the nodes that were found.
*
* @return Collection All found nodes of the requested type
*/
public Collection<T> getSearchResults() { // TODO specify collection type without breaking callers
return objectsOfClass;
}

/**
* Given a specific found node, this method will return the sub tree of that
* node.
*
* @param root
* the node for which the sub tree is requested
* @return HashTree
*/
public HashTree getSubTree(Object root) {
return subTrees.get(root);
}

/** {@inheritDoc} */
@SuppressWarnings("unchecked")
@Override
public void addNode(Object node, HashTree subTree) { // 某一层HashTre 的数据访问
if (searchClass.isAssignableFrom(node.getClass())) {
objectsOfClass.add((T) node);
ListedHashTree tree = new ListedHashTree(node);
tree.set(node, subTree);
subTrees.put(node, tree);
}
}

/** {@inheritDoc} */
@Override
public void subtractNode() {
}

/** {@inheritDoc} */
@Override
public void processPath() {
}
}

我们看一下业务代码怎么使用访问者模式来选择 jmx 中的某个子节点,这里借用核心的StandardJMeterEnginerconfigure函数来说明一下

@Override
public void configure(HashTree testTree) {
// Is testplan serialised?
SearchByClass<TestPlan> testPlan = new SearchByClass<>(TestPlan.class);
testTree.traverse(testPlan);
Object[] plan = testPlan.getSearchResults().toArray();
if (plan.length == 0) {
throw new IllegalStateException("Could not find the TestPlan class!");
}
TestPlan tp = (TestPlan) plan[0];
serialized = tp.isSerialized();
tearDownOnShutdown = tp.isTearDownOnShutdown();
active = true;
test = testTree;
}

目标是要获取 TestPlan 的节点,我们看怎么调用的

  1. 创建SearchByClass对象,传入TestPlan.class 用来构造
  2. 调用 HashTree 资深的 traverse 方法,传入SearchByClass 对象
  3. 通过 testPlan.getSearchResults().toArray(); 获取访问结果

关于HashTree就说到这了,下次我们深入到JMeter 的运行原理去进一步分析

Author: 妞爸爸
Link: http://markshao.github.io/2019/03/06/hashtree/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.