第一次接触Hutool的TreeUtil是在去年重构公司CMS系统时,当时需要处理一个多级分类目录的场景。传统递归算法写了200多行代码,而改用TreeUtil后仅用30行就搞定了,这种效率提升让我印象深刻。但随着业务复杂度增加,我发现官方文档中的基础用法越来越难以满足实际需求。
比如在最近做的供应链管理系统中,遇到两个典型问题:一是部门组织架构需要按照"成立时间+人员规模"复合排序;二是物料分类树需要动态映射数据库中的多语言字段。这些场景暴露了基础用法的三大局限:
这些痛点恰恰是很多中级开发者遇到的典型问题。下面我将通过一个电商后台菜单管理的完整案例,带你突破这些限制。这个案例需要实现:
官方示例中的排序是这样的:
java复制// 传统weight排序
nodeList.add(new TreeNode<>("1", "0", "系统管理", 5));
但在真实场景中,我们可能需要这样排序:
java复制// 需要实现的业务排序逻辑
menuList.sort(Comparator.comparing(Menu::getCreateTime)
.thenComparing(Menu::getClickCount));
通过分析源码发现,TreeUtil内部实际依赖TreeNode的Comparable接口实现。我们可以通过扩展字段+自定义比较器来突破限制:
java复制// 构建节点时注入业务字段
TreeNode<String> node = new TreeNode<>("1", "0", "控制台");
node.setExtra("createTime", 1640966400000L);
node.setExtra("clickCount", 150);
// 自定义比较器
Comparator<TreeNode<String>> comparator = (n1, n2) -> {
Long t1 = (Long)n1.getExtra().get("createTime");
Long t2 = (Long)n2.getExtra().get("createTime");
int timeCompare = t2.compareTo(t1); // 倒序
if(timeCompare != 0) return timeCompare;
Integer c1 = (Integer)n1.getExtra().get("clickCount");
Integer c2 = (Integer)n2.getExtra().get("clickCount");
return c2.compareTo(c1);
};
// 应用自定义排序
List<Tree<String>> trees = TreeUtil.build(nodeList, "0");
trees.sort(comparator);
在实际测试中发现几个易错点:
这是我优化后的安全写法:
java复制Comparator<TreeNode<String>> safeComparator = (n1, n2) -> {
Map<String, Object> e1 = n1.getExtra();
Map<String, Object> e2 = n2.getExtra();
// 带默认值的比较
long t1 = (long)e1.getOrDefault("createTime", 0L);
long t2 = (long)e2.getOrDefault("createTime", 0L);
int timeCompare = Long.compare(t2, t1);
if(timeCompare != 0) return timeCompare;
int c1 = (int)e1.getOrDefault("clickCount", 0);
int c2 = (int)e2.getOrDefault("clickCount", 0);
return Integer.compare(c2, c1);
};
在跨境电商项目中,我们需要处理这样的数据结构:
sql复制CREATE TABLE menu (
id VARCHAR(32),
parent_id VARCHAR(32),
name_en VARCHAR(100),
name_zh VARCHAR(100),
icon_class VARCHAR(50),
sort_order INT
);
通过TreeNodeConfig可以实现字段别名映射:
java复制TreeNodeConfig config = new TreeNodeConfig();
config.setIdKey("id");
config.setParentIdKey("parent_id");
config.setWeightKey("sort_order");
config.setNameKey("name_zh"); // 默认中文名
// 动态名称映射
Function<TreeNode<String>, String> nameMapper = node -> {
String lang = LocaleContextHolder.getLocale().getLanguage();
return "en".equals(lang) ?
(String)node.getExtra("name_en") :
(String)node.getExtra("name_zh");
};
List<Tree<String>> trees = TreeUtil.build(nodeList, "0", config,
(treeNode, tree) -> {
tree.setId(treeNode.getId());
tree.setParentId(treeNode.getParentId());
tree.setWeight(treeNode.getWeight());
tree.setName(nameMapper.apply(treeNode)); // 动态名称
tree.putExtra("icon", treeNode.getExtra("icon_class"));
});
当遇到数据库字段是JSON格式时,可以这样处理:
java复制// 假设extra_info是JSON字段:{"icon":"home","color":"#FF0000"}
tree.putExtra("icon", JsonUtil.getJsonValue(
(String)treeNode.getExtra("extra_info"),
"$.icon"));
当节点超过5000个时,需要特别注意:
java复制// 分页构建示例
int batchSize = 1000;
List<Tree<String>> result = new ArrayList<>();
for (int i = 0; i < total; i += batchSize) {
List<TreeNode<String>> batch = queryNodes(i, batchSize);
result.addAll(TreeUtil.build(batch, "0"));
}
java复制(treeNode, tree) -> {
if (treeNode.getId().equals(treeNode.getParentId())) {
throw new IllegalArgumentException("检测到循环引用: " + treeNode.getId());
}
// ...正常处理
}
java复制Long weight = Convert.toLong(treeNode.getExtra("sort_value"), 0L);
java复制Log.debug("构建前节点数: {}", nodeList.size());
List<Tree<String>> trees = TreeUtil.build(nodeList, "0");
Log.debug("构建后树节点数: {}", TreeUtil.count(trees));
在OA系统中处理部门-人员混合树时,可以这样实现:
java复制// 部门节点
List<TreeNode<String>> deptNodes = queryDepts();
// 人员节点
List<TreeNode<String>> userNodes = queryUsers();
// 统一构建
List<Tree<String>> allTrees = TreeUtil.build(
Stream.concat(deptNodes.stream(), userNodes.stream())
.collect(Collectors.toList()),
"0",
config,
(node, tree) -> {
if ("dept".equals(node.getType())) {
// 部门特有处理
} else {
// 人员特有处理
}
});
推荐使用ResultHandler进行即时转换:
java复制@Select("SELECT id, parent_id, name FROM menu")
@Results({
@Result(property = "id", column = "id"),
@Result(property = "parentId", column = "parent_id")
})
@ResultType(TreeNode.class)
void queryMenuTree(@Param("rootId") String rootId, ResultHandler handler);
// 调用时直接构建
List<Tree<String>> buildMenuTree(String rootId) {
List<TreeNode<String>> nodes = new ArrayList<>();
queryMenuTree(rootId, context -> {
nodes.add((TreeNode<String>)context.getResultObject());
});
return TreeUtil.build(nodes, rootId);
}
在真实项目中,我发现合理使用TreeUtil可以节省约70%的树结构处理代码量,但要注意避免过度依赖工具类导致业务逻辑模糊化。建议将核心业务规则(如排序策略)单独封装,保持工具类只做结构转换。当遇到超深层级树(超过10层)时,可能需要考虑改用图数据库等替代方案。