需求场景

有下面一张菜单表,典型的树形结构设计

java 树状图展示_子节点


现前端需要后端返回树形数据结构用于构造展示树。

代码实战

  1. 首先我们根据数据库结构创建实体对象
@Data
@TableName("tb_menu")
public class Menu extends BaseEntity {
    private static final long serialVersionUID = 1L;

    /**
     * 菜单ID
     */
    @TableId
    private Long menuId;

    /**
     * 菜单名称
     */
    private String menuName;

    /**
     * 父菜单名称
     */
    @TableField(exist = false)
    private String parentName;

    /**
     * 父菜单ID
     */
    private Long parentId;

    /**
     * 显示顺序
     */
    private String orderNum;

    /**
     * 路由地址
     */
    private String path;

    /**
     * 组件路径
     */
    private String component;

    /**
     * 是否为外链(0是 1否)
     */
    private String isFrame;

    /**
     * 是否缓存(0缓存 1不缓存)
     */
    private String isCache;

    /**
     * 类型(M目录 C菜单 F按钮)
     */
    private String menuType;

    /**
     * 显示状态(0显示 1隐藏)
     */
    private String visible;

    /**
     * 菜单状态(0显示 1隐藏)
     */
    private String status;

    /**
     * 权限字符串
     */
    private String perms;

    /**
     * 菜单图标
     */
    private String icon;

    /**
     * 子菜单
     */
    @TableField(exist = false)
    private List<Menu> children = new ArrayList<Menu>();

最重要的还是id、parentId、children属性

最通用的写法(递归)

这种写法毫无节操可言,全部通过数据库递归查询。

