尧图精选

Java Swing + MySQL 教务管理系统开发实战:从建表到并发控制

🕒 发布时间:2026/9/16 6:35:27 📁 来源:尧图网络
简介基于Java Swing与MySQL的学校教务管理系统完整项目定位于帮助高校学生、Java初学者及教务管理人员快速掌握桌面端信息管理系统开发可用于课程设计、毕业设计及真实教务场景二次开发。压缩包共含245个文件以54个java源码和168个class编译文件为主另有SQL数据库脚本、jar依赖、项目配置文件及界面预览图整体大小2.74MB源码与可执行资源配套齐全。系统涵盖用户管理、课程管理、学生管理、教师管理、选课系统、考勤管理和报表生成等模块并采用MVC架构实现界面、业务与数据分离。已有3349人学习下载适合需要参考完整SwingMySQL项目结构、学习界面布局与数据库交互设计或直接在此基础上扩展功能的开发者。1. 为什么学校教务系统要选 Java Swing MySQL每年开学季的选课、期末的成绩录入很多学校的信息中心还在用 Excel 加微信群来回传文件。不是没有统一的教务平台而是 Web 版 OA 的权限流程太重排课老师、辅导员、教务处各角色要学半天。这个时候一个双击就能跑、数据落在本机 MySQL、界面响应跟手的管理系统反而更实用。Java Swing MySQL 的组合正好覆盖这类内部工具的核心诉求Swing 负责桌面端的表格、弹窗、表单校验MySQL 负责数据的持久化和多终端共享Java 负责把两者用 JDBC 串起来。这套技术栈看着“老”但在局域网环境下的教务管理、实验室设备登记、小型企业进销存里仍然是投入产出比最高的选型之一。本文从数据库设计、界面搭建到并发控制把一条能落地的完整路径讲清楚。2. 拆数据模型教务系统的表结构设计与连接配置2.1 从角色权限开始拆user、student、course、score 四张表怎么建学校教务系统的核心不是“增删改查”而是角色和数据的对应关系。管理员要维护学生和课程老师要录成绩学生要查成绩。如果一开始只用一张大表后面加字段、加权限都会变成灾难。常见的做法是拆成四张表CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role ENUM(ADMIN,TEACHER,STUDENT) NOT NULL DEFAULT STUDENT, real_name VARCHAR(50) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, stu_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, gender CHAR(1), class_name VARCHAR(50), enroll_year YEAR ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_no VARCHAR(20) NOT NULL UNIQUE, course_name VARCHAR(100) NOT NULL, credit DECIMAL(3,1), teacher_id INT, FOREIGN KEY (teacher_id) REFERENCES user(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE score ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,2), semester VARCHAR(20), UNIQUE KEY uk_stu_course (student_id, course_id, semester), FOREIGN KEY (student_id) REFERENCES student(id), FOREIGN KEY (course_id) REFERENCES course(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有几个选型细节user表单独存登录凭证student、teacher的业务字段不混进去避免“改个手机号要动权限表”的尴尬score表加uk_stu_course唯一键防止同一个学生同一学期同一门课被录两次成绩这是很多“线上 Excel”方案里最容易漏的约束外键在中小数据量下能保证引用完整性不要因为听人说“互联网大厂不用外键”就一刀切去掉教务系统的并发远没到那个量级。字符集统一用utf8mb4不然插入生僻字姓名会报Incorrect string value。2.2 用 HikariCP 管理 MySQL 连接单机桌面也不裸连很多 Swing 教程会直接写DriverManager.getConnection()每次操作都新建连接。这套写法在演示项目里没问题但教务系统在期末周会有人连续点“导出成绩”“批量录入”连接频繁开关会让 MySQL 的threads_created飙升界面也会因为等待连接而卡顿。常见的做法是用 HikariCP 连接池。在pom.xml里加依赖dependency groupIdcom.zaxxer/groupId artifactIdHikariCP/artifactId version4.0.3/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency然后写一个单例类管理数据源public class DbUtil { private static HikariDataSource dataSource; static { HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/school?useSSLfalseserverTimezoneAsia/Shanghai); config.setUsername(root); config.setPassword(your_password); config.setMaximumPoolSize(10); config.setMinimumIdle(2); config.setConnectionTimeout(30000); dataSource new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }2.2.1 mysql 连接串里最重要的 3 个参数serverTimezoneAsia/Shanghai是 MySQL 8.x 的硬性要求不指定时区会出现The server time zone value йʱ is unrecognized把报错里的乱码修复成%2B08:00也能跑但可读性更差useSSLfalse关闭 SSL本地局域网环境下没必要为加密握手多花时间allowPublicKeyRetrievaltrue在 MySQL 8 用caching_sha2_password插件时经常会用到连接串里没有它第一次握手会报Public Key Retrieval is not allowed。maximumPoolSize设为 10 对桌面应用足够minimumIdle保持 2 个常驻连接避免频繁建连。HikariCP 自己的日志会打 DEBUG 级别排错时可以先看它输出的连接获取耗时。3. 用 Swing 把教务页面画出来登录、主窗体和卡片式导航3.1 登录窗口先回答“谁在用”登录界面决定了一个教务系统是“能跑”还是“能用”。如果只有一个账号那不如不做权限。Swing 里做一个登录窗口的核心代码并不复杂关键是把JDialog的模态行为、回车默认按钮、密码框的echoChar设置好public class LoginFrame extends JFrame { private JTextField usernameField; private JPasswordField passwordField; public LoginFrame() { setTitle(学校教务管理系统登录); setSize(360, 220); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); JPanel panel new JPanel(new GridBagLayout()); GridBagConstraints gbc new GridBagConstraints(); gbc.insets new Insets(10, 10, 10, 10); gbc.gridx 0; gbc.gridy 0; panel.add(new JLabel(用户名), gbc); gbc.gridx 1; usernameField new JTextField(15); panel.add(usernameField, gbc); gbc.gridx 0; gbc.gridy 1; panel.add(new JLabel(密码), gbc); gbc.gridx 1; passwordField new JPasswordField(15); panel.add(passwordField, gbc); JButton loginBtn new JButton(登录); gbc.gridx 0; gbc.gridy 2; gbc.gridwidth 2; panel.add(loginBtn, gbc); loginBtn.addActionListener(e - doLogin()); getRootPane().setDefaultButton(loginBtn); add(panel); } private void doLogin() { String username usernameField.getText().trim(); String password new String(passwordField.getPassword()); // 调用 JDBC 查询 user 表验证成功则打开 MainFrame } }getRootPane().setDefaultButton(loginBtn)让用户输完密码直接敲回车就能提交这个细节在桌面端很影响体验。密码框的getPassword()返回char[]是为了避免String不可变导致密码残留在内存里实际项目中可以顺手Arrays.fill(chars, )清空。登录逻辑里要注意不要用 manage 拼接 SQL后面第 4 章会专门讲PreparedStatement这里先用注释占位。3.2 主窗体 CardLayout从学生管理到成绩录入的页面切换登录成功后的主窗体如果每点一个菜单就new一个JFrame弹出来屏幕上会堆满窗口。教务系统的常见做法是主窗体左边放JList或JTree导航右边放一个JPanel用CardLayout切换不同的业务面板。这样页面切换不创建新窗口数据回显也更方便public class MainFrame extends JFrame { private JPanel cardPanel; private CardLayout cardLayout; public MainFrame(String role) { setTitle(学校教务管理系统); setSize(1024, 768); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); cardLayout new CardLayout(); cardPanel new JPanel(cardLayout); // 根据 role 决定显示哪些菜单 cardPanel.add(new StudentManagePanel(), student); cardPanel.add(new ScoreManagePanel(), score); cardPanel.add(new CourseManagePanel(), course); JListString navList new JList(new String[]{学生管理, 成绩管理, 课程管理}); navList.addListSelectionListener(e - { if (!e.getValueIsAdjusting()) { switch (navList.getSelectedIndex()) { case 0: cardLayout.show(cardPanel, student); break; case 1: cardLayout.show(cardPanel, score); break; case 2: cardLayout.show(cardPanel, course); break; } } }); add(navList, BorderLayout.WEST); add(cardPanel, BorderLayout.CENTER); } }3.2.1 JTable 绑定 DefaultTableModel 的 3 个注意点学生管理面板的核心是一个JTable最常见的翻车现场有三个。第一个是DefaultTableModel默认不允许编辑单元格但老师录成绩时需要双击修改这时候要重写isCellEditable方法按列控制第二个是表格数据刷新时直接model.setRowCount(0)清空再重新添加比fireTableDataChanged()更直观但前者会触发多次重绘数据量在几千行时问题不大超过一万行就要改用table.setModel(newModel)整体替换第三个是JTable的表头不参与滚动要放在JScrollPane的setColumnHeaderView才能固定。一个可编辑的成绩表格模型长这样DefaultTableModel model new DefaultTableModel(columns, 0) { Override public boolean isCellEditable(int row, int column) { // 只允许编辑“成绩”列假设它是第 3 列索引 2 return column 2; } };3.3 千万别在事件线程里查数据库Swing 是单线程模型所有 UI 更新必须在事件分发线程EDT上执行。如果在按钮的ActionListener里直接查数据库用户会看到窗口“卡死”因为 EDT 被 JDBC 的阻塞调用占住了。正常的做法是把耗时的数据库操作放进SwingWorker查完再回到 EDT 更新表格SwingWorkerListStudent, Void worker new SwingWorker() { Override protected ListStudent doInBackground() { return studentDao.findAll(); } Override protected void done() { try { ListStudent list get(); // 更新 JTable } catch (Exception e) { JOptionPane.showMessageDialog(MainFrame.this, 加载失败: e.getMessage()); } } }; worker.execute();doInBackground在后台线程跑done回到 EDT。这个模式是 Swing 应用响应性的分水岭用了它之后导出一千条成绩时用户还能拖动窗口体验接近 Web 应用。4. 增删改查落库JDBC 的 PreparedStatement 与事务边界4.1 学生管理的标准 CRUD 写法JDBC 的PreparedStatement是这套技术栈里最值得写对的代码。先说结论任何拼接 SQL 的做法都要避免不是因为“网上都这么说”而是教务系统的学生姓名、班级名都可能包含单引号、百分号拼进去轻则报错重则被 OR 11直接绕过登录。一段标准的按学号查询方法public Student findByStuNo(String stuNo) { String sql SELECT id, stu_no, name, gender, class_name FROM student WHERE stu_no ?; try (Connection conn DbUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, stuNo); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { Student s new Student(); s.setId(rs.getInt(id)); s.setStuNo(rs.getString(stu_no)); s.setName(rs.getString(name)); s.setGender(rs.getString(gender)); s.setClassName(rs.getString(class_name)); return s; } } } catch (SQLException e) { e.printStackTrace(); } return null; }这里的try-with-resources保证Connection、PreparedStatement、ResultSet都会自动关闭即使中间抛异常也不会泄漏连接。注意PreparedStatement的参数下标从 1 开始ps.setString(1, stuNo)就是把?替换成学生学号。查询逻辑上stu_no有唯一索引所以executeQuery()最多返回一条记录。如果是模糊查询姓名SQL 要写成WHERE name LIKE ?然后传% keyword %而不是拼一个LIKE %张三%进 SQL 串。4.2 成绩批量录入必须开事务commit 和 rollback 怎么摆老师录成绩的场景经常是一次导入 Excel 里的几十上百条记录。如果逐条executeUpdate()默认自动提交第 50 条数据格式出错前 49 条已经写进库了老师得一条条找哪条没进去这是教务系统最败口碑的体验。正确的做法是把批量插入包进一个事务public void batchSaveScores(ListScore scoreList) { String sql INSERT INTO score (student_id, course_id, score, semester) VALUES (?, ?, ?, ?); Connection conn null; try { conn DbUtil.getConnection(); conn.setAutoCommit(false); try (PreparedStatement ps conn.prepareStatement(sql)) { for (Score s : scoreList) { ps.setInt(1, s.getStudentId()); ps.setInt(2, s.getCourseId()); ps.setBigDecimal(3, s.getScore()); ps.setString(4, s.getSemester()); ps.addBatch(); // 先攒着 } ps.executeBatch(); // 一次性发给 MySQL conn.commit(); // 全部成功才提交 } } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } throw new RuntimeException(批量保存成绩失败, e); } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }4.2.1 setAutoCommit(false) 之后的执行顺序setAutoCommit(false)之后所有 SQL 都不会自动落盘直到你显式调用commit()。这里有三件事的顺序不能乱先addBatch()把参数累积到PreparedStatement内部缓冲区再executeBatch()发给 MySQL最后commit()。如果executeBatch()抛出BatchUpdateException说明某一条违反了约束比如唯一键uk_stu_course冲突此时rollback()会把前面成功的语句全部回滚。finally块里把autoCommit恢复成true是因为连接会归还给 HikariCP 连接池如果保持false下一个使用者拿到这条连接时会有意想不到的脏读或不可提交。还有个细节executeBatch()返回int[]数组每个元素代表对应语句影响的行数调试时可以检查是否都是Statement.SUCCESS_NO_INFO或1。4.3 并发录成绩行锁、乐观锁和死锁超时怎么选教务系统的高峰期两个老师可能同时给同一个班的不同学生录成绩极端情况下还会两个人同时修改同一条成绩记录。MySQL 的 InnoDB 引擎在score表的唯一索引上会加行锁UPDATE score SET score ? WHERE student_id ? AND course_id ? AND semester ?在事务里执行时后一个会话会等待前一个提交。这是默认行为不需要额外加SELECT ... FOR UPDATE。但要注意innodb_lock_wait_timeout默认 50 秒如果一个事务把一批数据锁住之后迟迟不提交另一个事务会等到超时抛Lock wait timeout exceeded。解决思路是批量操作拆小批比如每 50 条一次 commit所有更新操作的事务时长控制在秒级Swing 端不要在一个事务里做耗时计算。如果场景是“老师修改一条已提交的成绩”并且担心并发覆盖可以在score表加一个version INT DEFAULT 0字段更新时带上版本号UPDATE score SET score ?, version version 1 WHERE id ? AND version ?受影响行数为 0 就说明数据已被别人改过弹个提示让老师刷新重试。这个乐观锁方案比全局锁表轻量得多也更符合教务“读多写少、偶尔冲突”的特征。5. 性能与安全边界索引、统计视图和一条 SQL 拿成绩单学生查成绩单是教务系统最高频的查询。如果按“先查 student 表拿到 id再循环查 score再关联 course 拿课程名”的写法30 门课就要发 31 次 SQL本地 MySQL 还好局域网远程库就会明显变慢。正常做法是用一条JOIN把成绩、课程、学生信息一次拉出来SELECT c.course_name, c.credit, s.score, s.semester FROM score s JOIN course c ON s.course_id c.id WHERE s.student_id ? ORDER BY s.semester DESC, c.course_name;配合EXPLAIN看执行计划时重点确认score表走的是uk_stu_course唯一索引而不是全表扫描。student_id是score表的索引前缀所以这个查询天然能命中索引。如果觉得每次都写这段 JOIN 太长可以建一个视图CREATE VIEW v_student_score AS SELECT stu.stu_no, stu.name AS student_name, c.course_name, c.credit, sc.score, sc.semester FROM student stu JOIN score sc ON stu.id sc.student_id JOIN course c ON sc.course_id c.id;视图在 MySQL 里本质是子查询的语法糖执行时还是会展开成原始 SQL但代码里查起来更简洁。性能上不要迷信“视图一定慢”MySQL 8.0 的优化器会把视图合并进主查询。Swing 端拿到ResultSet后直接填充JTable的模型即可不需要在 Java 里做二次关联。安全边界上再补一个细节user表里的密码必须存哈希。最少也要用SHA-256加盐更稳妥的是BCrypt。注册或改密码时在 Java 端算好哈希再存库登录时把用户输入的密码哈希后和库里比对这样即使数据库文件被拷走也不会直接泄露明文密码。用PreparedStatement的同时还要检查 JDBC 连接串是否开启了useSSLfalse这个参数在本地开发没问题但如果教务系统部署在跨网段环境建议打开 SSL 或改用 SSH 隧道。最后验证整套系统是否健壮可以写一个小的压测程序模拟 20 个线程并发查询成绩单每个线程循环 50 次观察 HikariCP 的活跃连接数是否稳定在配置的maximumPoolSize以内以及Lock wait timeout有没有被触发。如果活跃连接数持续打满优先检查是不是某个PreparedStatement忘了关闭而不是盲目调大连接池数值。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →