java后端:菜单的生成与读取
概述
在后端开发的过程中,我们经常会遇到一类问题,那就是关于前端页面菜单的展示,一般来说,从数据库中获取到的是一个List集合,而我们需要将这个List集合根据菜单本身的id和它的parentId来把List转换成一个Tree,然后将这个Tree交给前端的框架进行展示。本篇主要针对List集合转换成Tree并遍历输出做一个介绍。
案例
1、首先定义实体类
getter和setter方法请自行定义
public class Menu {
// 菜单id
private String id;
// 菜单名称
private String name;
// 父菜单id
private String parentId;
// 菜单顺序
private int order;
// 子菜单
private List<Menu> childMenus;
}
2、初始化菜单
这里进行了简化,不从数据库里读取,直接人为制造。将数据添加到集合之后用Colletions进行了排序,也是为了模拟从数据库取出时的order by操作。
public class MenuDao {
private static ArrayList<Menu> rootMenu = new ArrayList<>();
static {
Menu menu = new Menu("1", "-一级菜单1", null, 0);
Menu menu1 = new Menu ("2", "-一级菜单2", null, 1);
Menu menu2 = new Menu ("3", "--first二级菜单2", "2", 0);
Menu menu3 = new Menu ("4", "--second二级菜单2", "2", 1);
Menu menu4 = new Menu ("5", "--first二级菜单1", "1", 2);
Menu menu5 = new Menu ("6", "---first三级菜单1", "5", 0);
Menu menu6 = new Menu ("7", "---second三级菜单1", "5", 1);
Menu menu7 = new Menu ("8", "----first四级菜单1", "7", 0);
rootMenu.add(menu);
rootMenu.add(menu1);
rootMenu.add(menu2);
rootMenu.add(menu3);
rootMenu.add(menu4);
rootMenu.add(menu5);
rootMenu.add(menu6);
rootMenu.add(menu7);
Collections.sort(rootMenu, new Comparator<Menu>() {
@Override
public int compare(Menu o1, Menu o2) {
return o1.getOrder() >= o2.getOrder()?1:-1;
}
});
}
public static ArrayList<Menu> queryMenuList(){
return rootMenu;
}
}
这里根据菜单的id和parentId可以还原如下
image.png
需要注意此处有2个根节点,一般在项目中都是只有一个根节点的。
3、菜单的生成
菜单的生成主要包括两个方面
①找出所有根节点
②递归生成每个菜单的子菜单
/**
* 生成树形结构
* @return
*/
private static List<Menu> createListTree(){
// 模拟从数据库取数据
List<Menu> rootMenu = MenuDao.queryMenuList();
List<Menu> menuList = new ArrayList<>();
for (Menu menu : rootMenu) {
// 先找到所有根节点,根节点没有parentId
if(menu.getParentId() == null){
// 以根节点为起点向下递归查找自己的子类
menu = buildChildTree(menu , rootMenu);
menuList.add(menu);
}
}
return menuList;
}
/**
* 递归装载子类
* @param menu 当前节点
* @param rootMenu 菜单列表
* @return
*/
private static Menu buildChildTree(Menu menu, List<Menu> rootMenu) {
// 用来存放自己的子节点
List<Menu> childMenus = new ArrayList<>();
for (Menu menuNode : rootMenu) {
// 如果当前节点的id 与 菜单列表中其他节点的parentId相同,就讲其放入到childMenus中
if(menuNode.getParentId()!=null && menuNode.getParentId().equals(menu.getId())){
// 递归查询子节点的child
childMenus.add(buildChildTree(menuNode,rootMenu));
}
}
// 每一个递归完成,给当前节点设置子节点
menu.setChildMenus(childMenus);
return menu;
}
递归有循环体和结束条件,在buildChildTree方法中结束条件不明显,因为每一次递归都会走到最后一步的return menu,所以再最终结果中所有菜单的子节点都会是[]
而不是null
。而且在这个递归中,其实就是从根节点开始一遍一遍的去跟菜单列表循环,找到菜单列表中parentId等于自己id的部分,但是它不是一次性就找完全部的,它找到子菜单之后,会再去看这个子菜单自己是否还会有子菜单,然后一遍遍套娃,直到它返回当前节点,然后可以继续寻找当前节点的下一个。举个例子,递归就像是看源码,一开始我们再看逻辑,突然看到一个方法,想知道它是怎么实现的,我们就点进去,然后不停的点,当我们又想继续看逻辑时,又必须一步一步的退回来。
4、编写客户端
public class Client {
public static void main(String[] args) {
List<Menu> listTree = createListTree();
for (Menu menu : listTree) {
System.out.println(menu);
}
}
}
Menu{id='1', name='-一级菜单1', parentId='null', order=0, childMenus=[Menu{id='5', name='--first二级菜单1', parentId='1', order=2, childMenus=[Menu{id='6', name='---first三级菜单1', parentId='5', order=0, childMenus=[]}, Menu{id='7', name='---second三级菜单1', parentId='5', order=1, childMenus=[Menu{id='8', name='----first四级菜单1', parentId='7', order=0, childMenus=[]}]}]}]}
Menu{id='2', name='-一级菜单2', parentId='null', order=1, childMenus=[Menu{id='3', name='--first二级菜单2', parentId='2', order=0, childMenus=[]}, Menu{id='4', name='--second二级菜单2', parentId='2', order=1, childMenus=[]}]}
可以看出返回的是两个根节点的部分,虽然看不出来是否的正确的(有条件的可以转化为JSON再去看)。此时还需要写一个输出根节点的方法
/**
* 从唯一根节点开始递归遍历
* @param menu
*/
private static void OutputTreeData(Menu menu){
System.out.println(menu.getName());
if(menu.getChildMenus()==null || menu.getChildMenus().size()==0){
return;
}
for(Menu item : menu.getChildMenus()){
// 子节点如果还有孩子将继续递归
OutputTreeData(item);
}
}
修改客户端代码
public class Client {
public static void main(String[] args) {
List<Menu> listTree = createListTree();
for (Menu menu : listTree) {
OutputTreeData(menu);
}
}
}
最终输出:
-一级菜单1
--first二级菜单1
---first三级菜单1
---second三级菜单1
----first四级菜单1
-一级菜单2
--first二级菜单2
--second二级菜单2