news 2026/9/17 16:26:43

Java Swing + MySQL 学生选课系统:从界面到数据库的完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java Swing + MySQL 学生选课系统:从界面到数据库的完整实现

简介:一套基于Java Swing与MySQL的学生选课管理系统,面向计算机相关专业毕设学生及需要项目实战的Java学习者。系统分为管理员和普通用户两个角色,覆盖用户登录、学生选课、课程添加/查看/修改/搜索(可按课程名称、上课时间或任课老师查询)、学生资料查看、个人信息管理及退出等完整功能,界面采用Swing绘制,结合Navicat与Eclipse/IDEA即可运行调试。资源共158个文件,以zip形式打包,压缩包约58.59MB,含java源码、form界面文件、class编译文件、png/jpg图片、sql数据库脚本、doc文档以及avi运行指导视频,另有db数据库文件与jar依赖,方便直接部署学习。目前已有451人学习使用,代码经过严格调试,可作为毕设或项目实战基础。附带完整数据库脚本与操作录屏,能帮助快速搭建环境、理解选课流程与Swing事件处理,也适合练习面向对象设计和MySQL表结构设计。

1. 学生选课管理系统用 Java Swing 和 MySQL 落地前,先想清楚分工

学生选课管理系统大概是 Java 课设里出现频率最高的题目,很多人的第一版就是把所有代码堆进一个JFrame,然后把 SQL 写在按钮的事件里,最后发现窗口一卡、数据一乱、成绩一改就崩。真正按工程思路做这个系统的第一步,不是打开 IDE 动手,而是先把 Swing 和 MySQL 的分工划清楚:Swing 只负责收集输入和展示结果,它不应该保存业务状态;MySQL 负责持久化,所有选课、退课、成绩录入的逻辑都应该在数据访问层和事务边界内完成。这套系统的核心难点也就在两层之间——JDBC 连接怎么管、SQL 怎么写、选课并发怎么控制。开发者的常见误区是觉得 Swing 老套、JDBC 简单,于是重点全放在界面美化和控件堆砌上,忽略了数据完整性。这篇东西会把从建表到打包的完整链路讲一遍,适合正在做课设、或者想快速搭一个桌面端管理系统的工程师,照着做能少踩一半坑。

2. Java Swing 界面层:从登录窗口到选课列表的骨架构建

2.1 界面模块划分与 Swing 组件选型

Swing 做桌面管理界面,最常见的主窗口结构是三段式:上方是工具栏或导航,中间是内容面板,下方是状态栏。学生选课系统里更实用的做法是按角色切分面板,而不是按按钮切分窗口。也就是说,登录后走一个JFrame,里面用CardLayout管理 "学生面板" 和 "教师面板",避免打开多个窗口导致业务数据不同步。组件选型上,表单输入用JTextFieldJPasswordField,下拉选择用JComboBox,数据列表用JTable配合DefaultTableModel,按钮统一用JButton。不要为了炫技引入太重的第三方组件库,对于这个规模的管理系统,原生 Swing 足够,而且可维护性最好。

JTable是这里最值得注意的组件。很多初学者直接用JTable当数据载体,把查询结果往里塞,刷新时调用table.revalidate()却发现界面没变化。正确做法是把数据维护在DefaultTableModel里,更新表模型后让JTable的视图自动刷新。这里给出一个通用的刷新方法:

public void refreshTable(JTable table, java.util.List<Object[]> rows, String[] columns) { DefaultTableModel model = (DefaultTableModel) table.getModel(); model.setRowCount(0); // 清空旧数据 model.setColumnIdentifiers(columns); for (Object[] row : rows) { model.addRow(row); // 逐行加入新数据 } }

这段代码的逻辑是:先获取表格当前的DefaultTableModel,把原有行数清零,再重新设置列名,最后把查询出的每一行数据追加进去。setRowCount(0)是刷新界面的关键,直接对JTable操作不会触发视图的彻底重绘。至于列名,可以在初始化时设置一次,但每次刷新时重新设置能避免中途改字段导致表头错乱。

2.2 用 CardLayout 管理登录、学生端、教师端三个面板

系统只有登录一个入口,但登录后角色不同,看到的功能也不同。不要写三个独立窗口,用CardLayout切卡片是最干净的方案。先建一个主窗口MainFrame,把自己的布局设为CardLayout,然后往里面 add 三个面板:loginPanelstudentPanelteacherPanel。登录成功后根据角色调用show方法切换。

CardLayout cardLayout = (CardLayout) getContentPane().getLayout(); cardLayout.show(getContentPane(), "studentPanel");

这行的前提是你在getContentPane()上设置了CardLayout,并且add(panel, "studentPanel")时给了字符串标识。切换的原理是CardLayout内部维护一个HashMapshow方法通过名字找到对应组件并显示,其余组件隐藏。这里有个容易掉坑的点:登录后如果不隐藏登录按钮或禁用当前输入,用户可能通过 Alt+F4 或快捷键重新显示登录卡片。我一般会在切换前remove(loginPanel),真正销毁登录界面,而不是留在卡片栈里。

另一个实际问题是学生面板和教师面板的内部布局。学生面板建议用JTabbedPane分成"课程列表"、"我的选课"、"成绩查询"三个页签,教师面板分成"课程管理"、"学生名单"、"成绩录入"三个页签。每个页签内部用BorderLayout,北边放查询条件输入和按钮,中间放JScrollPane包住的JTable,南边放操作按钮。这样做的好处是模块之间通过事件传递数据,界面不需要互相引用。

2.3 表格刷新与事件监听里的线程陷阱

Swing 的单线程模型要求所有 UI 操作都在事件分发线程(EDT)上执行,但 JDBC 查询是耗时操作。如果你在按钮的actionPerformed里直接执行SELECT,点一下按钮窗口就会卡住。正确做法是用SwingWorker把查询放到后台线程,查询完成后再回到 EDT 更新表格。下面是学生查询课程列表的典型写法:

btnSearch.addActionListener(e -> { String keyword = txtKeyword.getText().trim(); new SwingWorker<List<Object[]>, Void>() { @Override protected List<Object[]> doInBackground() throws Exception { CourseDao dao = new CourseDao(); return dao.queryByKeyword(keyword); } @Override protected void done() { try { List<Object[]> rows = get(); refreshTable(courseTable, rows, new String[]{"课程号", "课程名", "学分", "名额"}); } catch (Exception ex) { JOptionPane.showMessageDialog(frame, "查询失败:" + ex.getMessage()); } } }.execute(); });

SwingWorkerdoInBackground()在后台线程执行,返回的List<Object[]>通过done()方法里的get()取回,此时线程已经切回 EDT,可以直接操作JTable。这里有几个参数值得说明:SwingWorker<T, V>的泛型第一个是最终结果类型,第二个是中间进度类型,本例不需要进度所以用Voidexecute()启动任务,它会在内部调用ThreadPoolExecutor分配线程,不要自己new Thread。有一个常见误用是每次点按钮都 new 一个 worker,在查询很慢时连续点击会产生大量并发请求,所以按钮在查询开始时应setEnabled(false),在done()里再setEnabled(true)

3. MySQL 表结构设计与 JDBC 连接池参数

3.1 五张核心表:学生、教师、课程、选课、用户表怎么建

学生选课系统的数据模型不复杂,但表结构决定了后续 SQL 好不好写。我一般拆成五张表:student学生表、teacher教师表、course课程表、sc选课表、user登录用户表。选课表是学生和课程多对多关系的中间表,它要记录选课时间、成绩以及选课状态。一张标准的建表 SQL 可以这样写:

CREATE TABLE student ( student_id CHAR(12) PRIMARY KEY, student_name VARCHAR(50) NOT NULL, major VARCHAR(50), grade INT ); CREATE TABLE course ( course_id CHAR(10) PRIMARY KEY, course_name VARCHAR(100) NOT NULL, credit DECIMAL(3,1), teacher_id CHAR(12), quota INT DEFAULT 50, selected INT DEFAULT 0, FOREIGN KEY (teacher_id) REFERENCES teacher(teacher_id) ); CREATE TABLE sc ( student_id CHAR(12), course_id CHAR(10), score DECIMAL(5,2), select_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TINYINT DEFAULT 0, PRIMARY KEY (student_id, course_id), FOREIGN KEY (student_id) REFERENCES student(student_id), FOREIGN KEY (course_id) REFERENCES course(course_id) );

建表时要注意几个点。student_idcourse_idCHAR不用VARCHAR,因为学号和课程号长度固定,CHAR在 InnoDB 里按固定长度存储,索引效率略高。quota是总名额,selected是已选人数,这两个字段放同一张表才能做原子扣减,如果拆到单独统计表就会引入并发问题。sc表中的status字段用于标记正常选课还是退课,默认 0 代表正常,1 可代表退课但保留记录,这样成绩和日志都能追溯。

外键约束在课设里容易被忽略,但实际有用:它保证选课表里不会出现不存在的学生或课程。不过也要知道外键的代价——插入sc时需要检查父表,批量导入时性能下降。如果数据量不大(课设级别),保留外键利大于弊。

3.2 使用连接池替代 DriverManager 的常见做法

很多示例代码用DriverManager.getConnection(url, user, password)每次新建连接,这在单用户桌面程序里勉强能用,但如果系统里同时有学生端和教师端多开窗口,连接频繁创建销毁会拖慢 MySQL 响应。更常见的做法是引入连接池,Druid 和 HikariCP 都是成熟方案。课设环境用 Druid 比较方便,因为它自带监控。配置参数如下:

HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/course_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"); config.setUsername("root"); config.setPassword("123456"); config.setMaximumPoolSize(10); config.setMinimumIdle(5); config.setConnectionTimeout(30000); HikariDataSource dataSource = new HikariDataSource(config);

其中maximumPoolSize=10表示连接池最多保留 10 个连接,minimumIdle=5表示空闲时至少保持 5 个连接。connectionTimeout=30000是获取连接的超时时间,单位毫秒。这里的serverTimezonecharacterEncoding是必须显式设置的,MySQL 8.0 默认时区可能为 UTC,不设时区会导致日期时间字段取出来差 8 小时。拿到DataSource后,获取连接的标准写法是:

try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { // 执行操作 } catch (SQLException e) { e.printStackTrace(); }

try-with-resources会自动关闭PreparedStatementConnection。注意Connection关闭在连接池里的语义是归还连接,不是真的关闭物理连接,所以不用担心里面连接耗尽。如果你在代码里手动conn.close(),池会回收这个连接以供下次使用。

3.3 防止 SQL 注入的 PreparedStatement 写法

登录功能最容易出现 SQL 注入,最常见的错误写法是用字符串拼接 SQL:

String sql = "SELECT * FROM user WHERE username='" + username + "' AND password='" + password + "'";

这种写法只要输入' OR '1'='1就能绕过密码验证。正确做法是全部使用PreparedStatement,参数用?占位。以登录查询为例:

public User login(String username, String password) { String sql = "SELECT * FROM user WHERE username = ? AND password = ?"; try (Connection conn = db.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { return new User(rs.getString("username"), rs.getString("role")); } } } catch (SQLException e) { logger.error("login failed", e); } return null; }

PreparedStatement会由 MySQL 驱动对参数做转义处理,?占位符的值不会被解析为 SQL 结构。这是一个必须养成的习惯,哪怕课设答辩时没有安全审计,也要把这条写进代码规范。另外注意密码不要明文存入数据库,JDBC 操作中可以用MessageDigest做 MD5 或加盐 SHA-256 后再存储,这里不展开,但至少不要明文。

4. 选课事务与并发控制:超选、退课、成绩录入的正确姿势

4.1 选课扣减剩余名额的原子操作

选课系统最常见的问题是多人同时选同一门课,导致已选人数超过课程容量。如果代码写成先查selected数量,再判断是否小于quota,然后UPDATE,在并发下必然超卖。正确的原子扣减是利用数据库更新的原子性,一条语句完成判断和扣减:

UPDATE course SET selected = selected + 1 WHERE course_id = ? AND selected < quota;

selected < quota在 UPDATE 的判断条件里由数据库行锁保证,同一时刻只有第一个线程能看到selected < quota为真并执行成功,第二个线程尝试更新时行已经被锁住,重新读取后条件不满足,返回影响行数为 0。业务代码根据影响行数决定是否继续插入选课记录:

int rows = update("UPDATE course SET selected = selected + 1 WHERE course_id = ? AND selected < quota", courseId); if (rows == 0) { throw new BusinessException("课程已经选满"); }

这样就不需要先查询后判断,也避免了应用层的synchronized只在单进程内有效的问题。注意这里的?参数要传课程 ID,不能把quota的值写死在 SQL 里。

4.2 事务边界:一个完整选课流程需要哪些步骤

一次选课涉及多步操作:检查课程状态、扣减名额、插入选课记录、可选地更新学生已选课程数。这些操作必须在一个事务里,否则可能出现名额扣了但选课记录没插入的情况。普通的 DAO 方法里每执行一条 SQL 自动提交一次,会破坏原子性,所以要在 Service 层手动控制事务边界。常见写法是先用setAutoCommit(false)关闭自动提交,执行完所有 SQL 后commit(),异常时rollback()

Connection conn = dataSource.getConnection(); try { conn.setAutoCommit(false); // 1. 检查课程是否存在 // 2. 原子扣减名额 // 3. 插入选课记录 sc // 4. 记录操作日志(可选) conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); // 恢复,方便连接池复用 conn.close(); }

这里的关键是事务默认的隔离级别。MySQL InnoDB 默认为REPEATABLE READ,在同一事务里两次查询结果一致,这对本场景是安全的。但要注意:如果步骤 2 执行UPDATE成功,步骤 3 插入选课记录时主键冲突(比如学生重复选同一门课),那整个事务回滚,名额扣减也会回滚,这是正确行为。反过来,如果先插入选课记录再扣减名额,虽然也能靠事务保证失败回滚,但逻辑顺序上建议先扣名额占坑,再插入明细。

4.3 数据库锁与隔离级别的取舍

在选课并发场景中,UPDATE course SET selected = selected + 1 WHERE course_id = ?会对命中行加排他锁(X 锁),其他事务的相同 UPDATE 会阻塞等待,直至前一个事务提交或回滚。这是典型的行级锁,性能开销可以接受。但有一个坑是:如果应用层在事务里先查询课程,然后根据查询结果决定是否更新,查询语句默认是快照读,不加锁,两个并发事务可能同时读到selected = 20quota = 50,然后都执行更新,最终selected变成 22 但插入两条选课记录,导致超选。避免这个问题的方法就是前面说的“条件更新原子扣减”,而不是“读取后判断”。如果业务要求更严格的校验,可以在查询时加FOR UPDATE,但课设系统没必要引入,反而可能因为死锁让学生摸不着头脑。

成绩录入也涉及事务。老师录入成绩时,往往要批量更新一张表的多条记录,如果更新到一半网络故障,回滚是必须的。我看到很多实现是把批量更新放在 for 循环里,每条记录一个UPDATE,每执行一条就自动提交,这种方式一旦中途失败,前面的数据已经落库,且无法判断哪些提交了、哪些没提交。正确做法也是包在事务里,批量更新失败全部回滚。

5. 运行指导视频里的隐性环节:把 Swing+MySQL 项目打包和部署

5.1 从 Eclipse/IDEA 到可执行 JAR 的打包路径

开发环境里点运行没问题,但交付成“源码+数据库+运行指导视频”时,必须提供一个别人能直接启动的产物。最常见的做法是导出可执行 JAR。IDEA 里通过 Project Structure -> Artifacts -> JAR -> From modules with dependencies 生成,Eclipse 里用 Export -> Runnable JAR file。关键点是选择“Extract required libraries into JAR”或者“Package required libraries into JAR”,否则运行时会报ClassNotFoundException: com.mysql.cj.jdbc.Driver。注意 MySQL 8.0 的驱动类名是com.mysql.cj.jdbc.Driver,而 MySQL 5.x 的驱动类是com.mysql.jdbc.Driver,别把老模板里的配置原样复制到新环境。

打包完成后,双击 JAR 并不一定直接打开窗口。因为 JAR 清单文件的Main-Class需要指向带 main 方法的类。如果你的包结构是com.course.MainFrame,那么 manifest 里要写Main-Class: com.course.MainFrame。如果打包时没设置,可以用java -jar course.jar启动并观察报错。还有一个常见问题是连接数据库报Access denied for user 'root'@'localhost',这多半是 MySQL 密码或用户权限问题,和代码无关,运行指导视频里最好演示一遍改配置的路径。

5.2 MySQL 8.0 免安装版配置与编码问题

很多同学用的是 MySQL 免安装版(zip 解压版),它不像安装版有/etc/my.ini或安装向导,需要手动初始化。基本步骤是在解压目录下写一个my.ini,内容最少要包含:

[mysqld] basedir=D:/mysql-8.0.39-winx64 datadir=D:/mysql-8.0.39-winx64/data port=3306 character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci

然后以管理员身份打开命令行,执行mysqld --initialize-insecure初始化,再执行mysqld --console启动,最后用mysql -u root --skip-password登录并ALTER USER 'root'@'localhost' IDENTIFIED BY '123456';设置密码。这里的编码设置是重中之重,很多数据库导入后中文变问号,就是字符集不对。Java 连接串里也要保持一致:characterEncoding=utf8会映射到服务器端的utf8mb4,但更稳妥的做法是连接串写characterEncoding=utf8mb4。注意这是 JDBC URL 的参数,和数据库配置独立,两边都要检查。

5.3 交付前一定要做的三项验证

给别人的“运行指导视频”里,至少要演示以下三项验证,否则代码在你自己机器上明明能跑,别人机器上就是不行。第一项是数据库初始化验证:用提供的course.sql脚本导入后,执行SHOW TABLES;应该能看到五张表,然后手动插入一条测试学生记录,确保FOREIGN_KEY_CHECKS设置正确。很多导出脚本默认带外键参数,导入顺序不对会导致建表失败,所以脚本开头要写SET FOREIGN_KEY_CHECKS=0;并在结尾恢复为 1。第二项是启动超时验证:如果 JAR 启动后窗口没出来,多半是数据库连接失败导致 main 方法抛异常,建议在任何数据库操作之前把JFramesetVisible(true)放在连接池初始化之后,并且连接池初始化设置一个较短的超时,比如connectionTimeout=5000,这样错误能快速反馈。第三项是中文乱码验证:分别录入中文学生姓名和中文课程名,重启程序再查询,确认显示正常。如果乱码,按前面说的字符集方向排查。

验证完成后,运行指导视频里的打包演示最后一步建议增加一个启动脚本start.bat,内容很简单:

@echo off java -jar course.jar pause

这样用户不需要记命令,双击就能启动。如果系统装了多个 JDK 版本,脚本里最好指定完整路径,或者用where java检查环境变量。到这里,从界面到数据库再到交付的整条链路就闭合了,剩下的就是将源码、数据库脚本和这份运行说明一起归档。

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

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

AnyPod 播客工具揭秘:用 MOSS-TTSD 打造生成式播客的完整指南

AnyPod 播客工具揭秘&#xff1a;用 MOSS-TTSD 打造生成式播客的完整指南 【免费下载链接】MOSS-TTS An open-source model family for long-form speech, dialogue synthesis, voice design, sound effects, and real-time streaming TTS 项目地址: https://gitcode.com/Git…

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

四类AI Agent工作范式:技能调度、本地OS、IDE增强与CLI胶水

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

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

CMSIS-4不是版本号,而是嵌入式开发的隐性接口冻结契约

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

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

顺丰快递作业成本法实战:成本动因、分摊模型与数据链路

简介&#xff1a;围绕作业成本法在顺丰快递公司的应用&#xff0c;这份山东财经大学燕山学院本科毕业设计论文提供了完整的案例研究文本&#xff0c;适合会计、财务管理专业学生及物流企业成本管理从业者参考。论文先梳理国内外作业成本法在成本控制领域的理论文献&#xff0c;…

作者头像 李华