1. 这个宿舍管理系统到底解决什么问题
如果你在学校宿管科或者后勤部门待过一段时间,就会知道宿舍管理工作远没有外人想得那么轻松。学生入住、退宿、调换房间,维修申报、来访登记、夜间查寝,还有防火防盗安全检查和每月的水电统计,大量的表格和纸质单据在宿管员、辅导员、维修师傅和学生之间流转。一件报修单从学生提交到维修师傅上门,中间往往要经过登记、转交、确认、回访好几个环节,任何一个环节脱节,问题就卡住了,学生只能一遍遍跑到值班室问。
我做这个SSM学生宿舍管理系统,目标就是把这些日常事务集中到一个Web平台里。系统基于Spring + Spring MVC + MyBatis这套经典Java Web技术栈实现,覆盖了宿舍管理的几个核心场景:学生信息维护、宿舍分配与调换、退宿处理、报修工单流转、楼栋与房间状态管理,以及管理员端的统计查看。不同角色各有独立的操作入口,学生在系统里能查自己的住宿信息和提交报修,管理员和宿管员则负责审核、分配和处理日常事务。
对下面这几类人来说,这套系统的参考价值很高:
- 正在做毕业设计或者Java课程设计的同学。SSM是目前高校Java方向最常见的课程要求,一个完整可运行的宿舍管理项目,能帮你把所有该展示的技术点都覆盖到。
- 刚开始接触SSM整合的开发者。很多人照着教程搭环境,但项目一跑起来就各种报错,这套源码的整合方式和配置文件可以作为对照样本。
- 需要给学校或培训机构快速搭建内部管理工具的人。系统本身功能完整,代码结构清晰,拿来改造二次开发都很方便。
我尽量把项目的结构、核心代码逻辑和部署过程都拆开讲清楚,包括我在实际开发和运行中踩过的坑。这样你拿到源码,不只是能跑起来,而是真的能看懂每一层在干什么、遇到问题了知道去哪儿改。
2. 为什么还是SSM:一套经典Java Web技术栈的取舍
我在这套系统里选择SSM,也就是Spring、Spring MVC、MyBatis的组合,可以说既是基于实际需求的考虑,也有技术学习的因素。很多人现在一上来就用Spring Boot,这个没问题,但如果你还在校、课程要求是SSM,或者你想彻底搞清楚Spring容器、MVC请求分发、MyBatis映射这几块底层的协作原理,SSM依然是绕不开的一课。
2.1 三层架构里每个框架扮演的角色
SSM把整个项目按职责拆得很清楚:
Spring(容器层):负责管理所有的Bean对象。从Service层的业务对象,到DAO层的Mapper对象,再到事务管理器,都由Spring容器统一创建和维护。没有Spring的时候,你需要在代码里手动new对象,对象之间的依赖关系完全是混乱的;有了Spring的依赖注入,Service要用哪个Mapper,直接声明一下,容器就帮你装配好。
Spring MVC(表现层):负责接收HTTP请求,解析URL参数,调用Service层处理业务,再把结果通过ViewResolver渲染成JSP页面返回给浏览器。还有一块容易被忽略的重要功能是拦截器,本项目里的登录状态校验就是靠它实现的。
MyBatis(持久层):负责和数据库打交道。SQL语句写在XML映射文件里,应用程序通过Mapper接口调用方法,MyBatis在底层帮你完成JDBC的连接管理、参数绑定、结果集到实体对象的映射。和Hibernate相比,MyBatis的好处是SQL完全可控,宿舍分配这种带条件查询和更新的逻辑,用SQL表达特别直观。
2.2 它和Spring Boot的差别在哪里
Spring Boot本质上是Spring生态的"自动化配置工具",它把Spring MVC、MyBatis等的配置大量简化,用application.yml和一套约定取代了XML配置。对写业务代码的人来说,开发体验确实顺滑很多。
但SSM的好处在于把很多配置细节暴露在了你面前。比如Spring的applicationContext.xml里怎么配置数据源、怎么开启注解扫描、事务增强需要切点表达式;Spring MVC的spring-mvc.xml里静态资源、视图解析器、注解驱动的配置项分别有什么作用。你把这些配置文件挨个看一遍,对框架的理解会有一个质的提升。之后再转Spring Boot,遇到问题排查起来也更有底。
2.3 这样选型对宿舍管理系统的实际好处
宿舍管理系统的业务规模不算大,并发量低,但是逻辑分支多:学生状态可能是入住、搬离、休学;宿舍有不同的床位类型;报修工单的状态流转涉及提交、处理中、完成、评价等多个阶段。SSM的清晰分层让这些业务逻辑分散到Service层里,每个方法对应一个事务边界,测试和排错都很容易。再加上JSP的服务端渲染方式,前后端数据交互不复杂,不引入前端框架也能把管理后台的交互做得足够完整。
3. 拿到源码后先看这几样:项目结构、数据库设计、角色权限
大多数人拿到一套源码,第一反应是直接解压、导入IDE、点运行。其实更明智的做法是先花二十分钟把项目结构和数据库设计看一遍,搞明白代码的分布和数据之间的关系,后面跑起来之后你才知道每个功能去哪改。
3.1 标准Maven工程目录到底长什么样
springmvc项目解压之后,你看到的标准目录结构是:
ssm-dormitory ├── pom.xml ├── src │ ├── main │ │ ├── java │ │ │ └── com/xx/dormitory │ │ │ ├── controller // 控制器层 │ │ │ │ ├── AdminController.java │ │ │ │ ├── StudentController.java │ │ │ │ ├── DormitoryController.java │ │ │ │ └── RepairController.java │ │ │ ├── service │ │ │ │ ├── StudentService.java │ │ │ │ ├── StudentServiceImpl.java │ │ │ │ ├── DormitoryService.java │ │ │ │ └── DormitoryServiceImpl.java │ │ │ ├── dao // Mapper接口 │ │ │ │ ├── StudentMapper.java │ │ │ │ ├── DormitoryMapper.java │ │ │ │ └── RepairMapper.java │ │ │ ├── entity // 实体类 │ │ │ │ ├── Student.java │ │ │ │ ├── Dormitory.java │ │ │ │ ├── Building.java │ │ │ │ └── Repair.java │ │ │ └── interceptor │ │ │ └── LoginInterceptor.java │ │ └── resources │ │ ├── jdbc.properties │ │ ├── spring-mvc.xml │ │ ├── applicationContext.xml │ │ ├── mybatis-config.xml │ │ └── mapper │ │ ├── StudentMapper.xml │ │ ├── DormitoryMapper.xml │ │ └── RepairMapper.xml │ └── webapp │ ├── WEB-INF │ │ ├── web.xml │ │ └── views │ │ ├── login.jsp │ │ ├── student │ │ │ ├── list.jsp │ │ │ └── add.jsp │ │ └── dormitory │ │ ├── list.jsp │ │ └── assign.jsp │ └── static │ ├── css │ └── js很多人会问为什么要分这么多层。一句话:每一层都有自己的职责,谁也不越界。Controller只做参数接收和页面跳转,不写业务;Service负责业务判断和事务边界;DAO只做SQL交互。这样分的好处是——改数据库字段时只需要动DAO层和实体类,不会牵连Controller;改业务规则时只动Service层;页面样式调整则和Java代码完全隔离。
3.2 数据库六张表的关联逻辑
宿舍管理系统数据库我设计了以下核心表,它们之间的关系很直白:
| 表名 | 主要字段 | 作用 |
|---|---|---|
admin | id, username, password, realname | 管理员账号,系统登录入口 |
student | id, student_no, name, gender, phone, building_id, dormitory_id, status | 学生基本信息与住宿关联 |
building | id, building_name, floors, manager_name | 宿舍楼栋基础信息 |
dormitory | id, building_id, room_number, bed_count, bed_used, floor | 具体房间及其床位使用情况 |
repair | id, student_id, dormitory_id, content, status, create_time | 报修工单 |
notice | id, title, content, create_time | 公告通知 |
从表结构可以看出,student表通过building_id和dormitory_id与楼栋、宿舍建立外键关系,这是"一个学生入住某个宿舍"这一核心逻辑的数据基础。dormitory表里的bed_count和bed_used两个字段决定了宿舍是否还有空床,分配学生入住时的关键判断就靠这两个字段。
3.3 前端操作背后的角色权限链路
系统默认只有管理员一种角色登录。登录拦截器会在用户访问任意页面时检查Session中是否存在admin对象:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object admin = request.getSession().getAttribute("admin"); if (admin == null) { String contextPath = request.getContextPath(); response.sendRedirect(contextPath + "/login.jsp"); return false; } if (request.getRequestURI().startsWith(contextPath + "/static/")) { return true; } return true; } }这里有一个细节值得注意:preHandle里返回false表示请求被拦截、不继续向后执行,返回true才放行。同时还需要在spring-mvc.xml里注册这个拦截器,并配置好排除路径,否则静态资源(CSS、JS)也会被拦下来,导致页面完全没有样式。
4. 从空壳到可运行:环境搭建与部署的完整步骤
拿到源码第一步就是让它跑起来。这个项目依赖的标准环境组合是JDK 8、Maven 3.6、MySQL 5.7、Tomcat 8.5,IDE用IDEA或Eclipse都可以。版本不用追新,SSM是老牌技术栈,用这些经典版本反而是最稳的。
4.1 环境准备清单与版本对照
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 | SSM对JDK版本兼容最好,新JDK可能遇到编译问题 |
| Maven | 3.6.x | 依赖管理 |
| MySQL | 5.7或8.0 | 8.0需注意驱动版本为com.mysql.cj.jdbc.Driver |
| Tomcat | 8.5 | 对应Java EE 7规范 |
| IDEA | 2020以上 | 导入Maven项目最方便 |
4.2 数据库导入与连接配置的坑
项目源码里通常有一个sql文件夹,里面是建库建表的SQL脚本。第一步是创建数据库:
CREATE DATABASE IF NOT EXISTS dormitory_ssm DEFAULT CHARACTER SET utf8mb4; USE dormitory_ssm; SOURCE /你的路径/init.sql;数据库初始化时要注意MySQL 5.7和8.0的区别:5.7一般不需要指定useSSL,但8.0版本的驱动类名变成了com.mysql.cj.jdbc.Driver,并且连接串必须带上时区参数,否则会报时区错误。
接下来找到src/main/resources/jdbc.properties文件:
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/dormitory_ssm?useUnicode=true&characterEncoding=utf8 jdbc.username=root jdbc.password=123456一定要确认三件事:数据库名和你建库时的名称一致、用户名密码正确、MySQL服务已经启动。很多人在这一步报错,八成是密码或库名写错了。
4.3 Tomcat配置与启动顺序详解
在IDEA里,先通过File -> Open导入Maven工程,等待依赖解析完成。然后配置Tomcat:
- 点击
Run -> Edit Configurations,点加号,选择Tomcat Server -> Local。 - 在
Deployment选项卡里,点击加号,选择Artifact,选中项目war包。 Application context填/ssm,访问路径就是http://localhost:8080/ssm/login.jsp。- 点运行前,先执行
clean再执行package,确保没有编译错误。
启动过程中,如果控制台一片红,先看是不是spring-mvc.xml或applicationContext.xml里的class路径写错了。比如context:component-scan base-package里的包名没有对应上项目里的Java包结构,容器初始化就会失败。
顺利启动之后,浏览器访问登录页,用默认管理员账号admin/123456登录。
5. 三个核心模块代码拆解:权限校验、宿舍分配、报修流转
项目功能多,我挑三个最能体现SSM设计和业务逻辑的模块,按真实业务场景拆开讲,包括对应的代码实现思路。
5.1 登录与权限校验的完整链路
登录逻辑本身不复杂,但一个好的登录模块要兼顾数据校验、密码存储和会话管理三个层次。登录的Controller代码如下:
@Controller public class AdminController { @Autowired private AdminService adminService; @RequestMapping(value = "/login", method = RequestMethod.POST) public String login(HttpServletRequest request, HttpServletResponse response, Admin admin) { Admin result = adminService.login(admin); if (result != null) { request.getSession().setAttribute("admin", result); return "redirect:/home"; } else { request.setAttribute("error", "用户名或密码错误"); return "login"; } } @RequestMapping(value = "/logout", method = RequestMethod.GET) public String logout(HttpServletRequest request) { request.getSession().invalidate(); return "redirect:/login.jsp"; } }对应Service层的登录校验用到了MyBatis的动态SQL,查询参数中使用了实体类:
public Admin login(Admin admin) { return adminMapper.selectByUsernameAndPassword( admin.getUsername(), admin.getPassword()); }这段逻辑里,密码是明文存储和比对的,对于课程设计级别够用,但如果你要放到真实环境,强烈建议至少改成MD5加盐,后面会说。
5.2 宿舍分配:事务和并发控制是关键
宿舍分配是最能体现业务复杂度的场景。分配逻辑是:管理员选择一栋楼、一个宿舍、一个学生,然后系统要做几件事——检查学生当前是否已有宿舍、检查目标宿舍是否有空床位、如果都通过,则修改学生记录的宿舍信息,同时把宿舍的bed_used加1。
@Service public class DormitoryServiceImpl implements DormitoryService { @Autowired private DormitoryMapper dormitoryMapper; @Autowired private StudentMapper studentMapper; @Override @Transactional(rollbackFor = Exception.class) public boolean assignDormitory(Integer studentId, Integer dormitoryId) { Dormitory dorm = dormitoryMapper.selectById(dormitoryId); Student student = studentMapper.selectById(studentId); if (dorm == null || student == null) { return false; } if (dorm.getBedUsed() >= dorm.getBedCount()) { throw new RuntimeException("宿舍床位已满"); } dormitoryMapper.incrementBedUsed(dormitoryId); studentMapper.updateDormitory(studentId, dormitoryId); return true; } }这个模块有两个必须强调的点:
一是事务注解不能少。@Transactional保证"床位加1"和"学生宿舍更新"两个操作要么同时成功,要么同时回滚。如果中途抛出异常只执行了一半,数据库就会出现数据不一致的问题——比如床位被占了,但学生没入进去。
二是并发访问隐患。假设两个管理员同时操作,都查到了某个宿舍还有最后一张空床,然后同时更新床位,就会超卖。课程设计一般不会遇到并发,但到了真实场景,这需要在数据库层做行锁(SELECT ... FOR UPDATE)或者使用乐观锁版本号思路。我在实际使用中给dormitory表加了一个version字段做过优化,这里先不展开。
5.3 报修工单:状态流转的精妙之处
报修模块的业务座标是:学生提交报修信息,管理员查看并修改状态。数据库里的repair表有一个status字段,我用整型数字表示状态,0为待处理,1为处理中,2为已完成。在页面展示时通过JSP标签把它映射成中文:
<c:choose> <c:when test="${repair.status == 0}"> <span class="badge badge-warning">待处理</span> </c:when> <c:when test="${repair.status == 1}"> <span class="badge badge-info">处理中</span> </c:when> <c:when test="${repair.status == 2}"> <span class="badge badge-success">已完成</span> </c:when> </c:choose>这种用数字代表业务状态的做法在数据库设计中非常常见,好处是节省存储空间、查询效率高,坏处是可读性差,需要代码里维护好状态字典。建议在Service层定义常量类统一管理这些状态码,而不是在Java代码里到处写魔法数字。
6. SSM整合最容易踩的五个坑,我一个个帮你们趟平了
SSM是一个"三个框架拼接"的工程,框架版本不对、配置不匹配、依赖冲突,这些问题比业务代码出错更让人崩溃。我把实际开发中遇到的高频问题整理一下,每个都附上解决思路。
6.1 Maven依赖冲突:老项目最头疼的问题
pom.xml里的依赖最容易出问题的几个点是:
javax.servlet-api和servlet-api版本冲突。Tomcat自带的servlet-api和Maven引入的版本不一致,编译报NoClassDefFoundError。- Spring框架各模块版本不统一。建议所有Spring相关的依赖都统一用同一个版本,比如
4.3.18.RELEASE,不要一个4.x一个5.x混用。 - MyBatis和mybatis-spring的版本对应关系。
解决方式是:在IDEA的Maven工具窗口里查看Dependencies分析依赖树,找到重复和冲突的包,用<exclusions>排除掉多余传递依赖:
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>4.3.18.RELEASE</version> </dependency>重点不是背版本号,而是理解版本匹配规则。不信你随便搜一个SSM整合教程,里面必定会强调Spring版本和Spring MVC版本必须一致,这就是最常见的坑之一。
6.2 MySQL驱动和连接串不匹配导致的时区报错
MySQL 8.0的驱动类名变了,连接时区问题也来了。如果配的是MySQL 5.7的com.mysql.jdbc.Driver,放到8.0上启动直接报ClassNotFoundException。反过来,8.0的驱动类com.mysql.cj.jdbc.Driver在5.7上反而没事,但URL里的serverTimezone参数不能省。
我用的是MySQL 5.7,连接串如下:
jdbc.url=jdbc:mysql://localhost:3306/dormitory_ssm?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/ShanghaiuseSSL=false也很关键,新版MySQL或某些连接方式默认开SSL,会有大段警告和潜在的握手失败。加上这些参数之后,连接稳定程度立竿见影。
6.3 Jackson序列化:遇到LocalDate就会翻车
Java 8引入的LocalDate、LocalDateTime默认不被Jackson正确处理。返回JSON数据时,日期会变成一串数组格式的数字,前端解析起来非常难受。解决办法是配置ObjectMapper,注册JavaTimeModule,并设定日期格式:
@Bean public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() { ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(mapper); return converter; }如果在实体类里用java.util.Date,就不会有这个问题。为了减少配置,我当时把实体里所有日期字段都统一用java.util.Date了。这个要看你自己的取舍。
6.4 文件上传时的CommonsMultipartResolver配置遗漏
宿舍管理系统里如果要传图片(比如学生照片、楼栋照片),Spring MVC的文件上传依赖CommonsMultipartResolver。很多人漏了这一步,导致multipart/form-data请求里的文件字段永远是null。
在spring-mvc.xml里加上:
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <property name="maxUploadSize" value="10485760"/> <property name="defaultEncoding" value="UTF-8"/> </bean>同时pom里需要引入commons-fileupload依赖。记住id必须叫multipartResolver,Spring MVC在接收到multipart请求时会按照这个id去容器里找解析器。
6.5 MyBatis XML映射文件namespace写错的排查思路
这个问题很多人踩坑。Mapper接口和Mapper.xml绑定的关键是XML根元素里的namespace="com.xx.dormitory.dao.StudentMapper",必须精确到接口的全限定名。如果namespace写错,运行时调用studentMapper.selectById(1)会报Invalid bound statement (not found)。
排查步骤很简单:
- 看编译后的
target/classes目录里有没有对应的Mapper.xml文件。 - 检查XML里namespace是否和接口全限定名一致。
- 检查MyBatis全局配置里的mapperLocations路径是否能匹配到XML文件。
这三个地方任一环节出错,都会得到同样的报错信息。很多人在这上面耗一下午,实际上就是配置路径少写了一层目录。
7. 如果有时间:从课程设计走向生产级的升级思路
最后这部分算是进阶思考。如果你只是完成一个毕业设计,前面做到第6章就足够了。但如果你想把系统真正交给学校后勤用,或者想在简历上有话可说,下面这几个升级方向很值得考虑。
7.1 密码存储的升级:从明文到加盐哈希
我现在这个项目登录密码是明文存的,课程设计演示完全没有问题,但真实环境绝对不能这么干。至少要升级成这样:
public static String encodePassword(String rawPassword, String salt) { String salted = salt + rawPassword; return DigestUtils.md5DigestAsHex(salted.getBytes(StandardCharsets.UTF_8)); }加盐之后,同一个密码加不同的salt存储结果不同,暴力破解成本大增。更进一步是使用BCrypt这类自适应哈希算法,内部自带随机盐,Spring的BCryptPasswordEncoder可以直接用。
7.2 引入Redis缓存热点数据
系统规模上来之后,宿舍的房源情况、学生的基本信息这类数据读多写少,非常适合做缓存。把高频读取的数据放进Redis,查询先从缓存获取,缓存不命中再走数据库,同时更新缓存。这个改造在SSM里也不难,Spring提供RedisTemplate,再在Service层加一层缓存逻辑即可。
我在另外一个小项目里试过,最简单的做法是只缓存dormitory表的查询结果——因为这个查询几乎每次打开分配页面都会触发。一个多月下来,数据库压力下降非常明显。对教学系统来说,这可能意义不大,但它可以作为你在简历项目里体现性能意识的亮点。
7.3 渐进迁移:给Spring Boot留一条出路
如果你以后要往Spring Boot方向走,这个SSM项目千万不要丢掉。Spring Boot很大程度是SSM的"自动化封装",你可以尝试在现有项目的基础上逐步迁移:先加一个Spring Boot的启动类,把MyBatis的Mapper和Service层原样搬过去,再把Controller换成REST风格接口,最后用Vue或React重构前端。每一步都能对比着看,比从零学Spring Boot要快很多。
最后说几句实际的
整个项目做下来,我的一个深刻体会是:SSM的价值不在于它有多先进,而在于它把Java Web开发中那些最底层的协作关系暴露得足够清楚。你在这里配过的每一个Bean、解决的每一次依赖冲突、调过的每一个Mapper路径,到了Spring Boot时代虽然都被自动处理了,但出了问题你能很快明白底层发生了什么。这也是我把这套SSM宿舍管理系统源码整理出来、并写下这篇拆解文章的初衷。
如果你是拿这套源码做毕设,建议不要只改个名字就交差,至少要把第5章讲的宿舍分配逻辑、第6章讲的报修状态机吃透,答辩时老师问到业务逻辑才能对答如流。如果你只是想快速跑通,严格按第4章的步骤操作,顺利的话半小时就能看到登录页面。
最后再分享一个小技巧:改造之前,先用Navicat或者MySQL Workbench把系统跑起来之后的所有表数据导出备份一份,数据库操作失误之后恢复十分方便。这步操作用不上几次,但每次用上都帮你节省大量时间。