news 2026/9/16 6:35:24

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java Swing + MySQL 教务管理系统开发实战:从建表到并发控制

简介:基于Java Swing与MySQL的学校教务管理系统完整项目,定位于帮助高校学生、Java初学者及教务管理人员快速掌握桌面端信息管理系统开发,可用于课程设计、毕业设计及真实教务场景二次开发。压缩包共含245个文件,以54个java源码和168个class编译文件为主,另有SQL数据库脚本、jar依赖、项目配置文件及界面预览图,整体大小2.74MB,源码与可执行资源配套齐全。系统涵盖用户管理、课程管理、学生管理、教师管理、选课系统、考勤管理和报表生成等模块,并采用MVC架构实现界面、业务与数据分离。已有3349人学习下载,适合需要参考完整Swing+MySQL项目结构、学习界面布局与数据库交互设计,或直接在此基础上扩展功能的开发者。

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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这里有几个选型细节:user表单独存登录凭证,studentteacher的业务字段不混进去,避免“改个手机号要动权限表”的尴尬;score表加uk_stu_course唯一键,防止同一个学生同一学期同一门课被录两次成绩,这是很多“线上 Excel”方案里最容易漏的约束;外键在中小数据量下能保证引用完整性,不要因为听人说“互联网大厂不用外键”就一刀切去掉,教务系统的并发远没到那个量级。字符集统一用utf8mb4,不然插入生僻字姓名会报Incorrect string value

2.2 用 HikariCP 管理 MySQL 连接:单机桌面也不裸连

很多 Swing 教程会直接写DriverManager.getConnection(),每次操作都新建连接。这套写法在演示项目里没问题,但教务系统在期末周会有人连续点“导出成绩”“批量录入”,连接频繁开关会让 MySQL 的threads_created飙升,界面也会因为等待连接而卡顿。常见的做法是用 HikariCP 连接池。在pom.xml里加依赖:

<dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>4.0.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency>

然后写一个单例类管理数据源:

public class DbUtil { private static HikariDataSource dataSource; static { HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/school?useSSL=false&serverTimezone=Asia/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 个参数

serverTimezone=Asia/Shanghai是 MySQL 8.x 的硬性要求,不指定时区会出现The server time zone value '�й���ʱ��' is unrecognized,把报错里的乱码修复成%2B08:00也能跑,但可读性更差;useSSL=false关闭 SSL,本地局域网环境下没必要为加密握手多花时间;allowPublicKeyRetrieval=true在 MySQL 8 用caching_sha2_password插件时经常会用到,连接串里没有它,第一次握手会报Public Key Retrieval is not allowedmaximumPoolSize设为 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弹出来,屏幕上会堆满窗口。教务系统的常见做法是:主窗体左边放JListJTree导航,右边放一个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"); JList<String> 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的表头不参与滚动,要放在JScrollPanesetColumnHeaderView才能固定。一个可编辑的成绩表格模型长这样:

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 更新表格:

SwingWorker<List<Student>, Void> worker = new SwingWorker<>() { @Override protected List<Student> doInBackground() { return studentDao.findAll(); } @Override protected void done() { try { List<Student> 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 '1'='1直接绕过登录。一段标准的按学号查询方法:

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保证ConnectionPreparedStatementResultSet都会自动关闭,即使中间抛异常也不会泄漏连接。注意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(List<Score> 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_INFO1

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_idscore表的索引前缀,所以这个查询天然能命中索引。如果觉得每次都写这段 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 连接串是否开启了useSSL=false,这个参数在本地开发没问题,但如果教务系统部署在跨网段环境,建议打开 SSL 或改用 SSH 隧道。

最后验证整套系统是否健壮,可以写一个小的压测程序:模拟 20 个线程并发查询成绩单,每个线程循环 50 次,观察 HikariCP 的活跃连接数是否稳定在配置的maximumPoolSize以内,以及Lock wait timeout有没有被触发。如果活跃连接数持续打满,优先检查是不是某个PreparedStatement忘了关闭,而不是盲目调大连接池数值。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 6:34:31

EFG1无网格法:原理、实现与工程实践

简介&#xff1a;无网格法是一种无需预先生成网格的数值计算方法&#xff0c;在处理自由边界、变形固体和非线性问题时比传统网格类方法更灵活。资源围绕 EFG1&#xff08;无网格伽辽金法&#xff09;的实现展开&#xff0c;包含完整的 MATLAB 函数脚本与配套数据&#xff0c;适…

作者头像 李华
网站建设 2026/9/16 6:34:28

UE5/UE4动态获取资源缩略图并导出PNG/Base64完整实践

前阵子给项目组写资产管理工具&#xff0c;需要在编辑器里遍历几百个StaticMesh&#xff0c;把资源缩略图统一导出成PNG&#xff0c;同时还要把每张图的Base64和宽高信息返回给Web接口。最后整理出这套用UE动态获取资源缩略图、下载到本地或返回图片信息的完整链路。刚开始想得…

作者头像 李华
网站建设 2026/9/16 6:34:25

Python在金融科技领域的核心应用与实战技巧

1. Python为何成为金融科技领域的首选语言在华尔街投行和对冲基金的交易大厅里&#xff0c;Python已经悄然取代了传统的Excel和SAS&#xff0c;成为量化分析师和金融工程师们的标准配置。这种转变并非偶然——根据2023年Wilmott杂志的行业调查&#xff0c;超过78%的金融机构在核…

作者头像 李华
网站建设 2026/9/16 6:34:17

Qt 5.14.2在aarch64平台静态交叉编译实践:工具链配置与部署全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 6:34:04

CalmAn神经科学工作流:钙成像分析的conda环境配置全指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 6:33:45

微信趣味恶搞小程序源码审计与流量主接入实战指南

简介&#xff1a;面向小程序开发者和娱乐类项目爱好者的前端源码包&#xff0c;定位为可直接上手的趣味恶搞娱乐小程序。包内集成流量主能力&#xff0c;无需搭建后端服务器&#xff0c;开发者将前端代码上传到微信等平台并添加合法域名即可运行&#xff0c;同时附带广告ID配置…

作者头像 李华