Java Swing + MySQL 学生选课成绩管理系统完整实现与避坑指南
简介这是一套基于Java Swing与MySQL实现的学生选课及成绩管理系统课程设计源码包适合Java初学者、毕业设计及课设学生使用。系统覆盖登录注册、管理员/教师/学生三类角色权限包含学生信息管理、课程管理、选课操作、成绩录入与查询等完整业务模块数据库脚本和初始账号管理员admin、教师tch、学生stu密码均为123已内置下载后修改JdbcConfig即可运行。压缩包共53个文件以16个Java源码文件为主辅以MySQL建表SQL脚本、IDEA配置、外部Jar驱动以及20张界面运行截图和E-R图整体大小8.63MB结构清晰便于对照开发。目前已有219人学习下载适合作为课程设计、大作业或初期项目立项的参考模板能够帮助理解Swing界面编程与JDBC数据库操作的完整流程。1. 基于 Java Swing MySQL 的学生选课及成绩管理系统先别急着说它老很多人一听“基于 Java Swing MySQL 的学生选课及成绩管理系统”就皱眉觉得 Swing 是十几年前的老古董。但课程设计答辩现场恰恰是这种组合最稳界面用 Swing 组件拖出来数据落在 MySQL 里中间用 JDBC 串起来逻辑清楚评委提问也有抓手。这个系统要解决的是学生登录、浏览课程、选课退课、查成绩以及老师录入成绩这些典型管理场景。它适合第一次做完整工程的人也适合想在短时间内交付可演示结果的在职读者。下面这套落地步骤是我带过三轮课程设计辅导以后沉淀下来的照着走新手能跑通熟手能避坑。2. 先把地基打牢环境变量、建表设计和项目骨架2.1 Java 环境变量配置与 MySQL 安装装完先验证三条命令课程设计翻车最多的地方往往不是代码而是环境。JDK 装了一半MySQL root 密码忘了驱动 JAR 没放进 lib这些问题在教室演示时一碰一个准。网上很多 java 环境变量配置和 mysql 安装教程都写得很详细但我要提醒的是装完以后一定要用命令行验证一遍别等双击 Main 才报错。java -version javac -version mysql --version逻辑说明第一条验证 JRE第二条验证 JDK 编译器第三条验证 MySQL 客户端是否在 PATH 中。前两条命令都来自 JDK如果 javac 提示找不到命令说明环境变量没配好而不是 JDK 没装上。参数说明JAVA_HOME 必须指向 JDK 安装根目录比如C:\Program Files\Java\jdk-17不能只配到 bin 目录。Windows 的 Path 里追加%JAVA_HOME%\binLinux 或 macOS 追加$JAVA_HOME/bin。MySQL 8.0 安装时默认选 caching_sha2_password 插件如果后面 JDBC 连接报错再回来改配置或换旧版驱动。提示本地课程设计用 MySQL 5.7 或 8.0 都可以但连接驱动 JAR 要和 MySQL 版本对应别拿 5.1 的驱动连 8.0 的库否则大概率报 “Unsupported major.minor version”。数据库连接测试建议用 MySQL Workbench。mysql workbench 使用教程里经常有人卡在连不上 localhost其实先检查服务有没有启动就行。Windows 下 Win R 输入 services.msc找到 MySQL80确认状态是“正在运行”。Workbench 里连接 root 的时候如果提示 Access denied就是密码不对没有快捷办法只能重置密码。2.2 建库建表学生、课程、选课成绩三张表字段就该这样定表结构是系统的地基。我的建议是三张表student、course、student_course。student_course 同时承担“选课关系”和“成绩表”两个职责成绩字段直接挂在选课记录上这是最省事也最容易被评委接受的做法。CREATE DATABASE IF NOT EXISTS course_system DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; USE course_system; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, sno VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, sname VARCHAR(50) NOT NULL COMMENT 姓名, password VARCHAR(64) NOT NULL DEFAULT 123456 COMMENT 登录密码, class_name VARCHAR(50) COMMENT 班级 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, cno VARCHAR(20) NOT NULL UNIQUE COMMENT 课程编号, cname VARCHAR(50) NOT NULL COMMENT 课程名, credit DECIMAL(3,1) NOT NULL DEFAULT 2.0 COMMENT 学分, capacity INT NOT NULL DEFAULT 30 COMMENT 选课容量 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE student_course ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,1) DEFAULT NULL COMMENT 成绩未录入为 NULL, UNIQUE KEY uk_student_course (student_id, course_id), CONSTRAINT fk_sc_student FOREIGN KEY (student_id) REFERENCES student(id), CONSTRAINT fk_sc_course FOREIGN KEY (course_id) REFERENCES course(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明student 表的 id 是代理主键sno 是业务主键登录用 sno。course 表同样用 id 做主键cno 做唯一编号。student_course 表的 score 设置为 NULL表示老师还没录入成绩不要用 0 或 -1 去代替。UNIQUE KEY 加在 student_id 和 course_id 上数据库层面直接拦住重复选课。参数说明DECIMAL(3,1) 表示最多三位数字、一位小数能存 0.0 到 99.9课程学分和成绩都够用。外键约束在课程设计里建议保留虽然很多人说生产环境不用外键但答辩时“数据完整性”是一个重要加分点。所有表显式指定 utf8mb4是为了和后面 JDBC 连接串里的 characterEncodingUTF-8 对齐避免中文乱码。建完表后再补几条测试数据后续界面调试会舒服很多INSERT INTO student (sno, sname, class_name) VALUES (2023001, 张三, 计科2301), (2023002, 李四, 计科2301); INSERT INTO course (cno, cname, credit, capacity) VALUES (CS101, Java程序设计, 3.0, 30), (CS102, 数据库原理, 2.5, 30); INSERT INTO student_course (student_id, course_id) VALUES (1, 1), (1, 2);这段 SQL 的用意是给登录和查询页面留好基础数据。张三有两门选课且都没成绩正好用来测试“未录入”的展示逻辑。2.3 项目骨架普通 Java 工程加 lib 目录不折腾 Maven 也够用很多 java 课程设计案例源码喜欢上 Maven但对课程设计来说Maven 第一次下载依赖就可能劝退一半人。我更推荐普通 Java 工程把 MySQL 驱动 JAR 放进 lib 目录五步就能跑起来course-system/ src/ com/example/ Main.java util/DBUtil.java dao/CourseDao.java dao/StudentDao.java model/Course.java model/ScoreVO.java view/LoginFrame.java view/MainFrame.java view/CoursePanel.java view/SelectedCoursePanel.java view/ScorePanel.java lib/ mysql-connector-java-8.0.x.jar入口类不需要任何花哨写法public class Main { public static void main(String[] args) { new LoginFrame().setVisible(true); } }逻辑说明main 方法只做一件事创建 LoginFrame 并显示。至于 LoginFrame 构造方法里要初始化组件、注册监听器那是 view 包自己的职责。这种写法虽然简单但答辩时可以顺手提一句“单层入口组件自包含”。参数说明这里不能忽略 IDEA 的类库添加操作。写完代码后右键 lib 目录并选择“Add as Library”再运行。如果漏了这一步控制台会报ClassNotFoundException: com.mysql.cj.jdbc.Driver。Eclipse 用户则要 Build Path - Configure Build Path - Add JARs。在正式写功能前建议先用一个测试类确认驱动能加载排除环境问题再往下写public class TestDriver { public static void main(String[] args) throws Exception { Class.forName(com.mysql.cj.jdbc.Driver); System.out.println(驱动加载成功); } }逻辑说明MySQL 8.0 的驱动类名是com.mysql.cj.jdbc.Driver旧版本是com.mysql.jdbc.Driver。写死这个类名跑通就说明 JAR 和环境变量都没问题。这里先走一遍能帮后面省掉大量无关报错。3. JDBC 连接层写好了后面所有功能都是照着填 SQL3.1 DBUtil 工具类连接串、驱动加载和资源关闭Swing 每个窗口都可能要查库所以连接逻辑必须收敛到一个工具类里。一个合格的 DBUtil 至少要做到三件事加载驱动、获取连接、释放资源。我的习惯是用静态块加载驱动用可变参数关闭资源。package com.example.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/course_system ?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingUTF-8; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(MySQL驱动未找到请检查lib目录); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(AutoCloseable... resources) { for (AutoCloseable r : resources) { if (r ! null) { try { r.close(); } catch (Exception ignored) {} } } } }逻辑说明静态块在类加载时执行一次Class.forName的作用是触发驱动注册到 DriverManager。getConnection 每次调用都返回一个物理连接课程设计阶段没必要做连接池。close 方法接收可变个数的 AutoCloseableConnection、Statement、ResultSet 都实现了这个接口finally 里一次调用就能全部关掉。参数说明URL 里有三个高频坑别删。useSSLfalse 关掉 SSL 握手本地开发能少一个证书报错serverTimezoneAsia/Shanghai 解决 MySQL 8.0 的时区校验characterEncodingUTF-8 和建库的 utf8mb4 配套防止中文乱码。USER 和 PASSWORD 要改成你自己安装 MySQL 时的账号密码。接着写一个查询方法看看 DBUtil 怎么用public ListCourse findAllCourses() { ListCourse list new ArrayList(); Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DBUtil.getConnection(); ps conn.prepareStatement(SELECT id, cno, cname, credit, capacity FROM course); rs ps.executeQuery(); while (rs.next()) { Course c new Course(); c.setId(rs.getInt(id)); c.setCno(rs.getString(cno)); c.setCname(rs.getString(cname)); c.setCredit(rs.getDouble(credit)); c.setCapacity(rs.getInt(capacity)); list.add(c); } } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.close(rs, ps, conn); } return list; }逻辑说明先拿连接再预处理 SQL然后执行查询。rs.next()每调用一次就向下移动一行返回 false 表示没有更多数据。最后 finally 里关闭三个资源顺序从 ResultSet 到 Statement 再到 Connection正好是反向创建顺序。参数说明rs.getInt(id)是按列名取数据比rs.getInt(1)更不容易出错SQL 里一旦改字段顺序按列名取就依然安全。3.2 PreparedStatement 登录校验为什么不用 Statement 拼接登录功能是评委最爱问的地方。网上很多旧教程还在用字符串拼接 SQL看着简单其实一个大坑。只要学生在学号框里输入 OR 11拼接出的 SQL 就变成了恒真条件密码直接绕过。最典型的反例长这样// 反例别这么写 String sql SELECT * FROM student WHERE sno sno AND password password ;所以我从第一版开始就只用 PreparedStatementpublic boolean login(String sno, String password) { String sql SELECT id FROM student WHERE sno ? AND password ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, sno); ps.setString(2, password); try (ResultSet rs ps.executeQuery()) { return rs.next(); } } catch (SQLException e) { e.printStackTrace(); return false; } }逻辑说明占位符?只代表参数位置驱动会把 setString 传入的值转义后再放进 SQL 中因此即使用户输入引号或关键字也不会改变 SQL 结构。只要rs.next()能返回 true就说明找到了匹配的学号和密码。参数说明这里没有做密码加密因为课程设计一般不需要但答辩时最好加一句“生产环境应该存加盐后的哈希值”。sno 和 password 都通过 setString 传入数据库字段长度要留够sno 用 20password 用 64足够装下常见的哈希串。3.3 DAO 接口与实现类把 SQL 从界面里赶出去Swing 界面里不能直接写 JDBC 代码否则按钮监听器会变得又长又乱。常见做法是拆出一个数据访问层接口定义行为实现类写 SQL。接口是“合同”实现类才是“施工”。public interface CourseDao { ListCourse findAllCourses(); boolean selectCourse(int studentId, int courseId); boolean dropCourse(int studentId, int courseId); ListScoreVO findScoresBySno(String sno); }逻辑说明CourseDao 把课程相关操作全部抽象出来。界面层只依赖接口不知道底层是 MySQL 还是别的数据库。这样做的好处是以后想换 ORM 框架Swing 界面一行都不用动。实现类里选课方法逻辑是插入一条选课记录public class CourseDaoImpl implements CourseDao { Override public boolean selectCourse(int studentId, int courseId) { String sql INSERT INTO student_course (student_id, course_id) VALUES (?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, studentId); ps.setInt(2, courseId); return ps.executeUpdate() 0; } catch (SQLException e) { e.printStackTrace(); return false; } } }逻辑说明executeUpdate 返回受影响行数。插入成功则返回 1返回 true失败则返回 false。如果学生已经选过这门课数据库的 UNIQUE KEY 会直接抛 SQLException界面层可以把这个异常翻译成“你已经选过这门课”的提示框。参数说明这里还要从登录 Session 拿到 studentId但登录时只有学号所以常见办法是在登录成功后执行一条SELECT id FROM student WHERE sno?把真实主键存起来。权限暂时不做管理员登录也走同一张 student 表只是角色字段没加如果评委问起来可以说“增加一个 role 字段即可”。3.4 数据模型类Course、Student、ScoreVO别用 Map 到处传查询结果变成对象后界面用起来才顺手。ListMapString, Object 虽然也能用但取值要转来转去代码可读性差。课程设计用三个类就够Course 对应课程表Student 对应学生表ScoreVO 对应成绩查询视图。public class Course { private Integer id; private String cno; private String cname; private Double credit; private Integer capacity; // 构造方法、getter/setter 省略 }逻辑说明Course 类的字段类型和数据库要保持一致但要小心数据库 DECIMAL 映射为 Double 或 BigDecimal 的选择。学分 DECIMAL(3,1) 用 Double 完全够但如果涉及金额计算更推荐 BigDecimal。课程设计里选 Double 代码更简单能在答辩时解释清楚“简单场景不追求高精度”就够了。4. Swing 界面从登录到主窗体布局、事件和表格刷新4.1 登录窗体的最小实现JFrame JTextField JPasswordFieldSwing 不需要任何第三方库JDK 自带就是最大的优势。登录窗体用最简单的 GridLayout三行两列足够应付演示。public class LoginFrame extends JFrame { private JTextField tfSno new JTextField(); private JPasswordField pfPassword new JPasswordField(); public LoginFrame() { setTitle(学生选课及成绩管理系统 - 登录); setSize(360, 220); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); setLayout(new GridLayout(3, 2)); add(new JLabel(学号)); add(tfSno); add(new JLabel(密码)); add(pfPassword); JButton btnLogin new JButton(登录); add(btnLogin); btnLogin.addActionListener(e - doLogin()); } private void doLogin() { String sno tfSno.getText().trim(); String password new String(pfPassword.getPassword()); if (new StudentDaoImpl().login(sno, password)) { new MainFrame(sno).setVisible(true); dispose(); } else { JOptionPane.showMessageDialog(this, 学号或密码错误); } } }逻辑说明GridLayout 三行两列依次摆放学号、输入框、密码、密码框、空组件、登录按钮布局虽然朴素但不会错位。密码框用 JPasswordField 而不是 JTextField用户输入的内容在界面上显示为圆点。doLogin 里先 trim 学号去掉前后空格再调用登录方法。参数说明setLocationRelativeTo(null)让窗体居中显示这个细节放在 360 乘 220 的窗口上刚刚好。dispose()会在登录成功后释放当前窗体不释放的话主窗体出现后登录窗口还留在任务栏里进程行为会很奇怪。注意JPasswordField 的 getPassword 返回的是 char[]用 new String 转成字符串后用完最好及时置空。课程设计不用太较真但代码里不要顺手 System.out.println 打印密码。4.2 主窗体用 JTabbedPane 拆页面课程列表、我的选课、成绩查询课程设计阶段的系统功能不会特别多主窗体用一个 JTabbedPane 就足够了。三个标签页分别对应三个 JPanel 子类每个面板只负责自己的表格和按钮。public class MainFrame extends JFrame { public MainFrame(String sno) { setTitle(欢迎 sno); setSize(800, 600); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); JTabbedPane tabs new JTabbedPane(); tabs.addTab(课程列表, new CoursePanel()); tabs.addTab(我的选课, new SelectedCoursePanel(sno)); tabs.addTab(成绩查询, new ScorePanel(sno)); add(tabs, BorderLayout.CENTER); } }逻辑说明登录时把学号 sno 传进来主窗体再把它分发给两个需要学生信息的页面。CoursePanel 不知道当前用户是谁因为它只负责展示所有课程和“选课”按钮选课时再根据学号回查学生 ID。这样划分每个类的职责比较清晰。参数说明setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)是主窗体特有的登录窗体如果也用 EXIT_ON_CLOSE那么 dispose 时整个进程都退出。登录成功后的主窗体采用 EXIT 是合理的关闭主界面等于退出程序。4.3 成绩单表格DefaultTableModel 怎么做到刷新不重复表格是 Swing 里最容易写错的部分尤其是刷新逻辑。常见错误是点一次查询表格里的成绩多一行越点越多。原因很直接每次都 addRow从不清理旧数据。正确的刷新方式是先清空再填充public void refreshTable(ListScoreVO scores) { DefaultTableModel model (DefaultTableModel) table.getModel(); model.setRowCount(0); for (ScoreVO vo : scores) { model.addRow(new Object[]{ vo.getCno(), vo.getCname(), vo.getScore() null ? 未录入 : vo.getScore() }); } }逻辑说明setRowCount(0) 把 TableModel 的所有现有行清空相当于一张白纸。然后再 addRow 循环添加新数据这样表格永远只显示最新查询结果。这里将 score 为 null 的情况显示为“未录入”比直接显示 null 要专业得多。参数说明如果使用的是自己继承 AbstractTableModel 的自定义模型改完数据后还要调用 fireTableDataChanged()否则界面不会重新绘制。JTable 默认列宽会自动均分课程列表课程名较长时容易被挤压建议加一行table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); JScrollPane sp new JScrollPane(table);逻辑说明把表放进 JScrollPane并把自动调整列宽关掉这样列宽由表格自身决定内容太长时会出现横向滚动条而不是把课程名压缩成省略号。4.4 选课按钮的事件处理先查后插再给用户一个反馈选课按钮是事件处理里最有代表性的。点击后不能只做一次 insert否则用户连点两下就出重复数据。除了数据库的唯一约束之外界面层也要给出友好反馈btnSelect.addActionListener(e - { btnSelect.setEnabled(false); int selectedRow courseTable.getSelectedRow(); if (selectedRow 0) { JOptionPane.showMessageDialog(this, 请先选择一门课程); btnSelect.setEnabled(true); return; } int courseId (int) courseTable.getValueAt(selectedRow, 0); boolean ok courseDao.selectCourse(studentId, courseId); if (ok) { JOptionPane.showMessageDialog(this, 选课成功); refreshTable(); } else { JOptionPane.showMessageDialog(this, 选课失败可能重复选课或课程不存在); } btnSelect.setEnabled(true); });逻辑说明按钮置灰是为了防止快速连点导致重复提交数据库唯一约束仍然是最底线的保护。getSelectedRow 返回 -1 表示没选中行此时直接提示不执行 SQL。成功后立即刷新课程列表的剩余容量和“我的选课”页签界面数据保持一致。参数说明getValueAt 返回的是 Object这里选课表的第 0 列是课程 ID所以强转成 int。如果你的表格列顺序调整过一定要同步修改这里这是表格代码里最常见的隐性 Bug。5. 避坑指南Swing MySQL 项目最容易翻车的 5 个细节5.1 中文乱码三个地方少一个就白搭现象界面上课程名显示成“???”或者 MySQL Workbench 里中文正常Java 程序里读出乱码。原因建库字符集、JDBC 连接串字符集、Java 源文件编码三者不一致。最常见的是建库用了默认 latin1而连接串里指定了 UTF-8两边对不上。解决先确认建库和建表都用了DEFAULT CHARSETutf8mb4然后在 JDBC URL 里加characterEncodingUTF-8。如果已经建好的库是 latin1用下面两条 SQL 补救ALTER DATABASE course_system CHARACTER SET utf8mb4; ALTER TABLE course CONVERT TO CHARACTER SET utf8mb4;如果是 IDEA 项目编译乱码则要检查 Settings - File Encodings把所有位置都设为 UTF-8。这三个地方对不上任何一个乱码就跑不了。5.2 MySQL 8.0 连不上Public Key Retrieval is not allowed现象点击登录按钮后控制台抛出java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed。原因MySQL 8.0 默认用 caching_sha2_password 认证客户端第一次连接时要从服务器获取公钥来加密密码但 Connector/J 出于安全考虑把 allowPublicKeyRetrieval 默认设为 false所以连接被拒绝。解决在 JDBC URL 中显式加上allowPublicKeyRetrievaltrue和useSSLfalse。本地开发没问题但不要让这套配置出现在生产环境里。String url jdbc:mysql://localhost:3306/course_system?useSSLfalseallowPublicKeyRetrievaltrueserverTimezoneAsia/ShanghaicharacterEncodingUTF-8;逻辑说明这两个参数通常成对出现useSSLfalse 避免证书校验allowPublicKeyRetrievaltrue 允许本地请求公钥。报了别的错误的话优先看完整异常堆栈而不是只搜前半段。5.3 JTable 数据不刷新或越插越多现象第一次点“查询成绩”显示三行第二次点变成六行第三次变成九行数据越查越多。原因点击事件里只调用 addRow没有先清理旧数据TableModel 是在老数据基础上不断追加。解决在每次查询填充数据前调用model.setRowCount(0)。如果是扩展 AbstractTableModel 的自定义模型则在数据更新后调用fireTableDataChanged()通知 JTable 重新绘制。还有一个小问题不要把查询逻辑写在 JTable 的鼠标事件里而是在一个独立的方法中完成查询加刷新按钮和别的事件都能复用它。5.4 选课重复提交唯一约束兜底按钮置灰防手滑现象快速双击“选课”按钮同一个学生同一门课程出现两条记录。原因第一次点击的插入还没执行完第二次点击已经发出了同样的 INSERT。界面层没有防抖数据库也没有约束时重复数据就进了表。解决数据库层面已经用 UNIQUE KEY 拦住重复第一次插入成功第二次会抛 SQLException。程序里可以捕获这个异常并提示“你已经选过这门课程”。更稳妥的做法是在点击后立刻 disable 按钮等数据库返回结果后再恢复。这个场景在 mysql 面试题里也很常问你答“唯一约束优先”比答“先查再插”更高级。5.5 界面卡死耗时查询不放在事件线程里现象点击“查询成绩”后整个窗口像死机一样拖不动也不能最小化。原因Swing 的事件处理代码运行在事件分发线程 EDT 上。数据库查询、网络请求这类阻塞操作如果直接写在按钮监听器里EDT 会被阻塞所有界面绘制都停住。解决最简单的处理是查询前禁用按钮并显示“查询中”查询完再恢复。想加分的话用 SwingWorker 把查询放到后台线程SwingWorkerVoid, Void worker new SwingWorkerVoid, Void() { Override protected Void doInBackground() { result courseDao.findScoresBySno(sno); return null; } Override protected void done() { refreshTable(result); } }; worker.execute();逻辑说明doInBackground 运行在后台线程不会卡住界面done 方法在主线程执行可以安全更新表格和按钮状态。这就是 Swing 应用处理耗时任务的标准姿势也是课程设计答辩中一个非常值的提的改进点。6. 答辩前做这 3 件事功能不变分数明显不一样如果你做完基本功能还有一两天时间把以下三件事补上系统会从“能跑”变成“能讲”。第一件事是把选课方法改成事务版。数据库事务不用整太复杂只要把原来的“一条 INSERT”改成“检查容量、插入选课记录、更新已选人数”的完整过程。手动调用conn.setAutoCommit(false)中间任何一步抛异常就conn.rollback()全部成功再conn.commit()。这会让评委觉得你不是只会拼 SQL而是考虑过数据一致性。第二件事是在成绩页面加一个统计栏。SQL 用一条聚合查询就能实现SELECT COUNT(*) AS total, AVG(score) AS avg_score, MAX(score) AS max_score FROM student_course WHERE student_id ?;把结果显示在成绩表格下方比如“共 3 门课平均分 86.3最高分 95”。这一小段能引出的问题包括聚合函数、NULL 值处理、分组查询都是 java 八股文和数据库面试的高频点你答起来也有底气。第三件事是把成绩导出成 CSV 文件。这个功能不依赖任何第三方库纯 JDK 就能写StringBuilder sb new StringBuilder(课程编号,课程名称,成绩\n); for (ScoreVO vo : list) { sb.append(vo.getCno()).append(,) .append(vo.getCname()).append(,) .append(vo.getScore() null ? : vo.getScore()).append(\n); } Files.write(Paths.get(scores.csv), sb.toString().getBytes(StandardCharsets.UTF_8));逻辑说明用逗号分隔每列用换行分隔每行。成绩为空时写空字符串而不是写 null。Files.write 会覆盖已有文件目录里有老文件时会直接替换。如果成绩里有特殊字符或姓名包含逗号记得再加一层引号包裹但课程设计场景一般用不到。我当年做完这套系统后觉得能跑通就算完事结果评委问“选课并发时怎么保证容量”我答不上来。后来把事务和统计加上同样的题目讲的内容一下子多了起来。你按照这个顺序往下做最后输出的一定是一个能演示、能讲清、能抗追问的完整工程。希望帮到你。本文还有配套的精品资源点击获取