1. 引言
工作中经常会碰到需要建立目录,菜单,角色等相关的树形结构,这种情况下少不了用到递归(recursion),但这种方式很容易造成StackOverflow 即堆栈溢出的问题;
2.java代码实现
- 树形结构关键pojo
@Setter //这里不能用@Data注解,@Data注解包含了(getter/setter、equals()、hashCode()、toString())
@Getter //而Lombok 工具中@Data注解生成hashCode()不适合,会导致StackOverflowError = null的异常
@NoArgsConstructor
@AllArgsConstructor
public class CategoryTree implements Serializable {
private Long id;
private Long pid;
private String name;
private String iconUrl;
private Boolean leaf = false;
private List<CategoryTree> children;
public CategoryTree(Long id,Long pid,String name){
this.id = id;
this.pid = pid;
this.name = name;
}
}
- 下面是分类树中的核心实现方法
package com.smart.service.impl;
import com.smart.pojo.Category;
import com.smart.util.IdWorker;
import com.smart.vo.CategoryTree;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@EnableScheduling
@Service
public class CategoryServiceImpl implements CommandLineRunner {
//注意这里的voltile 关键字
private static volatile CategoryTree categoryTree;
@Autowired
private CategoryMapper categoryMapper;
@Override
public void run(String... args){
log.info("\n------------------------------------------------------\n" +
"\t启动程序后初始化构建分类树\n" +
"\n------------------------------------------------------\n");
buildCategoryTree();
}
@Scheduled(cron = "${catrgoryTree.buildCycle:0 10 0 * * ? }")
private void buildCategoryTree() {
log.info("\n------------------------------------------------------\n" +
"\t开始构建分类关系树构\n" +
"\n------------------------------------------------------\n");
long startTime1 = System.currentTimeMillis();
List<Category> categoryList = this.listAllCategory();
categoryTree = buildCategoryListToTree(categoryList,0L);
log.info("\n------------------------------------------------------\n" +
"\t原理图分类树构建完毕,共构建{}个分类数据,耗时:{} ms\n" +
"\n------------------------------------------------------\n",
categoryList.size(),(System.currentTimeMillis() - startTime1));
}
public CategoryTree getCategory() {
return categoryTree;
}
public CategoryTree buildCategoryListToTree(final List<Category> categoryList, final Long rootId) {
final List<CategoryTree> categoryTrees = new ArrayList<>(categoryList.size());
if (categoryList != null && !categoryList.isEmpty()) {
for (Category category : categoryList) {
categoryTrees.add(bulidCategoryTree(category));
}
}
final CategoryTree root = findCategoryTreeRoots(categoryTrees, rootId);
// 移除根节点
categoryTrees.remove(root);
root.setChildren(findDeptTreeChildren(root, categoryTrees));
return root;
}
//查询出所有的分类表中的数据
public List<Category> listAllCategory() {
return categoryMapper.listAllCategory();
}
//找到分类树中的root节点
public CategoryTree findCategoryTreeRoots(final List<CategoryTree> allNodes, final Long rootId) {
final CategoryTree result = new CategoryTree();
for (CategoryTree node : allNodes) {
if(node.getId().equals(rootId)) {
result.setId(rootId);
result.setPid(node.getPid());
result.setName(node.getName());
}
}
return result;
}
private CategoryTree bulidCategoryTree(final Category category) {
return new CategoryTree(category.getId(),category.getPid(),category.getName());
}
private List<CategoryTree> findDeptTreeChildren(final CategoryTree root, final List<CategoryTree> allNodes) {
final List<CategoryTree> children = new ArrayList<>();
for (CategoryTree comparedOne : allNodes) {
if(null == comparedOne || null == comparedOne.getPid()) {
continue;
}
if (comparedOne.getPid().equals(root.getId())) {
children.add(comparedOne);
}
}
final List<CategoryTree> notChildren = (List<CategoryTree>) CollectionUtils.subtract(allNodes, children);
for (CategoryTree child : children) {
final List<CategoryTree> tmpChildren = findDeptTreeChildren(child, notChildren);
if (tmpChildren == null || tmpChildren.isEmpty()) {
child.setLeaf(true); //设置是否是叶子节点的标志
} else {
child.setLeaf(false);
}
child.setChildren(tmpChildren);
}
return children;
}
}
注意:
- 实现了CommandLineRunner接口,会在springboot项目启动后初始执行一次,并在内存中创建出树形结构
- 其中加入了@EnableScheduling注解,是为了定时去刷新整个树形结构,当然也可以定义接口,在目录发生了变化后来手动刷新,根据实际情况来选择;
- 以上的两点根据情况结合自己项目实际来决定,下面简单讲一下关键要点;
- volatile
//注意这里的voltile 关键字
private static volatile CategoryTree categoryTree;
- CollectionUtils.subtract方法
这里导入的是 import org.apache.commons.collections4.CollectionUtils;
作用是从大的集合中减去小的集合,在这里是为了除去已经加入树形结构的数据,将剩余的节点继续进行递归
final List<CategoryTree> notChildren = (List<CategoryTree>) CollectionUtils.subtract(allNodes, children);
- 叶子节点的标志
child.setLeaf(true); //设置是否是叶子节点的标志
3. 实际使用过程出现的问题
- 原本我在核心的pojo类中还加入了一个private CategoryTree parent;(箭头所指的地方);
原本考虑的是方便子节点去向上直接找到父节点,但这是不行的,会报StackOverflow=null的异常,因为这里子节点通过指针指向父节点,再加一个父节点指向子节点的指针,我个人的理解是会陷入一种循环中,对象的判断是通过hashCode方法区分的,实际也不需要加入这个父节点,通过父id去找具体节点也一样。
@Setter //这里不能用@Data注解,@Data注解包含了(getter/setter、equals()、hashCode()、toString())
@Getter //而Lombok 工具中@Data注解生成hashCode()不适合,会导致StackOverflowError = null的异常
@NoArgsConstructor
@AllArgsConstructor
public class CategoryTree implements Serializable {
private Long id;
private Long pid;
private String name;
private String iconUrl;
private Boolean leaf = false;
private CategoryTree parent; <-------------------------------------------
private List<CategoryTree> children;
public CategoryTree(Long id,Long pid,String name){
this.id = id;
this.pid = pid;
this.name = name;
}
}
- 通过getCategory()方法获取到tree,实际使用中会围绕这个tree做很多节点的增删查改的操作,具体大家根据实际情况自行拓展;
- 递归很容易造成StackOverflow问题,那么有没有什么方式将递归方式转化为非递归方法,答案是肯定的,这里我贴上我找到的一个介绍得很清楚的帖子漫谈递归转非递归
4.结尾
最后以一个递归美图结束这无休止的文章编写!