Java实现多级菜单编写

本文介绍了如何在Java环境下,结合SpringBoot和Mybatis-Plus实现多级菜单功能,包括树状结构查询、单级分类操作、批量操作、同级目录中的上下移动以及新增分类。详细讨论了实体类、BO类、Mapper接口和Controller的实现,并提供了SQL语句和代码示例。

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

多级菜单的编写,对于大多数Java开发来讲,这都是一个再平常不过的功能了,由于原来业务性质原因,并没有做过,为此自己也通过查阅博客,完成了该功能,慢慢进步学习。记录一下,方便日后查阅,如果有博友发现有更好的功能,欢迎留言给我,我也学学。

  • 编写的多级分类的主要功能有:
  1. 树状结构查询所有
  2. 查询单极分类
  3. 批量操作(只写了批量显示,其它的同理)
  4. 同级目录中上下移动操作
  5. 新增分类
  • 说明一下,由于不是单独工程,我就不单独放maven坐标了,springBoot+Mybatis-Plus为主题,另外使用了开源工具类,HuTool。还有常用的swagger和lombok。
SQL语句
-- ----------------------------
-- Table structure for menu
-- ----------------------------
DROP TABLE IF EXISTS `menu`;
CREATE TABLE `menu`  (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
  `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_czech_ci NULL DEFAULT NULL COMMENT '菜单名称',
  `status` smallint(2) NULL DEFAULT NULL COMMENT '是否显示(1:显示,0:隐藏)',
  `level` smallint(10) NULL DEFAULT NULL COMMENT '菜单级别',
  `parentId` int(11) NULL DEFAULT NULL COMMENT '父ID,一级菜单为0',
  `sort` smallint(10) NULL DEFAULT NULL COMMENT '排序',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 16 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_czech_ci ROW_FORMAT = Compact;

实体类(我是直接使用插件生成的)
package cn.zxw.entity;

import java.io.Serializable;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

/**
 * menu
 * @author 
 */
@Data
@TableName("menu")
public class Menu implements Serializable {
    /**
     * 菜单ID
     */
    @TableId(type = IdType.AUTO)
    private Integer id;

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

    /**
     * 是否显示(1:显示,0:隐藏)
     */
    private Short status;

    /**
     * 菜单级别
     */
    private Short level;

    /**
     * 父ID,一级菜单为0
     */
    private Integer parentid;

    /**
     * 排序 值越大越排前
     */
    private Short sort;

    private static final long serialVersionUID = 1L;
}
BO类(一共三个)
package cn.zxw.param;

import cn.zxw.entity.Menu;
import lombok.Data;

import java.util.List;

/**
 * @author 华安小书童
 * @description 分类返回结果
 * @data: 2021/1/30 15:08
 */
@Data
public class MenuVo extends Menu {

    /**
     * 存放子级菜单数据
     */
    private List<MenuVo> children;
}
--------------------------------------------
package cn.zxw.param;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.util.List;

/**
 * @author 华安小书童
 * @description 菜单批量操作
 * @data: 2021/1/31 11:23
 */
@Data
@ApiModel("菜单批量操作")
public class MenuBatch {

    @ApiModelProperty("id集合")
    private List<Integer> ids;

    @ApiModelProperty("0:隐藏;1:显示")
    private Short isShow;

    @ApiModelProperty("0:否;1:是")
    private Short isDelete;
}
-----------------------------------------
package cn.zxw.param;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

/**
 * @author 华安小书童
 * @description 分类移动前台传递参数
 * @data: 2021/1/30 20:15
 */
@Data
@ApiModel("分类移动前台传递参数")
public class MobileBo {

    @ApiModelProperty("需要操作当前分级的ID")
    private Integer id;

    /**
     * 传递父id,是为了操作只是限于本级别的操作,新增时也会区分
     */
    @ApiModelProperty("需要操作当前分级的父ID,一级分类传0")
    private Integer parentId;

    @ApiModelProperty("0:上移;1:下移")
    private Integer mobile;

    @ApiModelProperty("当前级别的排序值")
    private Integer sort;
}
Mapper接口
package zxw.mapper;


import cn.zxw.entity.Menu;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;
import java.util.Map;

public interface MenuMapper extends BaseMapper<Menu> {

    /**
     * 新增
     * @param record
     * @return
     */
    int insert(Menu record);

    /**
     * 通过父id查询其子级
     * @param id
     * @return
     */
    List<Integer> querySubMenuIdByPid(@Param("id") Integer id);

    /**
     * 按条件更新
     * @param map
     * @return
     */
    int updateById(Map<String, Object> map);

    /**
     * 批量更新
     * @param param
     * @return
     */
    int updateByIds(Map<String, Object> param);
}
Mapper.XML
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="zxw.mapper.MenuMapper">
    <resultMap id="BaseResultMap" type="cn.zxw.entity.Menu">
        <id column="id" jdbcType="INTEGER" property="id"/>
        <result column="name" jdbcType="VARCHAR" property="name"/>
        <result column="status" jdbcType="SMALLINT" property="status"/>
        <result column="level" jdbcType="SMALLINT" property="level"/>
        <result column="parentId" jdbcType="INTEGER" property="parentid"/>
        <result column="sort" jdbcType="SMALLINT" property="sort"/>
    </resultMap>
    <sql id="Base_Column_List">
        id, `name`, `status`, `level`, parentId, sort
    </sql>

    <insert id="insert" keyColumn="id" keyProperty="id" parameterType="cn.zxw.entity.Menu" useGeneratedKeys="true">
        insert into menu (`name`, `status`, `level`,
                          parentId, sort)
        values (#{name,jdbcType=VARCHAR}, #{status,jdbcType=SMALLINT}, #{level,jdbcType=SMALLINT},
                #{parentid,jdbcType=INTEGER}, #{sort,jdbcType=SMALLINT})
    </insert>
    <!--根据id查找父id集合-->
    <select id="querySubMenuIdByPid" resultType="integer" parameterType="integer">
        select id
        from menu
        where parentId = #{id}
    </select>
    <!--根据id按条件更新-->
    <update id="updateById" parameterType="map">
        update menu set
        <if test="isShow != null">
            status = #{isShow}
        </if>
        <if test="isDelete != null">
            <!--省略操作-->
        </if>
        where id = #{id}
    </update>
    <!--根据id按条件批量更新-->
    <update id="updateByIds" parameterType="map">
        update menu
        <set>
            <if test="isShow != null">
                `status` = #{isShow},
            </if>
        </set>
        <where>
            id
            <foreach collection="ids" item="id" open="in (" close=")" separator=",">
                #{id}
            </foreach>
        </where>
    </update>
</mapper>
Controller(练习时没有写Service)
package zxw.controller;

import cn.zxw.entity.Menu;
import cn.zxw.param.MenuBatch;
import cn.zxw.param.MenuVo;
import cn.zxw.param.MobileBo;
import cn.zxw.result.CommonResult;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.*;
import zxw.mapper.MenuMapper;

import java.util.*;
import java.util.stream.Collectors;

/**
 * @author 华安小书童
 * @description 菜单管理
 * @data: 2021/1/30 15:09
 */
@Slf4j
@RestController
@AllArgsConstructor
@Api(tags = "MenuController", description = "菜单管理")
@RequestMapping("/menu")
public class MenuController {

    private MenuMapper menuMapper;

    @GetMapping("/getAll")
    @ApiOperation("获取所有菜单")
    public CommonResult getAll() {
        try {
            //先查询出所有的菜单
            List<Menu> menus = menuMapper.selectList(new QueryWrapper<>());
            //获取一级菜单
            List<MenuVo> firstMenuVos = menus.stream().filter(menu -> menu.getParentid() == 0 && menu.getStatus() == 1).map(this::menuToMenuVO).sorted(Comparator.comparingInt(Menu::getSort).reversed()).collect(Collectors.toList());
            //查询子级
            findSubMenu(menus, firstMenuVos);
            return CommonResult.success(firstMenuVos);
        } catch (Exception e) {
            log.error(e.getMessage());
            return CommonResult.failed();
        }
    }

    @GetMapping("/getDetail")
    @ApiOperation("查询该级分类详情或单个详情")
    public CommonResult getDetail(@ApiParam(name = "type", value = "0为父id(1级分类传0)2为单个id") @RequestParam Integer type,
                                  @ApiParam(name = "id", value = "具体的id") @RequestParam Long id) {
        try {
            LambdaQueryWrapper<Menu> wrapper = null;
            if (type == 0) {
                wrapper = Wrappers.<Menu>query().lambda().eq(Menu::getParentid, id).eq(Menu::getStatus, 1).orderByDesc(Menu::getSort);
                List<Menu> menus = menuMapper.selectList(wrapper);
                return CommonResult.success(menus);
            } else {
                wrapper = Wrappers.<Menu>query().lambda().eq(Menu::getId, id).eq(Menu::getStatus, 1);
                Menu menu = menuMapper.selectOne(wrapper);
                return CommonResult.success(menu);
            }

        } catch (Exception e) {
            log.error(e.getMessage());
            return CommonResult.failed();
        }
    }

    @PostMapping("/save")
    @ApiOperation("新增分类")
    public CommonResult save(@RequestBody Menu menu) {
        try {
            //新增时应该先查询当前分类下,最大的排序字段值是多少
            LambdaQueryWrapper<Menu> queryWrapper = Wrappers.<Menu>query().lambda()
                    .eq(Menu::getParentid, menu.getParentid())
                    .select(Menu::getSort).orderByDesc(Menu::getSort).last("LIMIT 1");
            Menu queryMenu = menuMapper.selectOne(queryWrapper);
            //如果为null则代表该级分类是新的一级,则传入1 不为null传入的排序增加1
            menu.setSort(queryMenu == null ? 1 : (short) (queryMenu.getSort() + 1));
            menuMapper.insert(menu);
            return CommonResult.success("保存成功");
        } catch (Exception e) {
            log.error(e.getMessage());
            return CommonResult.failed();
        }
    }

    @PostMapping("/mobile")
    @ApiOperation("上下移动分类等级")
    public CommonResult mobile(@RequestBody MobileBo mobileBo) {
        try {
            Menu menu = null;
            //判断是上移还是下移操作
            if (mobileBo.getMobile() == 0) {
                //查值越大排序等级越高 应选当前分级下比sort大最近的一条数据进行sort替换  这应该升序排序
                LambdaQueryWrapper<Menu> wrapper = Wrappers.<Menu>query().lambda().select(Menu::getId, Menu::getSort)
                        .eq(Menu::getParentid, mobileBo.getParentId()).gt(Menu::getSort, mobileBo.getSort()).orderByAsc(Menu::getSort).last("LIMIT 1");
                menu = menuMapper.selectOne(wrapper);
            } else {
                //下移操作 应选择当前分级下比sort值小的最近一条数据进行sort值替换  降序排序
                LambdaQueryWrapper<Menu> wrapper = Wrappers.<Menu>query().lambda().select(Menu::getId, Menu::getSort)
                        .eq(Menu::getParentid, mobileBo.getParentId()).lt(Menu::getSort, mobileBo.getSort()).orderByDesc(Menu::getSort).last("LIMIT 1");
                menu = menuMapper.selectOne(wrapper);
            }
            //将两条数据的sort进行替换
            if (Objects.nonNull(menu)) {
                Short newSort = menu.getSort();
                menu.setSort(mobileBo.getSort().shortValue());
                //更新对比数据
                menuMapper.updateById(menu);
                Menu newMenu = new Menu();
                newMenu.setId(mobileBo.getId());
                newMenu.setSort(newSort);
                menuMapper.updateById(newMenu);
            }
            return CommonResult.success("操作成功");
        } catch (Exception e) {
            log.error(e.getMessage());
            return CommonResult.failed();
        }
    }

    @PostMapping("/batch")
    @ApiOperation("批量操作")
    public CommonResult batchShow(@RequestBody MenuBatch menuBatch) {
        try {
            //取出id集合和是否隐藏
            List<Integer> ids = menuBatch.getIds();
            Short isShow = menuBatch.getIsShow();
//            ids.forEach(id -> showById(id, isShow));
            ids.forEach(id -> batchNoHaveRecursion(id, isShow));
            return CommonResult.success("成功");
        } catch (Exception e) {
            e.printStackTrace();
            log.error(e.getMessage());
            return CommonResult.failed();
        }
    }

    /**
     * 递归 通过id批量操作
     *
     * @param id
     * @param isShow
     */
    private void batchById(Integer id, Short isShow) {
        //查找子菜单的id列表
        List<Integer> subIds = menuMapper.querySubMenuIdByPid(id);
        for (Integer subId : subIds) {
            //递归调用设置子菜单的数据
            batchById(subId, isShow);
        }
        //设置本级菜单
        Map<String, Object> param = new HashMap<>();
        param.put("isShow", isShow);
        param.put("id", id);
        menuMapper.updateById(param);
        //如果有关联资源操作可以进行编辑 eg:  resourcesDao.deleteResourceByMenuId(id);
    }

    /**
     * 不使用递归进行批量更新
     * @param id
     * @param isShow
     */
    private void batchNoHaveRecursion(Integer id, Short isShow) {
        //需要删除的菜单ID列表
        List<Integer> idList = new ArrayList<>();
        //临时队列
        Queue<Integer> queue = new LinkedList<>();
        //先把最上面的菜单ID放入对列
        queue.offer(id);
        Integer menuId = null;
        //出队列删除,poll如果没有会返回null,没有代表所有子菜单全部查完了
        while ((menuId = queue.poll()) != null) {
            //要删除的菜单ID
            idList.add(menuId);
            //查询子菜单ID 并 入对列
            queue.addAll(menuMapper.querySubMenuIdByPid(menuId));
        }
        //批量删除菜单
        Map<String,Object> param = new HashMap<>();
        param.put("ids",idList);
        param.put("isShow",isShow);
        menuMapper.updateByIds(param);
        //批量删除资源resourcesDao.deleteResourcesByMenuId(idList);
    }


    /**
     * 递归查询子级
     *
     * @param menus        查询的所有菜单
     * @param firstMenuVos 第一级的菜单
     */
    private void findSubMenu(List<Menu> menus, List<MenuVo> firstMenuVos) {
        //遍历第一级
        firstMenuVos.forEach(firstMenuVo -> {
            //子级菜单集合
            List<MenuVo> secondMenuVos = new LinkedList<>();
            //查找子级  递归的出口就是循环完毕
            for (Menu menu : menus) {
                //判断当前是否为子父级关系
                if (firstMenuVo.getId().equals(menu.getParentid())) {
                    //如果是添加到子级中
                    secondMenuVos.add(menuToMenuVO(menu));
                }
                //递归调用,几级菜单都可以
                findSubMenu(menus, secondMenuVos);
                //增加排序
                secondMenuVos.sort(Comparator.comparingInt(MenuVo::getSort));
            }
            //将子级放入一级目录
            firstMenuVo.setChildren(secondMenuVos);
        });
    }

    /**
     * 视图与实体类数据转换
     *
     * @param menu
     * @return
     */
    private MenuVo menuToMenuVO(Menu menu) {
        MenuVo menuVo = new MenuVo();
        BeanUtils.copyProperties(menu, menuVo);
        return menuVo;
    }

}

完毕了,有更好解决办法的小伙伴可以留言,这些都是自己通过博客总结的

<think>嗯,用户想了解Java项目中频繁Full GC的现象、原因和影响。首先,我得回忆一下JVM的垃圾回收机制。根据引用[2],Full GC会清理整个堆内存,包括新生代、老年代和元空间。频繁的Full GC肯定会导致应用性能下降,比如停顿时间变长,影响用户体验。 那现象方面,用户可能会注意到应用卡顿,监控工具显示Full GC次数多,内存使用率高,甚至OOM错误。比如引用[3]提到Young GC频繁导致对象进入老年代,进而触发Full GC。所以需要解释现象的时候,要结合监控数据和实际应用表现。 接下来是原因。可能的原因有很多,比如内存分配不合理,老年代空间不足,元空间不足,代码中有内存泄漏,或者显式调用了System.gc()。引用[1]和[3]提到高并发或大数据处理导致对象快速进入老年代,Survivor区太小。这时候需要分点说明每个原因的具体情况,比如大对象直接进入老年代,或者长期存活的对象积累。 然后是影响,主要涉及性能、可用性和资源成本。频繁的Full GC会导致长时间的停顿,影响响应时间,甚至让用户觉得应用卡死。引用[4]强调优化GC的重要性,所以影响部分要突出对用户体验和系统稳定性的威胁。 最后,用户可能需要知道如何排查和解决这些问题。比如检查内存配置,分析GC日志,优化代码减少对象创建。引用[4]提到合理设置堆内存和GC策略,所以优化策略部分需要给出具体的建议,比如调整Survivor区比例,使用G1GC等。 还要注意用户可能没有说出来的需求,比如他们可能遇到了具体的性能问题,需要快速定位原因。所以在回答时要结构清晰,分点列出,便于用户对照检查自己的系统情况。同时,生成的相关问题要引导用户深入探讨解决方案和优化方法,帮助他们进一步学习。</think>### Java频繁Full GC的现象、原因及影响分析 #### 一、现象描述 1. **应用性能显著下降** 频繁的Full GC会导致应用线程暂停(Stop-The-World),表现为接口响应时间变长、吞吐量降低,严重时可能触发超时或服务不可用[^2][^4]。 2. **监控指标异常** - GC日志中`Full GC`关键字出现频率高,且单次耗时超过1秒(如`Full GC (Ergonomics) 1000ms`) - 老年代内存使用率持续接近100%,或频繁出现内存锯齿状波动[^3][^4] - 系统监控工具(如Prometheus、JVisualVM)显示堆内存无法有效回收 3. **伴随异常现象** - `OutOfMemoryError`错误频繁出现 - 容器化部署时因内存超限触发Pod重启 --- #### 二、核心原因 ##### 1. **内存分配不合理** - **Survivor区过小**:Young GC后存活对象无法被Survivor区容纳,直接晋升老年代 $$ \text{晋升条件} = \text{对象年龄} > \text{MaxTenuringThreshold} \quad \text{或} \quad \text{Survivor空间不足} $$ - **大对象直接分配老年代**:如未设置`-XX:PretenureSizeThreshold`,大数组或缓存对象跳过Eden区 ##### 2. **内存泄漏** - 静态集合类持有短生命周期对象 - 未关闭的数据库连接、文件流等资源 - 第三方库未正确释放Native Memory ##### 3. **GC触发条件** - **老年代空间不足**:Major GC后仍无法满足新对象分配 - **元数据区溢出**:未设置`-XX:MaxMetaspaceSize`时,动态生成类过多(如Spring AOP) - **显式调用System.gc()**:未配置`-XX:+DisableExplicitGC`时可能误触发 ##### 4. **外部因素** - 高并发场景下请求量突增,瞬时产生大量临时对象 - 缓存策略不当,如未设置LRU淘汰机制导致缓存膨胀 --- #### 三、主要影响 | 影响维度 | 具体表现 | |-----------------|--------------------------------------------------------------------------| | **性能** | 99%响应时间(TP99)恶化,吞吐量下降超过50%[^4] | | **可用性** | 网关层触发熔断机制,微服务调用链路中断 | | **资源成本** | 为缓解问题被迫扩容机器,CPU/内存资源浪费 | | **数据一致性** | 长时间GC导致分布式事务超时,引发数据回滚 | --- #### 四、排查与优化建议 1. **基础排查步骤** ```bash # 查看GC统计(JDK9+) jcmd <pid> GC.class_histogram # 生成堆转储文件 jmap -dump:live,format=b,file=heap.hprof <pid> ``` 2. **关键JVM参数调整** ```ini # 老年代与新生代比例 -XX:NewRatio=3 # Survivor区与Eden区比例 -XX:SurvivorRatio=8 # 大对象阈值(单位KB) -XX:PretenureSizeThreshold=1024 ``` 3. **高级优化方向** - 使用G1GC替换ParallelGC:`-XX:+UseG1GC` - 启用元空间监控:`-XX:NativeMemoryTracking=detail` - 限制动态类生成:如配置CGLIB缓存策略 ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值