资讯详情 Java+JSP+MySQL宿舍管理系统实战:从E-R图、SQL建模到Servlet部署排错
📅 2026/10/6 3:11:28
简介这套JavaWeb实训项目以JSPServletMySQL实现学生宿舍管理系统面向高校JavaWeb课程设计、毕业设计以及需要快速搭建宿舍管理演示系统的开发者。系统采用管理员与学生双角色结构管理员可完成学生信息增删改查、卫生信息登记查看、综合宿舍信息维护与维修状况处理学生可查询和修改个人信息、在线报修、查看卫生评比覆盖宿舍管理的主要业务闭环。资源为zip压缩包共137个文件包含44个Java源码及对应class文件、26个JSP页面、1个MySQL建表SQL脚本以及CSS、JS、GIF图片等前端资源与项目说明整体约3.39MB目录结构直观。包内提供E-R图与SQL语句可帮助理解数据表关系与初始化数据便于二次开发或课程答辩项目经多次测试可运行无误已有3001人学习下载适合需要完整参考实现、快速跑通并继续扩展功能的学习者。1. 从Excel登记到Web系统这个宿舍管理项目到底在做什么如果你在高校的后勤办公室待过或者做过这类内部系统的外包大概率见过这样的场景宿管阿姨手里一本纸质台账学生调宿要翻半天报修单靠手写月底统计楼栋入住率要靠人工数。标题里这套「JavaJSPMySQL实现Web学生宿舍管理系统含E-R图和SQL语句」本质就是把这些手工流程搬上Web的完整工程示例。它不追新潮技术但胜在链路完整——从E-R图建模到SQL建库再到JSP页面和Servlet控制器的 MVC 闭环全都能在一台电脑上跑通并演示。适合三类人正在做课设或毕设、需要能答辩的完整项目的学生想用最低成本给内部团队做管理后台的初中级开发者以及准备Java Web面试、想把JDBC和Session鉴权功底补扎实的求职者。一句话这个标题的价值不在技术新而在「完整、可复现、能落地」。2. 先把技术栈拆明白JSPServlet做MVCMySQL做数据底座2.1 为什么还在用JSP老技术栈在内部系统里的真实优势看到JSP很多人的第一反应是「这都哪年的技术了」。但如果你真正维护过一个只需要给几十个人用的内部管理系统就会明白前后端分离在这里反而是负担你得同时维护Vue项目和Java接口项目部署要配Nginx联调要处理跨域。而JSPServlet的组合Tomcat一扔就能跑一个JSP文件里既能写HTML又能渲染动态数据改起来最快。对于宿舍管理系统这种「用户量小、逻辑不复杂、维护者可能只有一两个人」的场景它是最省钱的方案。从面试角度讲这套栈也是Java Web的基础功。JSP负责视图Servlet负责请求分发和业务控制DAO负责数据库访问这就是最经典的Model 2 MVC模型。很多面试题——比如Session和Cookie的区别、Filter怎么拦截未登录请求、JDBC的Connection为什么要关闭——都在这个项目里能亲手碰到。学一遍这套东西再去看Spring Boot你会更容易理解框架到底帮你省掉了哪些活。以下是一个简要的技术选型对比方案部署成本适合场景主要短板纯JSPJDBC最低Tomcat即可课设、毕设、极小型内部系统业务代码和页面混杂难维护JSPServletDAO本文方案低Tomcat即可中小型内部管理系统配置式开发少写起来偏手工Spring Boot 前后端分离高需NodeNginx后端服务生产级对外系统学习曲线陡小项目成本溢出从E-R图到SQL语句标题里已经把这个项目的建模产物给定死了你不需要纠结框架选型重点关注的是表结构设计和业务逻辑怎么落地。2.2 E-R图里到底有哪些实体关系怎么定E-R图实体-联系图是建库前的设计蓝图。宿舍管理系统里核心实体至少有四个学生、宿舍、宿舍楼、宿管员再加上报修单这类业务实体。实体间的关系要明确一栋宿舍楼对应多间宿舍1:N一间宿舍对应多个学生1:N一个学生可以提交多条报修记录1:N。宿管员和宿舍楼是管理关系可以是M:N——一个宿管员可能负责多栋楼一栋楼也可能有几个宿管员轮班。这里有一个我在实际项目中反复调整过的决策学生的床位信息是放在student表里做冗余字段还是通过多对多关联表来维护。很多课设一开始会设计成「学生表只存宿舍ID床位号写在宿舍表里」结果调宿、退宿时要改两张表事务稍不注意就数据不一致。我的做法是在student表里直接加bed_number字段配合dorm_room_id。这样查「某间宿舍住了谁」和「某学生住哪」都只要一条SQL虽然有一点冗余但换来了查询效率和代码简单。E-R图上要标明这个冗余字段来自「住宿」联系的属性。表结构设计直接决定后续SQL好不好写下面是核心表的字段规划表名关键字段说明dorm_buildingid, name, address, manager_id宿舍楼name唯一dorm_roomid, building_id, room_number, capacity, floor房间capacity为床位总数studentid, student_no, name, gender, dorm_room_id, bed_number, password床位号冗余在这里dorm_managerid, username, password, real_name, phone系统后台登录用户repair_orderid, student_id, room_id, description, status, create_timestatus待处理/处理中/已完成visit_recordid, student_id, visitor_name, visit_time, leave_time访客登记按需设计我一般建议把E-R图和表设计文档放在项目根目录的docs文件夹里写代码前先把表和关系定死而不是边写边加字段。因为宿舍管理系统这种课设级项目评委最常问的问题就是「为什么这么设计表」「外键为什么这么设」你答得上来项目价值立刻不一样。3. 从E-R图到MySQL建库SQL语句一次写对后面少踩一半坑3.1 建库建表的完整SQL字符集、外键和索引一次到位拿到E-R图后第一步是把图转成MySQL能执行的建表语句。这里有个核心原则字符集、存储引擎、外键约束在建表时就定好不要等数据录入了再改。很多人在Windows上按mysql 5.7.44安装教程装完MySQL后直接默认建表结果插入中文变成乱码根源就是字符集没指定。下面是一套可直接执行的建库建表SQL。-- 建库统一utf8mb4避免中文乱码 CREATE DATABASE IF NOT EXISTS student_dorm DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE student_dorm; -- 宿舍楼表 CREATE TABLE dorm_building ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL UNIQUE COMMENT 楼栋名称如1号楼, address VARCHAR(100) COMMENT 楼栋位置, manager_id INT COMMENT 负责管理员ID可为空 ) ENGINEInnoDB COMMENT宿舍楼; -- 宿舍房间表 CREATE TABLE dorm_room ( id INT PRIMARY KEY AUTO_INCREMENT, building_id INT NOT NULL COMMENT 所属楼栋, room_number VARCHAR(10) NOT NULL COMMENT 房间号如101, floor TINYINT NOT NULL COMMENT 楼层, capacity INT NOT NULL DEFAULT 4 COMMENT 床位数, UNIQUE KEY uk_building_room (building_id, room_number), CONSTRAINT fk_room_building FOREIGN KEY (building_id) REFERENCES dorm_building(id) ON DELETE CASCADE ) ENGINEInnoDB COMMENT宿舍房间;这一段建表SQL里有三个关键设计。第一UNIQUE KEY uk_building_room在楼栋ID和房间号上建联合唯一索引防止同一个楼栋里出现两个「101」第二外键fk_room_building用ON DELETE CASCADE意思是一栋楼删了它下面的房间记录自动清掉避免留下大量孤儿数据第三所有字段都加了COMMENT注释这个习惯在答辩和维护时非常加分数据库元数据本身就是文档。接着建学生表和报修表。学生表是系统的核心床位冗余字段在这里起作用报修表则记录了从提交到完成的整个状态流转。-- 学生表核心表床位号冗余在student表 CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, name VARCHAR(30) NOT NULL, gender ENUM(男,女) NOT NULL, dorm_room_id INT COMMENT 当前宿舍房间ID, bed_number VARCHAR(10) COMMENT 床位号如A床, password VARCHAR(64) NOT NULL COMMENT 登录密码MD5存储, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_student_room FOREIGN KEY (dorm_room_id) REFERENCES dorm_room(id) ON DELETE SET NULL ) ENGINEInnoDB COMMENT学生; -- 报修单表 CREATE TABLE repair_order ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, room_id INT NOT NULL, description VARCHAR(500) NOT NULL COMMENT 报修描述, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待处理 1处理中 2已完成, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, finish_time DATETIME COMMENT 完成时间, CONSTRAINT fk_repair_student FOREIGN KEY (student_id) REFERENCES student(id) ON DELETE CASCADE, CONSTRAINT fk_repair_room FOREIGN KEY (room_id) REFERENCES dorm_room(id) ON DELETE CASCADE ) ENGINEInnoDB COMMENT报修单; -- 常用查询索引 CREATE INDEX idx_student_room ON student(dorm_room_id); CREATE INDEX idx_repair_status ON repair_order(status);学生表的外键fk_student_room用的是ON DELETE SET NULL这和楼栋表的外键策略故意不同房间被删了学生记录还得保留只是先把宿舍关联置空等管理员重新分配。报修表的两个外键都用ON DELETE CASCADE因为报修记录依附于学生和房间存在主体没了记录就没有保留意义。这种「按业务语义选择外键动作」的思路是E-R图设计落到SQL时最值得讲的点。最后用一条统计语句验证数据表能正确关联。sql语句去重、排序在后面的管理页面里会频繁出现比如查一栋楼当前住了多少人-- 统计每栋楼的入住人数关联三张表 SELECT b.name AS building_name, COUNT(s.id) AS student_count FROM dorm_building b LEFT JOIN dorm_room r ON r.building_id b.id LEFT JOIN student s ON s.dorm_room_id r.id GROUP BY b.id, b.name ORDER BY student_count DESC;注意这里用的LEFT JOIN因为一栋楼可能还没有分配任何学生如果用INNER JOIN空楼会被过滤掉统计结果就会露数据。排序用ORDER BY student_count DESC这就是mysql排序的一个典型场景。3.2 Tomcat下的Web工程目录与JDBC连接从项目骨架到DBUtil建好表后开始搭Web工程。标准的Java Web工程要遵循Maven或传统WebContent的目录约定。下面的目录结构是课设项目最常见的样子不需要Maven也能在Eclipse或IDEA里直接跑student_dorm_web/ ├── src/ │ └── com/dorm/ │ ├── model/ # JavaBeanStudent, DormRoom, RepairOrder │ ├── dao/ # JDBC数据访问StudentDAO, RoomDAO │ ├── servlet/ # 控制器LoginServlet, AssignRoomServlet │ └── util/ # DBUtil, MD5Util ├── WebContent/ │ ├── login.jsp │ ├── student_list.jsp │ ├── room_assign.jsp │ └── WEB-INF/ │ └── web.xml └── sql/ └── init.sql # 建库建表脚本对应3.1节接下来写JDBC连接工具类DBUtil。这个类是整个项目访问数据库的入口核心就三件事加载驱动、获取连接、关闭资源。很多人在这一步翻车问题几乎都集中在连接串参数写不对。package com.dorm.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class DBUtil { // 驱动和连接串统一管理换环境只改这里 private static final String DRIVER com.mysql.jdbc.Driver; private static final String URL jdbc:mysql://localhost:3306/student_dorm ?useUnicodetruecharacterEncodingutf8 useSSLfalseserverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(DRIVER); // 加载驱动驱动版本要和MySQL版本匹配 } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(MySQL驱动加载失败检查jar包是否放入WEB-INF/lib); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } // 关闭资源后打开的先关防止连接泄漏 public static void close(ResultSet rs, Statement stmt, Connection conn) { try { if (rs ! null) rs.close(); } catch (SQLException e) { } try { if (stmt ! null) stmt.close(); } catch (SQLException e) { } try { if (conn ! null) conn.close(); } catch (SQLException e) { } } }参数说明useUnicodetruecharacterEncodingutf8和建库时的utf8mb4配套保证插入中文不乱码。三处编码——数据库字符集、JDBC连接串、JSP页面编码——必须一致少一处都会出问题。useSSLfalse本地开发不需要SSL加密MySQL 8.0以上版本默认开SSL不关会报警告甚至连接失败。serverTimezoneAsia/ShanghaiMySQL 8.0之后驱动强制要求指定时区不写报Server returns invalid timezone。如果你按mysql 5.7.44安装过程部署这条可以留但无害。如果你本机装的是MySQL 8.0而不是5.7驱动建议换成com.mysql.cj.jdbc.Driver对应的jar包也要换。这里报ClassNotFoundException时先排查jar包版本再排查驱动类名别一上来就改代码。4. 三个核心功能写进代码登录鉴权、床位分配、报修分页4.1 登录与Session鉴权Filter拦截比到处写if判断省心宿舍管理系统的第一个功能必须是登录。角色可以分管理员和学生两种这里先讲管理员的登录实现。login.jsp负责展示表单LoginServlet负责校验用户名密码。密码不能明文存课设阶段用MD5就够了但生产环境务必换成BCrypt加盐。%-- login.jsp登录表单提交到LoginServlet --% % page contentTypetext/html;charsetUTF-8 languagejava % html headtitle宿舍管理系统登录/title/head body h2宿舍管理系统/h2 form actionlogin methodpost 用户名input typetext nameusername required /br/ 密 码input typepassword namepassword required /br/ button typesubmit登录/button /form p stylecolor:red${errorMsg}/p /body /html这里刻意不写CSS先让功能跑通。form的actionlogin会匹配URL为/login的Servletmethodpost保证密码不出现在地址栏。${errorMsg}是EL表达式用来回显登录失败原因比out.println()干净。package com.dorm.servlet; import com.dorm.dao.DormManagerDAO; import com.dorm.model.DormManager; import com.dorm.util.MD5Util; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; WebServlet(/login) public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username request.getParameter(username); String passwordInput request.getParameter(password); // 密码加盐后MD5这里盐值固定课设够用 String passwordMd5 MD5Util.md5(passwordInput); DormManagerDAO dao new DormManagerDAO(); DormManager manager dao.findByUsernameAndPassword(username, passwordMd5); if (manager ! null) { // 登录成功把用户信息放进Session后续页面靠它判断身份 HttpSession session request.getSession(); session.setAttribute(loginUser, manager); session.setMaxInactiveInterval(30 * 60); // 30分钟无操作自动过期 response.sendRedirect(index.jsp); } else { request.setAttribute(errorMsg, 用户名或密码错误); request.getRequestDispatcher(login.jsp).forward(request, response); } } }逻辑说明先用MD5工具类把用户输入的明文密码做哈希再去数据库比对避免密码在代码里和日志里以明文出现。比对成功则创建Session并跳转到主页面失败则转发回登录页并显示错误信息。注意sendRedirect和forward的区别——前者是浏览器重新发起一次请求地址栏会变适合登录成功后的跳转后者是服务端内部转发地址栏不变适合把错误信息带回登录页。Session建好了还得管住未登录的用户。如果每个页面都写一遍「校验Session是否为空」代码会非常啰嗦而且容易漏。正确做法是写一个Filter统一拦截package com.dorm.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; WebFilter(/*) // 拦截所有请求 public class LoginFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String path request.getRequestURI(); // 放行登录页和登录接口本身否则会死循环 if (path.endsWith(/login) || path.endsWith(/login.jsp)) { chain.doFilter(req, resp); return; } HttpSession session request.getSession(false); if (session ! null session.getAttribute(loginUser) ! null) { chain.doFilter(req, resp); // 已登录放行 } else { response.sendRedirect(request.getContextPath() /login.jsp); } } }这个Filter的注释里有一个最容易踩的坑拦截路径必须放过/login本身否则用户提交登录表单后请求又被拦截回登录页形成重定向死循环。用request.getSession(false)而不是getSession()是为了避免「未登录用户访问时被强行创建一个空Session」这是面试里常考的一个细节。4.2 床位分配一个事务里完成「查余位→占位→回写」三步宿舍分配是业务复杂度最高的功能也是这个系统最值得演示的部分。分配床位的逻辑是学生选定宿舍楼和房间后先查这个房间已住人数如果小于容量就分配床位同时更新学生表的宿舍ID和床位号。这三步操作不能分成独立的SQL执行否则高并发下两个学生可能同时分到同一个床位——课设没人跟你并发但你写代码时养成事务习惯以后做生产系统就少踩坑。package com.dorm.dao; import com.dorm.util.DBUtil; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class RoomDAO { /** * 分配宿舍一个事务完成三步检查与更新 * return true表示分配成功 */ public boolean assignDorm(int studentId, int roomId, String bedNumber) { Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交开启事务 // 第一步查房间已住人数 String sqlCheck SELECT COUNT(*) FROM student WHERE dorm_room_id?; ps conn.prepareStatement(sqlCheck); ps.setInt(1, roomId); rs ps.executeQuery(); int occupied 0; if (rs.next()) { occupied rs.getInt(1); } rs.close(); ps.close(); // 第二步查房间容量 String sqlCap SELECT capacity FROM dorm_room WHERE id?; ps conn.prepareStatement(sqlCap); ps.setInt(1, roomId); rs ps.executeQuery(); int capacity 0; if (rs.next()) { capacity rs.getInt(capacity); } rs.close(); ps.close(); if (occupied capacity) { conn.rollback(); // 满员回滚并返回失败 return false; } // 第三步更新学生的宿舍ID和床位号 String sqlAssign UPDATE student SET dorm_room_id?, bed_number? WHERE id?; ps conn.prepareStatement(sqlAssign); ps.setInt(1, roomId); ps.setString(2, bedNumber); ps.setInt(3, studentId); int rows ps.executeUpdate(); conn.commit(); // 三步全部成功提交事务 return rows 0; } catch (SQLException e) { e.printStackTrace(); try { if (conn ! null) conn.rollback(); // 任何一步失败全部回滚 } catch (SQLException ex) { ex.printStackTrace(); } return false; } finally { DBUtil.close(rs, ps, conn); } } }逻辑说明conn.setAutoCommit(false)之后后续所有SQL不会立即生效直到显式调用commit()。房间满员时走rollback()把事务终止掉不留下脏数据代码异常时在catch里回滚最后在finally里释放连接。注意PreparedStatement用固定SQL加?参数的方式一是防止SQL注入二是MySQL会对预编译语句做缓存循环执行时性能更好。这一步里还隐藏了一个高频需求宿舍管理系统的「退宿」操作本质上就是把student表的dorm_room_id置为NULL并清空bed_number正好用上第一节里说的ON DELETE SET NULL策略。你可以在自己的代码里加一个cancelAssign方法逻辑比分配简单得多只需要一条UPDATE语句。4.3 报修列表分页查询LIMIT和COUNT的组合系统里第三个必写功能是报修管理核心在于分页。如果报修单只有几十条一次性查出来无所谓但真实场景下一年上千条记录不分页页面会越开越慢。分页SQL的标准写法是LIMIT offset, size配合一条COUNT语句算出总页数。这里用DISTINCT去重热词做个小补充如果你查询时JOIN了多张表统计总数建议用COUNT(DISTINCT 主键)避免因为一对多关联导致计数重复。public ListRepairOrder getRepairOrders(int page, int pageSize) { ListRepairOrder list new ArrayList(); String sql SELECT id, student_id, room_id, description, status, create_time FROM repair_order ORDER BY create_time DESC LIMIT ?, ?; // 偏移量页码*每页条数 try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, (page - 1) * pageSize); ps.setInt(2, pageSize); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { RepairOrder order new RepairOrder(); order.setId(rs.getInt(id)); order.setStudentId(rs.getInt(student_id)); order.setStatus(rs.getInt(status)); list.add(order); } } } catch (SQLException e) { e.printStackTrace(); } return list; } // 取出总数用于计算总页数 public int countRepairOrders() { String sql SELECT COUNT(*) FROM repair_order; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { if (rs.next()) { return rs.getInt(1); } } catch (SQLException e) { e.printStackTrace(); } return 0; }参数说明页码从1开始数据库偏移量从0开始所以取第page页的记录时偏移量是(page - 1) * pageSize。比如每页10条第2页的偏移量就是10。ORDER BY create_time DESC让最新报修排在最前这是管理后台的默认排序习惯。JSP页面里展示时你会需要计算总页数int totalPages (int) Math.ceil(totalCount * 1.0 / pageSize)整数除法会丢精度乘1.0转成浮点数再向上取整。报修状态流转待处理→处理中→已完成可以再写一个UpdateStatusServlet接收报修ID和新状态执行一条UPDATE repair_order SET status?, finish_timeNOW() WHERE id?即可。这里注意finish_time只在状态变为2时更新判断逻辑写在Servlet里而不是SQL里这样更直观。5. 部署排错五个真实踩坑记录从乱码到连不上库5.1 页面中文全是问号三处编码必须一致现象插入的学生姓名在数据库里正常但JSP页面显示成???或者反过来页面输入的中文存进数据库变成乱码。原因字符集链路有三环——MySQL表字符集、JDBC连接串的characterEncoding、JSP页面的pageEncoding。任何一环断掉中文就出问题。MySQL建表时用了utf8mb4但JDBC连接串没指定characterEncodingutf8驱动使用默认的ISO-8859-1中文当然变问号。解决按顺序排查。第一建库建表统一用utf8mb4见3.1节的SQL第二JDBC连接串必须带上useUnicodetruecharacterEncodingutf8第三JSP页面顶部加% page contentTypetext/html;charsetUTF-8 languagejava %。如果用了Tomcat再检查server.xml里Connector是否设置了URIEncodingUTF-8未设置时表单POST的URL参数也可能乱码。这三处对齐后乱码基本绝迹。5.2 Tomcat 10 报 ClassNotFoundExceptionjavax 变成 jakarta 了现象项目在Tomcat 9上跑得好好的换了Tomcat 10启动后登录页面直接500日志里显示java.lang.ClassNotFoundException: javax.servlet.http.HttpServlet。原因Tomcat 10开始把Java EE的包名从javax.*迁移到了jakarta.*旧项目里所有import javax.servlet.*的类都找不到。这个坑对课设项目尤其常见很多人从网上下载Tomcat最新版一跑就傻眼。解决两个方案二选一。方案一下载Tomcat 9.0.x这是兼容javax的最后一版课设项目最省事方案二全局替换import语句把javax.servlet改成jakarta.servlet同时更新web.xml的命名空间。我建议初学者直接用Tomcat 9把精力花在业务逻辑上别在包名迁移上耗时间。这块在java面试题里也常被拿来考「你对Servlet容器版本的了解程度」。5.3 MySQL 8 报 Public Key Retrieval is not allowed认证插件差异现象本地MySQL装的是8.0驱动用的是5.x的com.mysql.jdbc.Driver连接时报Public Key Retrieval is not allowed或者Client does not support authentication protocol requested by server。原因MySQL 8.0默认的认证插件是caching_sha2_password老版本驱动不认识。第一次连接时客户端需要向服务器请求公钥来加密密码驱动默认关闭了这个行为。解决两种办法。一是连接串里加allowPublicKeyRetrievaltrueuseSSLfalse二是执行SQL把用户认证插件改回mysql_native_password比如ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码;。我个人倾向第二种改一次一劳永逸还能顺手解决驱动版本兼容问题。如果不想动MySQL配置就干脆换com.mysql.cj.jdbc.Driver加对应8.x的jar包。5.4 删楼栋被外键拦住Cannot delete or update a parent row现象想删掉一栋测试楼的数据执行DELETE FROM dorm_building WHERE id1;时报Cannot delete or update a parent row: a foreign key constraint fails。原因这就是第3章建表时外键策略的体现。楼栋被房间表的外键fk_room_building引用房间表里还有数据时默认约束RESTRICT会禁止删除父表记录。这不是bug是数据库保护数据完整性的正常行为但没接触过外键的人第一次遇到会觉得莫名其妙。解决按数据依赖顺序删先删子表数据再删父表记录DELETE FROM dorm_room WHERE building_id1;然后删楼栋。更稳妥的做法是改外键定义用ON DELETE CASCADE让数据库自动级联删除——如果你确定子表数据不需要保留就重建外键加上CASCADE。另外补充一个血泪经验生产环境删主表数据前先备份或者用事务包起来发现删错了立刻ROLLBACK这是你的后悔药。5.5 跑一上午报 Too many connections连接没释放现象系统刚启动一切正常用一两个小时后页面开始报Too many connectionsMySQL重启后恢复过一会儿又报错。原因代码里获取了Connection但没有在finally块里关闭。JDBC的Connection对象如果只建不关连接池或MySQL服务端的连接数会被耗尽。这是新手最常见的资源泄漏问题尤其在页面频繁刷新的测试阶段暴露得最快。解决第一所有DAO方法务必用try-catch-finally在finally里调用DBUtil.close(rs, ps, conn)第二Java 7之后可以用try-with-resources语法try (Connection conn DBUtil.getConnection())会自动关闭代码更简洁第三开发期可以把MySQL的max_connections调大比如SET GLOBAL max_connections500;但这只是治标治本还是得确保每个连接都释放。连接池如Druid、HikariCP是生产环境的解法课设阶段手工管理连接finally关闭就够。6. 动手验证的最后一关验收清单与部署细节功能写完后不要急着打包交差。我习惯在交付前跑一遍完整的验收流程当作功能清单也是自查清单。以一个干净数据库为起点按编号顺序操作验证执行sql/init.sql确认6张表全部创建成功无外键报错启动Tomcat访问http://localhost:8080/student_dorm/login.jsp未登录访问index.jsp应被重定向回登录页用管理员账号登录密码错误时能看到错误提示密码正确时跳转主页新增一间宿舍和一个学生给该学生分配床位刷新页面确认床位号已写入尝试给已满员的房间分配学生系统返回「房间已满」且学生数据没有脏写入提交一条报修单状态从「待处理」改到「处理中」再改到「已完成」退出登录点击浏览器后退按钮应无法回到主页。这套流程跑通说明从E-R图到SQL再到JSP页面的整条链路是通的。有一个部署细节值得刻意练习——导出一份干净的初始化SQL。在命令行用mysqldump导出时加--default-character-setutf8mb4参数否则导出的SQL文件在别的机器上导入时可能因为编码不匹配产生新乱码。另外把INSERT INTO的种子数据管理员账号、测试楼栋、房间放在init.sql末尾方便别人拿到项目后一键还原演示环境。最后一个技术细节是war包部署。用Eclipse或IDEA导出war包复制到Tomcat的webapps目录启动后会以war包文件名作为上下文路径自动解压。这里的坑是你本地的init.sql里的数据库密码是123456换到别人机器上可能不一样一定要把DBUtil.java里的配置改成从配置项读取或者至少在文档里用醒目标注写清楚改哪里。我曾经因为在数据库连接串上写死密码导致演示当天连不上库场面相当尴尬。从那以后我每次做这类维护型小系统都会把「换机器十分钟跑通」当成硬指标。黑匣子只在你自己机器上能跑不算数要别人也能复现才算真正交付。宿舍管理系统这个题目技术含量不算高但它覆盖了Java Web开发最重要的基本功E-R图建模、SQL设计、JDBC事务、Session鉴权、Filter拦截、分页查询。把这些点吃透往后学框架、做生产项目你遇到的大部分Web层问题都能溯回到这套基础功上。希望帮到你。本文还有配套的精品资源点击获取