ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于Java Swing的学生选课成绩信息管理系统设计与实现

基于Java Swing的学生选课成绩信息管理系统设计与实现 简介一份基于Java Swing的学生选课成绩信息管理系统完整项目包面向正在做课程设计、毕业设计或希望巩固Java GUI与数据库开发的读者。以Swing构建登录、注册、选课、成绩查看等前端界面后台结合数据库表结构完成学生、课程、选课等核心业务适合在IntelliJ IDEA或Eclipse中导入运行学习。压缩包共142个文件包含36个java源码、75个class编译文件、sql脚本、ER图与设计文档、avi演示视频及若干运行截图整体约131MBjava和class便于对照源码理解实现sql帮助快速初始化数据库视频演示可直接观察系统操作流程。目前已有746人学习下载通过该案例可系统掌握Swing组件使用、数据库ER图设计和Java Web增删改查逻辑是一份综合性与实操性兼具的参考项目。1. 学生选课成绩系统Swing 桌面端的技术演进价值很多人一听到 Java Swing 就觉得是老古董但如果你去翻最近几年的毕业设计选题和中小型企业内部工具会发现 Swing 依然占据着相当比例。原因很直白它不依赖浏览器不需要部署 Web 容器双击就能跑对于课设、毕设和内部管理系统来说交付成本极低。这套《基于 Java Swing 的学生选课成绩信息管理系统》其实踩中了一个很多人忽略的点它不只是一堆界面代码的堆砌而是把数据库建模、GUI 事件驱动、分层架构这三件事串在了一起。压缩包里不仅有源码还附带 ER 图文档和演示视频正好补上了大多数同类项目“只给代码不给设计过程”的短板。适合两类人一是面临课设或毕设、需要快速搭建一个可演示系统的学生二是想回顾 Java 桌面应用开发全流程的从业者。下面从 ER 图开始按开发顺序逐步拆解这套系统的实现逻辑。2. 从 ER 图到建表语句选课系统的数据根基2.1 三实体两关系的语义梳理打开压缩包里的 ER 图文档你会发现核心实体只有三个学生Student、课程Course、选课记录Enrollment。关键不在实体本身而在于它们之间的关系类型。学生与课程之间是典型的多对多关系一个学生可选多门课程一门课程可以被多名学生选择因此必须引入选课记录作为中间实体把 M:N 拆成两个 1:N。这与图书借阅系统的建模思路类似但有一个细节值得注意选课记录并非简单地存放学生编号和课程编号它还承担了成绩属性的载体。ER 图中成绩Score字段的位置决定了表结构的设计——如果放在课程表里那么一个学生的一门课成绩只能存在一条记录这显然不合理。所以成绩必须挂在选课记录实体下这是理解整套数据模型的关键。2.2 根据 ER 图生成 MySQL 建表语句有了实体关系图下一步就是把概念模型转换成物理表结构。大多数毕业设计项目会用 MySQL这里给出兼容性较好的建表 SQL-- 学生表 CREATE TABLE student ( student_id VARCHAR(20) PRIMARY KEY, sname VARCHAR(50) NOT NULL, gender CHAR(2) DEFAULT 男, major VARCHAR(50), class_name VARCHAR(50) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 课程表 CREATE TABLE course ( course_id VARCHAR(20) PRIMARY KEY, cname VARCHAR(50) NOT NULL, credit DOUBLE DEFAULT 0, teacher VARCHAR(50), max_students INT DEFAULT 30 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 选课记录表关联学生与课程并承载成绩 CREATE TABLE enrollment ( enroll_id INT AUTO_INCREMENT PRIMARY KEY, student_id VARCHAR(20) NOT NULL, course_id VARCHAR(20) NOT NULL, score DOUBLE DEFAULT NULL, enroll_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_enroll_student FOREIGN KEY (student_id) REFERENCES student(student_id), CONSTRAINT fk_enroll_course FOREIGN KEY (course_id) REFERENCES course(course_id), UNIQUE KEY uk_student_course (student_id, course_id), CHECK (score 0 AND score 100) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段建表 SQL 背后有几个值得展开的设计决策。主键选择上学生编号和课程编号都使用业务主键VARCHAR 类型这在教务系统里很常见因为学号、课号本身具有业务含义且全局唯一。选课记录表使用自增 INT 主键避免复合主键在维护时带来额外复杂性。UNIQUE KEY 约束在代码层面防止重复选课——即便业务逻辑漏判数据库也能兜底。外键约束在这个表结构里起到了数据完整性的守门员作用。试想一个场景某学生已经选了课并有了成绩如果直接删除学生表的记录MySQL 会因外键约束拒绝执行从而保护选课记录不被孤立。2.3 用 PowerDesigner 还是直接用 SQL压缩包里的 ER 图如果打开过你会发现它的绘制模式是“先逻辑、再物理”。很多人在这个环节会纠结要不要用 PowerDesigner 这类专业建模工具。我的建议是分场景处理如果系统只有这几张表直接用 Navicat 的模型功能或手写 SQL 更快如果将来要扩展成包含教师表、班级表、教室表的复杂教务系统才值得用专业工具正向工程生成 ER 图再反向工程到建表脚本。这套系统的实体数量决定了手写 SQL 是更务实的路径。提示MySQL 8.0 以上版本还用得上一个技巧——用SHOW CREATE TABLE student;查看实际建表语句再结合SELECT * FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_NAMEenrollment;验证外键是否生效。这一招在答辩演示时很加分比口头说“我设计了外键”有说服力得多。3. Swing 分层架构与界面模块实现3.1 界面类与业务逻辑的分层边界打开源码包的类清单你会发现 MainFrame、CourseManage、TeacherManage、ClassManage 这些类名——它们都属于界面层。真正干活的是 CCourseDao 这类 DAO 类。这四个类名之间体现了一个典型的分层原则界面层只负责接收用户输入和展示数据不直接写 SQL所有查询请求都转发给 DAO 层处理。这种分层的核心动机是当数据库结构变化时可以只改 DAO 层而不动界面代码。举例来说假设课程表的 max_students 字段更名为 capacity只需要修改 CourseManage 中对应的 SQL 语句和结果集映射MainFrame 里的表格展示逻辑完全不用动。如果当初把 SQL 散落在各个按钮的事件监听器里改起来就要在十几个地方排查。具体到 Swing 的落地方式常见做法是给每个功能模块建一个 JPanel 子类在主窗口 JFrame 中用 CardLayout 切换面板。看一下 MainFrame 的结构public class MainFrame extends JFrame { private JTabbedPane tabbedPane; private CourseManage coursePanel; private TeacherManage teacherPanel; private CCourseAdd courseAddPanel; public MainFrame() { setTitle(学生选课成绩信息管理系统); setSize(900, 600); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); tabbedPane new JTabbedPane(); coursePanel new CourseManage(); // 课程管理选项卡 teacherPanel new TeacherManage(); // 教师管理选项卡 courseAddPanel new CCourseAdd(); // 选课选项卡 tabbedPane.addTab(课程管理, coursePanel); tabbedPane.addTab(教师管理, teacherPanel); tabbedPane.addTab(学生选课, courseAddPanel); setContentPane(tabbedPane); setLocationRelativeTo(null); } public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { public void run() { new MainFrame().setVisible(true); } }); } }这段代码体现了 Swing 开发的几个关键约定。SwingUtilities.invokeLater 是必须的——Swing 组件不是线程安全的所有 UI 操作必须在事件调度线程EDT上执行否则会出现界面卡死或组件状态错乱。JTabbedPane 将不同功能模块隔离成独立选项卡每个选项卡对应一个 JPanel 子类这也是大多数管理系统的通用布局方式。3.2 用 JTable 展示课程列表课程列表是这类管理系统最核心的展示形态。源码包里 CourseManage.class 对应的实现思路是用 JTable 配合 DefaultTableModel 完成数据展示和刷新。看一个简化但可运行的核心片段public void loadCourseData() { // 清空旧数据防止重复加载 model.setRowCount(0); ListCourse courses courseDao.findAll(); for (Course c : courses) { model.addRow(new Object[]{ c.getCourseId(), c.getCname(), c.getCredit(), c.getTeacher(), c.getMaxStudents() }); } // 触发表格重绘 table.updateUI(); } // 初始化表格模型定义列名 model new DefaultTableModel(); model.setColumnIdentifiers(new String[]{ 课程编号, 课程名称, 学分, 任课教师, 容量上限 }); table new JTable(model);这里有几个在选择数据展示方案时会遇到的坑。DefaultTableModel 的 setColumnIdentifiers 必须在添加行数据之前调用否则列名和列数据会错位。JTable 默认不支持直接编辑但如果你不小心继承了 AbstractTableModel 却没重写 isCellEditable表格会出现可编辑状态用户误点会触发异常。在更换数据库或调整列字段时这个模块也暴露了一个架构问题DAO 返回的是 Course 实体对象但界面层直接访问了 getMaxStudents() 这类方法名。如果数据库字段名变了DAO 层要处理映射界面层也要跟着改。一个常见的改良做法是用 Map 封装返回结果减少界面层对实体类的直接依赖。3.3 Swing 事件监听与用户输入校验选课界面的核心交互是学生输入学号、勾选课程、点击提交。事件监听器里的业务逻辑往往决定了这个系统在演示中会不会出丑。下面这段代码展示了如何监听“提交选课”按钮private void initListeners() { submitBtn.addActionListener(new ActionListener() { Override public void actionPerformed(ActionEvent e) { String studentId studentField.getText().trim(); String courseId courseCombo.getSelectedItem().toString(); if (studentId.isEmpty() || courseId.isEmpty()) { JOptionPane.showMessageDialog(rootPane, 学号和课程编号不能为空, 输入错误, JOptionPane.ERROR_MESSAGE); return; } if (!studentId.matches(\\d{8,12})) { JOptionPane.showMessageDialog(rootPane, 学号格式不正确8-12位数字, 输入错误, JOptionPane.ERROR_MESSAGE); return; } boolean success enrollService.enroll(studentId, courseId); if (success) { refreshTable(); JOptionPane.showMessageDialog(rootPane, 选课成功, 提示, JOptionPane.INFORMATION_MESSAGE); } } }); }这段事件监听代码在用户体验上替初学者避免了两个常见问题。空格处理使用的是 trim() 方法但它的局限也很明显如果用户在学号中间输入了空格trim 方法无能为力这时需要 replaceAll(\s, ) 做进一步处理。正则表达式校验学号格式看似多了一步实则在数据库访问之前就挡住了大量脏数据减轻了 DAO 层的校验压力。4. 选课与成绩录入的实际业务链路4.1 选课接口的完整流程从界面到数据库选课操作的背后是一连串数据库操作和业务规则的叠加。它不仅仅是 INSERT 一条记录那么单纯——先查学生是否存在、再查课程是否存在且未满、最后检查是否重复选课。按这套系统的设计最标准的流程是这样的public boolean enroll(String studentId, String courseId) { Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DBUtils.getConnection(); conn.setAutoCommit(false); // 开启手动事务 // 1. 检查学生是否存在 ps conn.prepareStatement(SELECT COUNT(*) FROM student WHERE student_id ?); ps.setString(1, studentId); rs ps.executeQuery(); if (rs.next() rs.getInt(1) 0) { return false; } // 2. 检查重复选课 ps conn.prepareStatement( SELECT COUNT(*) FROM enrollment WHERE student_id ? AND course_id ?); ps.setString(1, studentId); ps.setString(2, courseId); rs ps.executeQuery(); if (rs.next() rs.getInt(1) 0) { return false; } // 3. 检查课程容量使用行级锁防止超选 ps conn.prepareStatement( SELECT max_students, (SELECT COUNT(*) FROM enrollment WHERE course_id ?) AS current_count FROM course WHERE course_id ? FOR UPDATE); ps.setString(1, courseId); ps.setString(2, courseId); rs ps.executeQuery(); if (rs.next()) { int max rs.getInt(max_students); int current rs.getInt(current_count); if (current max) { conn.rollback(); return false; } } // 4. 插入选课记录 ps conn.prepareStatement( INSERT INTO enrollment (student_id, course_id) VALUES (?, ?)); ps.setString(1, studentId); ps.setString(2, courseId); ps.executeUpdate(); conn.commit(); // 显式提交 return true; } catch (SQLException e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) {} e.printStackTrace(); return false; } finally { DBUtils.close(conn, ps, rs); } }这段代码的 SELECT ... FOR UPDATE 是选课系统防超卖的关键。不加这个子句时两个并发用户同时选修只剩一个名额的课程会同时读到 current_count 29 max_students 30然后双双 INSERT 成功最终选课人数变成 31。加锁后第二个事务会阻塞等待第一个事务提交然后用 updateUI 刷新界面时才能看到正确的剩余容量。事务的原子性在这段代码里也至关重要如果步骤 1、2、3 都通过了但第 4 步 INSERT 时数据库报错比如磁盘已满不加事务的话前面步骤白白执行加了事务可以把整个操作回滚到选课前的状态。提示这套代码里使用的 row-level lock 是 InnoDB 引擎的特性。如果建表时用了 MyISAM 引擎FOR UPDATE 会退化成表级锁虽然选课的并发性能受影响但至少逻辑上不会出错。检查你用的 MySQL 默认引擎确保在建表语句里指定 ENGINEInnoDB。4.2 成绩录入的批处理实现CCourseMarkAdd 对应的是成绩录入功能。课堂上的真实场景是教师一次性选择一门课看到所有选课的学生列表然后逐个输入成绩。如果每输一个成绩就 commit 一次数据库交互次数太多界面刷新也会迟钝。合理的做法是批量收集成绩一次性提交。public void addMarks(ListScoreEntry entries) { String sql UPDATE enrollment SET score ? WHERE student_id ? AND course_id ?; try (Connection conn DBUtils.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { conn.setAutoCommit(false); for (ScoreEntry entry : entries) { ps.setDouble(1, entry.getScore()); ps.setString(2, entry.getStudentId()); ps.setString(3, entry.getCourseId()); ps.addBatch(); } ps.executeBatch(); conn.commit(); } catch (SQLException e) { e.printStackTrace(); } }addBatch 方法在这里的收益可以从时间成本上量化。假设一门课有 50 个学生逐个 executeUpdate 需要 50 次网络往返而 executeBatch 在 MySQL 默认配置下可以把多次更新合并为一次或少量几次发送。当成绩录入界面一次要处理上百条记录时优化前后肉眼可辨的卡顿差距就在这里。这个模块中还需要注意一个数据边界问题成绩的取值必须在建表语句里 CHECK 约束的 0-100 之间。除了数据库层的约束在 ScoreEntry 类里也应该做一次防御性校验因为 CHECK 约束在某些 MySQL 版本或数据库导入导出过程中可能被忽略。4.3 DateChooser 与界面组件的复用细节DateChooser.class 是压缩包里容易被忽略但实际很有参考价值的组件。它提供了日期选择功能在成绩录入时记录考试日期或选课时间。Swing 没有原生日期选择器这个类一般通过 JPopupMenu 弹出日历面板在用户点击日期后把值回填到 JTextField 中。这里的懒加载模式值得学习DateChooser 类把日期计算逻辑如月份天数、闰年判断和界面渲染封装在一起业务代码只需要调用 getText() 就能拿到格式化后的日期字符串。如果在项目里发现 JTextField 手动输入日期经常格式错误直接用这个组件替代就能解决问题。5. 在 IntelliJ IDEA 与 Eclipse 中运行源码的完整路径5.1 导入源码前的 JDK 与编码配置拿到压缩包里的 .java 源文件后第一步不是双击打开 MainFrame而是先确认开发环境的三个参数JDK 版本、项目编码、数据库驱动 JAR。这个项目基于 Java 桌面应用开发JDK 8 最稳妥——如果本机装的是 JDK 17 以上Swing 组件的运行几乎不变但编译时可能出现找不到符号或模块访问错误。在 IntelliJ IDEA 中导入的步骤是File → New → Project from Existing Sources选中项目根目录后IDEA 会自动识别源码目录。关键步骤是 Project Structure → Project Settings → Project把 SDK 选为本地 JDKLanguage Level 选 8。Eclipse 用户则是 File → Import → General → Existing Projects into Workspace导入后右键项目 → Build Path → Configure Build Path → Add External JARs把 mysql-connector-java 的 JDBC 驱动加进去。编码格式是另一个无声杀手。源码中如果包含注释或中文字符串项目默认编码是 GBK而 IDEA 默认 UTF-8导入后会出现中文乱码。需要在 Settings → Editor → File Encodings 中把 Global Encoding 和 Project Encoding 都设为 UTF-8再从菜单 File → File Properties → File Encoding 对具体源文件做一次强制转换。5.2 数据库连接池与驱动配置系统的 DBUtils 类通常负责建立数据库连接但如果源码里硬编码了驱动类名数据库版本变了就会直接抛 ClassNotFoundException。检查一下你的 mysql-connector-java 驱动版本5.x 对应 MySQL 5.x 及之前8.x 对应 MySQL 8.0两者的 Driver 类名完全相同com.mysql.jdbc.Driver但 JDBC URL 有细微差别// MySQL 5.x 的 JDBC URL String url5 jdbc:mysql://localhost:3306/course_system?useUnicodetruecharacterEncodingutf8; // MySQL 8.x 需要追加时区配置 String url8 jdbc:mysql://localhost:3306/course_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse;如果在运行期报错 The server time zone value Öйú±ê׼ʱ¼ä is unrecognized说明你正用 MySQL 8.x 的驱动连接数据库但 URL 少了 serverTimezone 参数。在 IDEA 里直接在运行配置的 VM options 中加 Duser.timezoneGMT8 也可以解决但最干净的方式还是改连接串。Eclipse 用户同理修改 DBUtils.java 中的连接常量即可。5.3 从编译错误到运行环境的排错清单我把这套源码在 IDEA 和 Eclipse 中运行时最容易遇到的几个问题整理成表方便对着排查报错现象根因分析解决方案Exception in thread main java.awt.HeadlessException服务器环境无图形界面在本地开发机运行或临时加-Djava.awt.headlesstrue测试非界面逻辑No suitable driver found for jdbc:mysql://...驱动 JAR 未添加到构建路径确认 mysql-connector 已 Add as LibraryIDEA或 Referenced Libraries 中可见Unknown database course_system建表脚本未执行在 MySQL 中先执行CREATE DATABASE course_system;再导入表结构Access denied for user rootlocalhost数据库账号密码与 DBUtils 中的配置不一致修改 DBUtils.java 中的用户名和密码中文乱码项目编码与数据库编码不一致项目统一 UTF-8MySQL 连接串加 characterEncodingutf85.4 视频教程对应的关键节点压缩包里的 .avi 视频演示了整个系统的运行效果建议对照源码一起看。里面经常会演示到的关键节点有三个系统启动后主界面各选项卡的布局、选课提交后的界面反馈、成绩录入刷新后的表格变化。从复现的角度来说视频中出现的数据特定学号、特定课程名不一定能对上你本地数据库的内容重点是确认每个按钮对应的功能模块到底是哪些类在负责这样调试时缩小范围会更快。6. 改造 DAO 层从 JDBC 裸连接到连接池源项目如果直接使用 DriverManager.getConnection 建立连接在高频访问场景下有一个明显问题每次数据库操作都经历“建立 TCP 连接 → 认证 → 执行 → 断开”的完整生命周期。当选课高峰期同时有几十个用户操作时数据库端会因为频繁创建连接而出现响应变慢。把 DAO 层的连接管理改成连接池是性价比最高的优化方式。这里以 HikariCP 为例public class DBUtils { private static HikariDataSource dataSource; static { HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/course_system ?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai); config.setUsername(root); config.setPassword(123456); config.setMaximumPoolSize(10); config.setMinimumIdle(2); config.setConnectionTimeout(30000); dataSource new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }改造成本非常低只需要在 pom.xml 中引入 com.zaxxer:HikariCP 依赖如果用 Maven或把 HikariCP 的 JAR 包加入构建路径然后替换 DBUtils 类中的静态连接方法。DBUtils 作为一个统一入口把连接池的初始化放在静态代码块中这样首次调用时自动完成预热后续所有 DAO 类获取连接的方式不变改动最小。核心参数有三个 —— maximumPoolSize 控制最大连接数对于课设系统 10 足够不必追求过大minimumIdle 指空闲时保持的最小连接数设为 2 可以避免系统启动后第一次查询较慢connectionTimeout 设定获取连接的超时时间超过即抛出异常防止界面无限等待。DAO 层还有一个容易踩的坑是 Statement 与 PreparedStatement 的混用。带参数的查询必须使用 PreparedStatement 以规避 SQL 注入这一点在选课系统中尤为重要因为 student_id 和 course_id 都来源于用户输入。如果你在源码中看到Statement.executeQuery(SELECT * FROM course WHERE course_id id )这类写法建议立即替换成 PreparedStatement 的占位符写法——这是代码审查环节最容易被问到的问题。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进