Java Swing+MySQL选课系统实战:JDBC事务与并发控制详解
2026/9/16 21:01:38 网站建设 项目流程

简介:这是一份基于Java Swing与MySQL的学生选课管理系统完整项目,面向计算机专业正在准备课程设计或期末大作业的学生,也适合需要Java桌面应用实战的初学者。项目源于作者大三期末大作业,经导师指导和认可,评审分99分,代码完整、可直接运行,能够帮助读者快速理解Swing图形界面与MySQL数据操作的整合方式,并掌握Java桌面项目从数据库设计到界面编码的完整流程。资源包共50个文件、约8.6MB,主要包含16个Java源文件、1个SQL数据库脚本、6个XML工程配置、MySQL驱动jar包以及README文档说明,另有20张界面截图和2张数据库E-R图、系统结构图,覆盖登录、选课、成绩管理、学生信息管理、修改密码等模块,方便对照界面和代码进行学习。目前已有140人学习下载,既可直接作为课程设计或期末大作业的参考,也可以从中吸收Swing+MySQL项目的分层结构、数据库表设计与异常处理思路,是Java初学者完成实战项目的实用性材料。

1. 为什么拿去做课设的Swing+MySQL选课系统值得重写一遍

从网盘或分享链接解压出 System-master 文件夹,里面是 Java+Swing+Mysql 学生选课管理系统,带着 mysql-connector-j-8.3.0.jar、十几张界面截图和 README,第一反应多半是“代码这么全,跑起来就能交差”。但真正打开后,ClassNotFoundException、连接超时、中文乱码轮着来,因为这类源码把环境依赖和数据库初始化藏在细节里。这套系统用 Swing 做桌面端、MySQL 做存储,覆盖登录注册、学生信息管理、课程管理、选课、成绩录入与查询,是期末大作业和课程设计里典型的 CRUD 项目。对新手,它是能看懂的课设源码;对工作几年的开发者,也值得拆开看 Swing 事件分发和 JDBC 事务怎么协作。接下来拆表结构、登录会话、选课并发三个关键点,最后给出可落地的排错清单。

2. 从数据库E-R图到建表SQL:选课系统的数据模型怎么落

2.1 先理解为什么是五张表而不是一张大表

压缩包里的 数据库E-R图.jpg 和 系统结构图.jpg 不是装饰,它们是期末大作业答辩时的第一张 PPT。选课管理系统的核心实体是学生、课程和成绩,但实际开发时还要把登录账号和选课关系拆出来。如果全塞进一张学生表,字段会出现大量 NULL,课程倍数的数据也会让后期统计无从下手。这个项目没有用 MyBatis,数据访问层是原生 JDBC,表结构直接决定 DAO 层要写多少个方法,所以建表之前务必先把 E-R 图对齐。

常见做法是拆成五张表:user 管账号和角色,student 管学生基本信息,course 管课程与容量,select_course 管选课关系,score 管成绩。选课和成绩分开,是为了让“学生选了课但老师没录入”的情况也有一条选课记录,成绩表里只放已出分的行。这样统计选课人数时不会把 NULL 成绩算进去,也符合第三范式。唯一需要额外处理的,是“一门课成绩只能有一条记录”,这个约束在业务代码里检查,也可以用唯一索引兜底,后面会讲到。

2.2 核心表结构和 E-R 关系说明

表名关键字段职责
userid, username, password, role登录账号,role 区分 student/ admin
studentstudent_no, student_name, gender, major, class_name学生档案
coursecourse_no, course_name, credit, teacher, capacity, selected课程信息与选课容量
select_courseid, student_no, course_no, select_time学生选课关系
scoreid, student_no, course_no, score, semester成绩记录

student_no 和 course_no 用业务编号而不是自增主键,因为界面查询太快,开发时不用 JOIN 也能辨识。course 表里的 capacity 和 selected 是专门为选课准备的:capacity 表示课程容量,selected 表示当前已选人数。这个设计会带来并发问题,第四章用事务解决。

2.3 建表 SQL 脚本的关键写法

下面是能在 MySQL 8.0 直接执行的建表脚本,重点看字符集、引擎和唯一索引:

CREATE DATABASE IF NOT EXISTS stu_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE stu_system; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(64) NOT NULL, password VARCHAR(128) NOT NULL, role VARCHAR(16) NOT NULL DEFAULT 'student', UNIQUE KEY uk_username (username) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE student ( student_no VARCHAR(32) PRIMARY KEY, student_name VARCHAR(64) NOT NULL, gender VARCHAR(8), major VARCHAR(64), class_name VARCHAR(64) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

user 表密码字段留到 128 位,是为了给哈希后的密文留空间,而不是直接存明文。所有表统一 InnoDB,事务和行级锁都依赖这个引擎。唯一键 uk_username 确保用户名不重复,业务层即使并发注册,数据库也会拦下重复账号。

下面的语句把选课表和成绩表的关联键建出来,并加唯一约束:

CREATE TABLE course ( course_no VARCHAR(32) PRIMARY KEY, course_name VARCHAR(128) NOT NULL, credit DECIMAL(3,1), teacher VARCHAR(32), capacity INT DEFAULT 50, selected INT DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE select_course ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(32), course_no VARCHAR(32), select_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_stu_course (student_no, course_no), CONSTRAINT fk_sc_student FOREIGN KEY (student_no) REFERENCES student(student_no) ON DELETE CASCADE, CONSTRAINT fk_sc_course FOREIGN KEY (course_no) REFERENCES course(course_no) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE score ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(32), course_no VARCHAR(32), score DECIMAL(5,2), semester VARCHAR(32), UNIQUE KEY uk_stu_course_semester (student_no, course_no, semester) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

参数说明:DECIMAL(5,2) 用于成绩,最大 999.99,足够课程设计;DATETIME DEFAULT CURRENT_TIMESTAMP 让插入时自动带时间,少写一行代码;唯一索引 uk_stu_course 在数据库层面阻止重复选课,uk_stu_course_semester 让同一学期只能录一次成绩。外键是双刃剑,导入测试数据时顺序不对会报 1452 错误,很多课设源码因此不敢加外键;我的建议是保留外键,然后用SET FOREIGN_KEY_CHECKS=0临时关闭。

提示:导入 SQL 文件时如果遇到语法错误,优先检查文件编码,Windows 下用记事本另存为 UTF-8 再执行。

3. 登录注册与会话保持:Swing界面背后的JDBC连接逻辑

3.1 登录按钮的监听器里最容易出现的问题

打开登录界面源码,最常见写法是在 JButton 的 ActionListener 里直接 new Connection、拼字符串 SQL,再 ResultSet 判断。代码能跑,但有两个隐患:一是 SQL 注入,用户输入' or '1'='1时 WHERE 条件恒真,直接绕过密码;二是连接未关闭,连续登录几次后 MySQL 报 Too many connections。这个问题也常出现在 java 面试题里。这个项目既然给了 mysql-connector-j-8.3.0.jar,说明驱动是 8.x,连接 URL 至少写成下面这样:

Class.forName("com.mysql.cj.jdbc.Driver"); Connection conn = DriverManager.getConnection( "jdbc:mysql://localhost:3306/stu_system?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&characterEncoding=utf8", "root", "123456"); String sql = "SELECT id, role FROM user WHERE username = ? AND password = ?"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { // 登录成功,写会话 } } }

class 名是 com.mysql.cj.jdbc.Driver,旧版的 com.mysql.jdbc.Driver 在 8.0 驱动里已经过时。allowPublicKeyRetrieval=true 解决 MySQL 8 默认 caching_sha2_password 报 Public Key Retrieval is not allowed 的问题;serverTimezone 指定时区,避免日期字段差 8 小时;characterEncoding=utf8 负责中文不乱码。PreparedStatement 用 ? 占位把输入当作参数而不是 SQL 片段,这是对 SQL 注入的第一层防御。

3.2 密码不能明文比对,但别在课设里引入太重的东西

源码里的 user 表如果直接存明文,答辩时大概率会被问“密码安全性怎么设计”。最简单有效的做法是加盐的 SHA-256 或 MD5,虽然不够现代,但作为课设表达“我知道密码要哈希”就够了。我一般在项目里加一个 PasswordUtils:

private static String toHash(String raw, byte[] salt) { try { MessageDigest md = MessageDigest.getInstance("SHA-256"); md.update(salt); byte[] digest = md.digest(raw.getBytes(StandardCharsets.UTF_8)); StringBuilder sb = new StringBuilder(); for (byte b : digest) { sb.append(String.format("%02x", b)); } return sb.toString(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } }

盐值不要用用户名,注册时生成一个随机串,存库时把盐和哈希拼接,比如随机盐$哈希。登录时先按 username 取盐,再重新计算比对。这里的 String.format("%02x") 是 Java 8 兼容写法,如果项目跑在旧 JDK 上也能通过。另一个容易忽略的点是 Swing 登录框用 JPasswordField 时,应该用new String(passwordField.getPassword())或者直接对 char[] 做哈希,不要用 getText() 返回的 String,后者的敏感信息会留在堆里等待 GC,Swing 的 char[] 可以被手动清空。

3.3 登录状态放哪里:用 UserContext 代替窗口传参

Swing 应用切换界面是 setVisible(true) 而不是浏览器跳转,如果每个窗口构造器都传 User 对象,代码会很散。常见做法是定义一个静态的 UserContext:

public class UserContext { private static User current; public static void login(User user) { current = user; } public static User getUser() { return current; } public static void logout() { current = null; } }

登录成功后在事件线程里 UserContext.login(user),主界面读取即可。注意 Swing 的 UI 操作必须在 Event Dispatch Thread 上执行,不要在 JDBC 回调里直接操作组件,否则界面可能出现半渲染状态。可以简单用 SwingUtilities.invokeLater(Runnable) 包一层。这里没有用线程池或缓存,课设阶段的复杂度刚刚好,也方便答辩时讲清楚数据流。

3.4 连接管理器:从 DriverManager 到最小连接池

登录代码每调用一次都 new Connection,严格来说不是好习惯。如果后续有选课、查询等大量操作,几十个并发请求就会压垮默认数据库连接数。对这个项目,你可以选择引入 HikariCP,但 Swing + 原生 JDBC 的约束下,我推荐做最小的连接管理:

方案优点缺点
每次操作新建连接实现简单,代码直观频繁建连开销大,连接数不可控
静态单例持有单个连接适合单窗口单用户长时间空闲会被 MySQL 断开
最小连接池复用连接,避免重复握手引入额外依赖,脚本变大

课程设计场景,静态单例是最稳的,配合 executeQuery 前后判断连接是否关闭。实际生产中还是交给连接池,这个度要看你的验收标准是什么。

4. 选课扣名额与成绩录入:事务边界和唯一约束怎么落地

4.1 先查再插入的常见写法在并发场景下会破功

大多数学生选课系统的业务代码长这样:先查 course 表 selected < capacity,再执行 INSERT,最后 UPDATE selected + 1。在单用户演示时完全正常,但课设答辩现场如果有两个窗口同时操作,或者老师在日志里看到重复选课记录,就会被问到“并发怎么办”。问题在于 check 和 insert 是两个独立请求,中间存在时间差,两个事务都读到名额为 1,然后都插入成功,最终 selected 变成 2。解决思路不是把代码写长,而是把数据权限交给事务和锁。

4.2 用 SELECT FOR UPDATE 锁住课程行

把选课操作包在一个事务里,先锁住 course 行再判断剩余名额。下面这段是 JDBC 写法,连接来自上一章说的连接管理器:

conn.setAutoCommit(false); try { PreparedStatement lockPs = conn.prepareStatement( "SELECT capacity, selected FROM course WHERE course_no = ? FOR UPDATE"); lockPs.setString(1, courseNo); ResultSet rs = lockPs.executeQuery(); if (!rs.next()) { throw new RuntimeException("课程不存在"); } int capacity = rs.getInt("capacity"); int selected = rs.getInt("selected"); if (selected >= capacity) { throw new RuntimeException("课程已满员"); } PreparedStatement insertPs = conn.prepareStatement( "INSERT INTO select_course(student_no, course_no, select_time) VALUES (?, ?, NOW())"); insertPs.setString(1, studentNo); insertPs.setString(2, courseNo); insertPs.executeUpdate(); PreparedStatement updatePs = conn.prepareStatement( "UPDATE course SET selected = selected + 1 WHERE course_no = ?"); updatePs.setString(1, courseNo); updatePs.executeUpdate(); conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); }

SELECT ... FOR UPDATE 是 InnoDB 的行级排他锁,从这条语句开始,其他事务对同一课程的读取和更新都要等待当前事务结束。这样新增选课记录和更新名额就成了一个不可分割的整体。需要注意,事务里所有 SELECT 都要带 FOR UPDATE 才有效,如果先普通 SELECT 再做 UPDATE,等于没锁。JDBC 默认隔离级别是 REPEATABLE_READ,选课场景下 READ_COMMITTED 更激进,但这里为了课设好解释,保持默认即可。

4.3 唯一索引兜底:遇到重复选课直接报 1062

事务锁有一个薄弱点:如果代码只锁 course 行,而另一个事务直接绕过业务层插入 select_course,还是会出现重复。因此建表时的 UNIQUE KEY uk_stu_course(student_no, course_no) 是最后一道防线。当数据库收到违反唯一约束的 INSERT 时,会返回错误码 1062,Java 端以 SQLIntegrityConstraintViolationException 抛出。

MySQL 错误码常见触发场景处理建议
1045用户名密码错检查 db.properties,root 是否授权
1146Table doesn't exist先导入脚本,再启动程序
1062重复选课/注册业务层捕获 DuplicateKey 后给出提示
1452外键关联不到主表确认 student、course 先有数据
1366中文内容无法插入表和库连接字符集改为 utf8mb4

演示时为了展示这个兜底,可以把唯一约束写在建表脚本里,然后打开两个选课窗口连续点选。正常情况是先到的事务成功,第二个事务在 commit 前抛出异常并回滚。

4.4 成绩录入的原子更新

成绩录入相对简单,但要注意按学期判断重复。如果业务允许同一学期重录,就做 UPDATE 而不是 INSERT;如果每学期一条记录,就用 INSERT ... ON DUPLICATE KEY UPDATE。下面这段适合“补录分数”的场景:

String sql = "INSERT INTO score(student_no, course_no, score, semester) VALUES (?, ?, ?, ?) " + "ON DUPLICATE KEY UPDATE score = VALUES(score)"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, studentNo); ps.setString(2, courseNo); ps.setBigDecimal(3, score); ps.setString(4, semester); ps.executeUpdate(); }

VALUES() 函数在 MySQL 8.0.20 已标记废弃,改写为AS NEW score别名也可以。对学生管理系统来说,这个写法比先 delete 再 insert 安全得多,不会因为中间状态丢失历史记录。成绩表有了唯一索引后,即便两个录入员同时对同一门课录分,也只会保留一条。

5. 十分钟跑通项目:mysql-connector-j-8.3.0 连接参数与改造技巧

5.1 依赖加载和数据库导入

在 IDEA 里打开 StuSystem.iml 后,先看 lib 目录下的 mysql-connector-j-8.3.0.jar 有没有被识别成依赖。File -> Project Structure -> Libraries 里添加 jar,然后确认src被标记为 Sources,resources被标记为 Resources。数据库先建库再导数据,命令行执行mysql -u root -p < stu_system.sql,如果文件过大或编码不一致,可以加--default-character-set=utf8mb4。之后修改 DBUtil 里的连接串,核心参数如下:

jdbc:mysql://localhost:3306/stu_system?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&characterEncoding=utf8

5.2 常见报错逐条排除

  • ClassNotFoundException:驱动 jar 没进 classpath,刷新项目或手动添加依赖。
  • Public Key Retrieval is not allowed:连接 URL 少了 allowPublicKeyRetrieval=true。
  • Unknown database:没执行建库脚本。
  • Bad connection:MySQL 服务没启动或端口被占用,先试telnet 127.0.0.1 3306

如果本地 MySQL 是 5.7,而项目自带的是 8.3.0 驱动,一般也能连,但建议驱动主版本和数据库主版本不要差太大,否则可能出现鉴权插件不兼容。最省事的验证方式是在代码里单独加一个 TestConnection 入口,执行SELECT 1

mysql -u root -p -e "SELECT 1" stu_system

能返回 1 就说明连接串、账号权限和库全部正常,之后再去启动 Swing 界面。

5.3 改造成你自己的课设时,优先改这三个点

第一个点是注册时生成随机盐并保存,答辩时可以现场演示“连续注册两个相同用户,第二个被数据库唯一索引拦下”。第二个点是选课事务里加入SELECT ... FOR UPDATE,并在日志里打印耗时,让老师看到你有意识处理并发。第三个点是修改密码功能,项目截图里已经有修改密码界面,建议把它从“输入旧密码直接改”改成“先校验旧密码,再更新哈希后的新密码”,这个流程在 java 面试八股文里也是常考点。

资源文件尽量用getResource("/images/login_bg.jpg")这种方式加载,而不是绝对路径,这样打成 jar 后图片不会丢。打包时用mvn package或手动jar cfe StuSystem.jar 启动类 -C out .,然后把lib/mysql-connector-j-8.3.0.jar复制到同一目录,用java -Djava.ext.dirs=lib -jar StuSystem.jar运行,或者直接在 IDEA 里运行包含 main 方法的入口类。整个流程跑通后,你手里留下的就是一份真正能演示、能讲清边界条件的 Java Swing 课程设计。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询