  1. 首先查到根节点,parent_id = 0
  2. 通过根节点id获取到所有一级节点,parent_id = 1
  3. 递归获取所有节点的子节点,然后调用setChildren()方法组装数据结构。
    好多项目都是用这种方法。

1、递归:

/**
 * 根据父节点的id获取所有子节点
 *
 * @param menus
 * @return
 */
private List<Menu> getChildPerm(List<Menu> menus, int parentId) {
    List<Menu> menuList = new ArrayList<>();
    //当parentId = 0时,是根节点;
    // 获取根节点下的子节点
    for (Menu menu : menus) {
        if (menu.getParentId() == parentId) {
            Menu menuData = this.recursionTree(menus, menu);
            menuList.add(menuData);
        }

    }
    return menuList;
}
```
/**
 * 递归树结构
 *
 * @param collect
 */
private Menu recursionTree(List<Menu> collect, Menu menu) {
    List<Menu> childList = this.getChildList(collect, menu);
    menu.setChildren(childList);
    for (Menu col : childList) {
        if (hasChild(collect, col)) {
            recursionTree(collect, col);
        }
    }
    return menu;
}
**
 * 得到子节点列表
 */
private List<Menu> getChildList(List<Menu> list, Menu t) {
    List<Menu> tlist = new ArrayList<Menu>();
    Iterator<Menu> it = list.iterator();
    while (it.hasNext()) {
        Menu n = (Menu) it.next();
        if (n.getParentId().longValue() == t.getMenuId().longValue()) {
            tlist.add(n);
        }
    }
    return tlist;
}

这样比较啰嗦,性能不是很好,逻辑也不是很清楚。

2、双重循环:

这种写法比较简单,也是比较容易想到的。通过双重循环确定父子节点的关系。

/**
 * 获取菜单的树状结构
 *
 * @return
 */
private List<Menu> menuTrees() {
    List<Menu> list = new ArrayList<>();
    List<Menu> menus = menuMapper.selectList(null);
    for (Menu m : menus) {
        //获取根节点
        if (m.getParentId().equals(0L)) {
            list.add(m);
        }
        for (Menu child : menus) {
            if (child.getParentId().equals(m.getMenuId())) {
                m.addChild(child);
            }
        }
    }
    return list;
}
//菜单的实体类中加addChild方法
public void addChild(Menu menu) {
    if (children == null) {
        children = new ArrayList<>();
    }
    children.add(menu);
}

3、双重遍历

第一次遍历借助hashmap存储父节点与子节点的关系,第二次遍历设置子节点,由于map中已经维护好了对应关系所以只需要从map取即可。

/**
 * 双重查询
 *
 * @return
 */
private List<Menu> menuList() {
    Map<Long, List<Menu>> menuMap = new HashMap<>();
    List<Menu> menuList = menuMapper.selectList(null);
    menuList.forEach(menu -> {
        List<Menu> children = menuMap.getOrDefault(menu.getParentId(), new ArrayList<>());
        children.add(menu);
        menuMap.put(menu.getParentId(), children);
    });

    menuList.forEach(menu -> menu.setChildren(menuMap.get(menu.getMenuId())));

    List<Menu> result = menuList.stream().filter(v -> v.getParentId().equals("0")).collect(Collectors.toList());
    return result;
}

4、Stream分组

/**
 * Stream分组
 */
@Override
public List<Menu> selectMenuTree() {
    List<Menu> menus = menuMapper.selectList(null);
    //操作所有菜单数据
    Map<Long, List<Menu>> groupMap = menus.stream().collect(Collectors.groupingBy(Menu::getParentId));
    menus.forEach(menu -> {
        menu.setChildren(groupMap.get(menu.getMenuId()));
    });
    List<Menu> collect = menus.stream().filter(menu -> menu.getParentId().equals(0L)).collect(Collectors.toList());
    return collect;
}

此方法主要通过Collectors.groupingBy(Menu::getParentId)方法对menus按照parentId进行分组,分组后父节点相同的都放一起了。

然后再循环menus,给其设置children属性。

执行完成后已经形成了多颗树,最后我们再通过filter()方法挑选出根节点的那颗树即可。

请求后返回的数据集如下:

"code": 200,
  "data": [
    {
      "searchValue": null,
      "createBy": "admin",
      "createTime": "2021-03-10 16:30:56",
      "updateBy": "",
      "updateTime": null,
      "remark": "系统管理目录",
      "params": null,
      "menuId": 1,
      "menuName": "系统管理",
      "parentName": null,
      "parentId": 0,
      "orderNum": "1",
      "path": "system",
      "component": null,
      "isFrame": "1",
      "isCache": "0",
      "menuType": "M",
      "visible": "0",
      "status": "0",
      "perms": "",
      "icon": "system",
      "children": [
        {
          "searchValue": null,
          "createBy": "admin",
          "createTime": "2021-03-10 16:30:56",
          "updateBy": "",
          "updateTime": null,
          "remark": "用户管理菜单",
          "params": null,
          "menuId": 100,
          "menuName": "用户管理",
          "parentName": null,
          "parentId": 1,
          "orderNum": "1",
          "path": "user",
          "component": "system/user/index",
          "isFrame": "1",
          "isCache": "0",
          "menuType": "C",
          "visible": "0",
          "status": "0",
          "perms": "system:user:list",
          "icon": "user",
          "children": [
            {
              "searchValue": null,
              "createBy": "admin",
              "createTime": "2021-03-10 16:31:00",
              "updateBy": "",
              "updateTime": null,
              "remark": "",
              "params": null,
              "menuId": 1001,
              "menuName": "用户查询",
              "parentName": null,
              "parentId": 100,
              "orderNum": "1",
              "path": "",
              "component": "",
              "isFrame": "1",
              "isCache": "0",
              "menuType": "F",
              "visible": "0",
              "status": "0",
              "perms": "system:user:query",
              "icon": "#",
              "children": null
            },
            {
              "searchValue": null,
              "createBy": "admin",
              "createTime": "2021-03-10 16:31:00",
              "updateBy": "",
              "updateTime": null,
              "remark": "",
              "params": null,
              "menuId": 1002,
              "menuName": "用户新增",
              "parentName": null,
              "parentId": 100,
              "orderNum": "2",
              "path": "",
              "component": "",
              "isFrame": "1",
              "isCache": "0",
              "menuType": "F",
              "visible": "0",
              "status": "0",
              "perms": "system:user:add",
              "icon": "#",
              "children": null
            },
            {
              "searchValue": null,
              "createBy": "admin",
              "createTime": "2021-03-10 16:31:01",
              "updateBy": "",
              "updateTime": null,
              "remark": "",
              "params": null,
              "menuId": 1003,
              "menuName": "用户修改",
              "parentName": null,
              "parentId": 100,
              "orderNum": "3",
              "path": "",
              "component": "",
              "isFrame": "1",
              "isCache": "0",
              "menuType": "F",
              "visible": "0",
              "status": "0",
              "perms": "system:user:edit",
              "icon": "#",
              "children": null
            }
          ]
        }
      ]
    }
  ]
}