ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java SSM教务系统项目剖析:从三层架构到Shiro权限控制

Java SSM教务系统项目剖析:从三层架构到Shiro权限控制 简介这是一份基于Java开发的教务查询系统练手项目适合正在学习SSM框架整合的后端开发者。项目采用SpringSpringMVCMybatis搭建三层架构并整合Shiro实现权限控制前端使用Bootstrap构建页面覆盖学生、教师、课程管理等典型教务场景。压缩包共271个文件约35.32MB包含55个Java源码、76个class字节码、42个XML配置、28个JSP页面、31个Jar依赖以及SQL脚本和文档便于直接导入IDE运行与二次学习。目前已有411人学习浏览通过该项目可以掌握SSM整合流程、用户登录鉴权和C3P0数据源配置等关键技能还能参考其代码结构规范作为课程设计或简历项目的良好起点。1. 这套基于 Java 的教务系统为什么适合当后端练手项目做 Java 后端最尴尬的阶段是学完 SSM 但没写过一个完整项目。这套基于 Java 开发的教务系统恰好是个标准的练手级 SSM 整合项目Spring 管对象、SpringMVC 接请求、MyBatis 操作 MySQL再加上 Shiro 做登录和权限控制。麻雀小但五脏全尤其适合正在跟 Java 学习路线走、准备做课程设计或写进简历的阶段。它的核心业务是「教务查询」——学生查课表、查成绩老师维护课程信息管理员管账号和角色。你不需要先理解复杂的业务规则而是把精力全部放在熟悉 SSM 整合开发这一件事上。对刚学完 Java 基础、准备接触框架的同学来说这套代码比那些动辄十几个模块的商业项目友好得多。我见过不少人在第一个项目上被业务绕晕反而把框架本身的主线丢了。2. 从 Controller 到 Mapper把系统按三层架构拆开看拿到这个项目先别急着跑起来。我一般会先按类名把整个工程的结构捋一遍搞清楚每个请求是怎么从浏览器走到数据库的。这套教务系统的代码产物是编译后的 class 文件类名非常清晰地暴露了包结构和职责边界顺着类名就能反推出源码的组织方式。2.1 先看懂 Controller 层的请求入口AdminController 是系统中比较核心的控制器负责管理员相关的所有操作。在 SSM 架构里Controller 只做三件事接收参数、调用 Service、返回视图或数据。看代码结构时你先找 Controller 注解的类再看类里的每个方法上的 RequestMapping 注解就能拼出完整的 URL 映射表。Controller RequestMapping(/admin) public class AdminController { Autowired private AdminService adminService; RequestMapping(/login) public String login(String username, String password, Model model) { // 调用业务层完成登录校验 Admin admin adminService.login(username, password); if (admin ! null) { // 登录成功后跳转到管理员首页 return admin/index; } model.addAttribute(msg, 用户名或密码错误); return admin/login; } }这里的 RequestMapping(/admin) 是类级别的路径前缀方法上的 RequestMapping(/login) 是具体路径两者拼起来就是完整的访问路径 /admin/login。Autowired 是 Spring 的依赖注入把 AdminService 实例注入进来你不需要手动 new。返回的字符串是逻辑视图名SpringMVC 会通过视图解析器找到对应的 JSP 页面。2.2 Service 层业务逻辑和事务边界就在这一层Service 层是很多初学者容易忽略的地方。有人图省事直接在 Controller 里写业务逻辑这在 SSM 项目里是坏味道。这个教务系统的业务逻辑虽然简单但结构是标准的Controller 调 Service 接口Service 实现类处理业务实现类里再用 Transactional 控制事务。Service Transactional public class AdminServiceImpl implements AdminService { Autowired private AdminMapper adminMapper; Override public Admin login(String username, String password) { // 按用户名查询密码校验放在业务层做 AdminExample example new AdminExample(); AdminExample.Criteria criteria example.createCriteria(); criteria.andUsernameEqualTo(username); ListAdmin admins adminMapper.selectByExample(example); if (admins ! null admins.size() 0) { Admin admin admins.get(0); // 简单项目直接比对明文密码生产环境必须用加密 if (admin.getPassword().equals(password)) { return admin; } } return null; } }Service 注解标记这是一个 Spring 管理的业务 BeanTransactional 是事务注解——方法里任何一个数据库操作报错整个事务回滚。这段代码里值得注意的地方是选用了 AdminExample Criteria 查询这正是 MyBatis 逆向工程生成的代码风格后面的章节我会专门讲。2.3 Mapper 层与 XML 映射文件怎么配合Mapper 层是 MyBatis 的核心它由接口和 XML 映射文件两部分组成。你看到的 TeacherMapper、StudentMapper、CourseMapper 这些接口定义了数据库操作方法而对应的 XML 文件里写着真正的 SQL 语句。看这套系统的代码逆向工程生成的特征很明显每个实体类对应一个 Mapper 接口接口方法名和 XML 里的 id 一一对应。mapper namespacecom.example.mapper.StudentMapper !-- 实体类属性和数据库字段的映射关系 -- resultMap idBaseResultMap typecom.example.pojo.Student id columnid propertyid jdbcTypeINTEGER/ result columnname propertyname jdbcTypeVARCHAR/ result columnpassword propertypassword jdbcTypeVARCHAR/ result columnrole propertyrole jdbcTypeVARCHAR/ /resultMap !-- 动态查询条件 -- sql idExample_Where_Clause where foreach collectionoredCriteria itemcriteria separatoror if testcriteria.valid trim prefix( prefixOverridesand suffix) foreach collectioncriteria.criteria itemcriterion choose when testcriterion.noValue and ${criterion.condition} /when when testcriterion.singleValue and ${criterion.condition} #{criterion.value} /when /choose /foreach /trim /if /foreach /where /sql /mappernamespace 必须和 Mapper 接口的全限定名一致这是 MyBatis 找映射文件的依据。resultMap 定义数据库字段和 Java 属性的对应关系如果字段名和属性名完全一致也可以省略但逆向工程总会生成一份留着没坏处。Example_Where_Clause 是逆向工程动态 SQL 的核心逻辑它根据查询条件自动拼接 where 子句这就是 Example 类能实现灵活查询的原因。2.4 前端页面和后端数据是怎么对接的这个项目的前端用的 Bootstrap后端返回视图名由 SpringMVC 找到 JSP 页面渲染。JSP 里会用 JSTL 标签和 EL 表达式把后端传过来的数据展示出来。比如管理员登录成功后Controller 把管理员信息放进 Model 或 SessionJSP 页面里用 ${sessionScope.admin.name} 就能拿到并显示。看这段的时候注意这套系统是老派做法——页面在后端渲染不是前后端分离。好处是代码直观请求-响应链路短适合学习坏处是页面的静态资源CSS、JS路径要特别小心容易出相对路径找不到文件的坑具体我在后面排查章节会讲。3. 用 Shiro 做登录认证与权限路由Realm 是理解的关键Shiro 是这套系统里最值得花时间研究的组件。很多人第一次接触安全框架时对「过滤器」「认证」「授权」这些概念一头雾水。实际上 Shiro 的设计非常简洁——你只需要搞清楚 Subject、SecurityManager、Realm 这三个核心概念登录认证的流程就全通了。3.1 Shiro 的核心组件和登录流程Shiro 的设计思路是Subject 表示当前用户SecurityManager 是总调度器Realm 是数据源——它负责从数据库查出用户信息交给 SecurityManager 做比对。登录流程是这样的前端提交用户名密码Controller 拿到后包装成 UsernamePasswordToken调用 Subject.login(token) 方法SecurityManager 把 token 交给 Realm 去查库比对。// 登录接口的核心代码 Subject subject SecurityUtils.getSubject(); UsernamePasswordToken token new UsernamePasswordToken(username, password); try { // 这行代码会触发 Realm 的认证方法 subject.login(token); // 登录成功跳转到角色对应的首页 return redirect:/index; } catch (UnknownAccountException e) { model.addAttribute(msg, 账号不存在); return login; } catch (IncorrectCredentialsException e) { model.addAttribute(msg, 密码错误); return login; }UsernamePasswordToken 是 Shiro 封装登录凭证的类携带用户名和密码。subject.login(token) 是认证入口一旦抛出异常就代表认证失败不同异常类型对应不同错误原因。右边的 catch 块分别捕获「账号不存在」和「密码错误」两类常见情况这是最常用的一组异常处理组合。3.2 自定义 Realm认证逻辑就写在 doGetAuthenticationInfo 里Shiro 默认的 Realm 不知道你的用户存在哪张表里所以你要写一个自定义 Realm告诉它去哪里查用户。这个系统里的 UserRealm 就是干这个的。核心方法是 doGetAuthenticationInfo它的返回值就是 Shiro 用来比对的凭证。public class UserRealm extends AuthorizingRealm { Autowired private UserloginMapper userloginMapper; // 认证核对用户名和密码 Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { UsernamePasswordToken upToken (UsernamePasswordToken) token; String username upToken.getUsername(); // 按用户名查数据库 UserloginExample example new UserloginExample(); UserloginExample.Criteria criteria example.createCriteria(); criteria.andUsernameEqualTo(username); ListUserlogin users userloginMapper.selectByExample(example); if (users null || users.size() 0) { // 查不到就抛异常Shiro 会转成 UnknownAccountException return null; } Userlogin user users.get(0); // 第一个参数是身份信息第二个参数是从库里查出的密码 return new SimpleAuthenticationInfo(user, user.getPassword(), getName()); } // 授权查询当前用户的角色权限 Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { Userlogin user (Userlogin) principals.getPrimaryPrincipal(); SimpleAuthorizationInfo info new SimpleAuthorizationInfo(); // 根据角色名加权限字符串要和配置文件里的保持一致 if (admin.equals(user.getRole())) { info.addRole(admin); } else if (teacher.equals(user.getRole())) { info.addRole(teacher); } else { info.addRole(student); } return info; } }doGetAuthenticationInfo 里返回 null 表示查无此人返回 SimpleAuthenticationInfo 表示查到了Shiro 会自动把 token 里的密码和第二个参数做比对。注意这里我没对密码做加密处理因为这是练手项目真实项目里必须用 MD5 加盐或者 BCrypt后面避坑章节我会细说。doGetAuthorizationInfo 是授权方法它返回当前用户的角色集合Shiro 用这些角色去匹配接口上的权限要求。3.3 过滤器链配置哪些页面需要登录才能进Realm 负责「你是谁」过滤器链负责「你能进哪里」。Shiro 的过滤器链在 Spring 配置文件里配定义了一组 URL 路径和对应的访问规则。这套系统的口径很清晰登录页和静态资源放行其他页面都要登录管理员和老师、学生的首页分开。bean idshiroFilter classorg.apache.shiro.spring.web.ShiroFilterFactoryBean property namesecurityManager refsecurityManager/ property nameloginUrl value/login/ !-- 过滤器链定义路径到权限的映射 -- property namefilterChainDefinitions value /static/** anon /login anon /logout logout /admin/** roles[admin] /teacher/** roles[teacher] /student/** roles[student] /** authc /value /property /beananon 表示匿名可访问authc 表示必须登录才能访问roles[admin] 表示必须有 admin 角色。配置的顺序从上到下匹配一旦匹配上就不再往后走所以 /** authc 必须放在最后否则它会把所有路径都拦截掉。logout 是 Shiro 内置的退出过滤器访问 /logout 会自动清理 Session 并跳转到登录页。3.4 C3P0 数据源与 log4j 日志的参数细节数据源是整个项目能跑起来的地基。这个项目用的是 C3P0虽然比现在主流的 Druid 和 HikariCP 老但它胜在配置直观适合学习时看清连接池的每一项参数。我建议你重点关注 initialPoolSize、maxPoolSize 这两个参数的值和关系——很多跑不起来的案例都是连接池太小导致并发时拿不到连接。bean iddataSource classcom.mchange.v2.c3p0.ComboPooledDataSource destroy-methodclose property namejdbcUrl valuejdbc:mysql://localhost:3306/edu_system?useSSLfalseamp;characterEncodingutf8/ property namedriverClass valuecom.mysql.jdbc.Driver/ property nameuser valueroot/ property namepassword value123456/ !-- 连接池初始连接数 -- property nameinitialPoolSize value5/ !-- 连接池最大连接数 -- property namemaxPoolSize value20/ !-- 连接池最小空闲连接数 -- property nameminPoolSize value5/ !-- 连接的最大空闲时间超时会被回收 -- property namemaxIdleTime value600/ /beanjdbcUrl 里有两个参数容易被忽略useSSLfalse 是关掉 SSL 连接因为本地开发环境没有配置证书打开反而会报警告characterEncodingutf8 是告诉 MySQL 用 UTF-8 编码通信不配的话插入中文可能变问号。顺便说一句Java 环境变量配置如果没弄好MySQL 8.0 以上的版本还需要改 driverClass 为 com.mysql.cj.jdbc.Driver这个我在排查章节还会提。4. 用好 MyBatis 逆向工程生成的 Example 类从生成到条件查询实战这套系统里你会频繁看到 CourseExample、StudentExample、TeacherExample 这一串类它们都是 MyBatis 逆向工程自动生成的。很多新手看到这些类名觉得头晕其实它们的规律完全一致——每个实体类对应一个 Example 类Example 的内部类 criteria 用来叠加查询条件。这个设计用好了你写查询代码的效率会明显提升。4.1 逆向工程怎么生成这一堆 Example 类逆向工程是 MyBatis 官方提供的一个工具无论 MyBatis 还是 MyBatis-Plus它都能根据数据库表结构反推出实体类、Mapper 接口和 XML 映射文件。你看到的这些类大概率就是用 generator 插件在命令行或者 IDE 里跑出来的。执行一次生成代码风格和表结构完全一致不会手写出拼写错误。!-- generator.xml 配置文件 -- generatorConfiguration context idmysqlContext targetRuntimeMyBatis3 !-- 数据库连接信息 -- jdbcConnection driverClasscom.mysql.jdbc.Driver connectionURLjdbc:mysql://localhost:3306/edu_system userIdroot password123456/ !-- 实体类生成位置 -- javaModelGenerator targetPackagecom.example.pojo targetProjectsrc/main/java/ !-- Mapper 接口生成位置 -- javaClientGenerator typeXMLMAPPER targetPackagecom.example.mapper targetProjectsrc/main/java/ !-- 表名与实体类名对照 -- table tableNamestudent domainObjectNameStudent/ table tableNameteacher domainObjectNameTeacher/ table tableNamecourse domainObjectNameCourse/ /context /generatorConfigurationtableName 是数据库里的表名domainObjectName 是生成的类名。比如 student 表会生成 Student 实体类、StudentExample 条件类、StudentMapper 接口三件套。targetPackage 定义了生成的包路径要和 Spring 扫描的包路径一致否则注入 Mapper 时会报找不到 Bean 的错误。4.2 Example 条件查询的两种写法与适用场景Example 类最常用的场景是按条件查列表。它有两条路一是用 createCriteria() 创建的条件对象逐条叠加二是用 or() 隔开多个条件组。前者是 AND 关系后者是 OR 关系。这个系统的登录查询就是典型的单条件查询而我更推荐你在写筛选功能时尝试多条件组合。// 场景按姓名模糊查询 限定角色 StudentExample example new StudentExample(); StudentExample.Criteria criteria example.createCriteria(); // 姓名模糊匹配name 列 like %张% criteria.andNameLike(%张%); // 角色精确匹配role 列等于 student criteria.andRoleEqualTo(student); ListStudent students studentMapper.selectByExample(example);createCriteria() 创建的条件组内部是 AND 关系——两个条件都会拼进 SQL 的 where 子句。如果查询条件里还有「或」的需求就要新建一组条件用 example.or() 来连接。这套系统的业务相对简单大多数查询用一组 Criteria 就够但你要知道有 or() 这个能力写复杂筛选时才能想到用。4.3 GeneratedCriteria 在内存里做了什么你看到的 TeacherExample$GeneratedCriteria、StudentExample$GeneratedCriteria 这些内部类是 Example 类的核心引擎。它干的事很简单把 Java 方法调用记录成查询条件对象最后在 XML 映射文件里被解析成 SQL 片段。理解它对排查「为什么条件没生效」特别有帮助。// GeneratedCriteria 的关键逻辑 protected Criteria addCriterion(String condition, Object value, String property) { if (value null) { // 值不能为 null否则会拼出错误的 SQL throw new RuntimeException(Value for property cannot be null); } Criterion criterion new Criterion(condition, value); criteria.add(criterion); return (Criteria) this; } public Criteria andNameLike(String value) { // 把 like 条件记录到条件集合里 addCriterion(name like, value, name); return (Criteria) this; }每个 andXxx 方法都只是把字段名、操作符和值塞进一个 Criterion 对象存到集合里。真正拼接 SQL 是在 XML 映射文件里的 Example_Where_Clause 那段 foreach 中执行的。所以如果你发现条件没生效大概率是先调用了 createCriteria()然后又用了 or()两组条件相互独立产生的效果和预期不一样——这是逆向工程代码里最经典的翻车点。5. 跑通这个项目之前必看的四个坑环境、编码、权限与配置练手项目最大的价值不在代码本身而在帮你提前把坑踩一遍。SSM 整合这个组合的技术栈偏老网上能找到的教程大多基于旧版本环境照抄经常跑不起来。我把这套系统最常见的四类问题整理成排查记录每一条都是实际会发生的事。5.1 MySQL 驱动类找不到明明配了 driverClass 还是报 ClassNotFound现象启动 Tomcat 时控制台抛 ClassNotFoundException指向 com.mysql.jdbc.Driver。原因MySQL 5.x 和 8.x 的驱动类名不一样。5.x 用 com.mysql.jdbc.Driver8.x 改用 com.mysql.cj.jdbc.Driver。如果你本机装的是 MySQL 8但按旧教程配了 5.x 的类名就会找不到类。另外如果 pom.xml 里没引入 mysql-connector-java 依赖即使类名写对了也会报同样的错。解决先去 maven 仓库确认依赖版本再看自己的 MySQL 版本。如果是 8.x把驱动类改成 com.mysql.cj.jdbc.Driver同时给 jdbcUrl 加上 serverTimezoneAsia/Shanghai因为 8.x 默认要求设置时区不设会直接报错。5.2 JSP 页面样式全丢了Bootstrap 的 CSS 加载不出来现象页面能打开功能也正常但整个页面没有样式控制台报 404找不到 static 目录下的 CSS、JS 文件。原因前后端分离的项目里静态资源路径用绝对路径很安全但老式 JSP 项目里必须动态拼接项目名。很多人写成href/css/bootstrap.min.css这在根路径部署时没问题一旦项目上下文路径变成 /edu-system-1.0浏览器请求的路径就变成 /css/bootstrap.min.css 而不是 /edu-system-1.0/css/bootstrap.min.css自然 404。解决在页面顶部用 EL 表达式取上下文路径c:set varctx value${pageContext.request.contextPath}/所有的 CSS、JS、图片引用都写成href${ctx}/css/bootstrap.min.css。这个坑我当初找了两个小时最后发现是少了个 contextPath。5.3 登录后页面跳转不对roles 过滤器不生效现象登录成功后访问管理员页面不仅没放行反而被 Shiro 拦截回登录页但账号角色确实是 admin。原因roles 过滤器和数据库里的角色字段是两回事。Shiro 的 roles[admin] 匹配的是 Subject 拥有的角色而 Subject 的角色来自 Realm 的 doGetAuthorizationInfo 返回值。如果 Realm 里返回的角色名是 管理员配置文件里写的是 admin大小写或叫法不一致就永远匹配不上。解决回到 Realm 的授权方法确认 addRole 的参数和 shiroFilter 配置里的 roles[...] 完全一致。我用项目里现成的角色枚举抄过来而不是凭记忆写字符串从那次之后所有角色名我都统一维护在一处常量类里避免各处魔法字符串不一致。5.4 中文乱码问题插入数据后 MySQL 里显示问号现象管理系统里录入学生姓名刷新后数据库表里显示 ??明明控制台输出正常。原因这是典型的字符集链路问题。MySQL 连接串里没指定 characterEncodingutf8连接池用默认字符集和数据库表字符集不一致。另外 JSP 页面本身的 contentType 没指定 UTF-8或者 Tomcat 的 URIEncoding 不是 UTF-8任何一个环节断了都会乱码。解决三层排查——数据库表字符集设为 utf8mb4jdbcUrl 加上 characterEncodingutf8 参数JSP 页面顶部写% page contentTypetext/html;charsetUTF-8 languagejava %。注意优先看连接参数因为项目可能跑起来了说明 Tomcat 那边一般没问题。6. 把练手项目升级成拿得出手的课程设计分页、统计与权限细化这个系统能跑通只是及格线。如果你准备拿它当 Java 课程设计或者写进简历至少要做三件事分页查询、统计图表、权限细化。三件事做完项目档次明显不一样。第一件是给查询列表加分页。逆向生成的 Mapper 自带的 selectByExample 是全量查询数据少没事但教务系统里的选课记录很容易上万行。引入 PageHelper 插件是 SSM 项目最常见做法在 mybatis-config.xml 里配一个拦截器然后在 Service 层调用 PageHelper.startPage(pageNum, pageSize) 就能自动拼接 LIMIT 语句。注意 startPage 必须紧跟第一条查询语句中间不能有别的数据库操作这是 PageHelper 老用户都知道的玄学但确实好用。第二件是把数据变成图表。可以引入 ECharts后端用一个 DashboardController 统计学生数、课程数、选课数返回 JSON 给前端渲染。这个系统里本来就有 Selectedcourse 选课表用它做数据源头非常顺手。第三件是把权限细化到按钮级。现在这套系统只有角色级别的粗粒度控制你可以给权限表加菜单 ID 字段做成「角色-菜单-按钮」三级结构把 Shiro 的过滤器从 roles 换成 perms例如配置perms[student:delete]这种粒度。做完这一步你在简历里可以写「基于 RBAC 模型的权限管理系统」这句话的分量比「熟悉 SSM」重得多。这个练手项目我前后建议至少完整跑两遍第一遍按默认流程走通第二遍把它当成自家人加一个字段、加一个页面每加一块都强迫自己确认对应表结构和 Mapper 需不需要动。从那以后我每次拿到新的 SSM 老项目都强制先跑通再改、先看清楚逆向生成的代码再动手这个习惯帮我避开了不少自找的麻烦。希望这份拆解对你有用项目目录里每个类都值得你打开看一遍。本文还有配套的精品资源点击获取
返回列表