JSP+MySQL宿舍管理系统实训:从跑通到面试避坑指南
简介这份实训作业资源面向计算机相关专业学生与Java Web初学者提供一套基于JSP与MySQL的学生宿舍管理系统完整实现可用于课程设计、毕业实训或自学练手。系统围绕学生信息、宿舍登记、住宿分配与调整、费用管理、在线报修、统计报表及用户权限等模块展开帮助理解动态网页与关系型数据库的整合思路。压缩包共67个文件约5.55MB包含17个java源文件与17个class编译文件、10个jsp页面、8个xml配置、7个jar依赖以及sql建库脚本、properties配置和docx设计文档覆盖从源码、数据库到说明文档的完整链路。已有69人学习关注。读者可据此掌握Servlet与JavaBean分层结构、数据库表设计与初始化流程并参考文档完成环境搭建与功能调试适合作为中小型Web管理系统的入门范本。1. 从一份实训作业说起JSPMySQL 宿舍管理系统到底能跑出什么很多人看到“实训作业基于JSPMySQL的学生宿舍管理系统源代码数据库文档”这个标题第一反应是这不就是大学里最典型的那类课程设计吗但如果你真把它当成一次“交完就删”的作业那就亏了。这套东西本质上是一个完整的、可运行的 Java Web 单体应用前端用 JSP 渲染页面后端用 Servlet 或简单 JavaBean 处理业务数据落在 MySQL 里配套还有建表脚本和一份需求文档。它覆盖了学生信息录入、宿舍分配、床位管理、报修登记、访客记录这些真实场景麻雀虽小五脏俱全。对新手来说它是你从“只会写 Hello World”到“能跑起一个带登录、带增删改查、带权限区分的系统”的最短路径对熟手来说它是一个极佳的“反面教材”——你能一眼看出哪些地方用了过时的写法哪些地方藏着 SQL 注入的风险哪些地方的事务边界没处理好。我见过不少同学简历上写“熟悉 Java Web”面试官一问“你做过什么项目”支支吾吾说不出细节。这套系统如果真能自己跑通、改通、讲通足够撑起一场 20 分钟的技术面。下面我就按“先跑起来、再改明白、最后避坑”的顺序把这条路走一遍。2. 把项目跑起来环境、建库与第一个登录页2.1 环境选型为什么 JDK 8 Tomcat 8.5 最稳这套实训作业的代码年代感通常比较强很多写法依赖javax.servlet包而不是 Jakarta EE 的jakarta.servlet。如果你上来就装 JDK 17 Tomcat 10大概率会在启动时看到ClassNotFoundException: javax.servlet.http.HttpServlet然后开始怀疑人生。我的建议是JDK 8 或 JDK 11Tomcat 8.5 或 9.0MySQL 5.7 或 8.0IDE 用 IntelliJ IDEA 社区版就够。别小看版本匹配这件事我见过太多人卡在“项目跑不起来”上最后发现只是 Tomcat 10 把包名换了。数据库驱动也要注意MySQL 8.0 用com.mysql.cj.jdbc.Driver5.7 用com.mysql.jdbc.Driver。连接串里如果没加serverTimezoneAsia/ShanghaiMySQL 8.0 会直接报时区错误。这些细节文档里不一定写但你不处理登录页都出不来。2.2 建库建表把 SQL 脚本跑对顺序拿到数据库脚本后别急着一次性全选执行。常见做法是先建库再建表最后插初始数据。顺序错了外键约束会直接拦你。下面是一段典型的建表语句我加了注释说明每个字段的用途-- 创建数据库字符集用 utf8mb4避免中文乱码 CREATE DATABASE dorm_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE dorm_db; -- 学生表存基础信息学号做唯一索引 CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, stu_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, name VARCHAR(50) NOT NULL COMMENT 姓名, gender TINYINT DEFAULT 1 COMMENT 1男 2女, class_name VARCHAR(50) COMMENT 班级, phone VARCHAR(20) COMMENT 联系电话 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 宿舍表记录楼号、房间号、床位数 CREATE TABLE dorm ( id INT PRIMARY KEY AUTO_INCREMENT, building VARCHAR(20) NOT NULL COMMENT 楼号, room_no VARCHAR(10) NOT NULL COMMENT 房间号, bed_total INT DEFAULT 4 COMMENT 总床位, bed_used INT DEFAULT 0 COMMENT 已用床位 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 住宿记录表学生和宿舍的关联一个学生只能有一条有效记录 CREATE TABLE live_record ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, dorm_id INT NOT NULL, bed_no INT COMMENT 床位号, status TINYINT DEFAULT 1 COMMENT 1在住 0退宿, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (student_id) REFERENCES student(id), FOREIGN KEY (dorm_id) REFERENCES dorm(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;执行完建表后再插入管理员账号和几条测试数据。注意live_record的外键依赖student和dorm所以插入顺序不能反。如果你用的是 Navicat 或 DataGrip建议把脚本拆成三段执行每段执行完检查一下有没有报错别一口气跑完看到一片红。2.3 配置数据源与跑通登录JSP 项目连接数据库通常有两种方式一种是在 JSP 页面里直接写 JDBC 代码另一种是用context.xml配置连接池。实训作业里前者更常见虽然不规范但胜在直观。下面是一个典型的数据库工具类// DBUtil.java统一获取连接避免每个 Servlet 都写一遍驱动加载 public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/dorm_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD 你的密码; static { try { // MySQL 8.0 用 cj 驱动5.7 去掉 cj Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { // 关闭顺序ResultSet - PreparedStatement - Connection try { if (rs ! null) rs.close(); } catch (SQLException e) {} try { if (ps ! null) ps.close(); } catch (SQLException e) {} try { if (conn ! null) conn.close(); } catch (SQLException e) {} } }这段代码的关键点有三个驱动类名要和 MySQL 版本匹配连接串里必须带时区和字符集参数关闭资源要按顺序来。登录逻辑一般放在LoginServlet里接收username和password用PreparedStatement查admin表查到了就session.setAttribute(user, admin)然后重定向到首页。如果登录后页面一片空白先看 Tomcat 日志里有没有SQLException再看web.xml里 Servlet 映射有没有写对。3. 读懂代码结构JSP 页面、Servlet 与 DAO 的分工3.1 典型目录结构与各层职责一个规范的 JSP 项目目录通常长这样src下按包名分servlet、dao、bean、utilweb或WebContent下放jsp页面、css、js、WEB-INF。WEB-INF里的web.xml是部署描述符lib下放 MySQL 驱动 jar 包。很多人把 jar 包放在项目根目录而不是WEB-INF/lib结果 Tomcat 启动时报NoClassDefFoundError这就是没搞清类加载路径。DAO 层负责和数据库打交道一个方法对应一条 SQLServlet 层负责接收请求、调 DAO、把结果塞进request或session再转发给 JSPJSP 只负责展示尽量别在里面写业务逻辑。我见过最离谱的写法是在 JSP 里直接new一个数据库连接然后循环查数据页面加载慢不说连接泄漏是迟早的事。3.2 用 Servlet 处理宿舍分配的核心逻辑宿舍分配是这套系统里业务最重的一块。它要同时更新dorm表的bed_used和插入live_record这两步必须在一个事务里否则会出现“床位占了但记录没写”的脏数据。下面是一个简化后的分配方法// DormDao.java分配床位事务保证一致性 public boolean assignBed(int studentId, int dormId, int bedNo) { Connection conn null; PreparedStatement ps1 null, ps2 null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 第一步检查床位是否已满 String checkSql SELECT bed_total, bed_used FROM dorm WHERE id ? FOR UPDATE; ps1 conn.prepareStatement(checkSql); ps1.setInt(1, dormId); ResultSet rs ps1.executeQuery(); if (rs.next() rs.getInt(bed_used) rs.getInt(bed_total)) { conn.rollback(); return false; // 床位已满 } // 第二步更新已用床位 String updateSql UPDATE dorm SET bed_used bed_used 1 WHERE id ?; ps2 conn.prepareStatement(updateSql); ps2.setInt(1, dormId); ps2.executeUpdate(); // 第三步插入住宿记录 String insertSql INSERT INTO live_record(student_id, dorm_id, bed_no) VALUES(?,?,?); ps2 conn.prepareStatement(insertSql); ps2.setInt(1, studentId); ps2.setInt(2, dormId); ps2.setInt(3, bedNo); ps2.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) {} e.printStackTrace(); return false; } finally { try { if (conn ! null) conn.setAutoCommit(true); } catch (SQLException e) {} DBUtil.close(conn, ps2, null); } }这里有几个参数和写法值得注意FOR UPDATE是行级锁防止两个管理员同时分配同一张床位setAutoCommit(false)开启事务后任何一步失败都要rollbackfinally里把autoCommit恢复成true再关闭连接避免连接池回收后状态污染。如果你用的是连接池这一步尤其重要否则下一个拿到这条连接的人会莫名其妙地处于事务中。3.3 JSP 页面里怎么安全地展示数据JSP 里展示数据最怕两件事一是中文乱码二是 XSS。中文乱码在页面顶部加% page contentTypetext/html;charsetUTF-8 languagejava %基本能解决XSS 则需要你在输出用户输入的内容时做转义。比如学生姓名如果直接% student.getName() %有人把名字改成一段脚本页面就会执行。常见做法是用 JSTL 的c:out标签它默认会转义 HTML% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table c:forEach items${studentList} varstu tr tdc:out value${stu.stuNo}//td tdc:out value${stu.name}//td tdc:out value${stu.className}//td /tr /c:forEach /table用c:out而不是% %是这套老项目里最容易补上、也最值得补的一个安全习惯。另外forEach的items属性对应 Servlet 里request.setAttribute(studentList, list)的 key名字必须一致大小写敏感。如果页面显示空白先检查 list 是不是 null再检查 JSTL 的 jar 包有没有放进WEB-INF/lib。4. 避坑与排查这套实训代码最容易翻车的 5 个地方4.1 中文乱码现象是问号根因在编码链现象登录后学生姓名显示成???或者乱码方块。原因通常有三层数据库表字符集不是utf8mb4、连接串没加characterEncodingutf8、JSP 页面没声明charsetUTF-8。解决方法是三层一起查先用SHOW CREATE TABLE student;看表字符集再检查 JDBC URL最后看 JSP 顶部 page 指令。三层都对了乱码基本消失。如果还有问题检查 Tomcat 的server.xml里 Connector 有没有URIEncodingUTF-8。4.2 空指针异常九成是 session 过期或参数没传现象点击某个功能页面报NullPointerException堆栈指向 Servlet 里某一行。原因通常是session.getAttribute(user)返回了 null而代码直接拿它调方法。解决方法是先判断 null再决定跳登录页还是继续。另一个常见来源是request.getParameter(id)拿到 null然后Integer.parseInt(null)直接炸。养成习惯所有外部输入先判空再转换。4.3 数据库连接泄漏Tomcat 跑一会儿就卡死现象系统刚启动正常操作几次后越来越慢最后所有请求都超时。原因是在 DAO 里getConnection()之后某个分支return了但没关闭连接。解决方法是把关闭逻辑统一放到finally块或者用 try-with-resources。如果你用的是连接池可以在池配置里加removeAbandonedtrue和removeAbandonedTimeout60做兜底但这只是后悔药根本还是要自己关。4.4 SQL 注入登录框输入 or 11就能进现象在登录页用户名输入 or 11密码随便填居然登录成功了。原因是登录 SQL 用了字符串拼接而不是PreparedStatement。解决方法是把所有带参数的 SQL 都改成?占位符用setString、setInt传值。这套实训代码里如果还有拼接写法建议你逐个改掉这不仅是安全问题也是面试必问的点。4.5 外键约束导致删除失败现象想删除一个学生报Cannot delete or update a parent row。原因是live_record表里有这个学生的住宿记录外键拦住了。解决方法有两种先删子记录再删主记录或者把外键改成ON DELETE CASCADE。但级联删除要慎用宿舍管理场景里退宿记录通常要保留所以更稳妥的做法是软删除——把status改成 0而不是物理删除。5. 从能跑到能讲把实训作业变成面试素材的三个技巧5.1 给系统加一个“操作日志”表讲清楚 AOP 思想面试官问“你项目里有没有做过日志”很多人只能答“用了 log4j 打日志”。你可以更进一步在数据库里建一张oper_log表记录谁在什么时间做了什么操作。然后在 Servlet 里用一个Filter统一拦截把请求参数和当前用户写进去。这样你就能讲清楚“过滤器链”“请求生命周期”“横切关注点”这些概念而不是干巴巴地说“我写过增删改查”。// OperLogFilter.java统一记录操作日志 public class OperLogFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpSession session request.getSession(false); if (session ! null session.getAttribute(user) ! null) { String uri request.getRequestURI(); String method request.getMethod(); // 异步写库避免阻塞主请求 LogService.save(uri, method, session.getAttribute(user).toString()); } chain.doFilter(req, resp); // 放行 } }这段代码的价值不在于多复杂而在于你能顺着它讲出“Filter 和 Interceptor 的区别”“为什么日志要异步写”“session 什么时候创建”。面试里细节就是说服力。5.2 用表格对比“物理删除”和“软删除”的取舍维度物理删除软删除数据可恢复不可恢复可恢复查询复杂度简单需加status1条件外键影响容易触发约束不影响存储占用释放空间持续占用适用场景临时数据业务记录这张表你可以直接用在面试回答里。宿舍管理系统中学生退宿、报修记录这些都应该软删除因为历史数据有查询价值。而像临时验证码这种物理删除没问题。能说清楚“什么场景用什么策略”比背概念强得多。5.3 把“文档”读成需求而不是附件很多人拿到实训作业的文档直接扔到一边只看代码。其实文档里往往写了功能清单和业务流程那是你理解“为什么这么设计”的钥匙。比如文档里写“学生退宿后床位释放”你就要去想释放床位是bed_used - 1还是重新计算如果并发退宿会不会算错顺着文档追问下去你就能发现代码里可能存在的并发问题然后自己动手改成UPDATE dorm SET bed_used (SELECT COUNT(*) FROM live_record WHERE dorm_id ? AND status 1)。这种“从文档到代码再到改进”的闭环才是实训作业最大的价值。我自己的习惯是每拿到一份这类项目先跑通再挑一个最不顺眼的地方改掉最后把改动过程写成笔记。改过三五个项目之后你会发现面试时能讲的东西根本不是背出来的而是改出来的。希望帮到你。本文还有配套的精品资源点击获取