简介:基于 Servlet、JSP 与 MySQL 技术栈实现的 JavaWeb 学生宿舍管理系统,面向 Java 初学者、课程设计及毕业设计人群,覆盖宿舍信息管理、楼栋/宿舍管理、管理员与登录验证等常见业务模块,可作为学习 MVC 分层开发与 JDBC 操作的完整参考项目。压缩包共 295 个文件,16.88MB,包含 30 个 Java 源文件、30 个 class 编译文件、13 个 JSP 页面、111 张界面图片及配套 CSS/JS,另有数据库脚本与配置文件,便于直接导入部署和二次开发。资源附带项目结构、前端页面素材和 jar 依赖包,能帮助读者快速理解代码组织方式并还原运行环境。已有 5921 人学习下载,适合需要从零搭建或扩展宿舍管理系统的开发者参考。
1. 基于 Servlet+JSP+MySQL 的 JavaWeb 学生宿舍管理系统:为什么 2025 年还在用这套组合
如果你打开招聘网站,会发现 Java 岗位几乎被 Spring Boot 占领,但打开学校课程设计和毕业设计的需求列表,"基于 Servlet+JSP+MySQL 的 JavaWeb 学生宿舍管理系统"依然是出现频率最高的一类题目。这不是技术倒退,而是这套组合在"教学价值"和"业务复杂度"之间找到了合适的平衡点:Servlet 让你理解 HTTP 请求的底层处理逻辑,JSP 让你看清页面渲染的本质,MySQL 则承担了所有数据持久化的工作。对于刚学完 Java 基础、急需一个完整项目串联知识点的人来说,宿舍管理系统是一个复杂度适中的练手对象——比图书管理系统多出寝室分配和出入登记的业务逻辑,又比电商系统少掉支付、权限、高并发这些重包袱。
但需要注意的是,"能跑通"和"能拿得出手"是两回事。我见过太多照着网上下载的源码改个数据库密码就交作业的案例,一旦被问到"你的登录逻辑是怎么防止 SQL 注入的""寝室空闲床位是怎么统计的",就容易卡壳。这篇文章会用一套完整的设计思路,从环境适配、数据库建模、Servlet+JSP 的请求流转,到三个核心功能模块的实现,最后给出真实的踩坑记录,让你对标着做一个能讲清楚、能复现、能回答追问的宿舍管理系统。
2. 环境选型与搭建:JDK 版本、Tomcat 版本和 MySQL 版本怎么配才不翻车
2.1 JDK 8 还是 JDK 17?版本匹配的底层逻辑
很多新手拿到项目第一步就卡在环境上,原因在于盲目下载最新版。常见做法是:Servlet 4.0 + JSP 2.3 这个组合在 JDK 8 和 Tomcat 9 上经过了最多验证,网上能找到的宿舍管理系统参考代码也基本都是这个环境。如果你用 JDK 17 + Tomcat 10,会遇到 javax.servlet 包名变成 jakarta.servlet 的兼容性问题,旧项目直接跑不起来,需要全局替换依赖坐标和 import 语句,这个工作量对新手来说是灾难。
我一般推荐的环境组合是:JDK 8(或者 OpenJDK 8)、Tomcat 9.0、MySQL 5.7(或者 MySQL 8.0)、IDEA 2023 版本。这里特别说一下 MySQL 版本的选择:如果你用的是 5.7,记得下载的是mysql-5.7.44-winx64.zip解压版,配合mysqld --initialize-insecure初始化,root 用户默认无密码;如果你用 MySQL 8.0,要注意com.mysql.jdbc.Driver已经废弃,必须用com.mysql.cj.jdbc.Driver,而且连接串需要加serverTimezone=Asia/Shanghai,否则会报时区错误。很多人在 MySQL 安装和配置上耗费的时间比写代码还长,这里直接给出一个稳定的操作序列:
# 1. 解压 mysql 到 D 盘,比如 D:\mysql-5.7.44-winx64 # 2. 新建 my.ini 配置文件,放在解压目录下 [mysqld] basedir=D:/mysql-5.7.44-winx64 datadir=D:/mysql-5.7.44-winx64/data port=3306 character-set-server=utf8mb4 default-storage-engine=INNODB # 3. 管理员权限打开 cmd,进入 bin 目录 mysqld --initialize-insecure # 4. 安装服务 mysqld install MySQL # 5. 启动服务 net start MySQL--initialize-insecure参数的含义是生成一个 data 目录且 root 用户密码为空,这一步如果跳过,MySQL 服务启动时会因为缺少 data 目录而报错。character-set-server=utf8mb4是为了让数据库支持中文和 emoji 字符,否则后面 JSP 页面往数据库里插入中文数据会出现乱码。如果你的服务启动失败,先看data目录下的.err日志文件,里面会明确告诉你配置文件的哪一行写错了。
2.2 IDEA 中配置 Tomcat 与项目骨架
环境层面的第二个高发问题,是用 IDEA 创建一个 JavaWeb 项目时找不到 Tomcat 选项,或者配置了 Tomcat 后启动报404。常见做法是:IDEA 中选择New Project→Jakarta EE→Web Application,然后选择Tomcat 9,IDEA 会自动生成web/WEB-INF/web.xml和index.jsp的骨架结构。如果你的 IDEA 版本不直接支持,可以手动创建一个普通 Java 项目,然后右键添加Web Application框架支持,再配置 Artifacts 和 Application Server。
这里有一个关键参数:在Run/Debug Configurations中,Application server选择你的 Tomcat 安装目录,Deployment选项卡里要添加war exploded类型的 Artifact,Application context建议直接写/而不是/war包名_war_exploded,这样访问的时候http://localhost:8080/login.jsp就能直接命中了,不用每次输入一长串路径。配置好后,启动 Tomcat 看到Artifact is deployed successfully日志,且浏览器能打开默认的 JSP 页面,环境就算通了一半。
2.3 新建 Maven 项目 vs 直接引入 jar 包
对学生宿舍管理系统这个体量而言,是个需要现场决策的点。王老师很重视风险管理流程,他有自己的风险偏好:强调岗位追责和主体责任划分,把风险控制着力在技术层面的闭环逻辑而不是终端的技术不确定性,所以在评标办法上要全面落实风险防控前置化,以技术评审分作为准入条件,同时强化评审专家的独立意见权重和组长负责制,在流程中重点强调过程追责与一票否决、责任倒查机制的有效落实。这里的关键在于把"用风险方法评技术"的主导思路转化为几个可操作的制度抓手。如果选择 Maven,pom.xml中加入依赖的方式对后续扩展更友好,但新手需要理解依赖坐标和 scope 的含义;直接导入 jar 包则更直观,不会有仓库下载失败的问题。
<dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>javax.servlet.jsp-api</artifactId> <version>2.3.3</version> <scope>provided</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.49</version> </dependency> </dependencies>注意scope=provided的语义:Servlet API 和 JSP API 的实现在 Tomcat 里已经存在,如果不加这个 scope,打包时会把它们的类打进去,和 Tomcat 自带的类冲突,启动时会报ClassNotFoundException或者NoSuchMethodError这类玄学问题。MySQL 驱动的 scope 不写 provided,因为它需要被打进最终的 war 包里,部署到任何一台机器上都能直接使用。
3. 数据库设计:学生、寝室、出入记录三张核心表的建模与建库脚本
3.1 宿舍管理系统的实体关系拆解
从需求出发,宿舍管理系统的核心业务是管理学生信息和寝室资源,最基础的需求有三个:记录学生住在哪个寝室、快速查询某个寝室还剩几个床位、登记学生晚归或外出的记录。围绕这三个需求,最少需要三张表:学生表、寝室表、出入记录表。这里有一个设计决策需要提前想清楚:学生和寝室的关系是"入住时写入学生表的寝室字段",还是单独建一张关联表来记录入住历史。对课程设计级别的系统,建议采用前者——学生表里直接存dorm_id外键,查询简单,代码量少;如果你的系统需要统计"某学生住过哪些寝室"这种历史数据,再考虑单独建关联表。
另外一个容易忽视的点是寝室表和楼栋的关系。如果你的学校有多栋宿舍楼,且每栋楼的寝室号会重复(比如 1 号楼 101 和 2 号楼 101),那么应该在寝室表加一个building字段,用"楼栋+寝室号"作为唯一逻辑键,而不是只用dorm_no。很多翻车的项目就是没考虑这个,后期加楼栋字段时前端页面和所有 SQL 都要跟着改。
3.2 三张表的建表脚本与字段解释
下面这份建表脚本是我在一套较完整的宿舍管理系统里会使用的方案,包含了字段注释和初始数据,可以直接在 Navicat 或命令行执行:
-- 创建数据库,指定 UTF-8 编码 CREATE DATABASE IF NOT EXISTS dormitory CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE dormitory; -- 寝室表 CREATE TABLE dorm ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '寝室ID', building VARCHAR(20) NOT NULL COMMENT '楼栋号', room_no VARCHAR(20) NOT NULL COMMENT '房间号', bed_num INT NOT NULL COMMENT '总床位数', used_bed INT DEFAULT 0 COMMENT '已使用床位数', UNIQUE KEY uk_building_room (building, room_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='寝室信息表'; -- 学生表 CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT '学号', name VARCHAR(50) NOT NULL COMMENT '姓名', gender ENUM('男','女') NOT NULL COMMENT '性别', phone VARCHAR(20) COMMENT '联系电话', dorm_id INT COMMENT '所属寝室ID', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', FOREIGN KEY (dorm_id) REFERENCES dorm(id) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生信息表'; -- 出入记录表 CREATE TABLE access_log ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL COMMENT '学生ID', action_type ENUM('外出','晚归','正常出入') DEFAULT '正常出入', access_time DATETIME NOT NULL COMMENT '出入时间', remark VARCHAR(255) COMMENT '备注', FOREIGN KEY (student_id) REFERENCES student(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='出入登记表'; -- 初始化寝室数据 INSERT INTO dorm (building, room_no, bed_num) VALUES ('1号楼', '101', 4), ('1号楼', '102', 4), ('2号楼', '201', 6);字段说明:dorm表的UNIQUE KEY uk_building_room是从数据库层面防止重复录入同号楼同房间号,属于必要的业务约束,这和后面 Servlet 代码里的前端校验是两道防线,只做前端校验的话,手工调接口也能插入重复数据。student.dorm_id的ON DELETE SET NULL含义是寝室被删除时学生记录保留,但寝室字段置空,防止删除寝室时把学生信息连带删掉。access_log的action_type用ENUM类型而不是VARCHAR,是为了让数据库层约束数据合法性,如果代码里传入一个枚举之外的值(比如"外出"和"晚归"混写),MySQL 会直接报错而不是把脏数据入库。access_log.student_id用ON DELETE CASCADE是因为出入记录没有独立保存价值,学生注销后记录一并清理。
3.3 空闲床位统计的核心 SQL
宿舍管理系统最常被问到的查询是"每个寝室还剩多少床位",对应到数据表上就是bed_num减去used_bed,但used_bed字段存在数据一致性风险:如果某个学生转寝,代码里更新了student.dorm_id但忘了同步更新dorm.used_bed,就会导致统计数据出错。要做到数据能对上、经得起追问,有两种方案:
第一种方案是在业务代码里手动维护used_bed:分配寝室时used_bed+1,换寝室时旧寝室used_bed-1且新寝室used_bed+1,退宿时used_bed-1。优点是查询速度快,缺点是每次寝室变动都必须放在同一个事务里,否则容易漏更新。
第二种方案是去掉used_bed字段,每次统计时手动用子查询实时计算:
SELECT d.id, d.building, d.room_no, d.bed_num, IFNULL(s.cnt, 0) AS used_bed, d.bed_num - IFNULL(s.cnt, 0) AS free_bed FROM dorm d LEFT JOIN ( SELECT dorm_id, COUNT(*) AS cnt FROM student WHERE dorm_id IS NOT NULL GROUP BY dorm_id ) s ON d.id = s.dorm_id;这段 SQL 的关键在于LEFT JOIN配合子查询,语义是"即使没有学生入住的寝室也要出现在结果里",IFNULL把 NULL 转成 0,这样free_bed等于总床位数。如果用了INNER JOIN,没有学生的空闲寝室会被过滤掉,查询结果就不完整。如果你的系统数据量在几万条以内,第二种方案完全够用,而且从根本上消除了used_bed字段的同步问题;如果以后数据量上来,再改动为第一种方案加定时任务做数据校准。
4. Servlet+JSP 的请求流转:登录验证到页面跳转的完整链路
4.1 Servlet 生命周期与请求映射
学生宿舍管理系统的控制层由一组 Servlet 组成,它们的职责范围要划分清楚,避免一个 Servlet 承载所有功能导致代码膨胀。常见思路是按业务模块建 Servlet:LoginServlet处理登录,DormServlet处理寝室增删改查,StudentServlet处理学生入住管理,AccessLogServlet处理出入登记。每个 Servlet 通过@WebServlet("/dorm")注解标注访问路径,这是 Servlet 3.0 之后的规范,不需要在web.xml里逐个配置映射。
这套体系有一个明确的分工链:浏览器发起 HTTP 请求到 Tomcat,Tomcat 根据注解里的 URL 路径找到对应 Servlet,Servlet 在doGet或doPost方法里调用request.getParameter()拿参数,调用 DAO 层查询数据库,然后把结果放进request.setAttribute("key", value)或session.setAttribute("key", value),最后通过request.getRequestDispatcher("/dorm_list.jsp").forward(request, response)把请求转给 JSP 页面渲染。这里有一个必须遵守的规则:JSP 页面负责显示,不写业务逻辑;Servlet 负责业务逻辑,不直接输出 HTML。如果你在 Servlet 里用response.getWriter().println("<html>")拼页面,代码会迅速进入无法维护的状态。
4.2 登录验证与 Session 会话管理
登录是宿舍管理系统的入口,也是最容易写出安全漏洞的地方。一个合格的登录流程应该包含:接收用户名和密码 → 先查询数据库判断用户是否存在且密码匹配 → 把用户信息存入 Session → 跳转到主页面 → 在需要受保护的页面做 Session 校验。这里给出一个基础版本的LoginServlet:
@WebServlet("/login") public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); UserDAO dao = new UserDAO(); User user = dao.findByUsernameAndPwd(username, password); if (user != null) { HttpSession session = request.getSession(); session.setAttribute("loginUser", user); session.setMaxInactiveInterval(30 * 60); // 30分钟超时 response.sendRedirect(request.getContextPath() + "/index.jsp"); } else { request.setAttribute("errorMsg", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); } } }这段代码有四个值得注意的参数和设计决策。第一,request.setCharacterEncoding("UTF-8")必须放在读取参数之前,否则 Post 请求里的中文用户名会乱码。第二,dao.findByUsernameAndPwd里的 SQL 要使用PreparedStatement的?占位符而不是字符串拼接,这是防 SQL 注入的底线。第三,登录成功后用sendRedirect而不用forward,是为了防止用户刷新页面时重复提交表单,这在浏览器行为上区别很大——forward是服务端内部跳转,刷新会重放 Post 请求。第四,setMaxInactiveInterval(30 * 60)设置 Session 的存活时间,30 分钟内无操作就要重新登录,这是宿舍管理系统这类后台管理系统应该有的安全底线。
4.3 DAO 层的封装与 JDBC 连接管理
宿舍管理系统的代码结构一般分为三层:Servlet(控制层)、Service(业务层)、DAO(数据访问层)。对这个项目来说,Service 层可以合并进 Servlet,但 DAO 层必须单独拎出来,因为 JDBC 操作代码的重复度很高。我在项目中会建一个BaseDAO类,把获取连接和关闭资源的逻辑统一封装:
public class DBUtil { private static String url = "jdbc:mysql://localhost:3306/dormitory?useSSL=false&characterEncoding=utf8"; private static String user = "root"; private static String password = "123456"; static { try { Class.forName("com.mysql.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } public static void close(ResultSet rs, PreparedStatement ps, Connection conn) { // 反向关闭,先关 ResultSet,再关 Statement,最后关 Connection } }useSSL=false这个参数是 MySQL 连接串里必须处理的一个坑:MySQL 8.0 默认开启 SSL,如果连接串不加useSSL=false,控制台会爆出一大段 SSL 相关的警告日志,虽然不影响程序运行,但在考试答辩或课堂演示的现场看到满屏 ERR 会显得项目不干净。如果驱动是mysql-connector-java 8.0.x,Class.forName的类名要换成com.mysql.cj.jdbc.Driver,这是版本升级带来的破坏性变更。
5. 三个核心功能模块的实现:查寝室、分配寝室、出入登记
5.1 按条件查询寝室:拼接动态 SQL 的规范做法
宿舍管理系统的寝室查询页,通常会提供楼栋、寝室号、剩余床位这几个筛选条件。这里最常见的问题是:用户没有输入某个条件时,程序应该怎么处理?如果还用WHERE building = ?去执行,结果是查不到任何数据。正确的做法是拼接动态 SQL,在 Service 层或 DAO 层判断参数是否为空来决定是否拼入查询条件。我一般会先构建一个可变的 SQL 前缀,再用List<Object>收集参数:
public List<Dorm> searchDorms(Dorm query) { StringBuilder sql = new StringBuilder("SELECT * FROM dorm WHERE 1=1 "); List<Object> params = new ArrayList<>(); if (query.getBuilding() != null && !"".equals(query.getBuilding())) { sql.append("AND building LIKE ? "); params.add("%" + query.getBuilding() + "%"); } if (query.getRoomNo() != null && !"".equals(query.getRoomNo())) { sql.append("AND room_no = ? "); params.add(query.getRoomNo()); } if (query.getFreeBed() != null) { sql.append("AND bed_num - used_bed >= ? "); params.add(query.getFreeBed()); } return queryBySql(sql.toString(), params.toArray()); }WHERE 1=1这个写法在初学者看来是多余的,但在动态拼接 SQL 的场景下它是主动走的一条路:后续每个条件都直接拼AND ...,不需要额外判断"当前是否已有条件",减少条件分支的代码量,可读性也更直观。LIKE ?配合%通配符做模糊查询,但要注意%是加在参数值上而不是直接拼进 SQL 字符串,这样PreparedStatement仍然能够正确转义通配符。寝室号用等号匹配而不是模糊查询,因为寝室号的输入通常是精确值。
5.2 分配寝室和换寝室的数据库事务边界
分配寝室是宿舍管理系统里数据一致性要求最高的一个操作,因为它同时要修改两张表的数据:在学生表里更新dorm_id,在寝室表里把used_bed加 1。如果这两步操作只成功了一半,就会出现"学生有寝室ID但寝室的已住人数没变"或反之的情况,这属于必须用数据库事务保证的原子性操作。在 JDBC 中开启事务的规范写法是:conn.setAutoCommit(false)→ 执行多条更新语句 →conn.commit();任一步抛异常则conn.rollback(),最后在finally中恢复setAutoCommit(true)并关闭连接。
更严谨的做法还包括并发控制:如果两个管理员同时对学生 2023001 执行分配寝室操作,后一个操作可能会覆盖前一个。如果寝室管理系统的用户量不大(单机、操作频率低),在分配前先查一次学生是否已有dorm_id并且不为空就拦截即可。如果追求更严格的机制,可以给student表的dorm_id加唯一索引,但这样设计会限制一个学生只有一条寝室记录——如果你的系统允许一个学生同时属于多个宿舍(比如临时住宿和正式住宿),这个方案就不通用了,或者记录每一条入住历史。在你动手之前,先把宿舍业务的实际规则研究清楚再决定索引怎么加,这是一个值得好好思考的设计决策。
以下是文档中"分配寝室和换寝室的数据库事务边界"部分的重写内容:
分配寝室是宿舍管理系统里数据一致性要求最高的一个操作,因为它同时要修改两张表的数据:在学生表里更新dorm_id,在寝室表里把used_bed加 1。如果这两步操作只成功了一半,就会出现"学生有寝室ID但寝室的已住人数没变"或反之的情况,这属于必须用数据库事务保证的原子性操作。在 JDBC 中开启事务的规范写法是:conn.setAutoCommit(false)→ 执行多条更新语句 →conn.commit();任一步抛异常则conn.rollback(),最后在finally中恢复setAutoCommit(true)并关闭连接。
更进一步的实战经验是:查询空闲床位和更新used_bed之间其实存在一个时间窗口(先查后改),理论上并发会出现超卖。对课程设计级别的系统,我通常会在寝室表加一个version字段用乐观锁,或者干脆在 UPDATE 语句里直接加条件限制:
UPDATE dorm SET used_bed = used_bed + 1 WHERE id = ? AND used_bed < bed_num这段 SQL 的巧妙之处在于把"校验床位是否充足"和"增加已住人数"合并成一条原子操作,如果used_bed < bed_num不成立,update影响的行数是 0,代码里判断int rows = ps.executeUpdate()是否为 0 即可知道分配是否失败,不需要提前用 SELECT 查询。这既避免了并发超卖,又减少了代码里的一个查询步骤。
5.3 出入登记:JSP 表单到数据库的完整闭环
出入登记模块的功能是记录学生的外出和晚归情况。这个模块的代码流程相对简单,但它是完整的 Servlet+JSP 数据流转示例:JSP 页面上有一个下拉框选择学生、单选按钮选择外出/晚归/正常出入、文本框填备注,提交后由AccessLogServlet接收参数并插入数据库。这里要注意的是 JSP 表单里的学生列表要通过 JSTL 标签循环输出,而不是手动写死:
<select name="studentId"> <c:forEach items="${studentList}" var="stu"> <option value="${stu.id}">${stu.studentNo} - ${stu.name}</option> </c:forEach> </select>这段代码里的${studentList}是AccessLogServlet在 doGet 方法中通过request.setAttribute("studentList", list)传来的数据。Servlet 里需要先查出所有学生列表传给 JSP,JSP 渲染完下拉框,用户提交表单后又回到 Servlet 的 doPost。这里有一个参数命名陷阱:request.getParameter("studentId")拿到的是<option>的value属性,也就是学生的id,但很多新手会误以为拿到的是studentNo(学号),导致查询学生信息时拿 id 去匹配学号字段,查不到数据。
出入登记的时间字段,我建议在 Servlet 里直接用new Date()生成,而不是让用户在页面上手填日期时间——手填的格式不可靠,而且有伪造时间的可能。如果确实需要支持补录(比如登记昨天的晚归),页面上用<input type="date">加一个时间输入框,后端用SimpleDateFormat解析时指定时区,否则会有 8 小时的隐性时差问题,这个问题在后面的避坑章节会细讲。
6. 避坑指南:JavaWeb 宿舍管理系统最常见的 5 个翻车现场
6.1 JSP 页面中文乱码:表象在页面,根源在编码链
现象:JSP 页面上的中文正常,但提交表单后存进 MySQL 的数据变成了问号或者乱码,反过来显示的时候也一片乱码。
原因:编码不一致分成四个环节——JSP 页面文件本身的编码、HTTP 请求的编码、Tomcat 连接器的编码、MySQL 表和字段的编码。任何一个环节不一致,中文在传递过程中就会被破坏。最常见的失误是 JSP 文件顶部漏掉<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>,或者my.ini里没有配置character-set-server=utf8mb4,又或者 JDBC 连接串里少了characterEncoding=utf8。
解决:四步一次性补齐。第一,JSP 页面统一加page指令指定 UTF-8;第二,所有 Servlet 的doPost方法第一行执行request.setCharacterEncoding("UTF-8");第三,连接串追加characterEncoding=utf8;第四,检查数据库表的排序规则,如果是utf8_general_ci改成utf8mb4_general_ci。排查顺序从浏览器端往数据库端逐层验证,用抓包工具或直接打印request.getParameter收到的原始值来确定是哪一层出了问题。
6.2 Servlet 返回 405 错误:doGet 和 doPost 没成对实现
现象:点击页面的查询按钮或表单提交按钮,浏览器地址栏的 URL 是对的,但页面报 405 HTTP 状态码,英文提示HTTP method GET is not supported by this URL。
原因:表单的method设为post,但 Servlet 只重写了doGet方法而没有doPost;或者反过来,浏览器地址栏访问默认发 GET 请求,但 Servlet 只写了doPost。Tomcat 的HttpServlet基类对未重写的请求方法默认返回 405,这是一种提醒你"方法不存在"的机制,也是一个新手常忽略的细节。
解决:在@WebServlet注解标注的 Servlet 里,把doGet和doPost都写上,一个常见的写法是让doGet直接调用doPost,或者都转发到一个公用的process方法。如果你的页面既有 GET 请求(展示表单)又有 POST 请求(提交表单),那就两个方法都必须实现,各自做各自的事。排查方式很简单:看浏览器开发者工具里请求的 Method 列和 Servlet 源码里的方法签名,一对应就清楚了。
6.3 MySQL 连接串报 SSL 错误和时区错误
现象:Tomcat 日志里爆出WARN: Establishing SSL connection without server's identity verification is not recommended,或者Cannot create PoolableConnectionFactory,错误原因指向The server time zone value '�й���ʱ��' is unrecognized。
原因:MySQL 8.0 的驱动默认要求服务端开启 SSL 且需要指定时区;而 5.7 的连接如果用了高版本驱动也会有类似警告。serverTimezone的值如果写成GMT+8或者中文的"中国标准时间",驱动识别不了时区名称。
解决:连接串统一写成jdbc:mysql://localhost:3306/dormitory?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8。其中Asia/Shanghai是 Java 时区体系里能被驱动的TimeZone类识别的合法 ID,比GMT+8更可靠。
6.4 Tomcat 启动闪退或端口占用
现象:双击 Tomcat 的startup.bat,窗口一闪而过,或启动时控制台报Port 8080 was already in use,又或者The CATALINA_HOME environment variable is not defined correctly。
原因:闪退的原因通常是CATALINA_HOME环境变量没配置,或 JDK 环境变量不对,或端口被占用导致 Tomcat 启动失败后窗口直接关闭。端口冲突多发生在已经装了其他 Web 服务(比如 Nginx、其他 Tomcat 实例)的机器上。
解决:先用java -version确认 JDK 已装且版本是 8;再确认JAVA_HOME指向 JDK 安装目录而不是 JRE 目录。然后打开 Tomcat 的conf/server.xml,把<Connector port="8080"改成没有被占用的端口,比如 8081。如果窗口一闪而过看不到具体报错,在 cmd 里手动执行catalina.bat run,它会将错误信息打印在当前窗口不闪退,方便定位问题。这个信息很重要,在开发环境省了很多时间。
6.5 JSP 页面加载后自动刷新一次导致数据重复提交
现象:宿管系统的登记页面,用户提交一条"晚归"记录后,页面好像自己刷新了一次,数据库里出现了两条一模一样的记录。
原因:在主流的浏览器行为模式里,用户点击提交按钮后,浏览器向服务器发送 POST 请求;如果服务器处理完请求后使用forward转发回 JSP 页面,此时浏览器的地址栏仍然是处理表单的 URL,用户按 F5 刷新或浏览器自动重新请求就会再次执行 Servlet 的doPost,数据被重复插入。这也是第 4 章里登录成功后用sendRedirect而不是forward的原因。
解决:遵循"Post/Redirect/Get"模式——Servlet 处理完写操作(插入出入记录、分配寝室、登记入住)后,不要直接forward到 JSP 页面,而是使用response.sendRedirect(request.getContextPath() + "/access_log_list")重定向到查询列表的 Servlet,再由查询的 Servlet 经过 GET 请求转发到 JSP 列表页。这样浏览器地址栏变为查询列表的 URL,刷新操作变成 GET 请求,不会重复提交。
7. 从能跑到能答辩:验证功能的三个视角与项目兜底技巧
在项目交付前验证功能是否完整,不只是"点一圈没报错"那么简单。我从三个视角做了最终检查:第一视角是普通管理员,能不能完成登录、查寝室、分配寝室、登记出入的完整业务链;第二视角是数据库,手动在 MySQL 里执行一条非法操作(比如插入一个不存在的学生 ID 的出入记录),看系统会不会报错提示还是直接静默失败——这决定了系统在答辩现场的健壮性表现;第三视角是代码审查,用Ctrl+F搜索Statement.createStatement(),如果查到了,就是个隐患——因为字面量拼接 SQL 的方式容易引发注入问题,要逐一替换为PreparedStatement,同时搜索printStackTrace()检查是否所有的异常处理都被正确保留。
最后提一个很实用的小技巧,Lombok 可能不适合出现在课程设计里,但可以用一个精简的工具类来消除 Bean 的样板代码——其实说穿了就是getter/setter/toString要让代码短一点,讲起来更有把握。对整个系统做一个完整的打包流程,在 IDEA 中构建 Artifacts,产出dormitory.war,丢到 Tomcat 的webapps目录下自动解压部署,这样即便把项目迁移到另一台机器上,也能做到同一套代码跑起来不依赖 IDE 的环境。如果系统想要在本机断开 MySQL 服务后依然能展示数据,可以在初始化时用mysqldump导出一份dormitory.sql备份,需要恢复数据时一行命令就能重建库表——这是我在课程设计阶段养成的习惯,面试官问起"你做过完整的项目部署吗",就能拿这一套流程来讲,而不是停留在"在 IDEA 里能跑"的程度。
我还有一个个人教训是:如果你要在一个已经运行过多轮的系统上改表结构,MySQL 里ALTER TABLE命令会自动提交事务,没有后悔药可吃。所以在动手改字段之前,务必先把原表结构和数据导出一份。包括:先通过命令行查看当前数据目录的存储引擎,理解索引关联关系,再做变更。感谢这里的经验沉淀成了一种直觉,也让后面几套系统避免踩类似的坑。希望这些细节对你的宿舍管理系统开发有真实的帮助。
本文还有配套的精品资源,点击获取