Unified TreeNode Utility Class for Multi‑Level Menus, Comments, Departments, and Categories in SpringBoot
This article explains how to create a unified SpringBoot utility class that builds hierarchical structures such as multi‑level menus, comments, departments, and categories, discusses database design choices like the optional tree_path field, provides a detailed ITreeNode interface, implementation of TreeNodeUtil with code examples, and demonstrates testing and filtering scenarios.
In this tutorial the author introduces a common problem when implementing multi‑level menus or comments: duplicated code and lack of a unified solution. To address this, a generic tool class is presented that can handle multi‑level menus, comments, departments, and categories in a SpringBoot project.
The article first discusses database field design, focusing on whether to include a tree_path column. It lists the advantages (fast read queries, easy hierarchical queries via fuzzy matching, simple deletion of sub‑trees) and disadvantages (update overhead on inserts, increased storage with deep trees) of using tree_path versus a simple parentId approach.
Next, a unified interface ITreeNode<T> is defined to standardize the required methods for any tree node:
/**
* @Description: Fixed attribute structure
* @Author: yiFei
*/
public interface ITreeNode
{
/** get current element Id */
Object getId();
/** get parent element Id */
Object getParentId();
/** get children list */
List
getChildren();
/** (optional) generate tree_path */
default Object getTreePath() { return ""; }
}The core implementation is the TreeNodeUtil class, which provides several overloaded buildTree methods. These methods accept a list of nodes, optional root ids, a mapping function, and a filter predicate, then recursively construct the hierarchical structure by separating parent and child nodes, linking children to their parents, and handling edge cases such as missing children lists.
/**
* @Description: Tree structure utility class
* @Author: yiFei
*/
public class TreeNodeUtil {
private static final Logger log = LoggerFactory.getLogger(TreeNodeUtil.class);
public static final String PARENT_NAME = "parent";
public static final String CHILDREN_NAME = "children";
public static final List
IDS = Collections.singletonList(0L);
public static
List
buildTree(List
dataList) {
return buildTree(dataList, IDS, (data) -> data, (item) -> true);
}
// ... other overloaded methods omitted for brevity ...
public static
List
buildTree(List
dataList, List
ids, Function
map, Predicate
filter) {
if (CollectionUtils.isEmpty(ids)) {
return Collections.emptyList();
}
Map
> nodeMap = dataList.stream()
.filter(filter)
.collect(Collectors.groupingBy(item -> ids.contains(item.getParentId()) ? PARENT_NAME : CHILDREN_NAME));
List
parent = nodeMap.getOrDefault(PARENT_NAME, Collections.emptyList());
List
children = nodeMap.getOrDefault(CHILDREN_NAME, Collections.emptyList());
if (parent.size() == 0) {
return children;
}
List
nextIds = new ArrayList<>(dataList.size());
List
collectParent = parent.stream().map(map).collect(Collectors.toList());
for (T parentItem : collectParent) {
if (nextIds.size() == children.size()) {
break;
}
children.stream()
.filter(c -> parentItem.getId().equals(c.getParentId()))
.forEach(c -> {
nextIds.add(c.getParentId());
try {
parentItem.getChildren().add(c);
} catch (Exception e) {
log.warn("TreeNodeUtil error: children cannot be null. Initialize it before use.");
}
});
}
buildTree(children, nextIds, map, filter);
return parent;
}
public static
String generateTreePath(Serializable currentId, Function
getById) {
StringBuffer treePath = new StringBuffer();
if (SystemConstants.ROOT_NODE_ID.equals(currentId)) {
treePath.append(currentId);
} else {
T byId = getById.apply(currentId);
if (!ObjectUtils.isEmpty(byId)) {
treePath.append(byId.getTreePath()).append(",").append(byId.getId());
}
}
return treePath.toString();
}
}To demonstrate usage, a concrete class TestChildren implements ITreeNode . Sample data is created, the tree is built with TreeNodeUtil.buildTree , and the resulting JSON structure is printed. Additional examples show how to apply a mapping function to modify node names and a filter predicate to prune specific branches.
// Define a class implementing ITreeNode
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@AllArgsConstructor
public class TestChildren implements ITreeNode
{
private Long id;
private String name;
private String treePath;
private Long parentId;
@TableField(exist = false)
private List
children = new ArrayList<>();
}
// Basic test
public static void main(String[] args) {
List
testChildren = new ArrayList<>();
testChildren.add(new TestChildren(1L, "父元素", "", 0L));
testChildren.add(new TestChildren(2L, "子元素1", "1", 1L));
testChildren.add(new TestChildren(3L, "子元素2", "1", 1L));
testChildren.add(new TestChildren(4L, "子元素2的孙子元素", "1,3", 3L));
testChildren = TreeNodeUtil.buildTree(testChildren);
System.out.println(JSONUtil.toJsonStr(Result.success(testChildren)));
}The article also covers edge cases such as providing incorrect root ids, handling null children lists, and generating the tree_path for a given node. Each scenario is described with expected behavior and sample output.
Finally, the author adds a brief closing remark encouraging readers to share the article and join a community group for further discussion.
Source: juejin.cn/post/7301909270907748378
Java Architect Essentials
Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.