Backend Development 12 min read

Build a Unified SpringBoot Tree Utility for Menus, Comments, and More

This guide explains how to design a database schema with optional tree_path, define a generic ITreeNode interface, implement a versatile TreeNodeUtil class in SpringBoot, and demonstrates comprehensive tests—including building, filtering, and path generation—for reusable multi‑level structures such as menus, comments, departments, and categories.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Build a Unified SpringBoot Tree Utility for Menus, Comments, and More

Introduction

You often see tutorials on implementing multi‑level menus, but they usually suffer from code duplication; the same problem appears when building multi‑level comments. To simplify development and improve maintainability, we create a unified utility class. This article shows how to use SpringBoot to create a tool that returns multi‑level menus, comments, departments, and classifications.

Database Field Design

Typical hierarchical tables contain id and parentId . An optional tree_path field can be added depending on design requirements.

Advantages:

If read operations are frequent and you need fast queries for all children or ancestors, tree_path improves query efficiency.

tree_path can store the hierarchy as a comma‑separated list of IDs, allowing fuzzy matching to retrieve descendants without recursion.

You can delete a subtree by matching the prefix of tree_path , avoiding recursive deletions.

Disadvantages:

Each insert must update tree_path , which may affect performance.

The length of tree_path grows with tree depth, consuming more storage.

Therefore, choose between tree_path and a simple parent ID based on read‑heavy versus write‑heavy scenarios.

Unified Utility Class Implementation

1. Define the ITreeNode interface

<code>
/**
 * @Description Fixed attribute structure
 * @Author yiFei
 */
public interface ITreeNode<T> {
    /** Get current element Id */
    Object getId();
    /** Get parent element Id */
    Object getParentId();
    /** Get children list */
    List<T> getChildren();
    /** (If tree_path is designed, override to generate it) */
    default Object getTreePath() { return ""; }
}
</code>

2. Implement TreeNodeUtil

<code>
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<Object> IDS = Collections.singletonList(0L);

    public static <T extends ITreeNode> List<T> buildTree(List<T> dataList) {
        return buildTree(dataList, IDS, (data) -> data, (item) -> true);
    }

    public static <T extends ITreeNode> List<T> buildTree(List<T> dataList,
            Function<T, T> map) {
        return buildTree(dataList, IDS, map, (item) -> true);
    }

    public static <T extends ITreeNode> List<T> buildTree(List<T> dataList,
            Function<T, T> map, Predicate<T> filter) {
        return buildTree(dataList, IDS, map, filter);
    }

    public static <T extends ITreeNode> List<T> buildTree(List<T> dataList,
            List<Object> ids) {
        return buildTree(dataList, ids, (data) -> data, (item) -> true);
    }

    public static <T extends ITreeNode> List<T> buildTree(List<T> dataList,
            List<Object> ids, Function<T, T> map) {
        return buildTree(dataList, ids, map, (item) -> true);
    }

    /**
     * Build tree from collection (if initial ids are not in dataList, nothing happens)
     *
     * @param dataList data collection
     * @param ids parent Id collection
     * @param map function to transform data
     * @param filter predicate to filter out nodes (false = prune)
     */
    public static <T extends ITreeNode> List<T> buildTree(List<T> dataList,
            List<Object> ids, Function<T, T> map, Predicate<T> filter) {
        if (CollectionUtils.isEmpty(ids)) {
            return Collections.emptyList();
        }
        // 1. Group into parent/children
        Map<String, List<T>> nodeMap = dataList.stream()
                .filter(filter)
                .collect(Collectors.groupingBy(item -> ids.contains(item.getParentId()) ? PARENT_NAME : CHILDREN_NAME));

        List<T> parent = nodeMap.getOrDefault(PARENT_NAME, Collections.emptyList());
        List<T> children = nodeMap.getOrDefault(CHILDREN_NAME, Collections.emptyList());

        if (parent.size() == 0) {
            return children;
        }

        // 2. Store next level ids
        List<Object> nextIds = new ArrayList<>(dataList.size());

        // 3. Process parents
        List<T> collectParent = parent.stream().map(map).collect(Collectors.toList());
        for (T parentItem : collectParent) {
            if (nextIds.size() == children.size()) {
                break;
            }
            children.stream()
                    .filter(childrenItem -> parentItem.getId().equals(childrenItem.getParentId()))
                    .forEach(childrenItem -> {
                        nextIds.add(childrenItem.getParentId());
                        try {
                            parentItem.getChildren().add(childrenItem);
                        } catch (Exception e) {
                            log.warn("TreeNodeUtil error, children cannot be null. Solutions: ...");
                        }
                    });
        }
        buildTree(children, nextIds, map, filter);
        return parent;
    }

    /**
     * Generate treePath string
     *
     * @param currentId current element id
     * @param getById function to fetch element by id
     */
    public static <T extends ITreeNode> String generateTreePath(
            Serializable currentId, Function<Serializable, T> 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();
    }
}
</code>

Testing

Define a class implementing ITreeNode

<code>
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@AllArgsConstructor
public class TestChildren implements ITreeNode<TestChildren> {
    private Long id;
    private String name;
    private String treePath;
    private Long parentId;
    @TableField(exist = false)
    private List<TestChildren> children = new ArrayList<>();
}
</code>

Basic functionality test

<code>
public static void main(String[] args) {
    List<TestChildren> testChildren = new ArrayList<>();
    testChildren.add(new TestChildren(1L, "Parent", "", 0L));
    testChildren.add(new TestChildren(2L, "Child1", "1", 1L));
    testChildren.add(new TestChildren(3L, "Child2", "1", 1L));
    testChildren.add(new TestChildren(4L, "Grandchild", "1,3", 3L));

    testChildren = TreeNodeUtil.buildTree(testChildren);
    System.out.println(JSONUtil.toJsonStr(Result.success(testChildren)));
}
</code>

Filtering and restructuring test

<code>
// Modify name of id 1L and prune node 3L
testChildren = TreeNodeUtil.buildTree(testChildren, (item) -> {
    if (item.getId().equals(1L)) {
        item.setName("Name of Id 1L changed");
    }
    return item;
}, (item) -> item.getId().equals(3L));
</code>

Additional tests cover passing incorrect ids, handling null children, and using generateTreePath to obtain hierarchical paths.

BackendJavaDatabase DesignSpringBootMulti-levelTree Utility
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.