Java菜单加载流程如何统一

wen java案例 29

本文目录导读:

Java菜单加载流程如何统一

  1. 核心思路:将菜单数据与视图逻辑分离
  2. 方案一:基于身份认证框架的统一拦截(后端为主,适合B/S管理后台)
  3. 方案二:基于动态路由与前端路由守卫的统一加载(前后端分离,前端主导)
  4. 方案三:基于AOP(面向切面编程)的菜单权限统一注入(高级、微服务友好)
  5. 方案四:基于事件驱动与启动加载器(适合系统初始化、配置中心)
  6. 最佳实践总结(如何选择?)
  7. 统一的共同点(你的代码也应该做到)

在Java应用中统一管理菜单加载流程,核心目标是避免在多个地方重复编写菜单权限判断、加载和渲染逻辑,实现配置化、集中化、可复用,以下是几种常见且有效的统一方案,涵盖了从后端到前端的典型实践。

核心思路:将菜单数据与视图逻辑分离

统一加载流程的关键在于:

  1. 数据驱动:菜单数据(结构、权限、路由)由后端统一提供,前端只负责渲染。
  2. 集中入口:在应用的唯一生命周期切入点(如登录成功、应用启动时)调用菜单加载逻辑。
  3. 缓存机制:减少重复加载,提高性能。

基于身份认证框架的统一拦截(后端为主,适合B/S管理后台)

这是最经典的Java Web方案,通常在Spring Security或Shiro框架基础上实现。

核心流程:

  1. 用户登录 -> 认证成功。
  2. 权限注入:在认证后的UserDetails对象中注入角色或权限列表(List<GrantedAuthority>)。
  3. 单点请求:前端在登录成功后,向后端发送一个统一的菜单请求,如 GET /api/menus
  4. 后端统一处理
    • 查询全部菜单(从数据库或配置文件)。
    • 递归过滤:根据当前用户的权限列表,过滤出用户有权限的菜单树。
    • 返回标准化JSON(如 { "code": 0, "data": [...] })。
  5. 前端存储与渲染:前端接收到菜单树后,存入Pinia/VuexlocalStorage,再动态生成侧边栏路由。

Java代码示例(Controller层):

@RestController
@RequestMapping("/api")
public class MenuController {
    @Autowired
    private MenuService menuService;
    // 统一入口:获取当前用户的菜单树
    @GetMapping("/menus")
    public ResponseEntity<Result<MenuTreeVO>> getMenus() {
        // 1. 从Spring Security上下文中获取当前用户
        UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext()
                .getAuthentication().getPrincipal();
        // 2. 调用统一的服务层方法,无需反复写权限判断
        MenuTreeVO menuTree = menuService.getMenuTreeByUsername(userDetails.getUsername());
        return ResponseEntity.ok(Result.success(menuTree));
    }
}

Service层统一逻辑(核心):

@Service
public class MenuServiceImpl implements MenuService {
    @Override
    public MenuTreeVO getMenuTreeByUsername(String username) {
        // 1. 查询用户所有角色
        Set<String> roleCodes = userRoleService.getUserRoleCodes(username);
        // 2. 查询所有菜单(可加缓存)
        List<Menu> allMenus = menuRepository.findAll(); // 或从缓存获取
        // 3. 递归过滤:只保留拥有匹配角色的菜单
        List<Menu> filteredMenus = filterMenusByRoles(allMenus, roleCodes);
        // 4. 构建树形结构
        return buildTree(filteredMenus);
    }
    private List<Menu> filterMenusByRoles(List<Menu> menus, Set<String> roleCodes) {
        return menus.stream()
                .filter(menu -> menu.getRoles().stream().anyMatch(role -> roleCodes.contains(role.getCode())))
                .collect(Collectors.toList());
    }
}

基于动态路由与前端路由守卫的统一加载(前后端分离,前端主导)

这是现代Spring Boot + Vue/React流行的方式,将菜单数据与前端路由深度绑定。

核心流程:

  1. 登录成功 -> 获取Token。
  2. 前端路由守卫router.beforeEachaxios拦截器)拦截所有非登录页路由。
  3. 首次加载判断:如果store中无菜单数据,则调用 GET /api/menus
  4. 动态添加路由:后端返回的菜单JSON中包含了前端路由组件路径
    • Vue示例:router.addRoute('dashboard', { path: '/user', component: () => import('./views/user/UserList.vue') })
  5. 持久化:将菜单树存入localStorageVuex/Pinia并持久化,避免刷新丢失。

统一代码段(Vue Router Guard示例):

