ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Java Swing+MySQL宿舍管理系统:从课设源码到事务排错

Java Swing+MySQL宿舍管理系统:从课设源码到事务排错

简介:面向高校计算机相关专业学生,尤其是正在完成数据库课程设计或 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

课设答辩现场的条件比想象中苛刻:演示机器是机房的 Windows,JDK 版本不确定,能不能联网不确定,老师也不会帮你先启动一个 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.Driver,5.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 字符串、Connection
daoRoomDao.assign()拼 SQL、设参数、处理结果集JOptionPane、JTextField
entityStudent、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已毕业' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 房间表: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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 住宿记录: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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 报修单:状态用 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已处理' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

几个决定值得说清楚。学号直接做主键,是因为课设场景里一个学生只对应一条身份记录,用自增 id 反而多一层转换;used_beds做成冗余列,是为了主界面房间列表加载时不用每行跟一条子查询,代价是它必须和accommodation同时变,这正是第 4 章要讲事务的原因;out_date IS NULL表达“当前在住”,比加一个is_active字段更省事,退宿时写个日期,历史记录还完整留着,统计“本学期住过多少人”的时候直接就能查。字符集统一用utf8mb4,这年头没人想再被 emoji 或者生僻字搞出乱码。

3. 跑通流程:从装 JDK 到登录框弹出第一条宿舍记录

3.1 先把三个版本对齐:JDK、MySQL、驱动包

拿到源码别急着点运行,先花两分钟确认环境。命令行里敲三条:

java -version # 期望 1.8.x,11 以上也能跑,但源码里若有 sun.* 的内部类就得改 javac -version mysql --version # 5.7.x 或 8.0.x,记住大版本

驱动包要和 MySQL 大版本对齐:MySQL 8 配mysql-connector-java-8.0.x,MySQL 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-set=utf8mb4 < 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" + "?useUnicode=true&characterEncoding=utf8" + "&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"; private static final String USER = "root"; private static final String PWD = "123456"; }

参数逐个解释:useUnicode和characterEncoding决定中文能不能正常往返,缺了它们姓名列会变成问号;serverTimezone在 MySQL 8 驱动下是必须的,不写会直接抛The server time zone value is unrecognized;useSSL=false只是为了让本机连接少一次握手警告;allowPublicKeyRetrieval=true是 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 '1'='1就能进去,答辩老师随手一试就翻车。

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_date=CURDATE() 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 一次拿全,比先查住宿记录再逐条查学生姓名要快得多,也避免了 N+1 次查库。

报修登记就是一条INSERT,没什么花活,关键是提交成功后立刻调用一次刷新方法,让新单子出现在列表里。管理员处理报修时改成UPDATE repair SET status=1 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,连接串带useUnicode=true&characterEncoding=utf8,编译命令加-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 返回的是本地化时区名,驱动解析不了。

解决:连接串补&serverTimezone=Asia/Shanghai。如果同时报公钥相关的错,再加&allowPublicKeyRetrieval=true&useSSL=false。

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 SwingWorker<List<Object[]>, Void>() { @Override protected List<Object[]> 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();

从那以后我每次接这类课设改造,都强制自己先用空库跑一遍、再用脏数据跑一遍,然后才敢往报告里写“支持并发”。脏数据能把冗余字段、事务边界、字符集这些问题一次性全暴露出来,比看十遍代码管用。希望这套拆法能帮你少走点弯路,把源码跑起来只是起点,能讲清每一行为什么这么写,才算真拿到了手。

本文还有配套的精品资源,点击获取

返回列表