ARTICLE DETAIL

资讯详情

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

SSM整合Shiro教务系统:MyBatis Generator与权限控制实战解析

SSM整合Shiro教务系统:MyBatis Generator与权限控制实战解析 简介基于 Java 开发的教务查询系统定位为 SSM 整合实战练手项目主要面向已有 Java 基础、希望熟悉后端分层开发流程的初学者用于帮助理解框架协同工作的核心思路。资源以 zip 压缩包提供共 271 个文件、大小 35.32MB其中 55 个 Java 源文件构成业务逻辑主体76 个 class 文件为编译产物42 个 XML 配置文件负责 Spring 与 Mybatis 的整合31 个 jar 依赖库支撑项目运行28 个 JSP 页面展示前台界面另有 CSS、JavaScript、SQL 脚本和文档方便部署和二次开发。功能上覆盖登录、选课、课程管理等常见教务场景后端采用 Spring 管理对象、SpringMVC 处理请求、Mybatis 操作 MySQL 数据库同时集成 Shiro 权限控制、C3P0 连接池和 Bootstrap 前端框架整体代码结构层次分明适合反复对照练习。目前已有 411 人浏览学习通过源码与配置可梳理 Controller、Service、Mapper 之间的调用关系理解 Shiro 过滤规则、事务控制和 SQL 初始化过程还能学习包结构规划与错误排查方式。整个项目从数据库脚本到前端页面成体系呈现对希望独立搭建类似 Java Web 管理系统并积累实战经验的学习者具有清晰直接的参考价值。1. 为什么拿SSMShiro搭教务查询系统值得再拆一遍这个项目是一个简单的教务查询系统用来练手SSM整合开发的。实际部署后你会发现它比普通的“增删改查”多了一层关键的部分权限。你在编译产物里看到的CourseExample$GeneratedCriteria、TeacherExample$Criteria这类类名说明底层用了 MyBatis Generator 自动生成映射代码这在 java后端 项目里是一个很经典的工程习惯。适合刚走完 java学习路线 的开发者拿来对照也适合工作三五年的人回头验证自己整合 Spring SpringMVC Mybatis 时遗漏的细节。技术栈是SpringIOC容器、SpringMVCWeb框架、MybatisORM框架、Shiro安全框架加 C3P0 连接池、log4j 日志、Bootstrap 前端。解决的问题很具体学生、教师、课程、选课记录的多角色查询与权限分离。不需要微服务就是最典型的中小规模 Java Web 管理系统。对于想快速熟悉 SSM 的开发者这个项目的配置方式几乎就是标准答案。但直接打开一个 SSM 项目最劝退的是配置链太长web.xml要加载 Spring 和 SpringMVC 两个容器Mybatis 要交给 Spring 管理Shiro 又要拦截所有请求。所以这篇不讲空理论直接拆整合步骤把最容易卡住的地方一个个过掉。2. SSM整合前的工程结构设计与Mybatis映射层拆解SSM 项目最怕的不是写业务而是工程结构没定好后面写代码时到处找包。这个项目的包结构是典型的 maven-web 分层controller、service、dao、entity、util。实体关系和表设计先理清楚再看 Mybatis Generator 怎么把这些 class 生成出来。2.1 教务系统最简表结构课程、学生、选课从 class 文件名能反推出核心表course、teacher、student、selectedcourse、userlogin、role。用户登录表和角色表分开学生和教师是独立实体选课表做关联。最小可运行的表结构如下表名核心字段说明userloginid, username, password, role_id登录账号不直接关联业务表roleid, role_name, description角色admin、teacher、studentteacherid, user_id, teacher_name, title教师信息关联 userloginstudentid, user_id, student_name, class_name学生信息关联 userlogincourseid, course_name, teacher_id, credit课程外键指向 teacherselectedcourseid, course_id, student_id, score选课记录成绩字段这里有个设计取舍学生和教师没有放在一张 user 表里而是分开表。这种冗余在简单教务系统里很常见因为教师和学生要维护的扩展字段完全不同。如果用单表 user 加 type 区分Mybatis 生成实体时会很别扭查询也要到处判断类型。反过来前后端只靠role表控制权限边界登录后userlogin表查出 user_id再按角色去查对应的 teacher 或 student 表。2.2 用 MyBatis Generator 生成 Example 与 Criteria 类项目里出现的CourseExample$GeneratedCriteria是 MyBatis Generator 自动生成的内部类。手动写dao和mapper.xml时最枯燥的就是拼接查询条件按课程名查、按教师查、按成绩范围查每一种组合都要写 if 判断。Generator 生成Example类以后条件全部变成 Java 对象的方法调用。Generator 的配置文件generatorConfig.xml核心内容如下generatorConfiguration context idmysqlContext targetRuntimeMyBatis3 defaultModelTypeflat jdbcConnection driverClasscom.mysql.jdbc.Driver connectionURLjdbc:mysql://localhost:3306/edu userIdroot password123456 / javaModelGenerator targetPackagecom.edu.entity targetProjectsrc/main/java / sqlMapGenerator targetPackagecom.edu.dao targetProjectsrc/main/resources / javaClientGenerator typeXMLMAPPER targetPackagecom.edu.dao targetProjectsrc/main/java / table tableNamecourse / table tableNameteacher / table tableNamestudent / table tableNameselectedcourse / /context /generatorConfigurationtargetRuntimeMyBatis3会生成XXXExample和XXXWithBLOBs这种风格的类defaultModelTypeflat让每个表只生成单独实体类不拆 key 和 base代码更简洁。javaClientGenerator指定为XMLMAPPER接口和 XML 分离方便维护。每张表一行table一旦表结构变动重新跑一次 Generator 就会覆盖旧文件不会影响手写的扩展方法。用 Maven 插件执行生成mvn mybatis-generator:generate -Dmybatis.generator.overwritetrueoverwritetrue是覆盖已有实体类和 XML但不会覆盖手动加的额外方法。我一般会把这个插件放在 profile 里避免每次打包都触发。2.3 Example 与 GeneratedCriteria 在查询中的真实用法Generator 生成后最常用的查询写法是用CourseExample.Criteria拼接动态条件。CourseExample example new CourseExample(); CourseExample.Criteria criteria example.createCriteria(); if (courseName ! null !courseName.isEmpty()) { criteria.andCourseNameLike(% courseName %); } if (teacherId ! null) { criteria.andTeacherIdEqualTo(teacherId); } example.setOrderByClause(course_id desc); ListCourse courseList courseMapper.selectByExample(example);createCriteria()创建条件对象andXxxLike、andXxxEqualTo这些方法内部会把条件装进GeneratedCriteria。这里要注意同一个example只能调用一次createCriteria()多次调用会覆盖前一个条件。如果有多组 or 条件要用example.or()后继续createCriteria。setOrderByClause是直接拼接在 SQL 末尾的传字符串就行但如果里面含用户输入就会有注入风险必须用白名单校验排序字段。代码里的selectByExample是 XML 里的动态 SQL它会根据 Example 中是否有条件自动选择WHERE还是isEmpty。所以哪怕条件全为空也会执行一个select * from course不会报语法错误。这也是只用 One-to-Many 简单查询时最稳的方案。2.4 生成代码之后最容易踩的坑GeneratedCriteria是 Example 的内部类正常业务代码不需要直接操作它。但有一个坑很隐蔽生成的updateByExampleSelective方法如果传入的record里字段都是 null生成的 SET 子句会是set id ?把主键一起改了。类似情况还有selectByPrimaryKey依赖主键策略如果表主键不是自增而是业务键需要手动改生成的insert语句。另一个常见问题是LIKE语句的拼接。MySQL 中LIKE %abc%没问题但 Generator 生成的条件不会自动加转义当用户输入了%或_时会被当成通配符。我一般会写一个工具方法把用户输入的%_\手动转义后再放进andCourseNameLike。3. SpringMVC控制器到ServiceImpl的事务边界设计这个项目的 Controller 层很薄业务逻辑放 Service。手写 SSM 工程时最常见的问题是事务注解乱放有人把Transactional放在 Controller 方法上有人放在 DAO 层结果事务要么失效要么粒度过粗。这一章拆清楚边界。3.1 Controller 只做参数绑定与视图转发以课程查询为例Controller 代码贴近实际项目写法Controller RequestMapping(/course) public class CourseController { Autowired private CourseService courseService; RequestMapping(/list) public String list(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 5) Integer pageSize, String courseName, Model model) { PageResultCourse result courseService.queryPage(pageNum, pageSize, courseName); model.addAttribute(page, result); return course/list; } }RequestParam(defaultValue 1)解决了用户不传页码时报错的问题Model直接放分页对象视图层用 Bootstrap 的分页组件渲染。这里的 Controller 不做业务判断只做了参数绑定和返回视图名。逻辑层全部交给CourseService这样写单元测试时构造 MockMvc 只需要给 Service 层打桩不用启动完整 Web 容器。3.2 Transactional 放在 Service 实现类而不是 ControllerSpring 事务是基于 AOP 的默认只拦截 RunTimeException。把注解放在 Controller 上的问题是Controller 会被代理但异常抛到视图渲染时已经脱离了事务边界回滚不可控。放 Service 实现类并且用rollbackFor Exception.class处理检查异常Service public class CourseServiceImpl implements CourseService { Autowired private CourseMapper courseMapper; Autowired private SelectedcourseMapper selectedcourseMapper; Transactional(rollbackFor Exception.class) public void addCourseWithScore(Course course, ListScoreItem items) { courseMapper.insertSelective(course); for (ScoreItem item : items) { selectedcourseMapper.insertScore(item); } } }这里插入课程和批量插入成绩是同一个事务只要一个成绩插入失败课程也不会被写入。insertSelective是 Generator 生成的方法它会跳过 null 字段避免数据库默认值被覆盖成 NULL。循环里逐条insertScore性能一般数据量大时应该用ExecutorType.BATCH但项目里选课记录量不大逐条反而好排查。Spring 的 XML 开启事务管理的配置bean idtxManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource / /bean tx:annotation-driven transaction-managertxManager /txManager必须引用同一个dataSourcebean如果 C3P0 数据源和 Mybatis 数据源不是同一个实例事务就完全失效了。这是 SSM 整合中非常容易踩的坑。3.3 分页查询的模型与手写 PageBean项目没贴出用什么分页依赖但从规模看手写分页最常见。我一般用 PageBean 封装 PageHelper 之外的轻量方案public class PageResultT { private ListT list; private int pageNum; private int pageSize; private long total; private int totalPages; }Service 层查询逻辑public PageResultCourse queryPage(int pageNum, int pageSize, String courseName) { CourseExample example new CourseExample(); if (StringUtils.hasText(courseName)) { example.createCriteria().andCourseNameLike(% courseName %); } long total courseMapper.countByExample(example); int offset (pageNum - 1) * pageSize; example.setOffset(offset); example.setLimit(pageSize); ListCourse list courseMapper.selectByExample(example); return new PageResult(list, pageNum, pageSize, total); }countByExample和selectByExample用的是同一个 Example 对象条件保持一致。setOffset和setLimit不是 Generator 默认生成的需要手动在CourseMapper.java加两个默认方法在 XML 里用#传参。这里不能用LIMIT #{offset}, #{pageSize}以外的写法因为setLimit在 MySQL 里要拼接成limit n如果直接用拼接字符串会产生 SQL 注入漏洞。当表数据量超过几十万时countByExample会比较慢这时可以在 SQL 里用分页 count 优化但中小系统不用管。4. Shiro安全框架下的登录认证与角色访问控制Shiro 是这个项目里最能拉源码阅读量的模块。它与 Spring Security 的区别是Shiro 不侵入 Spring整个认证和授权过程与 Spring 的Session、Request解耦。对于java面试中经常问到的认证逻辑这里正好用实际项目拆开。4.1 Shiro 的核心链路Subject、SecurityManager、RealmShiro 的调用链一句话讲所有请求都变成SubjectSubject把认证和授权交给SecurityManagerSecurityManager再查询一个或多个Realm。Realm是唯一写数据访问的地方。在教务系统里Realm 的职责是先根据用户名查userlogin表拿到密码休和 salt再查角色表拿到权限字符串。这个查询组合起来就是一次登录校验。4.2 自定义 Realm 实现登录认证public class UserRealm extends AuthorizingRealm { Autowired private UserloginService userloginService; Autowired private RoleService roleService; Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token) throws AuthenticationException { UsernamePasswordToken upToken (UsernamePasswordToken) token; String username upToken.getUsername(); Userlogin user userloginService.findByUsername(username); if (user null) { throw new UnknownAccountException(账号不存在); } return new SimpleAuthenticationInfo( user, user.getPassword(), ByteSource.Util.bytes(user.getSalt()), getName()); } Override protected AuthorizationInfo doGetAuthorizationInfo( PrincipalCollection principals) { Userlogin user (Userlogin) principals.getPrimaryPrincipal(); SimpleAuthorizationInfo info new SimpleAuthorizationInfo(); Role role roleService.findById(user.getRoleId()); info.addRole(role.getRoleName()); return info; } }SimpleAuthenticationInfo的第三个参数是 salt 字节源Shiro 在校验密码时会用这个 salt 加上前端传输的密码做一次哈希。如果数据库里的密码是用 MD5(password salt) 存的这里必须和存储方式一致否则认证永远失败。doGetAuthorizationInfo只做角色加载项目里没有细粒度权限按钮所以只把admin、teacher、student角色名放进了SimpleAuthorizationInfo。如果用注解式权限控制后续可以在这里addStringPermission。4.3 URL 过滤器链配置spring-shiro.xml里的过滤链是这个系统的权限边界bean idshiroFilter classorg.apache.shiro.spring.web.ShiroFilterFactoryBean property namesecurityManager refsecurityManager / property nameloginUrl value/login / property nameunauthorizedUrl value/403 / property namefilterChainDefinitions value /login anon /logout logout /course/list roles[teacher,student,admin] /student/** roles[admin,teacher] /teacher/** roles[admin] /** authc /value /property /beananon放行登录页logout是 Shiro 内置退出过滤器roles[teacher,student,admin]表示这三种角色都能访问authc表示必须登录。这里的顺序很关键/**必须放在最后否则后面的规则全部失效。如果想让某部分接口只做认证不做角色区分就用authc而不是roles。4.4 密码匹配与 Salt 处理密码对比不放在 Realm 里写 if而是交给HashedCredentialsMatcher配置bean idcredentialsMatcher classorg.apache.shiro.authc.credential.HashedCredentialsMatcher property namehashAlgorithmName valuemd5 / property namehashIterations value1 / /bean bean iduserRealm classcom.edu.shiro.UserRealm property namecredentialsMatcher refcredentialsMatcher / /beanhashAlgorithmNamemd5、hashIterations1与注册时加密的方式完全一致。如果哈希迭代次数不匹配登录一直报 IncorrectCredentialsException但日志里没有详细原因这是 Shiro 最坑的配置点。注册时我一般写成String hashPwd new SimpleHash(md5, rawPassword, saltBytes, 1).toString();SimpleHash的第一个参数是算法名第二个是明文第三个是 salt第四个是迭代次数。任何一个参数和 matcher 配置不一致登录都会静默失败。因为 Shiro 不会告诉你 hash 不一致只报密码错误。5. C3P0连接池与log4j排查SSM启动慢、连接泄漏的实战SSM 项目里连接池选 C3P0较新的 HikariCP 性能更好但老项目里 C3P0 稳。真正麻烦的是 C3P0 配置不对启动慢得离谱或者用得久了请求卡死。这一章直接从配置和日志两个方向拆。5.1 C3P0 数据源配置参数解释bean iddataSource classcom.mchange.v2.c3p0.ComboPooledDataSource destroy-methodclose property namedriverClass valuecom.mysql.jdbc.Driver / property namejdbcUrl valuejdbc:mysql://localhost:3306/edu?useSSLfalseamp;characterEncodingutf8 / property nameuser valueroot / property namepassword value123456 / property nameinitialPoolSize value3 / property nameminPoolSize value3 / property namemaxPoolSize value20 / property nameacquireIncrement value3 / property namemaxIdleTime value60 / property namecheckoutTimeout value5000 / /beaninitialPoolSize是启动时创建的连接数设成 3 会让 Tomcat 启动变快一点因为不用等 Mybatis 第一次查询才建立连接。maxIdleTime60是空闲 60 秒后连接被回收必须比 MySQL 的wait_timeout小否则 MySQL 把连接断了C3P0 还以为是好的下次请求拿到死连接直接抛异常。checkoutTimeout5000表示连接池没有空闲连接时等 5 秒还拿不到就丢弃请求。并发高的时候把它调大并发低则调小否则会出现“请求一直卡在池子里”的现象。连接池销毁时要用destroy-methodcloseSpring 容器关闭时才能释放连接否则 MySQL 端会留下大量 sleep 连接。5.2 C3P0 与 Mybatis 整合时的属性传递把 dataSource 直接交给 SqlSessionFactorybean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource / property namemapperLocations valueclasspath:com/edu/dao/*.xml / property nametypeAliasesPackage valuecom.edu.entity / property nameconfigurationProperties props prop keymapUnderscoreToCamelCasetrue/prop /props /property /beanmapUnderscoreToCamelCase会自动把teacher_name映射到teacherNameSetter 都能对上实体不用加TableField这种注解。如果数据库字段是course_id实体属性是courseId这个配置开启前查出来是 null开启后正常。这个坑常常被当成“C3P0 连接问题”去排查其实和连接池没关系。5.3 用 log4j 定位启动慢与 SQL 错误log4j 配置里只要把com.edu.dao这个包设为 DEBUG就可以打印 Mybatis 执行的所有 SQL 和参数log4j.logger.com.edu.daoDEBUG log4j.logger.org.mybatisINFO启动慢的时候优先看日志里有没有Initializing c3p0 pool...后面卡住。一般原因是 C3P0 启动时试图获取连接但 MySQL 连不上或者是 jdbcUrl 里的字符集配置不对。如果看到Connections could not be acquired或Data source rejected establishment of connection优先检查 MySQL 端口、账号权限而不是代码。如果系统运行一段时间后突然变慢日志里出现Connection is closed那基本是maxIdleTime比 MySQL 的wait_timeout大连接被 MySQL 端回收。执行一下show variables like wait_timeout确认后设 C3P0 的maxIdleTime为 60问题就消失。5.4 连接泄漏的监控方式C3P0 自带com.mchange.v2.c3p0.management.ActiveManagementCoordinator开启 JMX 后可以用 JConsole 查看连接池使用情况。更简单的方式是定期执行mysql -u root -p -e show processlistsleep 状态的连接数量持续增长且 Command 列全是Sleep再查time大于 60 的连接来自哪个端口再到 C3P0 的checkoutTimeout配置确认没有回收。如果还是没有释放检查代码里有没有手动从Connection拿数据源连接但没关闭的地方。C3P0 对线程数比较敏感acquireRetryAttempts默认是 30数据库恢复前会一直重试这是日志里大量异常的原因开发环境可以把它调成 3。6. 用BootstrapAjax改造教务查询的异步刷新与权限按钮这个项目前端框架是 Bootstrap这里给出一个实用技巧把原来表单同步刷新改为 Ajax 异步查询并根据 Shiro 的角色动态渲染权限按钮改动量不大但体验提升明显。6.1 把表单查询改成 Ajax 分页原来course/list是表单 GET 请求改造成 Ajax 时Controller 可以直接返回 JSONRequestMapping(/listJson) ResponseBody public PageResultCourse listJson( RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 5) Integer pageSize, String courseName) { return courseService.queryPage(pageNum, pageSize, courseName); }前端用 Bootstrap 的表格和白页组件渲染function loadPage(pageNum) { $.get(/course/listJson, { pageNum: pageNum, pageSize: 5, courseName: $(#courseName).val() }, function (data) { var html ; $.each(data.list, function (i, course) { html trtd course.courseName /td/tr; }); $(#courseTable tbody).html(html); }); }ResponseBody加上后返回 Json顺序是PageResult里的list字段注意字段名和前端取值对齐。6.2 根据 Shiro 角色控制按钮显隐服务端在渲染页面时用 Shiro 的shiro:hasRole标签控制按钮但 Ajax 渲染的 HTML 里不能直接用标签。我一般把当前角色字符串放在页面全局变量里var currentRole $(meta[namerole]).attr(content);后端在控制器里取得当前登录用户角色Subject subject SecurityUtils.getSubject(); String role ((Userlogin) subject.getPrincipal()).getRoleName(); model.addAttribute(currentRole, role);渲染时根据角色拼接按钮。admin才能看到“教师管理”入口教师和学生看不到保证前后端两层权限验证。这个技巧对后续接入前端框架 Vue 或 React 也友好权限点通过接口返回前端每次渲染前先检查角色集合而不是依赖 Cookie 骗过页面。这就是把刚登录的 Shiro 认证信息延伸到前端的最直接方式也是这个练手项目里最有实际价值的一个改造点。本文还有配套的精品资源点击获取
返回列表