
简介面向高校计算机相关专业学生尤其是正在完成数据库课程设计或 Java 桌面应用实训的学习者这套基于 MySQL 与 Java Swing 的学生宿舍管理系统工程可以作为选题参考、二次开发基础或答辩演示材料。压缩包共 334 个文件、约 112.72MB其中 47 个 java 源文件包含界面与业务逻辑202 个 class 文件是编译产物22 张 png 是界面截图与流程示意9 个 xml 以及 iml、md 文件负责工程配置与说明另有 db 数据库文件、jar 依赖和少量 h、cpp、sln 等工程文件。系统按管理员管理、宿舍管理、学生管理、学生宿舍管理等模块拆分Swing 组件与 JDBC 数据访问分层清晰读者可据此理解表结构设计、实体关系与增删改查实现并用图表与文档素材撰写课程设计报告。已有 268 人学习适合希望快速获得可运行工程与配套素材的读者。1. 一份能当场演示的课设Java Swing MySQL 宿舍管理系统适合谁接手一份 MySQL 课程设计最怕的不是功能点不够而是打开源码之后根本跑不起来。这套基于 Java Swing MySQL 开发的学生宿舍管理系统把学生入住、床位分配、退宿、报修登记、来访记录、楼栋住宿统计做成了一个双击就能运行的桌面程序压缩包里除了源码还带了 ER 图、数据字典、测试用例和课程设计报告。它适合两类人一是这周就要交课设、需要一个能演示也能应付答辩的完整骨架二是刚学完 JDBC、想拿一个真实点的增删改查场景练手的人。下面按“先认清结构、再跑起来、最后动手改”的顺序拆。2. 结构拆解Swing 窗体、DAO 与四张核心表的对应关系2.1 为什么是 Swing JDBC 直连而不是 JavaFX 或 SSM课设答辩现场的条件比想象中苛刻演示机器是机房的 WindowsJDK 版本不确定能不能联网不确定老师也不会帮你先启动一个 Tomcat。Swing 打成 jar 双击就能出界面MySQL 装完建个库就完事链路短、故障点少这是它在这类场景里最大的优势和技术先不先进没多大关系。JavaFX 要多带一堆运行时依赖SSM 要配 web 容器和一堆 XML答辩前半小时还在排查 404 的经历实在没必要再来一次。数据访问层用 JDBC 直连而不是 MyBatis理由同样朴素SQL 全部写在 Java 代码里一个方法对应一条语句翻到哪看到哪。老师问“这个楼栋入住率是怎么算的”你直接把GROUP BY那一行指给他看就行。MyBatis 的 XML 映射对刚学完 JDBC 的人来说是额外的抽象层namespace 或 resultType 写错一个字母就抛异常排查成本远高于省下来的那点样板代码。代价当然是重复代码多第 6 章我会讲怎么用一层薄薄的 DAO 接口把它收一收。版本上这套源码的常见组合是 JDK 8 MySQL 5.7 或 8.0 mysql-connector-java 8.0.x。这里有一个必须提前记住的差异MySQL 8 的驱动类名是com.mysql.cj.jdbc.Driver5.x 是com.mysql.jdbc.Driver连接串参数也不一样。很多人拿到源码第一件事就是改数据库密码改完发现还是连不上八成就是驱动和数据库大版本没对上。2.2 目录结构四个包各管一摊一份能拿得出手的课设源码目录不该是十几个.java文件平铺在src下面。常见的分层是这样dorm-system/ ├── src/ │ └── com/dorm/ │ ├── App.java # main 入口只做一件事new LoginFrame().setVisible(true) │ ├── entity/ # Student、Room、Accommodation、Repair 实体类 │ ├── dao/ # StudentDao、RoomDao、RepairDao所有 SQL 都在这层 │ ├── ui/ # LoginFrame、AdminFrame、StudentFrame 及各功能 Dialog │ └── util/ # DBUtil、MD5Util、DateUtil ├── sql/ │ └── dorm_db.sql # 建库建表 初始数据source 一下就能跑 ├── lib/ │ └── mysql-connector-java-8.0.28.jar ├── doc/ │ ├── ER图.png │ ├── 数据字典.xlsx │ └── 课程设计报告.docx └── README.md层典型类职责不该出现的东西uiAdminFrame、AssignDialog收集输入、展示表格、弹提示SQL 字符串、ConnectiondaoRoomDao.assign()拼 SQL、设参数、处理结果集JOptionPane、JTextFieldentityStudent、Room数据搬运字段与表列对应业务判断、数据库操作utilDBUtil建连接、关资源、MD5具体业务 SQL判断分层有没有破有个很土但好用的标准在ui包里全局搜SELECT如果搜出来一堆说明业务和界面搅在一起了后面加功能会很痛苦。课设阶段不要求严格三层但至少做到“SQL 只在 dao 里出现”答辩时讲得清改起来也顺手。2.3 四张核心表怎么定字段为什么这么设整个系统跑起来其实只围绕四张表学生、房间、住宿记录、报修单。建表脚本是整套源码的地基字段设计得对不对直接决定后面要不要写一堆补丁代码。-- 学生信息表学号天然唯一直接拿来做业务主键演示时比自增 id 直观 CREATE TABLE student ( stu_id VARCHAR(12) PRIMARY KEY COMMENT 学号, stu_name VARCHAR(20) NOT NULL, gender CHAR(1) DEFAULT 男, class_name VARCHAR(30) COMMENT 班级如 计算机2201, phone VARCHAR(15), status TINYINT DEFAULT 1 COMMENT 1在册 0已毕业 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 房间表used_beds 是冗余字段为了列表页不用每次 count代价是必须和住宿记录同步 CREATE TABLE dorm_room ( room_id INT AUTO_INCREMENT PRIMARY KEY, building_no VARCHAR(10) NOT NULL COMMENT 楼号如 6号楼, room_no VARCHAR(10) NOT NULL, capacity TINYINT NOT NULL DEFAULT 4 COMMENT 床位上限, used_beds TINYINT NOT NULL DEFAULT 0 COMMENT 已住人数, UNIQUE KEY uk_room (building_no, room_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 住宿记录out_date 为 NULL 表示在住退宿是 update 而不是 delete历史留得住 CREATE TABLE accommodation ( id INT AUTO_INCREMENT PRIMARY KEY, stu_id VARCHAR(12) NOT NULL, room_id INT NOT NULL, bed_no TINYINT NOT NULL COMMENT 1~4 号床, in_date DATE NOT NULL, out_date DATE DEFAULT NULL, KEY idx_stu (stu_id), KEY idx_room (room_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 报修单状态用 tinyint前端再翻译成待处理/已处理 CREATE TABLE repair ( repair_id INT AUTO_INCREMENT PRIMARY KEY, stu_id VARCHAR(12) NOT NULL, content VARCHAR(200) NOT NULL, apply_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TINYINT DEFAULT 0 COMMENT 0待处理 1已处理 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;几个决定值得说清楚。学号直接做主键是因为课设场景里一个学生只对应一条身份记录用自增 id 反而多一层转换used_beds做成冗余列是为了主界面房间列表加载时不用每行跟一条子查询代价是它必须和accommodation同时变这正是第 4 章要讲事务的原因out_date IS NULL表达“当前在住”比加一个is_active字段更省事退宿时写个日期历史记录还完整留着统计“本学期住过多少人”的时候直接就能查。字符集统一用utf8mb4这年头没人想再被 emoji 或者生僻字搞出乱码。3. 跑通流程从装 JDK 到登录框弹出第一条宿舍记录3.1 先把三个版本对齐JDK、MySQL、驱动包拿到源码别急着点运行先花两分钟确认环境。命令行里敲三条java -version # 期望 1.8.x11 以上也能跑但源码里若有 sun.* 的内部类就得改 javac -version mysql --version # 5.7.x 或 8.0.x记住大版本驱动包要和 MySQL 大版本对齐MySQL 8 配mysql-connector-java-8.0.xMySQL 5.7 配 5.1.x 或 8.0.x 都能连但连接串参数不同。lib目录里已经带了 jar 的直接用它没有的话去仓库下载后放进lib并且记得在 IDE 里Add as Library否则编译期不报错、运行期抛ClassNotFoundException这种错误新手很难自己定位。提示JDK 8 之后的版本对 Swing 的JFrame没有破坏性改动Exception in thread AWT-EventQueue-0这类堆栈基本都是业务代码抛出来的别一上来就怀疑 JDK 版本。3.2 建库建表一条 source 命令搞定sql/dorm_db.sql里通常已经写好了建库语句直接整份执行最省事-- 建库这一步放在脚本外面避免脚本里 DROP DATABASE 把已有数据带走 CREATE DATABASE IF NOT EXISTS dorm_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE dorm_db; SOURCE D:/dorm-system/sql/dorm_db.sql;如果习惯用命令行等价写法是mysql -uroot -p --default-character-setutf8mb4 sql/dorm_db.sql执行完做三项检查SHOW TABLES;看到四到六张表SELECT COUNT(*) FROM dorm_room;有初始房间数据SELECT * FROM sys_user;里有一条管理员账号常见的是admin / 123456。初始数据很重要一张空表点进去界面全白会让人误以为程序没跑起来。如果脚本里带了DROP TABLE IF EXISTS先确认库里没有你要留的数据再执行。3.3 改连接串、编译、看到登录框连接配置集中在一个地方改这是好习惯。DBUtil里的三行常量就是全部public class DBUtil { // 库名、端口、密码改这里别的文件一律不动 private static final String URL jdbc:mysql://127.0.0.1:3306/dorm_db ?useUnicodetruecharacterEncodingutf8 useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue; private static final String USER root; private static final String PWD 123456; }参数逐个解释useUnicode和characterEncoding决定中文能不能正常往返缺了它们姓名列会变成问号serverTimezone在 MySQL 8 驱动下是必须的不写会直接抛The server time zone value is unrecognizeduseSSLfalse只是为了让本机连接少一次握手警告allowPublicKeyRetrievaltrue是 MySQL 8 默认加密插件下常见的补丁配上它才不会报认证失败。不用 IDE 的话编译命令要显式带 jar 和编码参数# -encoding UTF-8 不能省源码里有中文Windows 默认 GBK 编译会报非法字符 javac -encoding UTF-8 -cp lib/mysql-connector-java-8.0.28.jar -d out $(find src -name *.java) # 运行Windows 下 classpath 分隔符是分号 java -cp out;lib/mysql-connector-java-8.0.28.jar com.dorm.App跑通的标志是登录窗口弹出来、输账号密码后能看到主界面左侧的房间树或表格。如果登录框能弹但点登录没反应八成是连接串错了却没弹异常框去控制台找SQLException的堆栈如果连登录框都不弹问题在 classpath 或者main方法入口跟数据库无关。4. 关键链路登录校验、床位分配、报修刷新三处关键代码4.1 登录校验与角色分流登录是整套程序唯一一个人人都会点的功能也是最容易写出安全漏洞的地方。课设里最常见的错误是用字符串拼接 SQL输入 or 11就能进去答辩老师随手一试就翻车。private void doLogin() { String user txtUser.getText().trim(); String pwd new String(txtPwd.getPassword()); if (user.isEmpty() || pwd.isEmpty()) { JOptionPane.showMessageDialog(this, 账号密码不能为空); return; } // 占位符而不是拼接顺带把注入问题解决掉 String sql SELECT user_id, role, stu_id FROM sys_user WHERE username? AND password?; try (Connection conn DBUtil.getConn(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, user); ps.setString(2, MD5Util.md5(pwd)); // 库里存 32 位小写注册时同一套算法 try (ResultSet rs ps.executeQuery()) { if (!rs.next()) { JOptionPane.showMessageDialog(this, 账号或密码不对, 提示, JOptionPane.WARNING_MESSAGE); return; } String role rs.getString(role); String stuId rs.getString(stu_id); dispose(); // 关掉登录窗否则主窗关了进程还在 if (admin.equals(role)) { new AdminFrame().setVisible(true); // 管理员全部楼栋 } else { new StudentFrame(stuId).setVisible(true); // 学生只看自己 } } } catch (SQLException ex) { JOptionPane.showMessageDialog(this, 数据库连接失败 ex.getMessage()); } }PreparedStatement在这里不只是为了防注入参数带引号或者中文时也不会拼错。dispose()那行经常被漏掉Swing 的JFrame默认关闭行为是HIDE_ON_CLOSE登录窗不销毁用户关掉主窗后进程依然挂着任务管理器里一堆java.exe答辩现场重启机器都救不回来。角色分流用role字段而不是两套登录页是课设里性价比最高的做法管理员和学生看到的菜单不同代码只写一份。密码存 MD5 是个折中比明文强但课设里也不用上 BCrypt重点是注册和登录必须调同一个工具方法否则你永远对不上。4.2 床位分配一个必须用事务的操作这是整套系统里唯一真正需要事务的地方。分配床位至少要动两张表往accommodation插一条住宿记录把dorm_room.used_beds加一。如果第一条成功第二条失败房间人数就和实际住宿记录对不上越用越乱最后只能手动改库。public boolean assign(String stuId, int roomId, int bedNo) { Connection conn null; try { conn DBUtil.getConn(); conn.setAutoCommit(false); // 关掉自动提交事务从这里开始 // 1. 锁住这一行防止两个窗口同时抢最后一张床 int capacity, used; try (PreparedStatement ps conn.prepareStatement( SELECT capacity, used_beds FROM dorm_room WHERE room_id? FOR UPDATE)) { ps.setInt(1, roomId); try (ResultSet rs ps.executeQuery()) { if (!rs.next()) { conn.rollback(); return false; } capacity rs.getInt(capacity); used rs.getInt(used_beds); } } if (used capacity) { conn.rollback(); return false; } // 满员 // 2. 这个床位是否已经有人只看在住的 try (PreparedStatement ps conn.prepareStatement( SELECT 1 FROM accommodation WHERE room_id? AND bed_no? AND out_date IS NULL)) { ps.setInt(1, roomId); ps.setInt(2, bedNo); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { conn.rollback(); return false; } } } // 3. 写住宿记录 累加人数两条 SQL 同生共死 try (PreparedStatement ps conn.prepareStatement( INSERT INTO accommodation(stu_id, room_id, bed_no, in_date) VALUES(?,?,?,CURDATE()))) { ps.setString(1, stuId); ps.setInt(2, roomId); ps.setInt(3, bedNo); ps.executeUpdate(); } try (PreparedStatement ps conn.prepareStatement( UPDATE dorm_room SET used_beds used_beds 1 WHERE room_id?)) { ps.setInt(1, roomId); ps.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ignored) {} e.printStackTrace(); return false; } finally { // 连接可能来自连接池归还前必须把自动提交恢复回去 try { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } catch (SQLException ignored) {} } }三个细节值得盯住。FOR UPDATE是行锁保证“读到还有空床”和“写入住宿记录”之间不会有别的窗口插进来单人演示看不出差别但这就是并发意识的加分项rollback()在每个提前返回的分支都要写漏一个就会出现“提示满员了但人数还是加了”的诡异现象finally里恢复setAutoCommit(true)是给后续可能换连接池留的余地不恢复的话下一个拿到这条连接的线程所有操作都在一个隐式事务里提交时机完全不可控。退宿是这个方法的逆操作UPDATE accommodation SET out_dateCURDATE() WHERE ...加used_beds - 1同样要包在一个事务里逻辑对称着写就行。4.3 报修登记与表格刷新列表页是学生和老师看得最多的地方用DefaultTableModel十分钟能搭出来但有几个小地方不写就会难受一整天。private void loadTable() { String[] cols {学号, 姓名, 楼号, 房间, 床位, 入住日期}; DefaultTableModel model new DefaultTableModel(cols, 0) { Override public boolean isCellEditable(int r, int c) { return false; } }; table.setModel(model); String sql SELECT s.stu_id, s.stu_name, r.building_no, r.room_no, a.bed_no, a.in_date FROM accommodation a JOIN student s ON s.stu_id a.stu_id JOIN dorm_room r ON r.room_id a.room_id WHERE a.out_date IS NULL ORDER BY r.building_no, r.room_no, a.bed_no; try (Connection conn DBUtil.getConn(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { model.setRowCount(0); // 先清空否则每次刷新都多一批重复行 while (rs.next()) { model.addRow(new Object[]{ rs.getString(1), rs.getString(2), rs.getString(3), rs.getString(4), rs.getInt(5), rs.getDate(6) }); } } catch (SQLException ex) { JOptionPane.showMessageDialog(this, 加载失败 ex.getMessage()); } }isCellEditable返回false那一行是新手最容易漏的不覆写的话双击单元格就进编辑态用户改完发现没保存体验很差setRowCount(0)同理很多人的刷新按钮点两次就出现重复数据原因就是只addRow不清空。查询用三表 JOIN 一次拿全比先查住宿记录再逐条查学生姓名要快得多也避免了 N1 次查库。报修登记就是一条INSERT没什么花活关键是提交成功后立刻调用一次刷新方法让新单子出现在列表里。管理员处理报修时改成UPDATE repair SET status1 WHERE repair_id?再把表格重刷一遍流程就闭环了。5. 排查清单这套源码最容易翻车的五个地方5.1 一启动就 ClassNotFoundException: com.mysql.cj.jdbc.Driver现象控制台第一行就是这串红字登录框可能弹出来但点登录必然失败。原因驱动 jar 没进 classpath。IDE 里表现为“没 Add as Library”命令行里表现为-cp只写了out忘了带 jar还有一种情况是驱动类名写错MySQL 5.x 的 jar 里根本没有cj这一层包。解决先看lib下 jar 的实际文件名mysql-connector-java-5.1.x就把类名改回com.mysql.jdbc.Driver命令行运行确认-cp里包含 jar 的完整路径Windows 用分号分隔Linux 和 macOS 用冒号。5.2 中文全变成问号或者插入时报 Incorrect string value现象界面上的姓名显示成???或者插一条带中文的记录直接抛异常。原因三个环节的字符集只要有一个不是 utf8链路就断了——数据库和表的字符集、连接串里的characterEncoding、javac编译时的-encoding。解决库和表统一utf8mb4连接串带useUnicodetruecharacterEncodingutf8编译命令加-encoding UTF-8。已经建错的表用ALTER TABLE student CONVERT TO CHARACTER SET utf8mb4;修别重建。5.3 The server time zone value 中国标准时间 is unrecognized现象MySQL 8 环境下连接直接失败异常信息里带时区名。原因8.0 驱动要求显式指定时区本机 MySQL 返回的是本地化时区名驱动解析不了。解决连接串补serverTimezoneAsia/Shanghai。如果同时报公钥相关的错再加allowPublicKeyRetrievaltrueuseSSLfalse。5.4 关掉主窗口后 java.exe 还在跑第二次启动连不上库现象任务管理器里堆了四五个 java 进程重新运行程序时界面卡住或者报连接数超限。原因JFrame默认关闭行为是隐藏而不是退出加上有代码在finally之外开了连接没关进程就一直挂着。解决主窗体setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)所有Connection、PreparedStatement、ResultSet用 try-with-resources 包住调试期间实在清不掉就去任务管理器手动结束别反复启动。5.5 分配床位后房间人数和住宿记录对不上现象房间里明明住了三个人used_beds显示 2或者提示已满员但表格里还有空床。原因两条 SQL 不在一个事务里中间抛了异常或者代码里某条UPDATE的WHERE room_id?参数传错还有一种隐蔽情况退宿时只改了out_date忘了减used_beds。解决按 4.2 的写法用setAutoCommit(false)包住任何分支失败都rollback实在已经脏了写一条校准 SQL 把冗余字段拉回来UPDATE dorm_room r SET used_beds (SELECT COUNT(*) FROM accommodation a WHERE a.room_id r.room_id AND a.out_date IS NULL);6. 二次开发把课设改成能讲清楚设计的那版课设交完就扔太可惜这套代码稍微动一动就是一个能在面试里讲十分钟的项目。我的建议是按成本从低到高做三件事每件都能独立验证。改造点具体做法收益成本抽 DAO 接口RoomDao改成接口原实现类改名RoomDaoImpl说得出“面向接口编程”后面换实现不动 UI半天查询搬出 EDT查库放到SwingWorker的doInBackground结果在done()里刷表格大数据量下界面不假死能讲线程模型一天加分页和条件查询LIMIT ?,? 动态拼WHERE参数仍走占位符数据量上千也不卡展示分页思路一天验证方法很土但有效往accommodation里灌两千条测试数据用存储过程循环插然后点刷新看界面是不是秒回再把 MySQL 服务停掉点一次查询看程序是弹一个友好的提示框还是直接卡死。这两步能过说明你的异步和异常处理是真做了不是摆样子。SwingWorker的骨架大概长这样重点是done()里拿结果别在doInBackground里碰任何 Swing 组件new SwingWorkerListObject[], Void() { Override protected ListObject[] doInBackground() throws Exception { return roomDao.listAll(); // 耗时查询在后台线程EDT 不受影响 } Override protected void done() { try { model.setRowCount(0); for (Object[] row : get()) model.addRow(row); // get() 拿返回值异常也在这里抛 } catch (Exception ex) { JOptionPane.showMessageDialog(null, 查询失败 ex.getMessage()); } } }.execute();从那以后我每次接这类课设改造都强制自己先用空库跑一遍、再用脏数据跑一遍然后才敢往报告里写“支持并发”。脏数据能把冗余字段、事务边界、字符集这些问题一次性全暴露出来比看十遍代码管用。希望这套拆法能帮你少走点弯路把源码跑起来只是起点能讲清每一行为什么这么写才算真拿到了手。本文还有配套的精品资源点击获取