恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
SpringBoot+Layui+Shiro+Ehcache学生管理系统实践
首页
资讯中心
/
SpringBoot+Layui+Shiro+Ehcache学生管理系统实践
SpringBoot+Layui+Shiro+Ehcache学生管理系统实践
发布时间:2026/9/16 10:47:37
简介这是一套基于Spring Boot和Layui搭建的学生管理系统同时整合Shiro安全框架与Ehcache缓存框架主要面向全栈初学者、课程设计及毕业设计人群。项目源码已通过本地编译测试配套环境配置文档齐全整体难度适中可作为学生信息管理场景的完整入门范例。压缩包共310个文件大小约30.39MB内含Java源码、Layui页面、数据库SQL脚本以及XML、YML等配置文件并配有大量图片与样式资源便于查看界面效果和前后端联调方式整体目录结构清晰。系统覆盖登录认证、权限分配、缓存处理等关键功能能够帮助学习者理解安全框架与缓存机制在真实项目中的整合用法具有较高的参考价值。目前已有94人学习适合需要快速搭建毕设项目或提升全栈开发能力的学习者参考与二次开发。1. 这套学生管理系统为什么值得从零搭一遍一个看起来“遍地都是”的学生管理系统其实是把 Java Web 开发里最常踩的坑集中到了一起表结构设计、权限控制、缓存穿透、前端渲染、会话管理。SpringBoot 负责把后端装配成本地可运行的服务Layui 承担后台管理页面的表格、表单和弹窗交互Shiro 解决“谁能访问哪个接口”的认证与授权问题Ehcache 则用来缓存热点数据——比如学生列表、字典项、权限数据避免每次请求都打数据库。这套组合的技术选型非常典型SpringBoot 2.x 时代Shiro 和 Ehcache 的整合方式已经非常成熟不依赖微服务体系也没有引入消息队列和分布式缓存单机部署即可支撑一个院系级别的日常使用。对初中级开发者来说把这条链路完整跑通比单纯背 Spring Security 的过滤器链要直观得多。对五年以上经验的人来说这套系统的价值在于排查一个问题Shiro 的 Session 和 Ehcache 的缓存边界在哪里写不好就会遇到“权限改了不生效”和“列表缓存和数据不一致”的经典故障。本文按实际开发顺序推进先搭建项目骨架和数据库层再做 Layui 管理界面与后端接口的对接然后集成 Shiro 做登录认证和权限拦截再配置 Ehcache 做二级缓存最后讲部署和排错。全程使用可复现的代码片段版本以 SpringBoot 2.5.x 为例。2. 从零搭建SpringBootLayui的项目骨架先跑通登录页2.1 创建一个不带前端构建工具的SpringBoot工程很多项目组会把 Layui 直接放在src/main/resources/static下不走 Node 构建链路。这是最省事的做法适合内部管理系统——不需要 webpack 打包、不需要 npm 安装、不需要处理跨域SpringBoot 的静态资源映射天然支持这个目录。使用 IDEA 创建项目时Spring Initializr 选择 Java 8、SpringBoot 2.5.x依赖勾选 Spring Web、Thymeleaf如果要用模板渲染、MyBatis或 MyBatis-Plus、MySQL Driver。创建完成后手动往 pom.xml 里补充 Shiro 和 Ehcache 的依赖dependency groupIdorg.apache.shiro/groupId artifactIdshiro-spring-boot-starter/artifactId version1.9.1/version /dependency dependency groupIdorg.apache.shiro/groupId artifactIdshiro-ehcache/artifactId version1.9.1/version /dependency这里有个容易踩的版本坑shiro-ehcache依赖的 Ehcache 版本是 2.10.x和 Spring Boot 2.5 自带的缓存抽象兼容性尚可但需要排除传递依赖中的ehcache-core改为显式引入net.sf.ehcache:ehcache:2.10.9.2避免和 Spring Boot 的缓存管理冲突。如果使用 SpringBoot 3.xShiro 的 javax.servlet 依赖会直接启动失败建议本项目锁定在 2.x 系列。依赖就绪后application.yml 配置数据源和 MyBatis 映射spring: datasource: url: jdbc:mysql://localhost:3306/student_ms?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver cache: type: ehcache mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.studentms.entity server: port: 8080 servlet: context-path: /spring.cache.typeehcache会让 Spring 的Cacheable注解走 Ehcache 实现这个后面讲缓存时会用到。Mapper 接口和 XML 放到对应位置写一个最简单的StudentMapper.selectPage查询验证数据库连通性然后搭建后端返回格式的基类统一code、msg、data三字段结构这也正是 Layui 表格组件要求的响应格式。2.2 Layui 页面引入方式和登录接口的第一版在static目录下创建layui/文件夹把下载的 Layui 2.8.x 文件拷贝进去然后在templates/login.html中引入link relstylesheet href/layui/css/layui.css script src/layui/layui.js/script登录页面用 Layui 的表单模块渲染关键逻辑是监听提交按钮用$.ajax把用户名密码发送到后端。这部分的前端代码不复杂真正的核心在后端登录接口如何处理 Shiro 的 SubjectPostMapping(/login) ResponseBody public Result doLogin(String username, String password) { Subject subject SecurityUtils.getSubject(); UsernamePasswordToken token new UsernamePasswordToken(username, password); try { subject.login(token); return Result.success(登录成功); } catch (UnknownAccountException e) { return Result.error(用户不存在); } catch (IncorrectCredentialsException e) { return Result.error(密码错误); } catch (LockedAccountException e) { return Result.error(账号被锁定); } }SecurityUtils.getSubject()拿到当前访问用户调用login()后Shiro 会走Realm的认证流程。这里不能直接操作 Session 存储用户信息Shiro 会把认证状态写入自身管理的 Session后续从subject.getPrincipal()获取当前登录用户。登录接口成功后前端跳转到/index首页再做一次权限校验渲染当前用户名和菜单。2.3 ShiroConfig 配置类过滤链和登录跳转的最小写法集成 Shiro 到 SpringBoot核心是一个ShiroFilterFactoryBean的配置类。初次搭建时过滤链规则越简单越好先只保护/student/**和/index放开/login、/layui/**、/css/**等静态资源Configuration public class ShiroConfig { Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean factoryBean new ShiroFilterFactoryBean(); factoryBean.setSecurityManager(securityManager); factoryBean.setLoginUrl(/login); factoryBean.setUnauthorizedUrl(/403); MapString, String filterChainDefinitionMap new LinkedHashMap(); filterChainDefinitionMap.put(/login, anon); filterChainDefinitionMap.put(/logout, logout); filterChainDefinitionMap.put(/layui/**, anon); filterChainDefinitionMap.put(/css/**, anon); filterChainDefinitionMap.put(/js/**, anon); filterChainDefinitionMap.put(/index, authc); filterChainDefinitionMap.put(/student/**, authc); filterChainDefinitionMap.put(/**, authc); factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return factoryBean; } }anon表示匿名可访问authc表示必须登录logout是 Shiro 内置的退出过滤器会清除 Session 后跳转到登录页。注意过滤链是有序的LinkedHashMap保证顺序/student/**必须在/**前面声明否则被通配规则提前拦截变成authc。静态资源的anon规则必须写全否则登录页引用的 Layui 脚本会被拦截页面裸奔且难以排查。配完 ShiroConfig 后还需要一个自定义 Realm。初学者最容易漏掉的是Realm 里不仅要实现认证还要实现授权。如果只做认证登录能成功但一访问需要权限的接口就会 403。第一版可以先让doGetAuthorizationInfo返回空权限集把登录链路验证通权限控制的细节放到下一章细讲。3. 深入Shiro认证授权模型把权限控制做到按钮级3.1 Realm 中认证与授权分离的设计边界Shiro 的设计理念里认证解决“你是不是合法用户”授权解决“你能干什么”。这两个动作分别对应doGetAuthenticationInfo和doGetAuthorizationInfo在 Realm 中分开实现。认证方法里查一次用户表授权方法里查角色和权限表两者不要混在一起做——授权的查询频率远高于认证混在一起会造成不必要的数据库开销。Component public class UserRealm extends AuthorizingRealm { Autowired private UserMapper userMapper; Autowired private PermissionMapper permissionMapper; Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { User user (User) principals.getPrimaryPrincipal(); SimpleAuthorizationInfo info new SimpleAuthorizationInfo(); ListString roles permissionMapper.selectRolesByUserId(user.getId()); ListString perms permissionMapper.selectPermissionsByUserId(user.getId()); info.addRoles(roles); info.addStringPermissions(perms); return info; } Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String username (String) token.getPrincipal(); User user userMapper.selectByUsername(username); if (user null) { return null; } return new SimpleAuthenticationInfo(user, user.getPassword(), getName()); } }授权方法最终返回的是角色标识和权限标识的集合。addStringPermissions里的字符串形如student:add、student:delete这是配合RequiresPermissions(student:add)注解使用的。注意 Shiro 的权限字符串是“资源:操作”的约定不是固定语法自己定义清晰即可但全项目要保持一致。doGetAuthenticationInfo返回null表示用户不存在Shiro 自动抛出UnknownAccountException返回SimpleAuthenticationInfo时Shiro 会拿传入的token.getPassword()和 user 的密码做比对密码错误时抛IncorrectCredentialsException。密码比对默认使用明文生产环境必须使用 MD5 加盐或 BCrypt 加密这个后面单独说。3.2 在 Service 层还是 Controller 层做权限校验很多管理系统的权限校验写在 Controller 方法上通过注解声明所需权限Controller RequestMapping(/student) public class StudentController { RequiresPermissions(student:add) PostMapping(/add) ResponseBody public Result addStudent(Student student) { studentService.insert(student); return Result.success(新增成功); } RequiresPermissions(student:delete) PostMapping(/delete) ResponseBody public Result deleteStudent(Integer id) { studentService.deleteById(id); return Result.success(删除成功); } }注解生效的前提是 Shiro 的 Spring AOP 支持已开启。在 ShiroConfig 中补充AuthorizationAttributeSourceAdvisor和DefaultAdvisorAutoProxyCreator两个 Bean缺一不可。DefaultAdvisorAutoProxyCreator负责扫描带 Shiro 注解的 Bean 生成代理AuthorizationAttributeSourceAdvisor把注解与 Shiro 的拦截逻辑绑定。这里有一个常见误用把RequiresPermissions加到 Service 层方法上。虽然 AOP 也能拦截但 Service 层往往被多个 Controller 调用权限校验粒度不好控制。建议权限注解统一放在 Controller 层Service 层专注业务逻辑。如果某些接口需要“登录即可访问”不加权限注解即可Shiro 的authc过滤器已经保证登录态。权限粒度再细致一层是按钮级控制。后端接口已经做了权限拦截前端菜单和按钮的显隐规则需要和后端保持一致。Layui 渲染表格操作列时根据当前用户权限决定是否渲染“删除”按钮function getOperationColumn() { var opHtml ; if (hasPermission(student:edit)) { opHtml a classlayui-btn layui-btn-xs lay-eventedit编辑/a; } if (hasPermission(student:delete)) { opHtml a classlayui-btn layui-btn-danger layui-btn-xs lay-eventdel删除/a; } return { title: 操作, toolbar: opHtml }; }hasPermission里的权限集合从哪儿来常见做法是登录成功后后端把当前用户的所有权限标识通过接口返回前端存到全局变量或 localStorage 中。这里的权限查询接口建议加一层缓存因为每个页面加载都会调用频繁查数据库完全没有必要——这正是 Ehrcache 切入的场景。3.3 用户、角色、权限三张表的设计与关联查询支撑上面权限模型的是经典的五张表用户表sys_user、角色表sys_role、权限表sys_permission、用户角色关联表sys_user_role、角色权限关联表sys_role_permission。权限表用树形结构存储父节点是菜单子节点是按钮操作字段类型说明idbigint主键namevarchar(50)权限名称如“学生新增”permsvarchar(100)权限标识如 student:addtypetinyint1菜单 2按钮parent_idbigint父级ID顶级为0sortint排序号查询用户权限的 XML 核心是一段拼接了两次关联表的 SQLselect idselectPermissionsByUserId resultTypestring SELECT DISTINCT p.perms FROM sys_user u INNER JOIN sys_user_role ur ON u.id ur.user_id INNER JOIN sys_role r ON ur.role_id r.id INNER JOIN sys_role_permission rp ON r.id rp.role_id INNER JOIN sys_permission p ON rp.permission_id p.id WHERE u.id #{userId} AND p.perms IS NOT NULL AND p.perms ! /selectDISTINCT是必要的一个用户有多个角色、多个角色有重叠权限时不去重会导致SimpleAuthorizationInfo里的权限集合出现重复项。Shiro 的权限匹配不排斥重复但会加大内存占用。另一个细节是p.perms IS NOT NULL过滤掉菜单节点——菜单节点本身的 perms 为空只有按钮节点才写权限标识。角色变更后权限实时生效的问题需要解释清楚。由于doGetAuthorizationInfo在每次权限校验时都会调用Shiro 会缓存授权信息默认缓存策略见下一章如果 Shiro 缓存了授权结果修改用户角色后必须清除该用户的授权缓存否则权限要等缓存过期才生效。缓存粒度控制得当的话这个问题的处理方式在 Ehcache 配置中解决。4. 集成Ehcache缓存框架缓存权限数据和热点查询4.1 为什么是Ehcache而不是Redis——单机系统的务实选择在这个学生管理系统的场景中用户量是千级而非百万级并发是几十而非几千引入 Redis 意味着多一套服务部署、多一份序列化配置、多一个网络延迟。Ehcache 是进程内缓存直接嵌在 JVM 里读取零网络开销配置只要一份 XML 文件。对单机部署的管理系统来说这是性价比最高的方案。Shiro 本身对 Ehcache 有原生支持shiro-ehcache依赖里就包含了EhCacheManager实现。Shiro 的认证和授权信息可以通过这个 CacheManager 做缓存这样用户第一次登录后权限数据不会每次都查数据库。同时 Spring 的Cacheable注解也能使用同一个 Ehcache 实例实现业务数据的缓存。需要特别说明的是进程内缓存的边界Ehcache 的数据存在当前 JVM 中如果系统做了多实例部署每个实例的缓存是独立的数据一致性无法保证。学生管理系统通常单实例部署这个问题不存在。如果要向分布式演进再考虑把 Shiro 的 Session 和缓存迁移到 Redis但那是另一个改造工程了。4.2 ehcache.xml 参数配置和三大缓存区域的划分在src/main/resources下创建ehcache.xml配置三个缓存区域ehcache xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:noNamespaceSchemaLocationhttp://ehcache.org/ehcache.xsd updateCheckfalse diskStore pathjava.io.tmpdir/studentms-ehcache/ defaultCache maxElementsInMemory1000 eternalfalse timeToIdleSeconds120 timeToLiveSeconds120 overflowToDiskfalse memoryStoreEvictionPolicyLRU/ cache nameauthorizationCache maxElementsInMemory2000 eternalfalse timeToIdleSeconds1800 timeToLiveSeconds1800 overflowToDiskfalse/ cache nameauthenticationCache maxElementsInMemory1000 eternalfalse timeToIdleSeconds1800 timeToLiveSeconds1800 overflowToDiskfalse/ cache namestudentPageCache maxElementsInMemory500 eternalfalse timeToIdleSeconds60 timeToLiveSeconds60 overflowToDiskfalse/ /ehcachetimeToIdleSeconds是元素闲置多少秒后被移除timeToLiveSeconds是元素从创建到过期的最长存活时间。两者都设置时先到者生效。授权缓存设置 30 分钟意味着管理员改完角色权限后最长 30 分钟缓存过期用户权限才刷新。如果这不可接受就需要在修改权限的接口里主动清缓存。memoryStoreEvictionPolicyLRU表示内存满了之后淘汰最久未使用的元素。对授权缓存这种“总量可控、单次读取频繁”的数据LRU 是合理的。注意overflowToDiskfalse—— 权限数据放磁盘没有意义反而引入 IO 延迟。配置类中让 Shiro 的 CacheManager 指向这个 Ehcache 实例Bean public EhCacheManager ehCacheManager() { EhCacheManager cacheManager new EhCacheManager(); cacheManager.setCacheManagerConfigFile(classpath:ehcache.xml); return cacheManager; } Bean public SecurityManager securityManager(UserRealm userRealm, EhCacheManager ehCacheManager) { DefaultWebSecurityManager securityManager new DefaultWebSecurityManager(); securityManager.setRealm(userRealm); userRealm.setCacheManager(ehCacheManager); return securityManager; }setCacheManager是AuthorizingRealm父类的方法必须等 SecurityManager 创建之前调用。Shiro 内部有默认的缓存命名约定授权缓存默认叫authorizationCache认证缓存默认叫authenticationCache这正好对应 ehcache.xml 中前两个 cache 的 name。如果自定义缓存名称需要在 realm 上手动指定setAuthenticationCacheName和setAuthorizationCacheName。4.3 Cacheable 注解缓存学生列表注意 key 和失效时机对于学生分页列表这类查询频率高、数据变化不频繁的接口直接在 Service 层加缓存注解Override Cacheable(cacheNames studentPageCache, key #pageNum _ #pageSize _ (#keyword null ? : #keyword)) public PageResultStudent selectPage(int pageNum, int pageSize, String keyword) { return studentMapper.selectPage(pageNum, pageSize, keyword); }cacheNames对应 ehcache.xml 里的studentPageCachekey用 SpEL 表达式拼接分页参数和查询关键字不同查询条件缓存互不干扰。这里有个隐患#keyword为 null 时直接拼接字符串会得到1_10_null下次传入空字符串时 key 变成1_10_导致同一份数据被缓存两份。解决方式是像上面代码一样做一次空值归一化。缓存失效的处理比缓存本身更关键。新增、修改、删除学生后列表缓存已经过期必须主动清除Override CacheEvict(cacheNames studentPageCache, allEntries true) public void updateStudent(Student student) { studentMapper.updateById(student); }allEntries true表示清空整个studentPageCache缓存区域而不是删除单个 key。因为分页列表的 key 是分页参数组合出来的你不知道这次修改会影响哪几页的数据全部清空是最简单可靠的做法。代价是清空后的第一次查询会重新走数据库但学生管理系统的写操作频率低这个代价完全可以接受。CacheEvict和Cacheable同时出现在 Service 实现类中时必须注意 Spring AOP 的自调用问题。如果同一个类内部方法调用updateStudent缓存拦截器不会生效。常见做法是 Controller 调 Service 接口或者内部调用时通过AopContext.currentProxy()获取代理对象。新手最容易在这个地方困惑注解写了但缓存完全没起作用。5. 生产环境部署与线上问题排查的五个关键点5.1 打包方式和外部配置文件分离开发环境用spring-boot-maven-plugin直接运行 main 方法即可。生产环境需要打可执行 JAR 包但数据库密码、缓存路径这些配置不应该打进 JAR 中。使用 Spring Boot 的配置外部化机制在 JAR 同级目录放一个application-prod.yml启动命令显式指定mvn clean package -DskipTests java -jar studentms.jar --spring.profiles.activeprod --spring.config.location./application-prod.ymlspring.config.location指定外部配置文件路径覆盖 JAR 内部的配置。这样换环境部署时不用重新打包运维直接改外部配置里的数据源和端口。MySQL 连接串建议追加connectTimeout5000socketTimeout60000避免数据库暂时不可用时请求线程无限等待。5.2 Layui 表格请求失败的三种典型表现管理页面常见的故障是表格区域一直转圈不加载数据。打开浏览器开发者工具Network 面板看请求状态。第一种情况请求返回 302 且 Location 指向/login说明当前 Session 失效或未登录Layui 的 table 请求默认携带 Same-Origin 凭据但 Shiro 的拦截规则没有放行/student/list接口需要检查过滤链配置。第二种情况请求返回 200 但表格空白多半是响应 JSON 格式不符合 Layui 表格的约定。Layui 要求返回结构为{code:0, msg:, count:100, data:[...]}其中 count 是总数data 是当前页数据。如果把整个 PageResult 对象直接返回而不拆出count和data表格就渲染不出来。第三种情况接口报 500查看后端日志常见原因是 PageHelper 分页插件没有在 MyBatis 配置中启用Configuration public class MybatisConfig { Bean public PaginationInterceptor paginationInterceptor() { return new PaginationInterceptor(); } }如果使用 MyBatis-Plus分页插件版本要和 SpringBoot 版本匹配。MyBatis-Plus 3.4.x 对应 SpringBoot 2.x 没问题但 3.5.3 之后的版本在 SpringBoot 2.5 上有兼容隐患可以把PaginationInterceptor换成MybatisPlusInterceptor并添加PaginationInnerInterceptor。5.3 Shiro 的授权缓存不刷新问题清缓存接口和触发时机上一章提到角色权限修改后授权缓存 30 分钟才过期运维反馈“用户权限改了半小时还没生效”。解决思路有两种优先推荐第二种。第一种是把授权缓存的timeToLiveSeconds改成 60 秒代价是每个用户每分钟最多一次权限查询数据库用户量大时数据库压力变大。第二种是做一个主动清缓存接口GetMapping(/admin/clearShiroCache) ResponseBody RequiresRoles(admin) public Result clearShiroCache(String username) { if (StringUtils.hasText(username)) { User user userService.selectByUsername(username); if (user ! null) { userRealm.clearCachedAuthorizationInfo(SecurityUtils.getSubject().getPrincipals()); return Result.success(已清除用户 username 的授权缓存); } return Result.error(用户不存在); } return Result.success(未指定用户缓存保留); }clearCachedAuthorizationInfo是AuthorizingRealm提供的方法传入PrincipalCollection即可清除该用户的授权信息。如果修改了角色的权限但用户未重新登录PrincipalCollection里的用户信息仍然有效调用这个方法就能生效。注意这个接口必须加管理员角色限制否则任何登录用户都能清除别人的缓存虽然这不是安全事故但会造成不必要的数据库查询。5.4 Session 超时和 RememberMe 的配合策略Shiro 默认 Session 超时时间是 30 分钟用户在使用系统时如果长时间不操作Session 过期后任意点击都会跳回登录页。对于学生管理系统来说30 分钟偏短建议在配置中调长Bean public DefaultWebSecurityManager securityManager() { DefaultWebSessionManager sessionManager new DefaultWebSessionManager(); sessionManager.setGlobalSessionTimeout(2 * 60 * 60 * 1000L); DefaultWebSecurityManager securityManager new DefaultWebSecurityManager(); securityManager.setSessionManager(sessionManager); return securityManager; }setGlobalSessionTimeout以毫秒为单位2 小时是管理系统比较合理的值。但要注意Session 是存在服务器内存中的maxElementsInMemory只控制缓存数量Session 本身没有配置 Ehcache 持久化用户量大时内存会膨胀。学生管理系统的用户量完全不用担心这个问题。RememberMe 功能要谨慎开启它会把用户身份序列化到 Cookie 中如果同时在多个浏览器使用可能出现身份混淆。建议关闭token.setRememberMe(false);5.5 密码明文存储的最后整改MD5 加盐与 Shiro 的匹配器如果系统已经用明文密码运行了一段时间整改分两步数据库中的密码改为MD5(密码 用户名)或MD5(密码 随机盐)Shiro 这边配置对应的凭证匹配器Bean public HashedCredentialsMatcher credentialsMatcher() { HashedCredentialsMatcher matcher new HashedCredentialsMatcher(); matcher.setHashAlgorithmName(MD5); matcher.setHashIterations(1); return matcher; }然后在UserRealm构造函数中传入credentialsMatcher。修改密码时用同样的算法生成密文再存库String encryptedPassword new SimpleHash(MD5, rawPassword, username, 1).toHex();注意盐用的字段要和 Realm 中SimpleAuthenticationInfo传入的盐一致。如果 Realm 里new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(user.getUsername()), getName())没有传盐那么匹配器在验证时会用空盐计算密码永远匹配不上。最常见的整改报错就是这个盐参数不对齐。setHashIterations(1)表示只做一次 MD5迭代次数越多越安全但也要考虑登录耗时一般 1024 次以内没问题。5.6 用 Arthas 在线诊断权限校验卡顿权限接口响应慢但本地复现不了生产环境又没有配置 SkyWalking 这类链路追踪。推荐用 Arthas 在线诊断不需要重启服务curl -O https://arthas.aliyun.com/arthas-boot.jar java -jar arthas-boot.jar启动后选择学生管理系统的进程 PID然后执行 trace 命令跟踪权限查询链路的耗时trace com.example.studentms.service.impl.UserServiceImpl selectPermissionsByUserId。这个命令会打印方法内部每一步的耗时分布很快就能看出是数据库查询慢还是缓存未命中导致走了全量 SQL。如果输出的耗时集中在 MyBatis 的 Mapper 调用上检查该 SQL 的索引是否生效EXPLAIN SELECT DISTINCT p.perms FROM sys_user u INNER JOIN sys_user_role ur ON u.id ur.user_id ...重点看key列如果为 NULL 说明没有走索引。sys_user_role和sys_role_permission两张关联表外键字段必须建索引否则联表查询随着数据量增长性能下降非常明显。对学生管理系统的数据量来说这个优化一次到位。Ehcache 的命中率也可以通过日志观察。在application-prod.yml中加一行配置logging.level.net.sf.ehcacheDEBUG然后观察日志中 Cache hit 和 Cache miss 的比例。如果 miss 率长期超过 30%说明缓存 key 设计不当或过期时间太短需要重新审视Cacheable的 key 组成。部署完成后建议把所有配置项按环境归纳成一张表MySQL 连接、Ehcache 磁盘路径、Shiro 会话超时、日志级别。开发环境用本地 MySQL生产环境换独立的库仅此一项就能避免大部分因为配置混用导致的低级故障。真正把一个管理系统做稳定靠的不是某个炫技的框架而是把这些边角配置一项项抠干净。本文还有配套的精品资源点击获取