资讯动态

SSM宿舍管理系统毕业设计源码解析:架构、业务与避坑指南

发布时间:2026/10/21 2:46:41 来源:尧图企业网站定制
简介面向高校毕业设计或课程实训的Java学生宿舍管理系统完整源码包适合需要快速搭建前后台一体化管理项目的学习者。系统采用Bootstrap搭建界面后台基于SpringBoot与MySQL覆盖管理员、宿管、学生三类角色实现学生、楼宇、宿舍、入住及管理员等模块的增删改查并支持学生/宿管/楼宇等信息的Excel导入导出功能链条完整。压缩包共661个文件约72.37MB以JavaScript、CSS、Java源码与class文件为主同时包含SQL脚本、XML配置、HTML页面以及MP4演示录像和文档说明便于部署参考。已适配MySQL5与MySQL8两种环境预览中可见分别整理好的项目压缩包可直接导入数据库运行。当前已有468人学习下载适合用于毕业设计参考、Java Web开发练习或宿舍管理场景的二次开发。1. 学生宿舍管理系统毕业设计源码包里到底装了什么第一次拆开这套学生宿舍管理系统源码包时我以为又是一份只有增删改查的 Java 课设。翻到 service 层才发现宿舍分配会校验床位余量退宿会联动释放床位报修单还自带状态流转——这不是堆 CRUD而是把“宿舍管理”这个场景做成了闭环。整包是标准 SSMSpring SpringMVC MyBatis分层工程MySQL 存数据JSP Bootstrap 做页面Tomcat 部署。这套系统的受众很明确准备交管理系统方向毕业设计、需要完整 Java 项目源码做参照的学生以及想快速理解老旧 SSM 工程怎么分层、怎么二次开发的开发者。它把管理员登录、学生入住退宿、宿舍分配、报修、卫生检查串在一条业务链上跑起来后能直接看到数据在页面、Service、Mapper 之间的完整流动。接下来按“架构设计、数据库、核心调用链、运行避坑、二次开发”的顺序拆。2. 技术选型与架构设计SSM 三件套 JSP 是怎么搭起来的2.1 技术栈与版本选型为什么这套组合还是毕业设计主流先看整体配置我在跑通这份 java 源码之后整理出下面的推荐组合技术版本建议在项目里负责的事JDK1.8编译与运行环境Spring / SpringMVC5.1.x依赖注入、事务管理、请求分发MyBatis3.5.xSQL 映射与数据访问MySQL5.7业务数据存储Tomcat8.5Servlet 容器war 包部署JSP JSTL Bootstrap随 Tomcat 自带页面渲染和基础样式这套组合放到现在看不算新但直到今天毕业设计管理系统类的项目源码里 SSM 依然占很大比例。原因其实不是它最先进而是它最“好答辩”。Spring Boot 把配置包进自动装配之后很多细节对新人来说就成了黑匣子SSM 的配置是显式的web.xml 里写了哪些 servlet、spring-mvc.xml 里扫描哪个包、事务织到哪个 service 方法每一样都能翻开看、照着讲。对课程设计或毕业设计来说能讲清楚比框架新更重要。另一个现实原因是兼容性。JDK 1.8 Spring 5.1 Tomcat 8.5 这套组合经过大量老项目验证网上能搜到的报错和解决方案几乎都是对着这个组合出现的。MySQL 用 5.7 而不是 8.0是因为老驱动com.mysql.jdbc.Driver在 5.7 上不需要额外处理时区参数如果换成 MySQL 8.x驱动和 URL 都要跟着调整这个坑后面避坑章节会专门展开。2.2 分层架构与请求流转Controller 不写业务、Mapper 不碰 Servlet整套代码分成 controller、service、dao也叫 mapper、entity、util 五层典型的三层架构换了一层皮。请求进来之后不是直接调 SQL而是走一条固定链路浏览器发起/student/list→ Tomcat 把请求交给 DispatcherServlet → 进入 LoginInterceptor 检查会话 → StudentController 接收参数 → StudentService 处理业务 → StudentMapper 执行 SQL → MySQL 返回结果 → Service 包装成分页对象 → Controller 塞进 Model → JSP 渲染成 HTML。这条链路里我最看重两条约束。第一Controller 里不写业务判断只做参数接收、结果装配、页面跳转第二Service 里不出现 JDBC 代码所有 SQL 都收在 Mapper.xml 里。这样做的直接收益是改数据库方言的时候只动 Mapper改业务流程只动 Service页面改版只碰 JSP三层之间可以独立替换。很多翻车案例都是因为在 Controller 里直接调了 mapper事务失效、参数校验散落一地后面越改越乱。下面这段是 spring-mvc.xml 里的关键配置也是理解整套请求转发机制的钥匙mvc:annotation-driven/ !-- 静态资源走这里不让 DispatcherServlet 接管 -- mvc:resources mapping/static/** location/static// !-- 只扫描 controller 包service 交给父容器去扫 -- context:component-scan base-packagecom.dorm.controller/ bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/jsp// property namesuffix value.jsp/ /bean mvc:interceptors mvc:interceptor mvc:mapping path/**/ mvc:exclude-mapping path/login/**/ mvc:exclude-mapping path/static/**/ bean classcom.dorm.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptors参数这里逐个说明。mvc:resources把/static/路径下的 css、js、图片直接交给容器处理否则登录页样式会全部 404view-resolver的 prefix 和 suffix 决定了 Controller 返回student/list时最终找的是/WEB-INF/jsp/student/list.jsp放在 WEB-INF 下能防止页面被直接 URL 访问最后一个拦截器配置了/login/**白名单未登录请求会全部被挡在 Controller 之外。这套配置的顺序是有讲究的静态资源放行必须写在拦截器之前或通过 exclude 明确排除两处都没有问题时样式丢失类问题基本不会出现。2.3 源码目录结构解压之后先认这五个文件夹我一般拿到源码先不看 README直接开目录树十秒钟能判断项目结构是否规范。这套宿舍管理系统的工程布局长这样dormitory-manager ├── pom.xml └── src/main ├── java/com/dorm │ ├── controller # 登录、学生、宿舍、报修、卫生控制器 │ ├── service # 业务接口 实现类 │ │ └── impl │ ├── dao # MyBatis Mapper 接口 │ ├── entity # 与表对应的实体类 │ ├── interceptor # 登录拦截器 │ └── util # 分页、MD5、日期工具 ├── resources │ ├── jdbc.properties # 数据库连接配置 │ ├── applicationContext.xml # 数据源、事务、MyBatis 配置 │ ├── spring-mvc.xml # 控制器与视图配置 │ └── mapper # StudentMapper.xml、DormitoryMapper.xml 等 └── webapp ├── WEB-INF │ ├── web.xml # 入口配置 │ └── jsp # 登录页、学生管理页、宿舍管理页 └── static # bootstrap、jquery、自定义 css/js这里重点提醒一下 resources 和 webapp 的区别。resources 下面的 XML 会被打进 classpathSpring 启动时通过classpath:spring-mvc.xml就能读到webapp 下面的 HTML、JSP 是运行时资源放在 WEB-INF 里外部无法直接访问这也是为什么 login.jsp 能通过 Controller 跳转但不能通过/login.jsp直接打开的常见原因。一开始不熟悉 Java Web 的同学经常把配置文件塞错地方启动时报“找不到 applicationContext.xml”先检查文件是不是真的在 resources 根目录下。pom.xml 里依赖不多核心就这几个dependencies dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.1.8.RELEASE/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.4/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.47/version /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency /dependencies这里有两个容易被忽略的细节。一是没有直接在 pom 里写 mybatis-spring 和 druid 连接池依赖但项目里用到Transactional和 SqlSessionFactoryBean所以完整 pom 里通常还需要 spring-jdbc、mybatis-spring 和连接池毕业设计源码常见的组合是druid-1.1.x mybatis-spring-2.0.x。二是 mysql 驱动版本必须和 MySQL 服务端版本匹配MySQL 8 还沿用 5.1.47 会在连接时报 SSL 和时区错误换成 8.0.x 驱动、driver-class 改成com.mysql.cj.jdbc.Driver即可。3. 数据库设计与业务模块一栋楼的房态是怎么被管起来的3.1 表结构设计用一张 student 表同时存学号和床位外键宿舍管理系统的核心不是“人”而是“床位”。我把这套源码的建表语句整理成下面这一组业务关系一眼能看懂building楼栋→ dormitory宿舍→ student学生报修和卫生检查挂在宿舍上。CREATE TABLE building ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 楼栋名如 1号楼, manager VARCHAR(50) COMMENT 宿管员姓名 ); CREATE TABLE dormitory ( id INT PRIMARY KEY AUTO_INCREMENT, building_id INT NOT NULL COMMENT 所属楼栋, room_no VARCHAR(20) NOT NULL COMMENT 房间号如 301, bed_total INT NOT NULL DEFAULT 4 COMMENT 床位总数, bed_used INT NOT NULL DEFAULT 0 COMMENT 已住人数 ); CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, name VARCHAR(50) NOT NULL, gender CHAR(1) COMMENT 男/女, phone VARCHAR(20), dorm_id INT COMMENT 当前宿舍NULL 表示未入住, check_in_time DATETIME, status TINYINT DEFAULT 1 COMMENT 1在住 0退宿 ); CREATE TABLE admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, role VARCHAR(20) NOT NULL DEFAULT admin COMMENT super/dorm/student );这几张表的设计有几个刻意为之的点。student 表里同时出现了student_no和dorm_id分别代表“身份”和“位置”两个维度这是宿舍管理业务里最高频的两个查询条件bed_total / bed_used是一对冗余字段专门用来在列表页直接显示空床数避免每次统计都要COUNT(*)扫学生表admin 表的 role 字段用字符串而不是数字牺牲一点存储空间换可读性答辩时解释起来也更直观。床位数据的一致性靠 Service 层保证而不是靠数据库触发器。入住时先查bed_used bed_total通过后插入学生、床位 1两步包在一个事务里退宿则反向操作床位 -1、学生状态置为 0。这套逻辑会在第四章展开表结构里只负责把bed_total默认值设为 4真实宿舍的床位改动走管理员的编辑页面。3.2 登录与权限拦截器搞定三类角色的会话管理登录模块在三层权限设计里属于“够用但不复杂”的做法。常见的毕业设计源码会有两种登录方案一种让学生和管理员共用 admin 表用 role 字段区分另一种学生走 student 表、管理员走 admin 表两张表分开校验。这套系统用的偏第一种好处是登录逻辑只需要一套session 里只存一个对象。Controller 里的登录接口长这样Controller RequestMapping(/login) public class LoginController { Resource private AdminService adminService; PostMapping(/doLogin) public String doLogin(String username, String password, HttpSession session, HttpServletRequest request) { // 密码加密后比对加密工具在 util 包里 Admin admin adminService.login(username, MD5Utils.md5(password)); if (admin null) { request.setAttribute(error, 用户名或密码错误); return login; // 回到登录页并回显错误 } // 登录成功把用户塞进 session后续拦截器识别 session.setAttribute(loginAdmin, admin); return redirect:/dormitory/list; } GetMapping(/logout) public String logout(HttpSession session) { session.invalidate(); return redirect:/login; } }参数上说明三点。login方法内部按 username 查库然后比对加密后的密码为什么不在 SQL 里直接WHERE username? AND password?因为那样会把加密方法散落在 SQL 里换加密算法要改 Mapper现在 Service 层先查出用户再比对以后升级 BCrypt 只动 Service。redirect:/dormitory/list用的是 302 重定向登录成功后地址栏会变成列表页用户刷新不会重复提交表单。session.invalidate()是把会话整个销毁避免只 remove 一个属性留下其他残留数据。角色控制没有做专门的注解或权限框架走的是拦截器加页面判断。登录拦截器只判断“有没有登录”Controller 里再判断“角色能不能操作这个接口”比如只有 super 和 dorm 角色能调退宿接口student 角色的前端页面不会渲染退宿按钮。这种粒度对课程设计完全够用真要上生产再考虑 Spring Security 或 Shiro 不迟——那是另一个量级的学习成本。3.3 宿舍分配与退宿床位 1/-1 的事务边界宿舍分配是这套系统里最有业务价值的一段代码。我在很多项目源码里见过把“分配”做成简单 UPDATE 的做法——不看床位余量直接更新学生宿舍号结果同一间 4 人间住进 8 个人。这套源码把校验逻辑放在了 Service 层实现如下Override Transactional(rollbackFor Exception.class) public boolean checkIn(Student student, Integer dormId) { // 1. 查目标宿舍当前占用情况 Dormitory dorm dormitoryMapper.selectById(dormId); if (dorm null || dorm.getBedUsed() dorm.getBedTotal()) { throw new BusinessException(该宿舍已满员请换一间); } // 2. 写入学生记录状态置为在住 student.setDormId(dormId); student.setCheckInTime(new Date()); student.setStatus(1); studentMapper.insert(student); // 3. 宿舍已住人数 1 dormitoryMapper.increaseBedUsed(dormId); return true; }逻辑拆开看是三步查房态、写学生、改床位。第一步的bedUsed bedTotal是业务上的唯一防线第三步没有用SET bed_used ?而是bed_used bed_used 1这样即使两个请求同时读到旧值数据库层面也会串行更新不会出现覆盖。事务注解Transactional(rollbackFor Exception.class)保证了第二步和第三步要么一起成功要么一起回滚——最常见的翻车写法是这两个操作分在两个方法里、或者 Controller 里先 insert 再 update事务不生效学生入库了床位没加越住越乱。退宿是对称操作把 student 的 status 改为 0、dorm_id 置空再把宿舍的 bed_used 减 1。这里有个容易遗漏的细节——退宿时如果学生本来就没有宿舍比如走读生被误登记要先判空再执行床位 -1否则会把宿舍床位改成负数。我在检查代码时看到这里有if (student.getDormId() ! null)的防御判断属于值得保留的好习惯。3.4 报修与卫生检查状态流转字段是怎么设计的报修模块没有单独建“报修人表”而是把报修内容、报修人、宿舍 ID 直接放在 repair 表里状态用 TINYINT 表达CREATE TABLE repair ( id INT PRIMARY KEY AUTO_INCREMENT, dorm_id INT NOT NULL COMMENT 哪个宿舍报修, content VARCHAR(500) NOT NULL COMMENT 报修内容如水管漏水, status TINYINT DEFAULT 0 COMMENT 0待处理 1处理中 2已完成, reporter VARCHAR(50) COMMENT 报修人姓名, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 提交时间 );状态字段是这个模块的骨架0 表示新提交管理员看到待处理的单子后点“开始处理”变成 1修完点“完成”变成 2。它不搞状态机表是因为这个流程只有单向流转不存在驳回、撤销、重新指派一个 TINYINT 加一个下拉框就够。卫生检查表类似字段是dorm_id check_date score remarkscore 存百分制分数列表页按 date 倒序排列就能看到最近一次检查结果。这两张表的设计思路其实是一致的能用一个字段表达的状态就不建一张关联表毕业设计的管理系统最怕过度设计。4. 核心代码逐段拆解一次入住登记从页面到数据库的完整链路4.1 控制层接收参数、调服务、回跳页面入住登记这个功能的关键在于它横跨了学生模块和宿舍模块的数据。前端页面上管理员从宿舍下拉框选一个目标房间房间信息通过隐藏域或下拉框的 value 传给后端后端 Controller 负责把表单里的 Student 对象和选中的 dormId 一起交给 Service。下面是学生管理模块的 Controller 核心方法Controller RequestMapping(/student) public class StudentController { Resource private StudentService studentService; GetMapping(/list) public String list(RequestParam(defaultValue 1) int pageNum, String keyword, Model model) { // keyword 可为空为空时查全部 PageResultStudentVO page studentService.findPage(pageNum, keyword); model.addAttribute(page, page); return student/list; } PostMapping(/checkIn) public String checkIn(Student student, Integer dormId, RedirectAttributes attr) { try { studentService.checkIn(student, dormId); // 用 flash 属性携带提示重定向后仍然能读出 attr.addFlashAttribute(msg, 入住登记成功); } catch (BusinessException e) { attr.addFlashAttribute(error, e.getMessage()); } return redirect:/student/list; } }要注意RequestParam(defaultValue 1)这个参数细节如果没有它访问/student/list不带 pageNum 时 Spring 会报参数缺失加了默认值之后空参数按第一页处理。RedirectAttributes是这里容易被忽略的组件——它和普通 Model 的区别是数据只活在重定向后的那次请求里用来做“提交成功后跳转列表页”的提示语刷新列表页时提示消失不会出现重复提交。Student 对象由 Spring 自动从表单字段绑定要求表单的 name 属性和实体字段名一致比如input namestudentNo对应student.studentNo对不上时绑定结果是 null后续插入就会报非空约束错误。4.2 服务层先查房态再落库业务规则都收在这里Service 层是整条链路里最值得反复读的部分因为所有业务规则都集中在这里而不是散落在 Controller 或 JSP 里。上面提到过 checkIn 的实现这里再补一个查询分页方法展示分页逻辑在 Service 层怎么接入Override public PageResultStudentVO findPage(int pageNum, String keyword) { // 手动计算偏移量也可以用 PageHelper.startPage 替代 int pageSize 10; int offset (pageNum - 1) * pageSize; ListStudentVO list studentMapper.selectPage(offset, pageSize, keyword); int total studentMapper.countByKeyword(keyword); return new PageResult(pageNum, pageSize, total, list); }分页这里展示了两种常见路线的取舍。很多 SSM 项目直接引入 PageHelper一行PageHelper.startPage(pageNum, pageSize)就能自动拼 LIMIT好处是省事但这套源码里的手写 offset 同样可行——表数据量在课程设计级别只有几百条手写分页不引入额外依赖还能在答辩时讲清楚 LIMIT 的数学关系。countByKeyword是另一条独立 SQL和分页查询分开写避免 MyBatis 自动把 count 和查询混在一个 statement 里导致统计不准确。这里补充一个实用建议如果要把 keyword 和 dormId 组合查询别在 Service 层用字符串拼接 SQL把条件封装成一个查询对象传进 MapperMapper.xml 里用if标签动态拼 WHERE这样既防注入又方便扩展。下一节会展示这种动态 SQL 的具体写法。4.3 数据层MyBatis 动态 SQL 与联表查询数据层是这套源码里 SQL 含量最高的地方也是“项目值不值得留”的判断标准。入住登记链路里涉及三个 SQL插入学生、床位 1、分页联查。前两个是单表操作重点看联查insert idinsert parameterTypecom.dorm.entity.Student INSERT INTO student (student_no, name, gender, phone, dorm_id, check_in_time, status) VALUES (#{studentNo}, #{name}, #{gender}, #{phone}, #{dormId}, #{checkInTime}, 1) /insert update idincreaseBedUsed parameterTypeint UPDATE dormitory SET bed_used bed_used 1 WHERE id #{dormId} /update select idselectPage resultTypecom.dorm.vo.StudentVO SELECT s.id, s.student_no, s.name, s.gender, s.phone, d.room_no, b.name AS building_name, s.check_in_time, s.status FROM student s LEFT JOIN dormitory d ON s.dorm_id d.id LEFT JOIN building b ON d.building_id b.id where if testkeyword ! null and keyword ! AND (s.student_no LIKE CONCAT(%, #{keyword}, %) OR s.name LIKE CONCAT(%, #{keyword}, %)) /if /where ORDER BY s.id DESC LIMIT #{offset}, #{pageSize} /select三个 SQL 各有值得说明的参数细节。insert 里的#{studentNo}是 MyBatis 预编译占位符最终会转成 JDBC 的?值由 PreparedStatement 自动转义这是防 SQL 注入的关键如果写成${studentNo}字符串拼接注入风险会恢复到 sql 拼接时代。bed_used bed_used 1是原子自增写法比“先 select 再 update set 固定值”少一次往返且天然防并发覆盖。联查里我用 LEFT JOIN 把宿舍和楼栋的名字一次性查出来学生还没有宿舍时 dorm_id 为 NULLLEFT JOIN 仍然能返回学生记录只是 room_no 和 building_name 是 null前端用空值判断显示“未分配”这是和 INNER JOIN 最大的行为差异。where标签的作用是智能处理 AND 前缀——如果条件都不满足它不会输出 WHERE如果第一个条件成立它会自动去掉第一个 AND。这也是为什么 keyword 为空时查询不会语法报错的原因。分页参数#{offset}, #{pageSize}直接拼在 LIMIT 里MySQL 语法要求顺序固定Mapper 接口里对应的参数名必须和这里一致否则启动时 MyBatis 直接报 BindingException。4.4 前端 JSP列表循环、状态徽标与表单回显前端这部分用的是 JSP JSTL本质上是把后端传来的数据在服务端拼成 HTML。列表页的核心循环如下% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % table classtable table-bordered thead tr th学号/thth姓名/thth宿舍/th th入住时间/thth状态/thth操作/th /tr /thead tbody c:forEach items${page.list} vars tr td${s.studentNo}/td td${s.name}/td td${s.buildingName} ${s.roomNo}/td tdfmt:formatDate value${s.checkInTime} patternyyyy-MM-dd//td td c:choose c:when test${s.status 1}在住/c:when c:otherwise已退宿/c:otherwise /c:choose /td td a href/student/toEdit?id${s.id}编辑/a a href/student/out?id${s.id} onclickreturn confirm(确认该学生退宿)退宿/a /td /tr /c:forEach /tbody /tableJSP 的取值逻辑和 Vue/React 完全不同这里值得讲清楚${s.studentNo}由 EL 表达式在服务端直接读取 StudentVO 的 getter 方法浏览器拿到的已经是渲染完的 HTML所以页面源码里能看到姓名和宿舍号而不是{{xxx}}之类的模板语法。fmt:formatDate负责把后端java.util.Date格式化成yyyy-MM-dd不做这步会显示成一长串英文格式的日期。${s.status 1}是状态判断的基础实际项目里通常配合 Bootstrap 的badge类在住显示绿色、退宿显示灰色。操作列的退宿链接直接用了onclick做前端确认避免误点——confirm 的返回值是 false 时浏览器不会跳转这是最原始的“二次确认”做法也是老项目里最常见的交互写法。表单回显的做法是编辑页面把 student 对象塞进 model然后value${student.studentNo}把值回填到 input 里注意需要判断空对象否则第一次新增时 student 为 null 会报错常见写法是先用c:if test${not empty student}包一层。4.5 鉴权链路拦截器里到底在拦什么最后把拦截器单独拎出来说因为它是整条请求链路里最先执行、也最容易写错的一环。前面 spring-mvc.xml 里已经配置了拦截路径这里看具体的处理逻辑public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 从 session 取登录标记 Object loginAdmin request.getSession() .getAttribute(loginAdmin); if (loginAdmin null) { // 未登录跳回登录页并携带来源路径 response.sendRedirect(request.getContextPath() /login); return false; } return true; } }preHandle 返回 false 表示请求到此为止不再进入 Controller返回 true 才继续往后走。这里有两个实际开发中才会踩到的细节。第一sendRedirect必须用request.getContextPath()拼接前缀因为部署时项目可能不是 ROOT 应用直接写/login会跳到别的应用的路径上去。第二只判断 session 属性还不够如果项目后面加了“记住我”功能还要在这里补 cookie 解析逻辑否则依赖 session 的登录态会在浏览器重启后失效。HandlerInterceptor 还有 postHandle 和 afterCompletion 两个方法分别用于处理器执行后、视图渲染前和整个请求完成后。这个宿舍系统只在 preHandle 做登录校验两个后置方法没用到但理解它们的调用时机对排查问题有帮助postHandle 里可以往 Model 加公共数据比如页面上的当前年份afterCompletion 里可以做日志清理。因为拦截器在 Controller 之前执行所以登录失效的接口不会暴露给业务代码这是把鉴权放在拦截器而不是 Controller 里的核心原因。5. 项目运行避坑指南从导入工程到访问首页五个高频翻车点5.1 Maven 依赖下载失败项目一启动就缺类现象用 IDE 导入工程后pom.xml 上一直有红色波浪线展开 Dependencies 看到大量 jar 缺失编译时报程序包 org.springframework.web 不存在或ClassNotFoundException: DispatcherServlet。原因Maven 默认从中央仓库下载构件在国内访问要么极慢要么直接超时依赖下载不完整IDE 对未下载完的 jar 标记错误。加上很多课程设计源码的 pom.xml 是复制来的本地仓库没有对应版本必须联网下载。解决在 Maven 安装目录的conf/settings.xml里配置国内公共镜像强制所有依赖走镜像settings xmlnshttp://maven.apache.org/SETTINGS/1.0.0 mirrors mirror idmirror-public/id namepublic mirror/name mirrorOf*/mirrorOf urlhttps://your-mirror.example.com/repository/public/url /mirror /mirrors /settings配置后重启 IDE 并执行一次mvn -U clean compile-U参数强制检查远程仓库的最新版本能把之前下了一半的依赖补齐。这里有个容易踩的细节mirrorOf可以写*表示拦截所有仓库也可以写central只拦中央仓库如果 settings.xml 里已经配置了多个仓库建议写*保证没有漏网的 repository 直接连外网。5.2 部署到 Tomcat 后 404 或找不到 DispatcherServlet现象项目能在 IDE 里启动但访问首页一直 404查看 Tomcat 日志发现java.lang.ClassNotFoundException: org.springframework.web.servlet.DispatcherServlet但 Maven 依赖里明明有 spring-webmvc。原因IDE 里 war 包的 Artifact 默认没有把 Maven 依赖打进WEB-INF/lib。编译能过是因为 IDE 编译时用到了依赖但部署时 Tomcat 的 classpath 里没有这些 jar容器当然找不到 DispatcherServlet。解决打开 Project Structure 的 Artifacts找到对应的 war exploded 或 war 包在 Output Layout 里把library files添加到WEB-INF/lib下重新 Build 再部署。另一个常见原因是 web.xml 里的servlet-class写成了简写类名必须是全限定名org.springframework.web.servlet.DispatcherServlet这个错在配置复制粘贴时经常出现启动日志不会立刻显示但所有请求都会在容器层直接 404。5.3 JDK 版本不对老 SSM 项目在新 JDK 上直接罢工现象启动时报UnsupportedClassVersionError提示 class file 版本号不支持或者项目能编译但 Spring 容器创建失败CGLIB 代理报Unable to load class。原因两个问题叠加。第一源码用 JDK 1.8 编译新 JDK 无法运行旧 class 文件第二Spring 5.1 依赖 CGLIB 通过反射创建代理类JDK 9 之后的模块化限制了深层反射老版本 Spring 在新 JDK 上经常直接翻车。解决给项目单独配一套 JDK 1.8。IDE 里把 Project SDK、Modules language level、Maven compiler 的 source/target 全部统一成 1.8同时检查JAVA_HOME指向 1.8 的目录。如果迫不得已必须在高版本 JDK 上跑要给 JVM 加--add-opens java.base/java.langALL-UNNAMED这类参数但老项目不值得这样折腾最省事且最稳妥的方案就是安装 JDK 1.8这也是这套 SSM 源码最推荐的运行环境。5.4 数据库连不上、中文乱码、时区报错现象Tomcat 启动快到结尾时报Cannot create PoolableConnectionFactory登录后页面上的学生姓名全部显示为??MySQL 连接时报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized。原因第一个是数据库连接参数错误包括密码不对、端口不是 3306、数据库名拼错第二个是 JDBC URL 缺少字符集参数连接没指定 utf8第三个是驱动版本和 MySQL 版本不匹配导致的时区识别问题最典型的是 MySQL 8 配了 5.1 驱动。解决检查src/main/resources/jdbc.properties把连接串对齐到下面的样式jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/dorm?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password你的密码这里按参数逐个说useUnicodetrue和characterEncodingutf8成对出现解决中文乱码serverTimezoneAsia/Shanghai是 5.1 驱动连 MySQL 8 的补救参数能用但不如换驱动干净如果数据库是 MySQL 8.xdriver 要改成com.mysql.cj.jdbc.DriverURL 里的 serverTimezone 仍然保留。建库时也注意CREATE DATABASE dorm DEFAULT CHARSET utf8三处字符集库、连接、页面编码一致才不会出现数据写到一半变问号的问题。5.5 静态资源被拦截登录页 CSS 全部失效现象Tomcat 能启动、登录页也能打开但整个页面没有样式浏览器控制台里一堆Failed to load resource: 404路径全是/static/css/xxx.css。原因DispatcherServlet 在 web.xml 里映射了/把包括 css、js、图片在内的所有请求都当成 Controller 处理而代码里没有对应的请求映射于是全部 404。配置了拦截器后还会叠加一层问题即使静态资源能走容器默认 servlet也先被 LoginInterceptor 挡住了未登录时 css 请求直接重定向到登录页。解决分两步。第一步在 spring-mvc.xml 中放行静态资源也就是前面 2.2 里写的mvc:resources mapping/static/** location/static/第二步在拦截器配置里加mvc:exclude-mapping path/static/**/让静态资源跳过登录校验。改完后记得清浏览器缓存再验证很多“改了没生效”其实只是缓存了旧的 404 页面。6. 二次开发拓展把单宿舍楼管成多校区我会怎么动手改6.1 先想清楚多校区到底多在哪一层这套源码最常见的二次开发需求是把单楼栋扩成多校区。动手前把改造点列成一张表数据模型新增 school 表building 挂 school_id查询条件增加校区筛选宿舍分配先限定校区管理员按校区隔离数据报表按校区聚合。核心原则是业务已经在 Service、SQL 已经在 Mapper所以多数改动只是“加一列、加一个参数、加一个下拉框”不需要动三层结构。6.2 实操新增 school 表并让楼栋挂到校区下数据库两步走建 school 表给 building 加列。CREATE TABLE school ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 校区名 ); ALTER TABLE building ADD COLUMN school_id INT DEFAULT 1 COMMENT 所属校区;然后把宿舍列表查询改成三表联查select idselectDormitoryVO resultTypecom.dorm.vo.DormitoryVO SELECT d.id, d.room_no, d.bed_total, d.bed_used, b.name AS building_name, sc.name AS school_name FROM dormitory d JOIN building b ON d.building_id b.id JOIN school sc ON b.school_id sc.id where if testschoolId ! null and schoolId ! AND b.school_id #{schoolId} /if /where ORDER BY sc.id, b.id, d.id /selectschoolId 是前端下拉框传来的参数为空时返回全部校区JOIN 而不是 LEFT JOIN是因为宿舍必须挂在楼栋上查不到说明数据异常应该暴露而不是静默。6.3 验证方法与一个取舍改造完按“数据库 → 接口 → 界面”的顺序验证先插入两个校区的最小测试数据直接跑联查 SQL 确认校区能分开再分别用两个管理员登录检查列表是否隔离、分配宿舍会不会跨校区串楼最后把入住、退宿、报修主链路完整跑一遍开启 MyBatis 日志对照实际 SQL。如果要接小程序或移动端把 Controller 从 Controller 换成 RestController 返回 JSONService 和 Mapper 不用动但 session 鉴权要改成 token 方案这是最大的重构点只为了答辩就继续用 JSP不必硬上前后端分离。我从前接手这类 SSM 源码第一步永远是先跑通再谈改造项目里藏着多少坑只有跑起来才知道改代码之前把数据库脚本和启动步骤对一遍能省下后面一整天排查时间。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