Java小技能:多级菜单排序并返回树结构菜单列表

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 需求: 菜单管理(服务商角色配置权限管理)、文章分类、MCC类目、区域信息。

引言

需求: 菜单管理(服务商角色配置权限管理)、文章分类、MCC类目、区域信息。

地区和菜单数据添加到Redis缓存

框架: SpringBoot+MybatisPlus 对数据表中的菜单进行排序并返回树形Json格式的菜单列表

实现思路:先获取全部菜单,然后再对菜单进行装配,生成树形结构。

先获取一级菜单,再递归获取子节点
{
  "data": [
    {
      "id": "1595742481192857601",
      "createTime": "2022-11-24 19:33:50",
      "createId": "1",
      "updateId": "1",
      "updateTime": "2022-11-24 19:33:50",
      "code": "1",
      "name": "string",
      "parentId": null,
      "url": "string",
      "isExpand": "0",
      "isShow": "0",
      "type": "1",
      "typeText": "菜单",
      "sortNum": "0",
      "tagsType": "PT",
      "tagsTypeText": "平台",
      "remark": "string",
      "children": []
    },
    {
      "id": "1595742537635606529",
      "createTime": "2022-11-24 19:34:03",
      "createId": "1",
      "updateId": "1",
      "updateTime": "2022-11-24 19:34:03",
      "code": "2",
      "name": "string",
      "parentId": null,
      "url": "string",
      "isExpand": "0",
      "isShow": "0",
      "type": "1",
      "typeText": "菜单",
      "sortNum": "0",
      "tagsType": "PT",
      "tagsTypeText": "平台",
      "remark": "string",
      "children": []
    },
    {
      "id": "1595742587770122242",
      "createTime": "2022-11-24 19:34:15",
      "createId": "1",
      "updateId": "1",
      "updateTime": "2022-11-24 19:34:15",
      "code": "3",
      "name": "string",
      "parentId": null,
      "url": "string",
      "isExpand": "0",
      "isShow": "0",
      "type": "1",
      "typeText": "菜单",
      "sortNum": "0",
      "tagsType": "PT",
      "tagsTypeText": "平台",
      "remark": "string",
      "children": [
        {
          "id": "1595742715377627138",
          "createTime": "2022-11-24 19:34:45",
          "createId": "1",
          "updateId": "1",
          "updateTime": "2022-11-24 19:34:45",
          "code": "4",
          "name": "string",
          "parentId": "1595742587770122242",
          "url": "string",
          "isExpand": "0",
          "isShow": "0",
          "type": "1",
          "typeText": "菜单",
          "sortNum": "0",
          "tagsType": "PT",
          "tagsTypeText": "平台",
          "remark": "string",
          "children": []
        },
        {
          "id": "1595742770520141826",
          "createTime": "2022-11-24 19:34:59",
          "createId": "1",
          "updateId": "1",
          "updateTime": "2022-11-24 19:34:59",
          "code": "5",
          "name": "string",
          "parentId": "1595742587770122242",
          "url": "string",
          "isExpand": "0",
          "isShow": "0",
          "type": "1",
          "typeText": "菜单",
          "sortNum": "0",
          "tagsType": "PT",
          "tagsTypeText": "平台",
          "remark": "string",
          "children": [
            {
              "id": "1595742832834916354",
              "createTime": "2022-11-24 19:35:13",
              "createId": "1",
              "updateId": "1",
              "updateTime": "2022-11-24 19:35:13",
              "code": "6",
              "name": "string",
              "parentId": "1595742770520141826",
              "url": "string",
              "isExpand": "0",
              "isShow": "0",
              "type": "1",
              "typeText": "菜单",
              "sortNum": "0",
              "tagsType": "PT",
              "tagsTypeText": "平台",
              "remark": "string",
              "children": []
            },
            {
              "id": "1595742854490107906",
              "createTime": "2022-11-24 19:35:19",
              "createId": "1",
              "updateId": "1",
              "updateTime": "2022-11-24 19:35:19",
              "code": "7",
              "name": "string",
              "parentId": "1595742770520141826",
              "url": "string",
              "isExpand": "0",
              "isShow": "0",
              "type": "1",
              "typeText": "菜单",
              "sortNum": "0",
              "tagsType": "PT",
              "tagsTypeText": "平台",
              "remark": "string",
              "children": []
            }
          ]
        }
      ]
    }
  ]

I 序列化:生成树形结构菜单列表

1.1 读取表数据,获取全部菜单

    /**
     * 先获取全部菜单,然后再对菜单进行装配,生成树形结构
     */

        List<TSysMenu> list = tSysMenuMapper.selectList(queryWrapper);

        List<SysMenuDto> listDto = getSortMenus(list);

1.2 序列化:生成树形结构菜单列表

获取一级菜单,递归获取子节点。

    @Override
    public List<SysMenuDto> getSortMenus(List<TSysMenu> sourceList) throws Exception {
        if (sourceList.size() < 1) {
            return null;
        }
        List<SysMenuDto> dtos = sourceList.stream().map(ele -> {
            SysMenuDto dto = new SysMenuDto();
            BeanUtils.copyProperties(ele, dto);
            return dto;
        }).collect(Collectors.toList());
        // 获取第一层SysMenuDto: 筛选parentId为空的   或者0的情况: item.getParentId().equals("0")
        return getChild(null,dtos);
    }

    /**
     * 递归设置节点
     *
     * @param id
     * @param allMenu
     * @return
     */
    private List<SysMenuDto> getChild(Long parentId, List<SysMenuDto> allMenu) {
        //item.getParentId().equals(id) 会出现空指针的情况
        return allMenu.stream()
                .filter(item -> {
                    if (parentId == null) {
                        return item.getParentId() == null;
                    } else {
                        return item.getParentId() != null && item.getParentId().equals(parentId);//集合filter过滤Integer数值为空问题解决方案:使用equal取代==判断。
                    }
                })
                .map(item -> item.setChildren(getChild(item.getId(), allMenu)))
                .sorted(Comparator.comparingInt(menu -> (menu.getSortNum() == null ? 0 : menu.getSortNum())))
                .collect(Collectors.toList());
    }

1.3 实体

  • 单表查询不用添加事务注解` @Transactional

`

  • 新生成实体时,加一下fill,时间和用户ID自动填充。

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("t_sys_menu")
@ApiModel(value="TSysMenu对象", description="系统菜单表")
public class SysMenu extends Model<SysMenu> {
    @ApiModelProperty(value = "主键id")
    @TableId("id")
    private Long id;
        @ApiModelProperty(value = "菜单父级id")
    @TableField("parent_id")
    private Long parentId;
        @ApiModelProperty(value = "排序")
    @TableField("sort_num")
    private Integer sortNum;
        @Override
    protected Serializable pkVal() {
        return this.id;
    }

}

1.4 Dto

@Data
@Accessors(chain = true)//链式访问
public class SysMenuDto extends SysMenu {

    @ApiModelProperty(value = "子菜单")
    private List<SysMenuDto> children;
}

II 查询优化

2.1 分页查询树结构菜单列表

  1. PageHelper直接对List进行分页
    /*
     * PageHelper直接对List进行分页
     */
    private PageInfo getPageInfoByList(List all,Integer pageNum,Integer pageSize) {
        //创建Page类
        Page page = new Page(pageNum, pageSize);
//为Page类中的total属性赋值
        int total = all.size();
        page.setTotal(total);
//计算当前需要显示的数据下标起始值
        int startIndex = (pageNum - 1) * pageSize;
        int endIndex = Math.min(startIndex + pageSize,total);
//从链表中截取需要显示的子链表,并加入到Page
        page.addAll(all.subList(startIndex,endIndex));
//以Page创建PageInfo
        PageInfo pageInfo = new PageInfo<>(page);
        return pageInfo;
    }
  1. PageHelper.startPage 开启分页,通过拦截MySQL的方式,把你的查询语句拦截下来加limit.将查询语句放到PageHelper.startPage后面进行执行
        PageHelper.startPage(input.getPageNum(), input.getPageSize());
         List<TSysTaoCollege> collegeList = tSysTaoCollegeService.list(lambda);
         PageInfo pageInfo = new PageInfo(collegeList);
         PageHelper.clearPage();

2.2 地区和菜单数据添加到Redis缓存

使用StringRedisTemplate

  1. 写入Redis时,手动把对象序列化为json格式字符串。
  2. 读取Redis时,手动把读取到的JSON反序列化成对象。
    /*
      先获取全部菜单,然后再对菜单进行装配,生成树形结构
     */
    public List<SysMenuDto> getMenus(ETagsType tagsType, Boolean isReadDb) throws Exception {
        List<TSysMenu> list = getTmenus(tagsType,isReadDb);//读取表数据
        List<SysMenuDto> listDto = getSortMenus(list);//序列化数据
        return listDto;
    }

    public List<TSysMenu> getTmenus(ETagsType tagsType, Boolean isReadDb) throws Exception{
        if (tagsType == null) {
            throw CommonException.create(ServerResponse.createByError("菜单对象类型不能为空"));
        }
        //rediskey名:rediskey常量类+菜单对象类型
        String keyName = RedisKeyConstant.MENU + "." + tagsType.getCode();//
        QueryWrapper<TSysMenu> queryWrapper = new QueryWrapper<>();
        //是否读取数据库的菜单,true读取,false不读取
        if (isReadDb) {
            queryWrapper.eq("tags_type", tagsType);
            List<TSysMenu> list = tSysMenuMapper.selectList(queryWrapper);
            stringRedisTemplate.opsForValue().set(keyName, JSONUtil.toJsonStr(list));//写入数据到缓存
            return list;
        } else {
            //读取缓存
            if (!stringRedisTemplate.hasKey(keyName)) {
                queryWrapper.eq("tags_type", tagsType);
                List<TSysMenu> list = tSysMenuMapper.selectList(queryWrapper);
                stringRedisTemplate.opsForValue().set(keyName, JSONUtil.toJsonStr(list));//写入数据到缓存
                return list;
            } else {
                //直接读取缓存数据进行序列化
                String jsonStr = stringRedisTemplate.opsForValue().get(keyName);
                List<TSysMenu> list = JSONUtil.toList(jsonStr, TSysMenu.class);
                return list;
            }
        }
    }

III 排序

3.1 新增节点的默认排序

/*
    用于新增时的排序字段
    */
    private void setSortNum4create(TSysMenu menu){
        LambdaQueryWrapper<TSysMenu> queryWrapper = new LambdaQueryWrapper<>();
        if(menu.getParentId() == null){
            queryWrapper.isNull(TSysMenu::getParentId);
        }else{
            queryWrapper.eq(null != menu.getParentId(),TSysMenu::getParentId,menu.getParentId());
        }
        queryWrapper.orderByDesc(TSysMenu::getSortNum);
        var sortCate = tSysMenuMapper.selectOne(queryWrapper.last("limit 1"));
        if (sortCate!=null)
        {
            menu.setSortNum(sortCate.getSortNum()+1);
        }else{
            menu.setSortNum(0);
        }
    }

3.2 对分类进行上移和下移排序

查询最近的兄弟节点进行交换

/*
    对分类进行排序
     */
    public TSysCollegeCategory sortCategory(Long id,Integer type) throws Exception {
        if(id ==null){
            throw CommonException.create(ServerResponse.createByError("id不存在"));
        }
        var current = tSysCollegeCategoryMapper.selectById(id);
        if(current ==null){
            throw CommonException.create(ServerResponse.createByError("分类不存在"));
        }
        LambdaQueryWrapper<TSysCollegeCategory> queryWrapper = new LambdaQueryWrapper<>();
        // 检查ParentId为空的情况。
        if(current.getParentId() == null){
            queryWrapper.isNull(TSysCollegeCategory::getParentId);
        }else{
            queryWrapper.eq(null != current.getParentId(),TSysCollegeCategory::getParentId,current.getParentId());
        }
        if(type ==1){//上移
            queryWrapper.lt(TSysCollegeCategory::getSortNum,current.getSortNum())
                    .orderByAsc(TSysCollegeCategory::getSortNum).last("limit 1");
        }else if(type ==2){//下移
            queryWrapper.gt(TSysCollegeCategory::getSortNum,current.getSortNum())
                    .orderByAsc(TSysCollegeCategory::getSortNum).last("limit 1");
        }else{
            throw CommonException.create(ServerResponse.createByError("type类型错误"));
        }
        var next = tSyCollegeCategoryMapper.selectOne(queryWrapper);
        if(next !=null)
        {
            var sort = current.getSortNum();
            current.setSortNum(next.getSortNum());
            next.setSortNum(sort);
            tSysCollegeCategoryMapper.updateById(current);
            tSysCollegeCategoryMapper.updateById(next);
        }
        return current;
    }

IV 常见问题

预备知识:序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。

4.1 no instance(s) of type variable(s) R exist so that void conforms to R,

实体类上标注有 @Accessors(chain = true)//链式访问

                .map(item -> item.setChildren(getChild(item.getId(), dtos)))

4.2 MybatisPlus QueryWrapper的null查询

        if(category.getParentId() == null){
        queryWrapper.isNull("parent_id");
        }else{
            queryWrapper
                    .eq("parent_id", category.getParentId());
        }

4.3 集合filter过滤Integer数值为空问题解决方案

    /**
     * @param parentId 传递的父id  用来过滤用 ,可以为空
     * @return
     * @throws Exception
     */
    private List<SysMenuDto> getChilds(Long parentId, List<SysMenuDto> sourceList) {

        List<SysMenuDto> menus = sourceList.stream()
                .filter(menu -> {
                    if (parentId == null) {
                        return menu.getParentId() == null;
                    } else {
                        return menu.getParentId() != null && menu.getParentId().equals(parentId);
                    }
                }).collect(Collectors.toList());
        //排序 0表示最前面
        return menus;
    }
目录
相关文章
|
2月前
|
安全 Java 数据库连接
2025 年最新 Java 学习路线图含实操指南助你高效入门 Java 编程掌握核心技能
2025年最新Java学习路线图,涵盖基础环境搭建、核心特性(如密封类、虚拟线程)、模块化开发、响应式编程、主流框架(Spring Boot 3、Spring Security 6)、数据库操作(JPA + Hibernate 6)及微服务实战,助你掌握企业级开发技能。
281 3
|
3月前
|
监控 Java API
Java语言按文件创建日期排序及获取最新文件的技术
这段代码实现了文件创建时间的读取、文件列表的获取与排序以及获取最新文件的需求。它具备良好的效率和可读性,对于绝大多数处理文件属性相关的需求来说足够健壮。在实际应用中,根据具体情况,可能还需要进一步处理如访问权限不足、文件系统不支持某些属性等边界情况。
206 14
|
7月前
|
人工智能 Java
Java 中数组Array和列表List的转换
本文介绍了数组与列表之间的相互转换方法,主要包括三部分:1)使用`Collections.addAll()`方法将数组转为列表,适用于引用类型,效率较高;2)通过`new ArrayList&lt;&gt;()`构造器结合`Arrays.asList()`实现类似功能;3)利用JDK8的`Stream`流式计算,支持基本数据类型数组的转换。此外,还详细讲解了列表转数组的方法,如借助`Stream`实现不同类型数组间的转换,并附带代码示例与执行结果,帮助读者深入理解两种数据结构的互转技巧。
374 1
Java 中数组Array和列表List的转换
|
7月前
|
人工智能 JSON Java
列表结构与树结构转换分析与工具类封装(java版)
本文介绍了将线性列表转换为树形结构的实现方法及工具类封装。核心思路是先获取所有根节点,将其余节点作为子节点,通过递归构建每个根节点的子节点。关键在于节点需包含 `id`、`parentId` 和 `children` 三个属性。文中提供了两种封装方式:一是基于基类 `BaseTree` 的通用工具类,二是使用函数式接口实现更灵活的方式。推荐使用后者,因其避免了继承限制,更具扩展性。代码示例中使用了 Jackson 库进行 JSON 格式化输出,便于结果展示。最后总结指出,理解原理是进一步优化和封装的基础。
205 0
|
9月前
|
Java 程序员
Java 排序神器:Comparable 和 Comparator 该怎么选?
嗨,大家好,我是小米!今天和大家聊一聊Java社招面试中常考的经典问题——Comparable和Comparator的区别。Comparable定义对象的自然排序,适用于单一固定的排序规则;Comparator则是策略接口,用于定义自定义排序规则,适用于多样化或多变的排序需求。掌握这两者的区别是理解Java排序机制的基础,也是面试中的加分题。结合实际项目场景深入探讨它们的应用,能更好地打动面试官。如果你觉得有帮助,欢迎点赞、收藏、分享,期待你的一键三连!我们下期见~ 我是小米,一个喜欢分享技术的程序员,关注我的微信公众号“软件求生”,获取更多技术干货!
113 20
|
12月前
|
缓存 前端开发 JavaScript
9大高性能优化经验总结,Java高级岗必备技能,强烈建议收藏
关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。本文介绍了9种性能优化方法,涵盖代码优化、数据库优化、连接池调优、架构层面优化、分布式缓存、异步化、Web前端优化、服务化、硬件升级、搜索引擎和产品逻辑优化。欢迎留言交流。
|
12月前
|
存储 搜索推荐 算法
【用Java学习数据结构系列】七大排序要悄咪咪的学(直接插入,希尔,归并,选择,堆排,冒泡,快排)以及计数排序(非比较排序)
【用Java学习数据结构系列】七大排序要悄咪咪的学(直接插入,希尔,归并,选择,堆排,冒泡,快排)以及计数排序(非比较排序)
115 1
|
11月前
|
存储 Java
在Java编程的世界里,标识符命名是一项基础且至关重要的技能
在Java编程的世界里,标识符命名是一项基础且至关重要的技能
75 0
|
存储 Java API
【Java高手必备】揭秘!如何优雅地对List进行排序?掌握这几种技巧,让你的代码瞬间高大上!
【8月更文挑战第23天】本文深入探讨了Java中对List集合进行排序的各种方法,包括使用Collections.sort()、自定义Comparator以及Java 8的Stream API。通过示例代码展示了不同情况下如何选择合适的方法:从简单的整数排序到自定义类对象的排序,再到利用Comparator指定特殊排序规则,最后介绍了Stream API在排序操作中的简洁应用。理解这些技术的区别与应用场景有助于提高编程效率。
561 4