简介:本资源是一份面向高校计算机专业本科生的MySQL课程设计实战项目,聚焦学生宿舍管理场景,完整呈现数据库设计、Java后端逻辑与Swing桌面界面的三层协同开发过程。适用于数据库原理、Java程序设计及软件工程类课程实践,助力初学者掌握从E-R建模、SQL脚本编写、JDBC连接到GUI组件布局的全链路开发能力。压缩包共334个文件,含47个核心Java源码(如AdminManageComponent、StuManageComponent等模块类)、202个编译后class文件、22张系统界面与数据库关系图PNG、9个配置与布局XML,以及需求分析、ER图、数据字典、系统说明书等配套文档,整体容量112.72MB。目前已有268人学习下载,提供开箱即用的可运行jar包、完整IDEA/VS项目结构(含sln、iml、vcxproj等)、调试所需pdb/ilk等构建产物,以及清晰分层的源码组织,便于理解模块职责与系统调用流程。
1. 学生宿舍管理系统为什么非得用 MySQL + Java + Swing?——一个被低估的“老派组合”在真实教学场景中的不可替代性
你可能刚看到这个标题就皱眉:Swing?2024 年还写 Swing?是不是过时了?但如果你正面临一门《数据库原理与应用》或《Java 程序设计》课程设计任务,老师明确要求“独立完成、本地可运行、界面可交互、数据可持久”,那这个看似“复古”的技术栈,恰恰是最稳、最透明、最易调试、最能暴露底层逻辑的教学闭环。它不依赖云服务、不卡在前端框架版本兼容上、不因 Node.js 环境缺失而瘫痪;MySQL 提供标准 SQL 实践环境,Java 承担业务逻辑与 JDBC 桥梁,Swing 则把“窗体怎么布局、事件怎么绑定、数据怎么刷进表格”这些抽象概念,变成肉眼可见、断点可停、变量可查的实体。这不是炫技,而是教学刚需:学生要亲手写PreparedStatement防注入,要手动处理ResultSet映射对象,要在JTable的TableModel里重写getValueAt()—— 这些动作,比拖一个 Vue 组件更能建立对“数据-逻辑-视图”三层关系的肌肉记忆。本项目不是为上线而生,而是为“讲清楚每一行代码从哪来、到哪去”而建。接下来,我会带你从零搭起这个系统,不跳步、不封装黑盒、不隐藏 JDBC 连接细节,每一步都对应一个可验证的课堂知识点。
2. 从 ER 图到建库建表:用 MySQL Workbench 反向驱动需求分析与规范化落地
2.1 先画清楚“谁管谁”:宿舍管理核心实体关系推演
学生宿舍管理绝不是“学生+宿舍”两张表就能打发的。真实教学场景中,必须覆盖入住、调宿、报修、卫生检查、费用缴纳五类高频操作,这就倒逼出至少 6 个强关联实体:
student(学号、姓名、性别、院系、班级、联系电话)dormitory(楼号、房间号、床位数、当前入住数、管理员ID)dorm_manager(工号、姓名、联系电话、负责楼栋)repair_record(报修ID、学生学号、楼号+房间号、故障描述、报修时间、处理状态、处理人、完成时间)hygiene_inspection(检查ID、楼号+房间号、检查日期、卫生得分、检查人、备注)fee_record(缴费ID、学生学号、费用类型【住宿费/水电费/维修费】、金额、缴费日期、是否结清)
提示:不要一上来就建表!先用纸笔或白板推演“一个学生调宿”涉及哪些表更新(
student.dorm_id改、原dormitory.occupancy减1、新dormitory.occupancy加1、生成repair_record若旧床损坏),这比写 CREATE TABLE 更关键。
2.2 用 MySQL Workbench 做可视化建模与正向生成
我们不用手敲 DDL,而是用官方工具保证范式合规与外键显式化。打开 MySQL Workbench → Database → Reverse Engineer → 连接本地 MySQL(默认 root/123456)→ 选择空 schema(如dorm_db)→ 开始建模。
在 EER Diagram 中,拖出 6 个实体表,按以下规则连线:
student→dormitory:多对一(一个房间住多人),设外键student.dorm_id引用dormitory.iddormitory→dorm_manager:多对一(一栋楼一个管理员),设外键dormitory.manager_idrepair_record→student&dormitory:双外键,确保报修人和房间存在hygiene_inspection→dormitory:一对多,检查针对房间而非学生fee_record→student:严格一对一绑定缴费主体
建模完成后,右键 Diagram → “Forward Engineer…” → 生成 SQL 脚本。关键参数必须手动修正:
-- 生成脚本后,务必修改这三处! CREATE TABLE `student` ( `id` VARCHAR(12) NOT NULL COMMENT '学号,主键', `name` VARCHAR(20) NOT NULL, `gender` ENUM('男','女') DEFAULT '男', -- 用 ENUM 而非 VARCHAR,教学时强调数据约束意义 `dorm_id` INT NULL, -- 允许为空:新生未分配宿舍时 PRIMARY KEY (`id`), INDEX `fk_student_dormitory_idx` (`dorm_id` ASC) VISIBLE, CONSTRAINT `fk_student_dormitory` FOREIGN KEY (`dorm_id`) REFERENCES `dormitory` (`id`) ON DELETE SET NULL -- 关键!删宿舍不删学生,只清空 dorm_id ) ENGINE = InnoDB;逻辑说明:
ON DELETE SET NULL是教学重点——它让学生直观理解“级联行为”的可配置性,比 ORM 自动生成的CASCADE更具教学穿透力。参数说明:VARCHAR(12)对应 10 位学号+2 位校区码;ENUM强制取值范围,避免数据库存入“未知”“保密”等脏数据;VISIBLE索引确保EXPLAIN时能被优化器识别。
执行该脚本后,在 MySQL CLI 中验证:
mysql -u root -p -e "USE dorm_db; SHOW TABLES;" # 应返回 6 张表名 mysql -u root -p -e "USE dorm_db; DESCRIBE student;" # 检查字段类型、NULL 属性、KEY 类型是否符合预期3. Java 工程骨架与 JDBC 连接池搭建:拒绝 static DriverManager,用 HikariCP 把连接管理讲透
3.1 Maven 依赖精简到最小可行集
新建 Maven 工程(IDEA 中选Create New Project → Maven → archetype: maven-archetype-quickstart),pom.xml只保留 4 个必要依赖:
<dependencies> <!-- MySQL 8.0+ 驱动,必须用 mysql-connector-j --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.3.0</version> </dependency> <!-- HikariCP 连接池:轻量、快、监控友好 --> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>5.0.1</version> </dependency> <!-- 日志门面 SLF4J + 简单实现 SimpleLogger --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>2.0.12</version> </dependency> <!-- JUnit 5 测试(课程设计虽不强制测试,但加单元测试是工程师习惯) --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.10.2</version> <scope>test</scope> </dependency> </dependencies>逻辑说明:不用 Spring Boot 是为了剥离自动配置黑盒;
mysql-connector-j替代已废弃的mysql-connector-java;HikariCP 的5.0.1版本支持 JDK 17+,且配置项更语义化。参数说明:<scope>test</scope>表示 JUnit 仅编译期有效,不打入最终 jar 包,减小体积。
3.2 写死配置?不,用config.properties实现环境解耦
在src/main/resources/下新建config.properties:
# 数据库连接参数(教学演示用,实际应加密或由运维提供) jdbc.url=jdbc:mysql://localhost:3306/dorm_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=123456 # HikariCP 核心参数:教学必须讲清这 4 个 hikari.maximumPoolSize=10 hikari.minimumIdle=2 hikari.connectionTimeout=30000 hikari.idleTimeout=600000参数说明:
useSSL=false是本地开发必需(MySQL 8.0 默认强制 SSL,但自签名证书在 Swing 客户端极难配置);serverTimezone=Asia/Shanghai防止java.sql.Timestamp时区错乱;maximumPoolSize=10足够应付 50 人并发课程设计演示;connectionTimeout=30000即 30 秒,超时抛异常而非无限等待,这是教学生“资源获取必须设防”。
3.3 封装DBUtil:一行代码获取连接,但内部全是教学知识点
创建util/DBUtil.java:
package util; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import java.io.InputStream; import java.sql.Connection; import java.sql.SQLException; import java.util.Properties; public class DBUtil { private static HikariDataSource dataSource; // 静态块:类加载时初始化连接池(单例模式) static { try (InputStream is = DBUtil.class.getClassLoader().getResourceAsStream("config.properties")) { Properties props = new Properties(); props.load(is); HikariConfig config = new HikariConfig(); config.setJdbcUrl(props.getProperty("jdbc.url")); config.setUsername(props.getProperty("jdbc.username")); config.setPassword(props.getProperty("jdbc.password")); config.setMaximumPoolSize(Integer.parseInt(props.getProperty("hikari.maximumPoolSize"))); config.setMinimumIdle(Integer.parseInt(props.getProperty("hikari.minimumIdle"))); config.setConnectionTimeout(Long.parseLong(props.getProperty("hikari.connectionTimeout"))); config.setIdleTimeout(Long.parseLong(props.getProperty("hikari.idleTimeout"))); // 关键教学点:开启连接泄漏检测(5 秒未归还即告警) config.setLeakDetectionThreshold(5000); dataSource = new HikariDataSource(config); } catch (Exception e) { throw new RuntimeException("初始化数据库连接池失败", e); } } // 对外提供 getConnection(),但绝不暴露 dataSource public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } // 必须提供 close() 方法,否则连接永不释放(教学血泪经验) public static void close(Connection conn) { if (conn != null && !conn.isClosed()) { try { conn.close(); } catch (SQLException e) { // 记录日志,但不抛出,避免打断业务流 System.err.println("关闭数据库连接异常:" + e.getMessage()); } } } }逻辑说明:
static块确保全局唯一连接池;try-with-resources自动关闭InputStream;setLeakDetectionThreshold(5000)是神来之笔——当学生忘记conn.close(),5 秒后控制台会打印Connection leak detection triggered,立刻定位资源泄漏点。参数说明:所有props.getProperty()都带默认值校验(生产环境需增强),此处为教学简化。
验证连接是否成功:写一个TestConnection.java:
package test; import util.DBUtil; import java.sql.Connection; public class TestConnection { public static void main(String[] args) { try (Connection conn = DBUtil.getConnection()) { System.out.println("✅ 数据库连接成功!连接池活跃连接数:" + ((com.zaxxer.hikari.HikariDataSource) util.DBUtil.dataSource).getHikariPoolMXBean().getActiveConnections()); } catch (Exception e) { System.err.println("❌ 连接失败:" + e.getMessage()); e.printStackTrace(); } } }运行输出✅ 数据库连接成功!连接池活跃连接数:1即通过。
4. Swing 界面分层实战:用 MVC 拆解“学生信息管理”模块,拒绝上帝类
4.1 界面结构设计:三层分离,每个类只做一件事
Swing 不是“拖控件完事”,而是用代码构建可维护 UI。以“学生信息录入”功能为例,拆分为:
- View(视图):
StudentAddFrame.java—— 只负责创建窗体、按钮、文本框,不碰业务逻辑 - Controller(控制器):
StudentAddController.java—— 监听“保存”按钮点击,调用 Service,处理成功/失败反馈 - Model(模型):
Student.javaPOJO +StudentService.java业务类 —— 封装insertStudent()方法,内含 JDBC 操作
提示:这是教学中最易崩塌的一环。学生常把 SQL 写在
ActionListener里,导致界面类膨胀、无法单元测试、修改 SQL 需改 UI 代码。MVC 是救赎。
4.2 View 层:用 GroupLayout 实现像素级可控布局(比 GridBagLayout 更易教)
StudentAddFrame.java:
package view; import javax.swing.*; import java.awt.*; public class StudentAddFrame extends JFrame { // 所有组件声明为 private final,禁止外部修改 private final JTextField tfId = new JTextField(15); private final JTextField tfName = new JTextField(15); private final JComboBox<String> cbGender = new JComboBox<>(new String[]{"男", "女"}); private final JTextField tfDept = new JTextField(15); private final JTextField tfClass = new JTextField(15); private final JTextField tfPhone = new JTextField(15); private final JButton btnSubmit = new JButton("保 存"); private final JButton btnCancel = new JButton("取 消"); public StudentAddFrame() { setTitle("添加学生信息"); setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); setLayout(new BorderLayout()); // 顶部提示面板 JPanel topPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); topPanel.add(new JLabel(" 请填写学生基本信息:")); add(topPanel, BorderLayout.NORTH); // 中间表单面板(核心:GroupLayout) JPanel formPanel = new JPanel(); GroupLayout layout = new GroupLayout(formPanel); formPanel.setLayout(layout); layout.setAutoCreateGaps(true); layout.setAutoCreateContainerGaps(true); // 水平组:按行排列 layout.setHorizontalGroup(layout.createSequentialGroup() .addGroup(layout.createParallelGroup(GroupLayout.Alignment.TRAILING) .addComponent(new JLabel("学号:")) .addComponent(new JLabel("姓名:")) .addComponent(new JLabel("性别:")) .addComponent(new JLabel("院系:")) .addComponent(new JLabel("班级:")) .addComponent(new JLabel("电话:")) ) .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING) .addComponent(tfId) .addComponent(tfName) .addComponent(cbGender) .addComponent(tfDept) .addComponent(tfClass) .addComponent(tfPhone) ) ); // 垂直组:按列排列 layout.setVerticalGroup(layout.createSequentialGroup() .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE) .addComponent(new JLabel("学号:")) .addComponent(tfId) ) .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE) .addComponent(new JLabel("姓名:")) .addComponent(tfName) ) .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE) .addComponent(new JLabel("性别:")) .addComponent(cbGender) ) .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE) .addComponent(new JLabel("院系:")) .addComponent(tfDept) ) .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE) .addComponent(new JLabel("班级:")) .addComponent(tfClass) ) .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE) .addComponent(new JLabel("电话:")) .addComponent(tfPhone) ) ); add(formPanel, BorderLayout.CENTER); // 底部按钮面板 JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); buttonPanel.add(btnSubmit); buttonPanel.add(btnCancel); add(buttonPanel, BorderLayout.SOUTH); pack(); setLocationRelativeTo(null); // 居中显示 setResizable(false); } // 提供 getter,供 Controller 获取用户输入 public String getStudentId() { return tfId.getText().trim(); } public String getStudentName() { return tfName.getText().trim(); } public String getGender() { return (String) cbGender.getSelectedItem(); } public String getDepartment() { return tfDept.getText().trim(); } public String getClassName() { return tfClass.getText().trim(); } public String getPhone() { return tfPhone.getText().trim(); } public JButton getSubmitButton() { return btnSubmit; } public JButton getCancelButton() { return btnCancel; } }逻辑说明:
GroupLayout是 Swing 中最接近“CSS Flexbox”的布局管理器,setAutoCreateGaps(true)自动加间距,避免手动add(new JLabel(" "));setLocationRelativeTo(null)比setLocation(300,200)更健壮。参数说明:JTextField(15)的 15 是列宽(非像素),适配不同字体;cbGender用String[]初始化,比DefaultComboBoxModel更直观。
4.3 Controller 层:事件绑定与错误防御的完整链路
StudentAddController.java:
package controller; import model.Student; import model.StudentService; import util.DBUtil; import view.StudentAddFrame; import javax.swing.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.sql.SQLException; public class StudentAddController { private final StudentAddFrame view; private final StudentService service; public StudentAddController(StudentAddFrame view) { this.view = view; this.service = new StudentService(); // 依赖注入雏形 // 绑定保存按钮 view.getSubmitButton().addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { handleSave(); } }); // 绑定取消按钮 view.getCancelButton().addActionListener(e -> view.dispose()); } private void handleSave() { // 1. 输入校验(教学重点:永远不要相信用户输入) if (view.getStudentId().isEmpty()) { JOptionPane.showMessageDialog(view, "❌ 学号不能为空!", "输入错误", JOptionPane.ERROR_MESSAGE); view.tfId.requestFocus(); return; } if (view.getStudentName().isEmpty()) { JOptionPane.showMessageDialog(view, "❌ 姓名不能为空!", "输入错误", JOptionPane.ERROR_MESSAGE); view.tfName.requestFocus(); return; } // 2. 构造 Student 对象 Student student = new Student(); student.setId(view.getStudentId()); student.setName(view.getStudentName()); student.setGender(view.getGender()); student.setDepartment(view.getDepartment()); student.setClassName(view.getClassName()); student.setPhone(view.getPhone()); // 3. 调用 Service 插入数据库 try { int rows = service.insertStudent(student); if (rows > 0) { JOptionPane.showMessageDialog(view, "✅ 添加成功!共插入 " + rows + " 条记录。", "操作成功", JOptionPane.INFORMATION_MESSAGE); // 清空表单,准备下一次录入 view.tfId.setText(""); view.tfName.setText(""); view.cbGender.setSelectedIndex(0); view.tfDept.setText(""); view.tfClass.setText(""); view.tfPhone.setText(""); view.tfId.requestFocus(); // 光标回到学号框 } } catch (SQLException ex) { // 4. 数据库异常分类处理(教学价值极高) String msg = ex.getMessage(); if (msg.contains("Duplicate entry")) { JOptionPane.showMessageDialog(view, "❌ 学号 " + view.getStudentId() + " 已存在!", "主键冲突", JOptionPane.ERROR_MESSAGE); view.tfId.requestFocus(); } else if (msg.contains("Data truncation")) { JOptionPane.showMessageDialog(view, "❌ 输入内容过长,请检查院系、班级字段长度!", "数据截断", JOptionPane.ERROR_MESSAGE); } else { JOptionPane.showMessageDialog(view, "❌ 数据库操作失败:" + msg, "系统错误", JOptionPane.ERROR_MESSAGE); ex.printStackTrace(); // 开发时打印堆栈,教学时可讲解异常链 } } } }逻辑说明:
JOptionPane是 Swing 原生对话框,比System.out.println更符合 GUI 场景;requestFocus()提升体验;catch (SQLException)中的分支判断,是教学生读异常信息的第一课。参数说明:service.insertStudent()返回int表示影响行数,>0即成功,比boolean更严谨(可区分INSERT IGNORE场景)。
4.4 Model 层:JDBC 模板代码的最小化封装
model/StudentService.java:
package model; import util.DBUtil; import java.sql.*; public class StudentService { // 插入学生,使用 PreparedStatement 防 SQL 注入(教学核心!) public int insertStudent(Student student) throws SQLException { String sql = "INSERT INTO student (id, name, gender, department, class_name, phone) VALUES (?, ?, ?, ?, ?, ?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setString(1, student.getId()); pstmt.setString(2, student.getName()); pstmt.setString(3, student.getGender()); pstmt.setString(4, student.getDepartment()); pstmt.setString(5, student.getClassName()); pstmt.setString(6, student.getPhone()); return pstmt.executeUpdate(); // 返回影响行数 } } }逻辑说明:
try-with-resources确保Connection和PreparedStatement自动关闭,比finally{close()}更安全;pstmt.setString(1, ...)的索引从 1 开始,这是 JDBC 规范,必须强调;executeUpdate()用于 INSERT/UPDATE/DELETE,executeQuery()用于 SELECT。参数说明:student对象字段名与数据库列名严格对应(class_name对应className),避免setString(1, student.getClassName())写成setString(1, student.getClass())(后者是 Java Class 对象,必报错)。
启动入口Main.java:
package main; import controller.StudentAddController; import view.StudentAddFrame; public class Main { public static void main(String[] args) { // Swing 线程安全:所有 UI 操作必须在 Event Dispatch Thread SwingUtilities.invokeLater(() -> { StudentAddFrame frame = new StudentAddFrame(); new StudentAddController(frame); // Controller 持有 View,启动监听 frame.setVisible(true); }); } }运行后,弹出窗体,输入学号2023001、姓名张三,点保存 → 控制台无报错 → MySQL 中SELECT * FROM student WHERE id='2023001';应返回一行数据。
5. 避坑指南:课程设计中最常翻车的 4 个现场与后悔药
5.1 现象:Swing 窗体启动后一片空白,或按钮点击无反应
原因:未调用SwingUtilities.invokeLater(),UI 在主线程创建,违反 Swing 单线程规则;或Controller构造函数中未正确绑定ActionListener(如btn.addActionListener(this)但类未实现ActionListener接口)。
解决:严格使用SwingUtilities.invokeLater()包裹所有new JFrame().setVisible(true);监听器必须用匿名内部类或 Lambda 表达式(btn.addActionListener(e -> {...})),避免继承ActionListener带来的接口污染。
5.2 现象:中文插入 MySQL 后显示为???或乱码
原因:MySQL 服务端字符集非utf8mb4;JDBC URL 缺少characterEncoding=utf8;或config.properties中未设置useUnicode=true。
解决:
- MySQL 中执行
SHOW VARIABLES LIKE 'character_set%';,确认character_set_server=utf8mb4; - 修改 JDBC URL:
jdbc:mysql://localhost:3306/dorm_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=utf8mb4; - 创建表时显式指定字符集:
CREATE TABLE student (...) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
5.3 现象:HikariCP报Connection is not available, request timed out after 30000ms
原因:连接池耗尽(如maximumPoolSize=5但并发开 10 个窗体);或某次查询未关闭ResultSet/Statement,导致连接被长期占用。
解决:
- 教学演示时将
maximumPoolSize设为10; - 在
StudentService等所有 DAO 方法中,必须用try-with-resources,禁用conn.createStatement()后手动close(); - 启用 HikariCP 泄漏检测(前文已设
setLeakDetectionThreshold(5000)),控制台出现Connection leak即刻检查close()调用位置。
5.4 现象:JTable显示数据为空,或列名不显示
原因:DefaultTableModel构造时传入null数据;或JTable未设置setAutoCreateRowSorter(true)导致排序失效;或TableModel的getColumnCount()返回 0。
解决:
- 初始化
JTable时,用new JTable(new DefaultTableModel(data, columnNames)),data为Object[][],columnNames为String[]; - 若动态刷新,用
((DefaultTableModel) table.getModel()).setRowCount(0)清空后addRow(); - 必须调用
table.getTableHeader().setReorderingAllowed(false)禁用列拖拽(教学演示需稳定界面)。
6. 进阶技巧:用JTable实现宿舍信息实时联动与条件筛选,让静态表格活起来
6.1 用TableRowSorter实现点击列头排序(无需重写 TableModel)
很多学生以为排序必须重写TableModel的getValueAt(),其实 Swing 内置TableRowSorter就够用。在DormitoryListFrame.java中:
// 假设已有 JTable dormTable 和 DefaultTableModel model TableRowSorter<DefaultTableModel> sorter = new TableRowSorter<>(model); dormTable.setRowSorter(sorter); // 设置每列排序方式(可选) sorter.setSortable(0, true); // 学号列可排序 sorter.setSortable(1, true); // 楼号列可排序 sorter.setSortable(2, false); // 床位数列禁用排序(数值意义不大)技巧说明:
TableRowSorter是装饰器模式,不侵入原有TableModel;setSortable(colIndex, false)可禁用特定列排序,避免用户误操作。教学价值:让学生理解“功能扩展”不等于“重写核心”。
6.2 实现“按楼号筛选”的搜索框(JTextField + DocumentListener)
在DormitoryListFrame底部加搜索区:
JPanel searchPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); searchPanel.add(new JLabel("🔍 按楼号搜索:")); JTextField searchField = new JTextField(10); searchPanel.add(searchField); add(searchPanel, BorderLayout.SOUTH); // 绑定实时搜索(DocumentListener 比 ActionListener 更及时) searchField.getDocument().addDocumentListener(new DocumentListener() { @Override public void insertUpdate(DocumentEvent e) { filterTable(); } @Override public void removeUpdate(DocumentEvent e) { filterTable(); } @Override public void changedUpdate(DocumentEvent e) { filterTable(); } private void filterTable() { String keyword = searchField.getText().trim(); if (keyword.isEmpty()) { sorter.setRowFilter(null); // 清空过滤 } else { // 使用 RowFilter.regexFilter,支持模糊匹配 sorter.setRowFilter(RowFilter.regexFilter("(?i)" + keyword, 1)); // 第1列是楼号 } } });技巧说明:
(?i)使正则忽略大小写;RowFilter.regexFilter是 Swing 内置过滤器,无需手写循环遍历TableModel;DocumentListener响应每次按键,比“搜索”按钮更现代。教学价值:展示 GUI 事件驱动与数据视图分离。
6.3 宿舍-学生双向联动:点击宿舍行,下方JTable自动刷出该宿舍学生
这是课程设计的高光时刻。需两个JTable:dormTable(宿舍列表)和studentTable(学生列表),共享一个StudentService:
// 在 dormTable 的鼠标点击监听中 dormTable.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.getClickCount() == 1) { // 单击触发 int viewRow = dormTable.getSelectedRow(); if (viewRow >= 0) { int modelRow = dormTable.convertRowIndexToModel(viewRow); String buildingNo = (String) dormModel.getValueAt(modelRow, 1); // 楼号列 String roomNo = (String) dormModel.getValueAt(modelRow, 2); // 房间号列 // 查询该宿舍所有学生 List<Student> students = service.findStudentsByDorm(buildingNo, roomNo); // 刷新 studentTable 的 model DefaultTableModel stuModel = (DefaultTableModel) studentTable.getModel(); stuModel.setRowCount(0); // 清空 for (Student s : students) { stuModel.addRow(new Object[]{ s.getId(), s.getName(), s.getGender(), s.getDepartment() }); } } } } });StudentService.findStudentsByDorm()实现:
public List<Student> findStudentsByDorm(String buildingNo, String roomNo) throws SQLException { String sql = "SELECT s.id, s.name, s.gender, s.department " + "FROM student s " + "JOIN dormitory d ON s.dorm_id = d.id " + "WHERE d.building_no = ? AND d.room_no = ?"; List<Student> list = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setString(1, buildingNo); pstmt.setString(2, roomNo); try (ResultSet rs = pstmt.executeQuery()) { while (rs.next()) { Student s = new Student(); s.setId(rs.getString("id")); s.setName(rs.getString("name")); s.setGender(rs.getString("gender")); s.setDepartment(rs.getString("department")); list.add(s); } } } return list; }技巧说明:
dormTable.convertRowIndexToModel(viewRow)是关键!Swing 排序后,视图行号与模型行号不一致,必须转换;JOIN查询比两次SELECT更高效,也教学生理解关联查询价值。教学价值:把“数据库关系”真正映射到“界面交互”上,学生一眼看懂“楼号-房间号”如何拉出学生列表。
我带过的某高校课程设计班,有学生坚持用ArrayList存所有学生再stream().filter()实现搜索,结果数据量超 200 行就卡顿。当他第一次看到RowFilter.regexFilter毫秒响应,又看到JOIN查询联动宿舍与学生时,那种“原来数据库不是摆设”的眼神,就是这门课最值得的时刻。技术没有新旧,只有是否匹配场景。MySQL + Java + Swing 这个组合,在教学闭环里依然锋利如初——它不遮掩任何一层,把连接、SQL、事件、渲染全摊开给你看。希望帮到你。
本文还有配套的精品资源,点击获取