开发日记:stream入门,树形菜单功能的实现及bug修复

本文档描述了一个关于菜单管理系统的bug,该bug导致某些菜单的子菜单在构建树形结构时丢失。问题源于在过滤子菜单时使用了'=='比较Long类型的catId和parentCid,而不是使用equals()方法。通过修改比较方式,从使用'=='改为使用equals(),成功解决了菜单子菜单丢失的问题。此外,文章还提到了Java对-128到127整数值的缓存特性,这可能导致某些情况下的不一致行为。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、需求:

有一个菜单表,字段catId用于唯一标识每一条记录,每一条记录代表一个菜单项;另有parentCid用于记录该记录的父级菜单,现需为每个菜单绑定他们的子菜单,表结构如下

CREATE TABLE `pms_category` (
  `cat_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '分类id',
  `name` char(50) DEFAULT NULL COMMENT '分类名称',
  `parent_cid` bigint(20) DEFAULT NULL COMMENT '父分类id',
  `cat_level` int(11) DEFAULT NULL COMMENT '层级',
  `show_status` tinyint(4) DEFAULT NULL COMMENT '是否显示[0-不显示,1显示]',
  `sort` int(11) DEFAULT NULL COMMENT '排序',
  `icon` char(255) DEFAULT NULL COMMENT '图标地址',
  `product_unit` char(50) DEFAULT NULL COMMENT '计量单位',
  `product_count` int(11) DEFAULT NULL COMMENT '商品数量',
  PRIMARY KEY (`cat_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1433 DEFAULT CHARSET=utf8mb4 COMMENT='商品三级分类';

2、实现(初版代码有bug,先理解思路再看bug解决)

        1、根据表字段创建实体类:CategoryEntity

        2、为实体类添加子菜单成员变量,每个菜单的子菜单有多个,是一个列表

//mybatis-puls注解,表示这不是一个表字段
@TableField(exist = false)
private List<CategoryEntity> children;

        3、在菜单业务实现类CategoryServiceImpl创建方法用于实现需求,其中baseMapper为父类ServiceImpl中的成员变量,在继承ServiceImpl指定泛型可指定baseMapper的dao层类型,从而执行sql

具体代码:

@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    @Override
    public List<CategoryEntity> listWithTree() {
        //查出所有分类
        List<CategoryEntity>  entities = baseMapper.selectList(null);
        //找出一级菜单及进行树形构建
        List<CategoryEntity> level1Menus = entities.stream().filter(categoryEntity ->
            //筛选出一级菜单,
            categoryEntity.getParentCid() == 0
        ).map(menu -> {
            //为一级菜单递归设置子菜单
            menu.setChildren(getChildren(menu, entities));
            return menu;
        }).sorted((menu1, menu2) ->
            //对一级菜单排序
            (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort())
        ).collect(Collectors.toList());

        return level1Menus;
    }

    /**
     * 递归设置子菜单
     * @param root 父菜单
     * @param all  所有菜单
     * @return
     */
    private List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> all) {
        //递归结束条件为filter结果为空
        List<CategoryEntity> childrenList = all.stream().filter(children ->
                //筛选出root的子菜单
                root.getCatId() == children.getParentCid()
        ).map(menu -> {
                //递归设置子菜单的子菜单
                menu.setChildren(getChildren(menu, all));
                return menu;
        }).sorted((menu1, menu2) ->
                //对root的子菜单排序
                (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort())
        ).collect(Collectors.toList());

        return childrenList;
    }

}

3、列表不全bug修复

1、原因及解决

        在完成树形菜单的代码后,经测试发现某些记录没有被收集,即有些上级菜单丢失了下级菜单,排查结果是在过滤时的条件出现问题了,实体类的catId和parentId都是采用的Long包装类型,使用

'=='比较的是对象的内存地址而非值,故需要进行以下修改,将==改为equals比较:

/**
     * 递归设置子菜单
     * @param root 父菜单
     * @param all  所有菜单
     * @return
     */
    private List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> all) {
        //递归结束条件为filter结果为空
        List<CategoryEntity> childrenList = all.stream().filter(children ->
                //此处修改
                root.getCatId().equals(children.getParentCid())
        ).map(menu -> {
                //递归设置子菜单的子菜单
                menu.setChildren(getChildren(menu, all));
                return menu;
        }).sorted((menu1, menu2) ->
                //对root的子菜单排序
                (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort())
        ).collect(Collectors.toList());

        return childrenList;
    }

