简介:这份实训作业资源面向计算机相关专业学生与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. 从一份实训作业说起:JSP+MySQL 宿舍管理系统到底能跑出什么
很多人看到“实训作业基于JSP+MySQL的学生宿舍管理系统源代码+数据库+文档”这个标题,第一反应是:这不就是大学里最典型的那类课程设计吗?但如果你真把它当成一次“交完就删”的作业,那就亏了。这套东西本质上是一个完整的、可运行的 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 11,Tomcat 8.5 或 9.0,MySQL 5.7 或 8.0,IDE 用 IntelliJ IDEA 社区版就够。别小看版本匹配这件事,我见过太多人卡在“项目跑不起来”上,最后发现只是 Tomcat 10 把包名换了。
数据库驱动也要注意:MySQL 8.0 用com.mysql.cj.jdbc.Driver,5.7 用com.mysql.jdbc.Driver。连接串里如果没加serverTimezone=Asia/Shanghai,MySQL 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 '联系电话' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 宿舍表:记录楼号、房间号、床位数 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 '已用床位' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 住宿记录表:学生和宿舍的关联,一个学生只能有一条有效记录 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;执行完建表后,再插入管理员账号和几条测试数据。注意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?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; 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、util;web或WebContent下放jsp页面、css、js、WEB-INF。WEB-INF里的web.xml是部署描述符,lib下放 MySQL 驱动 jar 包。很多人把 jar 包放在项目根目录而不是WEB-INF/lib,结果 Tomcat 启动时报NoClassDefFoundError,这就是没搞清类加载路径。
DAO 层负责和数据库打交道,一个方法对应一条 SQL;Servlet 层负责接收请求、调 DAO、把结果塞进request或session,再转发给 JSP;JSP 只负责展示,尽量别在里面写业务逻辑。我见过最离谱的写法是在 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)开启事务后,任何一步失败都要rollback;finally里把autoCommit恢复成true再关闭连接,避免连接池回收后状态污染。如果你用的是连接池,这一步尤其重要,否则下一个拿到这条连接的人会莫名其妙地处于事务中。
3.3 JSP 页面里怎么安全地展示数据
JSP 里展示数据,最怕两件事:一是中文乱码,二是 XSS。中文乱码在页面顶部加<%@ page contentType="text/html;charset=UTF-8" language="java" %>基本能解决;XSS 则需要你在输出用户输入的内容时做转义。比如学生姓名如果直接<%= student.getName() %>,有人把名字改成一段脚本,页面就会执行。常见做法是用 JSTL 的<c:out>标签,它默认会转义 HTML:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <table> <c:forEach items="${studentList}" var="stu"> <tr> <td><c:out value="${stu.stuNo}"/></td> <td><c:out value="${stu.name}"/></td> <td><c: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、连接串没加characterEncoding=utf8、JSP 页面没声明charset=UTF-8。解决方法是三层一起查:先用SHOW CREATE TABLE student;看表字符集,再检查 JDBC URL,最后看 JSP 顶部 page 指令。三层都对了,乱码基本消失。如果还有问题,检查 Tomcat 的server.xml里 Connector 有没有URIEncoding="UTF-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。如果你用的是连接池,可以在池配置里加removeAbandoned="true"和removeAbandonedTimeout="60"做兜底,但这只是后悔药,根本还是要自己关。
4.4 SQL 注入:登录框输入' or '1'='1就能进
现象:在登录页用户名输入' or '1'='1,密码随便填,居然登录成功了。原因是登录 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 用表格对比“物理删除”和“软删除”的取舍
| 维度 | 物理删除 | 软删除 |
|---|---|---|
| 数据可恢复 | 不可恢复 | 可恢复 |
| 查询复杂度 | 简单 | 需加status=1条件 |
| 外键影响 | 容易触发约束 | 不影响 |
| 存储占用 | 释放空间 | 持续占用 |
| 适用场景 | 临时数据 | 业务记录 |
这张表你可以直接用在面试回答里。宿舍管理系统中,学生退宿、报修记录这些都应该软删除,因为历史数据有查询价值。而像临时验证码这种,物理删除没问题。能说清楚“什么场景用什么策略”,比背概念强得多。
5.3 把“文档”读成需求,而不是附件
很多人拿到实训作业的文档,直接扔到一边,只看代码。其实文档里往往写了功能清单和业务流程,那是你理解“为什么这么设计”的钥匙。比如文档里写“学生退宿后床位释放”,你就要去想:释放床位是bed_used - 1还是重新计算?如果并发退宿会不会算错?顺着文档追问下去,你就能发现代码里可能存在的并发问题,然后自己动手改成UPDATE dorm SET bed_used = (SELECT COUNT(*) FROM live_record WHERE dorm_id = ? AND status = 1)。这种“从文档到代码再到改进”的闭环,才是实训作业最大的价值。
我自己的习惯是:每拿到一份这类项目,先跑通,再挑一个最不顺眼的地方改掉,最后把改动过程写成笔记。改过三五个项目之后,你会发现面试时能讲的东西,根本不是背出来的,而是改出来的。希望帮到你。
本文还有配套的精品资源,点击获取