ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

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

2026/9/17 16:26:46 拓冰建站 浏览量
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管理 学生面板 和 教师面板避免打开多个窗口导致业务数据不同步。组件选型上表单输入用JTextField、JPasswordField下拉选择用JComboBox数据列表用JTable配合DefaultTableModel按钮统一用JButton。不要为了炫技引入太重的第三方组件库对于这个规模的管理系统原生 Swing 足够而且可维护性最好。JTable是这里最值得注意的组件。很多初学者直接用JTable当数据载体把查询结果往里塞刷新时调用table.revalidate()却发现界面没变化。正确做法是把数据维护在DefaultTableModel里更新表模型后让JTable的视图自动刷新。这里给出一个通用的刷新方法public void refreshTable(JTable table, java.util.ListObject[] 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 三个面板loginPanel、studentPanel、teacherPanel。登录成功后根据角色调用show方法切换。CardLayout cardLayout (CardLayout) getContentPane().getLayout(); cardLayout.show(getContentPane(), studentPanel);这行的前提是你在getContentPane()上设置了CardLayout并且add(panel, studentPanel)时给了字符串标识。切换的原理是CardLayout内部维护一个HashMapshow方法通过名字找到对应组件并显示其余组件隐藏。这里有个容易掉坑的点登录后如果不隐藏登录按钮或禁用当前输入用户可能通过 AltF4 或快捷键重新显示登录卡片。我一般会在切换前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 SwingWorkerListObject[], Void() { Override protected ListObject[] doInBackground() throws Exception { CourseDao dao new CourseDao(); return dao.queryByKeyword(keyword); } Override protected void done() { try { ListObject[] rows get(); refreshTable(courseTable, rows, new String[]{课程号, 课程名, 学分, 名额}); } catch (Exception ex) { JOptionPane.showMessageDialog(frame, 查询失败 ex.getMessage()); } } }.execute(); });SwingWorker的doInBackground()在后台线程执行返回的ListObject[]通过done()方法里的get()取回此时线程已经切回 EDT可以直接操作JTable。这里有几个参数值得说明SwingWorkerT, V的泛型第一个是最终结果类型第二个是中间进度类型本例不需要进度所以用Void。execute()启动任务它会在内部调用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_id和course_id用CHAR不用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?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8); config.setUsername(root); config.setPassword(123456); config.setMaximumPoolSize(10); config.setMinimumIdle(5); config.setConnectionTimeout(30000); HikariDataSource dataSource new HikariDataSource(config);其中maximumPoolSize10表示连接池最多保留 10 个连接minimumIdle5表示空闲时至少保持 5 个连接。connectionTimeout30000是获取连接的超时时间单位毫秒。这里的serverTimezone和characterEncoding是必须显式设置的MySQL 8.0 默认时区可能为 UTC不设时区会导致日期时间字段取出来差 8 小时。拿到DataSource后获取连接的标准写法是try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { // 执行操作 } catch (SQLException e) { e.printStackTrace(); }用try-with-resources会自动关闭PreparedStatement和Connection。注意Connection关闭在连接池里的语义是归还连接不是真的关闭物理连接所以不用担心里面连接耗尽。如果你在代码里手动conn.close()池会回收这个连接以供下次使用。3.3 防止 SQL 注入的 PreparedStatement 写法登录功能最容易出现 SQL 注入最常见的错误写法是用字符串拼接 SQLString sql SELECT * FROM user WHERE username username AND password password ;这种写法只要输入 OR 11就能绕过密码验证。正确做法是全部使用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 20且quota 50然后都执行更新最终selected变成 22 但插入两条选课记录导致超选。避免这个问题的方法就是前面说的“条件更新原子扣减”而不是“读取后判断”。如果业务要求更严格的校验可以在查询时加FOR UPDATE但课设系统没必要引入反而可能因为死锁让学生摸不着头脑。成绩录入也涉及事务。老师录入成绩时往往要批量更新一张表的多条记录如果更新到一半网络故障回滚是必须的。我看到很多实现是把批量更新放在 for 循环里每条记录一个UPDATE每执行一条就自动提交这种方式一旦中途失败前面的数据已经落库且无法判断哪些提交了、哪些没提交。正确做法也是包在事务里批量更新失败全部回滚。5. 运行指导视频里的隐性环节把 SwingMySQL 项目打包和部署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 rootlocalhost这多半是 MySQL 密码或用户权限问题和代码无关运行指导视频里最好演示一遍改配置的路径。5.2 MySQL 8.0 免安装版配置与编码问题很多同学用的是 MySQL 免安装版zip 解压版它不像安装版有/etc/my.ini或安装向导需要手动初始化。基本步骤是在解压目录下写一个my.ini内容最少要包含[mysqld] basedirD:/mysql-8.0.39-winx64 datadirD:/mysql-8.0.39-winx64/data port3306 character-set-serverutf8mb4 collation-serverutf8mb4_unicode_ci然后以管理员身份打开命令行执行mysqld --initialize-insecure初始化再执行mysqld --console启动最后用mysql -u root --skip-password登录并ALTER USER rootlocalhost IDENTIFIED BY 123456;设置密码。这里的编码设置是重中之重很多数据库导入后中文变问号就是字符集不对。Java 连接串里也要保持一致characterEncodingutf8会映射到服务器端的utf8mb4但更稳妥的做法是连接串写characterEncodingutf8mb4。注意这是 JDBC URL 的参数和数据库配置独立两边都要检查。5.3 交付前一定要做的三项验证给别人的“运行指导视频”里至少要演示以下三项验证否则代码在你自己机器上明明能跑别人机器上就是不行。第一项是数据库初始化验证用提供的course.sql脚本导入后执行SHOW TABLES;应该能看到五张表然后手动插入一条测试学生记录确保FOREIGN_KEY_CHECKS设置正确。很多导出脚本默认带外键参数导入顺序不对会导致建表失败所以脚本开头要写SET FOREIGN_KEY_CHECKS0;并在结尾恢复为 1。第二项是启动超时验证如果 JAR 启动后窗口没出来多半是数据库连接失败导致 main 方法抛异常建议在任何数据库操作之前把JFrame的setVisible(true)放在连接池初始化之后并且连接池初始化设置一个较短的超时比如connectionTimeout5000这样错误能快速反馈。第三项是中文乱码验证分别录入中文学生姓名和中文课程名重启程序再查询确认显示正常。如果乱码按前面说的字符集方向排查。验证完成后运行指导视频里的打包演示最后一步建议增加一个启动脚本start.bat内容很简单echo off java -jar course.jar pause这样用户不需要记命令双击就能启动。如果系统装了多个 JDK 版本脚本里最好指定完整路径或者用where java检查环境变量。到这里从界面到数据库再到交付的整条链路就闭合了剩下的就是将源码、数据库脚本和这份运行说明一起归档。本文还有配套的精品资源点击获取