排查过程中还发现编号为22的菜单没有丢失下级菜单,是mybatis进行调用了实体类的set方法,采用的是以下赋值方法:

编号为22的菜单:

Long catId = 22L;

编号22的子菜单:

Long parentCid = 22L;

而java对-128到127的值进行了缓存,因此不会出现问题

2、排查过程如下:

1、在测试工程中模拟该上级菜单的子菜单组装过程,发现在过滤时缺失了相关子菜单

@SpringBootTest
class MxshopProductApplicationTests {
    //注入mapper
    @Autowired
    BaseMapper<CategoryEntity> baseMapper;
    @Test
    void contextLoads() {
        //查询出所有数据
        List<CategoryEntity> all = baseMapper.selectList(null);
        //筛选出缺失下级菜单的上级菜单,id为163
        List<CategoryEntity> list = all.stream().filter(categoryEntity -> {
            return categoryEntity.getCatId() == 163;
        }).map(menu -> {
            menu.setChildren(getChildrens(menu, all));
            return menu;
        }).collect(Collectors.toList());

        //查看该菜单的数据
        System.out.println(list.get(0));

    }
    private List<CategoryEntity> getChildrens(CategoryEntity root,List<CategoryEntity> all){

        List<CategoryEntity> children = all.stream().filter(categoryEntity -> {
            return categoryEntity.getParentCid() == root.getCatId();
        }).map(categoryEntity -> {
            //1、找到子菜单
            categoryEntity.setChildren(getChildrens(categoryEntity,all));
            return categoryEntity;
        }).sorted((menu1,menu2)->{
            //2、菜单的排序
            return (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort());

        }).collect(Collectors.toList());

        //查看子菜单数量,上级菜单是2级,子菜单为末级菜单
        System.out.println("================");
        System.out.println(children.size());
        System.out.println("================");
        
        return children;
    }


}

打印结果如下:

1、上级菜单的数据,children列表为空:

CategoryEntity(catId=163, name=全新整车, parentCid=21, catLevel=2, showStatus=1, sort=0, icon=null, productUnit=null, productCount=0, children=[])
2、递归方法中也显示子菜单为空:

2、查询数据库发现有下级菜单,说明菜单丢失

 3、设置对比样例

    //获取编号163的菜单实体
        CategoryEntity categoryEntity = baseMapper.selectById(163);
        System.out.println("========");
        List<CategoryEntity> l1 = all.stream().filter(c -> {
            return c.getParentCid() == 163;
        }).collect(Collectors.toList());

        List<CategoryEntity> l2 = all.stream().filter(c -> {
            return c.getParentCid() == categoryEntity.getCatId();
        }).collect(Collectors.toList());
        System.out.println("l1===" + l1.size());
        System.out.println("l2===" + l2.size());
        System.out.println("=========");

 对比结果:

基本数据类型与对象中的成员变量进行==比对正常,从对象获取成员变量进行==比对异常,考虑是引用类型自动拆箱导致前者正常

 4、查看实体类

package com.mx.mxshop.product.entity;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;

import java.io.Serializable;
import java.util.Date;
import java.util.List;

import lombok.Data;

/**
 * 
 * 
 * @author mx
 * @email mx@gmail.com
 * @date 2021-12-29 19:12:58
 */
@Data
@TableName("pms_category")
public class CategoryEntity implements Serializable {
	private static final long serialVersionUID = 1L;

	/**
	 * 
	 */
	@TableId
	private Long catId;
	/**
	 * 
	 */
	private String name;
	/**
	 * 
	 */
	private Long parentCid;
	/**
	 * 
	 */
	private Integer catLevel;
	/**
	 * 
	 */
	@TableLogic
	private Integer showStatus;
	/**
	 * 
	 */
	private Integer sort;
	/**
	 * ͼ
	 */
	private String icon;
	/**
	 * 
	 */
	private String productUnit;
	/**
	 * 
	 */
	private Integer productCount;

	/**
	 * 子菜单,非表字段
	 */
	@TableField(exist = false)
	private List<CategoryEntity> children;

}

果然是引用类型导致的,问题解决

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值