// router/index.js
router.beforeEach(async (to, from, next) => {
  if (to.path === '/login') {
    next();
    return;
  }
  // 检查是否已加载菜单
  const menuStore = useMenuStore();
  if (!menuStore.menuTree.length) {
    try {
      // 调用统一的后端点
      const res = await axios.get('/api/menus');
      const menuTree = res.data.data;
      // 统一递归:根据菜单数据动态添加路由
      const asyncRoutes = generateRoutes(menuTree);
      asyncRoutes.forEach(route => {
        router.addRoute('root', route); // 假设root是布局路由
      });
      // 存储菜单元数据
      menuStore.setMenuTree(menuTree);
      // 必须重定向当前路由,确保新添加的路由生效
      next({ ...to, replace: true });
    } catch (error) {
      // 跳转到错误页或登录页
      next('/login');
    }
  } else {
    next();
  }
});

基于AOP(面向切面编程)的菜单权限统一注入(高级、微服务友好)

适合需要深度自定义、或菜单加载逻辑散落在多个Service中的场景。

核心思想: 使用@Around注解,在所有返回菜单数据的方法上自动注入权限过滤。

@Aspect
@Component
public class MenuPermissionAspect {
    @Around("@annotation(com.example.annotation.InjectMenuPermission)")
    public Object injectPermission(ProceedingJoinPoint pjp) throws Throwable {
        // 1. 执行原方法(获得原始菜单列表)
        Object result = pjp.proceed();
        if (result instanceof MenuTreeVO) {
            MenuTreeVO menuTree = (MenuTreeVO) result;
            // 2. 获取当前用户权限
            Set<String> userPermissions = getCurrentUserPermissions();
            // 3. 统一过滤
            filterMenuTree(menuTree, userPermissions);
            return menuTree;
        }
        return result;
    }
    // 递归过滤菜单树...
}

使用: 在需要统一加载菜单的Service方法上加注解:

@Service
public class MenuService {
    @InjectMenuPermission
    public MenuTreeVO getMenuBySystem(String systemCode) {
        // 这里是原始的数据查询逻辑,权限过滤由AOP统一处理
        return menuRepository.findBySystemCode(systemCode);
    }
}

基于事件驱动与启动加载器(适合系统初始化、配置中心)

适合应用的菜单结构相对固定,但权限配置频繁变化的场景。

核心流程:

  1. 应用启动CommandLineRunner@PostConstruct 从数据库加载所有菜单到本地内存缓存(如 ConcurrentHashMap)。
  2. 权限变更监听:使用消息队列(如RabbitMQ)或WebSocket监听“权限变更”事件。
  3. 缓存刷新:监听到事件后,自动重新加载菜单缓存。
  4. 用户请求:直接查询缓存,无需每次查数据库。

统一入口代码:

@Component
public class MenuCacheLoader implements CommandLineRunner {
    @Autowired
    private MenuRepository menuRepository;
    public static Map<String, MenuTreeVO> menuCache = new ConcurrentHashMap<>();
    @Override
    public void run(String... args) {
        // 统一加载所有菜单(按系统分组)
        List<Menu> allMenus = menuRepository.findAll();
        Map<String, List<Menu>> grouped = allMenus.stream().collect(Collectors.groupingBy(Menu::getSystemCode));
        grouped.forEach((systemCode, menus) -> {
            menuCache.put(systemCode, buildTree(menus));
        });
        log.info("菜单缓存已加载完毕");
    }
    // 提供给外部调用的刷新方法(当发生权限变更时)
    public void refresh() {
        menuCache.clear();
        run(); // 重新加载
    }
}

最佳实践总结(如何选择?)

场景 推荐方案 核心原因
单体应用、管理后台 方案一(Security拦截 + Service统一) 简单、易维护,与Spring生态绑定深
前后端分离、微前端 方案二(动态路由 + 路由守卫) 前端主导,后端只提供JSON,灵活性最高
菜单结构复杂、多系统 方案一 + 方案四(缓存) 避免频繁查询数据库,提升性能
遗留系统改造、迁移 方案三(AOP切面) 侵入性最小,无需改动业务代码
高并发、权限实时性要求高 方案四(事件驱动 + 缓存) 对数据库压力小,权限变更可实时生效

统一的共同点(你的代码也应该做到)

无论选择哪种具体方案,统一的菜单加载流程都应遵循:

  1. 唯一数据源:菜单的CRUD、权限绑定都在一个地方(如Menu表)进行。
  2. 无重复逻辑:所有Controller/View在渲染菜单前,不再自己编写if-else权限判断,而是调用统一的MenuService.getMenuTree()
  3. 标准化返回:所有菜单接口返回的数据结构一致(如{id, path, name, icon, children, permission})。
  4. 错误处理统一:在统一加载入口处集中处理Token过期无权限数据库异常,而非在每个页面处理。

通过以上任一种方案,可以彻底告别“每个页面写一遍菜单加载逻辑”的混乱局面。

抱歉,评论功能暂时关闭!