尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Java Swing+MySQL学生选课及成绩管理系统实战:从建表到答辩
简介基于Java Swing MySQL的学生选课及成绩管理系统是一套适合课程设计、毕设项目或Java入门实践的综合案例面向需要完成选课、成绩管理等模块开发的学习者。资源包共包含50个文件其中16个java源码文件覆盖登录、学生信息管理、课程管理、选课及成绩查询等核心功能6个xml为IDEA项目配置sql脚本提供建库建表及初始账号20个jpg和2个png为系统运行界面与数据库E-R图另有README说明文档和MySQL驱动jar包整体压缩包大小8.62MB。项目内置管理员、教师、学生三类测试账号数据库账号密码在JdbcConfig中集中配置SQL脚本和文档中均已标明可快速理解系统结构并掌握运行逻辑。目前已有80人学习下载适合希望参考完整前后端实现、理解Swing与MySQL交互逻辑、以及需要结合界面截图撰写课程设计报告或答辩展示的读者。1. 学生选课及成绩管理系统的技术栈为什么选得这么“老”每个学 Java 的人迟早都要交一次课程设计而“学生选课及成绩管理系统”几乎是出镜率最高的题目。这道题看似简单但很多人在答辩时被问倒核心原因不是业务复杂而是把 Java、Swing、MySQL 这三样东西的关系当作“会调 API”来理解没有按真实桌面应用的层次去组织代码。用 Java Swing MySQL 做选课系统本质上是搭一个 C/S 架构的桌面应用Swing 负责界面和事件处理MySQL 负责持久化Java 负责把两者黏起来。但课程设计往往只验证了你“能不能连上数据库”没有验证你是否想清楚“谁在什么时候去查数据、查出错了怎么回滚、界面卡住了是 GC 问题还是查询问题”。这篇内容会沿着“建表 → 连库 → 搭界面 → 写业务 → 调优排错”的顺序讲清楚一套能跑的方案适合正在做课程设计的学生也适合想快速上手 Swing MySQL 组合的初入行开发者。这里没有框架没有 Spring这反而是好事你至少能看见每一行代码在做什么。2. 先把 MySQL 的表结构立稳选课系统的数据模型与建库 SQL2.1 最小可用的四张表以及它们之间的约束关系学生选课及成绩管理系统的核心不是界面上有几个按钮而是数据库能不能保证“一个学生的成绩只属于一门选课记录”。用 MySQL 设计这套模型时我一般不会一上来就画 ER 图而是先列业务规则再定表。常见的规则是一个学生可以选多门课程一门课程可以被多个学生选这是典型的多对多关系成绩必须挂在“某个学生选了某门课”这个动作上不能单独存在一门课的授课教师如果系统需要属于课程的基本信息不单独建教师模块时可以先做成字段选课要有时间窗口概念即使课程设计不强制也要为semester学期字段留出位置。按这个规则最少需要四张表学生表、课程表、选课表关联表带上平时成绩和期末成绩字段、用户表做登录因为课程设计通常要求区分管理员和学生身份。我见过不少同学把密码存在 student 表里这不违法但一旦老师要求区分权限你就会推翻重来。最好从一开始就把登录账号拆出去。下面是建库建表语句MySQL 版本建议 5.7 及以上字符集统一utf8mb4避免写入中文姓名或课程名称时出现乱码。CREATE DATABASE IF NOT EXISTS course_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE course_system; CREATE TABLE t_student ( student_id VARCHAR(20) PRIMARY KEY COMMENT 学号主键, student_name VARCHAR(50) NOT NULL COMMENT 学生姓名, gender CHAR(1) DEFAULT 男 COMMENT 性别, class_name VARCHAR(50) COMMENT 班级, enroll_year YEAR NOT NULL COMMENT 入学年份 ) ENGINEInnoDB COMMENT 学生表; CREATE TABLE t_course ( course_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 课程ID, course_name VARCHAR(100) NOT NULL COMMENT 课程名称, credit DECIMAL(3,1) NOT NULL COMMENT 学分, teacher_name VARCHAR(50) COMMENT 授课教师, max_students INT DEFAULT 50 COMMENT 选课人数上限, semester VARCHAR(20) NOT NULL COMMENT 开课学期如 2025-2026-1 ) ENGINEInnoDB COMMENT 课程表; CREATE TABLE t_student_course ( id INT PRIMARY KEY AUTO_INCREMENT, student_id VARCHAR(20) NOT NULL, course_id INT NOT NULL, regular_score DECIMAL(5,1) DEFAULT NULL COMMENT 平时成绩, exam_score DECIMAL(5,1) DEFAULT NULL COMMENT 期末成绩, final_grade DECIMAL(5,1) GENERATED ALWAYS AS (regular_score * 0.3 exam_score * 0.7) STORED COMMENT 总评30%平时70%期末, select_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 选课时间, UNIQUE KEY uk_student_course (student_id, course_id), FOREIGN KEY (student_id) REFERENCES t_student(student_id), FOREIGN KEY (course_id) REFERENCES t_course(course_id) ) ENGINEInnoDB COMMENT 选课与成绩关联表; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT 密码建议存MD5或SHA-256, role ENUM(admin, student) NOT NULL DEFAULT student, student_id VARCHAR(20) DEFAULT NULL COMMENT 关联学生表学生角色时使用 ) ENGINEInnoDB COMMENT 系统用户表;这段 SQL 里有三个点值得在答辩时主动讲出来。第一个是t_student_course表上的联合唯一键uk_student_course它从数据库层面杜绝了同一位学生重复选同一门课的可能比在 Java 代码里先用 SELECT 判断再 INSERT 可靠得多。第二个是生成列final_grade它把“总评 平时成绩 × 30% 期末成绩 × 70%”的计算规则固化在库里不管成绩从哪个入口写入总评都不会因为某个界面忘了算而缺失。第三个是外键约束虽然互联网公司的高并发场景普遍禁用外键但课程设计属于典型的管理系统数据量小、写多读少外键能帮你省掉大量 Java 侧的一致性判断代码。2.2 JDBC 连接参数与 MySQL 驱动版本选择表建好之后紧接着就是解决 Java 怎么连上 MySQL。这个环节有两种常见方案直接使用 JDBC或者用 MyBatis 这类 ORM 框架。但课程设计通常不允许引入重量级框架而且引入后你的答辩重点就从“数据模型和 SQL”变成了“框架配置”老师未必认可。建议用最朴素的 JDBC 封装一个工具类。连接数据库之前先看三个关键配置参数jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/course_system?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8allowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password你的密码com.mysql.cj.jdbc.Driver是 MySQL Connector/J 8.x 版本的驱动类名旧版 5.x 是com.mysql.jdbc.Driver。如果用了 MySQL 8.0 以上的数据库却配错驱动类名启动时会直接报ClassNotFoundException。serverTimezoneAsia/Shanghai必须显式指定因为新版驱动不再允许服务器时区为 UTC 时静默处理时间字段。allowPublicKeyRetrievaltrue是 MySQL 8.0 使用 caching_sha2_password 认证插件时常见的坑不配这个参数某些环境下首次连接会报Public Key Retrieval is not allowed。有了配置文件再写一个 DBUtil 类import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.util.ResourceBundle; public class DBUtil { private static String driver; private static String url; private static String username; private static String password; static { try { ResourceBundle bundle ResourceBundle.getBundle(jdbc); driver bundle.getString(jdbc.driver); url bundle.getString(jdbc.url); username bundle.getString(jdbc.username); password bundle.getString(jdbc.password); Class.forName(driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(MySQL驱动加载失败检查jdbc.properties中jdbc.driver配置); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } public static void close(AutoCloseable... resources) { for (AutoCloseable r : resources) { if (r ! null) { try { r.close(); } catch (Exception ignored) { } } } } }这个工具类里藏了一个值得强调的细节使用ResourceBundle而不是Properties的load()方法来读取配置文件。前者默认从 classpath 根目录查找.properties文件不需要写绝对路径在 IntelliJ IDEA 里跑课程设计时不会出现“文件路径找不到”的诡异问题。getConnection()每次调用都会创建物理连接这在小系统里完全够用不过后续如果要接大量并发查询可以换成数据库连接池第 5 章会展开。2.3 登录验证的 SQL 写法与密码安全性登录窗口几乎每份课程设计都有但很多人的实现是先查出所有用户再用 for 循环比对用户名和密码。这是错误示范数据量一大内存就遭殃。正确做法是把验证条件放进 SQL让数据库只返回一条记录甚至只返回一个 count。public User login(String username, String password) { // 对密码做SHA-256散列避免明文保存 String hashedPwd DigestUtils.sha256Hex(password); String sql SELECT u.id, u.username, u.role, s.student_id, s.student_name FROM t_user u LEFT JOIN t_student s ON u.student_id s.student_id WHERE u.username ? AND u.password ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, hashedPwd); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { return new User( rs.getString(username), rs.getString(role), rs.getString(student_id), rs.getString(student_name) ); } } } catch (SQLException e) { e.printStackTrace(); } return null; }必须用PreparedStatement拼接 SQL。在这里用字符串拼接用户输入等于把 SQL 注入漏洞直接交给答辩老师看。DigestUtils来自 Apache Commons Codec也可以用MessageDigest.getInstance(SHA-256)手动算课程设计允许引入这个 jar 包。密码不存明文是一个谈资你可以主动和老师说如果 MySQL 被脱库明文密码会直接泄露散列之后至少能多争取一点响应时间。3. Swing 界面层怎么搭才不像“玩具代码”从登录框到主窗体3.1 为什么 Swing 需要 MVC而不是把 SQL 写在按钮事件里如果说 MySQL 侧的核心是约束那 Swing 侧的核心就是用 MVC 思路去组织 UI 代码。Swing 本身是 MVC 框架的体现——JTable的数据用TableModel管理界面用JTable渲染控制器由事件监听器充当。但课程设计里最常见的写法是把Connection和Statement直接写在ActionListener里比如点一下“查询成绩”按钮按钮事件里连数据库、跑 SQL、封装 ResultSet、弹对话框。这种写法能跑但有几个必然出现的后果同一个查询在“学生管理”和“成绩查看”两个界面里要复制两遍 SQL按钮事件方法动辄两三百行出问题后没有办法单独测试某段逻辑数据库异常会被迫处理在 UI 线程窗口卡死或直接崩溃。我一般会把代码分成 Controller 和 DAO 两层。按钮事件只做两件事收集界面输入、调用 Service 层的业务方法、把结果回填到界面组件。SQL 和 JDBC 全部收进 DAO数据访问对象类里。这是一种很轻量的分层没有引入 Spring 容器纯粹靠 Java 对象自己 new 自己但代码结构会清晰非常多。下面这个例子是“按学号查询成绩”的 Controller 端写法。注意看事件监听器一共只有五行代码——它不关心 SQL不关心连接状态。btnQuery.addActionListener(e - { String studentId txtStudentId.getText().trim(); if (studentId.isEmpty()) { JOptionPane.showMessageDialog(this, 请输入学号, 校验提示, JOptionPane.WARNING_MESSAGE); return; } // 调用业务层查询拿到结果后刷新表格 ListScoreVO list scoreService.getScoresByStudentId(studentId); ScoreTableModel model new ScoreTableModel(list); table.setModel(model); lblTotal.setText(共查询到 list.size() 条成绩记录); });ScoreService.getScoresByStudentId()内部再去调用ScoreDAODAO 里用PreparedStatement执行 SQL把ResultSet转成ScoreVO集合返回。这样做的第一个好处是将来如果老师要求把数据源从 MySQL 换成文件或其他数据库只需要改 DAO界面完全不动。第二个好处是可以在getScoresByStudentId()上直接打日志或加耗时统计而不用改动界面类。提示Swing 组件必须在事件分发线程 EDT 上创建和更新。如果用Thread.sleep()模拟耗时操作直接放在按钮事件里界面会卡成“白框”Mac 上甚至会被系统判定无响应。正确的后台任务做法请参见第 5 章的批量导入部分。3.2 用 JTable TableModel 展示选课列表自带排序和过滤大部分课程设计里学生选课和成绩查询的最终落点都放在JTable上。这就要理解 Swing 里视图和数据分离的机制JTable只是视图层真正的数据容器是TableModel。默认的DefaultTableModel可以直接用但它存在一个开发常见问题往DefaultTableModel里塞String数组任何列都会被当成字符串那么“按分数排序”的时候会出现 88 排在 9 后面的情况。解决方案是自定义 TableModel重写getColumnClass方法让数值列返回Integer.class或Double.class这样JTable自带的排序器RowSorter才能按数值比较。public class ScoreTableModel extends AbstractTableModel { private final String[] columnNames {选课ID, 课程名称, 学分, 平时成绩, 期末成绩, 总评, 学期}; private final ListScoreVO data; public ScoreTableModel(ListScoreVO data) { this.data data; } Override public int getRowCount() { return data.size(); } Override public int getColumnCount() { return columnNames.length; } Override public String getColumnName(int column) { return columnNames[column]; } Override public Class? getColumnClass(int columnIndex) { // 关键点把数值列的类型暴露给JTable if (columnIndex 3 || columnIndex 4 || columnIndex 5) { return Double.class; } return String.class; } Override public Object getValueAt(int rowIndex, int columnIndex) { ScoreVO vo data.get(rowIndex); return switch (columnIndex) { case 0 - vo.getId(); case 1 - vo.getCourseName(); case 2 - vo.getCredit(); case 3 - vo.getRegularScore(); case 4 - vo.getExamScore(); case 5 - vo.getFinalGrade(); case 6 - vo.getSemester(); default - null; }; } }给JTable设置 RowSorter 的代码写在创建表格时JTable table new JTable(scoreTableModel); table.setAutoCreateRowSorter(true); // 点击列头排序 TableRowSorterScoreTableModel sorter new TableRowSorter(scoreTableModel); table.setRowSorter(sorter); // 表格右上角加一个筛选输入框 txtFilter.getDocument().addDocumentListener(new DocumentListener() { Override public void insertUpdate(DocumentEvent e) { filter(); } Override public void removeUpdate(DocumentEvent e) { filter(); } Override public void changedUpdate(DocumentEvent e) { filter(); } private void filter() { String keyword txtFilter.getText().trim(); sorter.setRowFilter(RowFilter.regexFilter((?i) Pattern.quote(keyword), 1)); // 只过滤课程名称列 } });这段代码的核心价值在于RowSorter和RowFilter是作用于视图层的模型层的行索引不会变。当你在表格里点“删除选课”时不能直接用table.getSelectedRow()拿到行号就去删数据因为排序和过滤之后视图行号不等于模型行号。正确写法是table.convertRowIndexToModel(table.getSelectedRow())再用转换后的行号去模型里取ScoreVO。这个细节超过一半的课程设计都没处理答辩时主动说出来老师会立刻意识到你清楚 Swing 的 MVC 设计。3.3 主窗体的菜单栏、工具栏与卡片式布局切换登录之后进入主窗体通常会提供“选课管理”“成绩查看”“学生管理”仅管理员等几个功能模块。这里我倾向用CardLayout做主布局左侧放一个JList或JTree做导航右侧卡片切换。原因很简单JTabbedPane会在启动时把所有页面一次性加载如果某个页面里的表格要连数据库初始化数据那么整个窗体打开时会卡住而CardLayout可以做到“点击哪个模块才加载哪个模块的数据”也叫懒加载体验好得多。public class MainFrame extends JFrame { private CardLayout cardLayout new CardLayout(); private JPanel cardPanel new JPanel(cardLayout); public MainFrame(User currentUser) { // 设置窗口基本属性 setTitle(学生选课及成绩管理系统 - 欢迎 currentUser.getStudentName()); setSize(1000, 650); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); // 左侧导航 JListString navList new JList(new String[]{选课中心, 我的成绩, 学生管理}); navList.setPreferredSize(new Dimension(180, 0)); // 右侧卡片 cardPanel.add(new CourseSelectPanel(currentUser), 选课中心); cardPanel.add(new ScoreQueryPanel(currentUser), 我的成绩); cardPanel.add(new StudentManagePanel(), 学生管理); // 导航点击切换卡片 navList.addListSelectionListener(e - { if (!e.getValueIsAdjusting()) { cardLayout.show(cardPanel, navList.getSelectedValue()); } }); setLayout(new BorderLayout()); add(navList, BorderLayout.WEST); add(cardPanel, BorderLayout.CENTER); JMenuBar menuBar createMenuBar(); setJMenuBar(menuBar); } }JList的监听器里那个getValueIsAdjusting()判断很重要。当鼠标拖选JList的多行时ListSelectionListener会被触发多次getValueIsAdjusting()会在“鼠标按下拖动”阶段返回 true弹起时才返回 false。没有这个判断一次点击可能会连续触发两次卡片切换界面出现闪烁。CardLayout.show()的第二个参数要和add()方法给的字符串键完全一致这个键就是个普通字符串拼错不会编译报错只会运行时无响应建议把模块名称抽成静态常量。4. 选课与成绩录入的核心业务事务边界和并发控制4.1 学生选课功能要做对哪几步预校验、插入、回滚选课接口的核心逻辑看起来只有一条 INSERT但真实业务场景里至少要回答三个问题这门课满了吗这个学生已经选过同一门课了吗选完课之后需要同步更新哪些信息假设课程表里有selected_count字段记录目前已选人数那一次选课操作就应该在一个事务里完成两步第一步用SELECT ... FOR UPDATE锁定课程行第二步检查selected_count max_students后插入选课记录并更新计数。为什么用SELECT FOR UPDATE而不是先普通查询再插入因为两个学生同时选同一门剩余名额只有 1 的课时普通查询在两个连接里都会读到“余 1 名额”然后两个插入都成功课程就超员了。FOR UPDATE把行锁住后第二个事务必须等第一个提交或回滚才能读取最新的人数从而被正确拒绝。这是数据库并发控制的基础姿势值得在课程设计报告中专门写一段。public boolean selectCourse(String studentId, int courseId) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交开启事务 // 1. 锁定课程行防止并发下超员 String lockSql SELECT course_name, max_students FROM t_course WHERE course_id ? FOR UPDATE; try (PreparedStatement ps conn.prepareStatement(lockSql)) { ps.setInt(1, courseId); try (ResultSet rs ps.executeQuery()) { if (!rs.next()) { throw new BusinessException(课程不存在); } // 注意FOR UPDATE的锁要等到事务结束才释放 } } // 2. 检查是否已选过 String countSql SELECT COUNT(*) FROM t_student_course WHERE student_id ? AND course_id ?; try (PreparedStatement ps conn.prepareStatement(countSql)) { ps.setString(1, studentId); ps.setInt(2, courseId); try (ResultSet rs ps.executeQuery()) { if (rs.next() rs.getInt(1) 0) { throw new BusinessException(你已经选过这门课程了); } } } // 3. 插入选课记录 String insertSql INSERT INTO t_student_course (student_id, course_id) VALUES (?, ?); try (PreparedStatement ps conn.prepareStatement(insertSql)) { ps.setString(1, studentId); ps.setInt(2, courseId); ps.executeUpdate(); } conn.commit(); // 全部成功提交事务 return true; } catch (BusinessException e) { rollbackQuietly(conn); throw e; } catch (SQLException e) { rollbackQuietly(conn); throw new BusinessException(选课失败数据库异常 e.getMessage()); } finally { closeQuietly(conn); } }setAutoCommit(false)是事务的开始。只要把这个开关关掉后续所有 SQL 都处于同一事务里直到commit()或rollback()。如果你不在 finally 里恢复setAutoCommit(true)连接还回连接池后可能残留事务状态。课程设计里一般用完直接conn.close()问题不大但如果后面接上连接池这就是一个隐坑。另外要注意FOR UPDATE锁定的范围是“扫过的索引区间”如果你在WHERE条件里用主键course_idMySQL 的 InnoDB 引擎会锁定这一行如果WHERE条件无法命中索引就可能锁整个表。所以在课程表上建索引是必须的主键就是索引这个语句没问题。4.2 成绩录入的两种模式按课程批量录入与按学生单人录入成绩管理模块通常由管理员操作常见的交互是“选择一门课程 → 表格里列出所有选课学生 → 每行填平时分和期末分 → 点击保存”。这种批量录入的性能关键在于使用PreparedStatement的addBatch()和executeBatch()而不是在 for 循环里执行几千次单条 INSERT。public void saveScores(ListScoreItem items, int courseId) { String sql UPDATE t_student_course SET regular_score ?, exam_score ? WHERE student_id ? AND course_id ?; Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); try (PreparedStatement ps conn.prepareStatement(sql)) { for (ScoreItem item : items) { ps.setDouble(1, item.getRegularScore()); ps.setDouble(2, item.getExamScore()); ps.setString(3, item.getStudentId()); ps.setInt(4, courseId); ps.addBatch(); // 先攒着 } int[] results ps.executeBatch(); // 一次性发给MySQL conn.commit(); } catch (SQLException e) { conn.rollback(); throw new RuntimeException(成绩保存失败请检查分数值是否合法, e); } } catch (SQLException e) { throw new RuntimeException(数据库连接异常, e); } finally { closeQuietly(conn); } }executeBatch()和单条循环执行的区别在于减少了客户端与 MySQL 服务器的往返次数。一次网络往返能处理一条 SQL 的话100 条数据就要 100 个 RTT批量提交时 MySQL 支持一条协议包带多条语句整体耗时往往能下降到原来的十分之一以下。成绩表的数据量在课程设计场景里不会太大但养成批量处理的习惯没坏处。提示分数录入界面需要在前端做输入校验规定平时成绩占比和总评计算方式。如果regular_score和exam_score填了超出 0~100 的数字这里直接报 SQLException 回滚不会造成部分成功部分失败。这是事务带来的最直观好处。4.3 我要不要用存储过程或触发器来做“退课人数回退”有些同学的课程设计报告里会写“存储过程实现选课”作为答辩加分项。但在 MySQL 里存储过程和触发器在互联网开发中已经越来越边缘化因为逻辑散布在数据库中会让排错和版本管理变得困难。对于这个课程设计我不推荐为了炫技而用触发器去维护selected_count——选课人数完全可以在SELECT里用COUNT(*)实时算出来而不是维护一个冗余字段。如果确实需要冗余字段来展示课程列表上的“已选人数”那这个字段的更新必须和选课/退课放在同一个事务里。比如退课的 SQL 就应该是public void dropCourse(String studentId, int courseId) { String deleteSql DELETE FROM t_student_course WHERE student_id ? AND course_id ?; String updateSql UPDATE t_course SET selected_count selected_count - 1 WHERE course_id ?; // 同一个Connection里先执行delete再执行update最后commit }这比写触发器更直观也比在 Java 里先在SELECT里拿到selected_count再在 Java 里减一后UPDATE更安全——后者的读取和更新之间存在时间差并发下会造成计数错误。把数值变化的逻辑放进 SQL 的表达式中去让数据库行锁来保证原子性是这一段的落点。触发器如果能被你用“事务里显式更新更清晰、更可控”这样的理由解释清楚反而比用了触发器更有说服力。5. 课堂之外也要能解释的选型细节Swing 与 MySQL 组合的边界和排错清单5.1 Swing 的单线程模型什么时候调用 SwingUtilities.invokeLaterSwing 的所有组件都不是线程安全的也就是说任何修改界面状态的操作比如setText()、setModel()、repaint()都必须在事件分发线程EDT中执行。那为什么很多时候直接在ActionListener里写代码也不出问题因为ActionListener本身就是由 EDT 回调的它天然运行在 EDT 上。真正的坑出现在你启动一个新线程去干耗时活的时候。比如第 4 章的批量导入 1000 条成绩如果直接写在主线程里界面上会看到按钮一直按不下去任务管理器里 Java 进程却在飙升。正确模型是把耗时操作丢到后台线程等结果准备好后再用SwingUtilities.invokeLater(() - ...)回到 EDT 刷新界面。btnImport.addActionListener(e - { btnImport.setEnabled(false); // 禁用按钮防止重复提交 new Thread(() - { try { // 模拟耗时导入逻辑实际代码调用Service层 boolean success scoreService.batchImportFromFile(filePath); SwingUtilities.invokeLater(() - { if (success) { refreshTable(); statusLabel.setText(导入完成); } else { statusLabel.setText(导入失败请检查文件格式); } btnImport.setEnabled(true); }); } catch (Exception ex) { SwingUtilities.invokeLater(() - { JOptionPane.showMessageDialog(MainFrame.this, ex.getMessage()); btnImport.setEnabled(true); }); } }, Import-Thread).start(); });这段代码里被invokeLater包裹的代码块会在 EDT 上执行它前面的耗时代码在普通线程里跑线程名设为Import-Thread方便在jstack或 Java VisualVM 中排查问题。后台线程里绝不能直接调用refreshTable()否则本质上属于数据竞争轻则界面刷新闪烁重则组件状态错乱而且这种错误是随机复现的答辩前很难捉住。5.2 连接池要不要上HikariCP 与 DBUtil 的平替方案课程设计阶段完全可以用自写的 DBUtil 管理连接但如果报告里想体现工程化意识建议引入 HikariCP 连接池。它的好处是连接复用、自动失效检测、可配置最大连接数和超时时间。对一个选课系统来说连接池不会让单次查询变快但它能统一管理连接的生命周期避免每次开窗都DriverManager.getConnection()带来的握手开销。引入 HikariCP 只需要一个配置类import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; public class DataSourceManager { private static final HikariDataSource dataSource; static { HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/course_system?useSSLfalseserverTimezoneAsia/Shanghai); config.setUsername(root); config.setPassword(your_password); config.setMaximumPoolSize(10); config.setMinimumIdle(2); config.setConnectionTimeout(3000); config.setPoolName(CourseSystemCP); dataSource new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }注意使用连接池后conn.close()不再真正关闭物理连接而是把连接归还池中。所以DBUtil.close(conn, ps, rs)的写法和裸 JDBC 时一样但语义变了。最坑的一点是如果代码里conn.setAutoCommit(false)后忘了在 finally 里setAutoCommit(true)或rollback()连接归还后事务状态是脏的下一个拿到这条连接的线程会在一个未提交的事务里操作产生非常隐蔽的数据不一致。对课程设计而言如果没有明确的高频访问需求不上连接池也可以但上了就一定要在关闭前清理事务状态。5.3 成绩统计与报表单个 SQL 输出班级平均分、及格率、最高分成绩管理模块的最后一个高频需求是统计。用 Java 查出所有成绩后在内存里算也可以但用 MySQL 聚合函数更简单也更能在答辩时展示 SQL 基本功。下面这条语句可以按课程统计平均分和及格率SELECT c.course_id, c.course_name, COUNT(*) AS total_students, ROUND(AVG(sc.final_grade), 2) AS avg_grade, SUM(CASE WHEN sc.final_grade 60 THEN 1 ELSE 0 END) / COUNT(*) * 100 AS pass_rate, MAX(sc.final_grade) AS max_grade, MIN(sc.final_grade) AS min_grade FROM t_course c LEFT JOIN t_student_course sc ON c.course_id sc.course_id WHERE c.semester ? GROUP BY c.course_id, c.course_name ORDER BY avg_grade DESC;AVG(sc.final_grade)会自动忽略 NULL所以学生还没录成绩时不会把分母算错。SUM(CASE WHEN ...)是标准 SQL 行转列技巧统计及格人数时用final_grade 60判断这里如果使用final_grade IS NOT NULL AND final_grade 60会更严谨因为 NULL 和 0 不是一回事。从 Java 侧执行这条语句时WHERE c.semester ?用PreparedStatement传入学期参数返回的ResultSet可以直接封装成报表模型显示在JTable中列的类型此时已经是计算后的BigDecimal或Long要记得在 TableModel 的getColumnClass中单独映射这两个类型。5.4 课程设计答辩中必问的两个坑时区导致时间差八小时中文乱码最后收在最常见的两个环境问题排查上。第一个是时区问题如果 JDBC URL 里没有指定serverTimezoneMySQL 8.x 新版驱动连接成功后CURRENT_TIMESTAMP写入的选课时间值到 Java 侧rs.getTimestamp()时往往会比预期的当地时间早 8 个小时。这是因为驱动默认把服务器时区当作 UTC而 Java 进程的本地时区是东八区。解决方式很直接URL 上加上serverTimezoneAsia/Shanghai。第二个是中文乱码。如果建表时没有指定utf8mb4字符集或者 JDBC URL 里漏了characterEncodingutf8那么从 Swing 输入框写入数据库的课程名称、学生姓名会出现“???”。这句话加在 URL 上只是解决客户端到服务器的编码协商表级别的字符集需要回到CREATE TABLE语句的ENGINEInnoDB DEFAULT CHARSETutf8mb4来兜底。老项目如果是 5.5 版本创建的库字段默认可能是latin1这类场景需要先用ALTER TABLE t_course CONVERT TO CHARACTER SET utf8mb4修复再测试写入和读取。课程设计里排查乱码时坚持一个原则从数据库字段、连接参数、客户端编码三段同时排查必要时在 Java 侧手动指定rs.getBytes()再重新按 UTF-8 构造字符串作为临时验证手段。本文还有配套的精品资源点击获取
RELATED

相关推荐

Telegraf Lustre2 输入插件实战指南:采集 Lustre 并行文件系统的 OST/MDS 运行指标

Telegraf Lustre2 输入插件实战指南:采集 Lustre 并行文件系统的 OST/MDS 运行指标

Telegraf Lustre2 输入插件实战指南:采集 Lustre 并行文件系统的 OST/MDS 运行指标 【免费下载链接】telegraf Agent for collecting, processing, aggregating, and writing metrics, logs, and other arbitrary data. 项目地址: https://gitcode.com/GitHub_Tre…

📅 2026/9/14 7:10:44
openpi 最小推理客户端实战指南:使用 examples/simple_client 连接远端策略服务器并测速

openpi 最小推理客户端实战指南:使用 examples/simple_client 连接远端策略服务器并测速

openpi 最小推理客户端实战指南:使用 examples/simple_client 连接远端策略服务器并测速 【免费下载链接】openpi 项目地址: https://gitcode.com/GitHub_Trending/op/openpi 导读 examples/simple_client 是 openpi 仓库中一个依赖极简、开箱即用的最小客…

📅 2026/9/14 7:10:44
vue-neo4j实战:从Cypher查询到D3力导向图的可视化

vue-neo4j实战:从Cypher查询到D3力导向图的可视化

简介:vue-neo4j是一套基于Vue与D3.js对Neo4j图数据库进行可视化展示的前端源码工程,面向需要在前端实现图数据交互式可视化的开发者,帮助解决复杂关系数据在页面中直观呈现的问题。压缩包共91个文件,以JavaScript、Vue组件、Coffe…

📅 2026/9/14 7:10:44
MORE NEWS

更多资讯

📰

《道德经》第二章的辩证智慧与现代应用

1. 解读《道德经》第二章的核心思想《道德经》第二章开篇便道出"天下皆知美之为美,斯恶已;皆知善之为善,斯不善已"的辩证观点。这句话揭示了老子思想中最为核心的相对论哲学——世间万物都是相互依存、对立统一的。当人们定义了&qu…

📰

C++ STL集合算法全解析:有序区间上的并集、交集与差集

每天一个STL知识点&#xff0c;今天轮到集合算法。说到“集合算法”&#xff0c;很多人第一反应是 std::set 的几个成员函数&#xff0c;其实STL里真正的集合运算是一组泛型算法&#xff0c;它们并不属于 set 类&#xff0c;而是藏在 <algorithm> 头文件里&#xf…

📰

SpringBoot+Vue医院食堂订餐系统开发实践

1. 项目概述医院食堂订餐系统是一个基于SpringBootVue技术栈的Web应用&#xff0c;旨在解决医院内部餐饮服务的数字化管理问题。这个系统将传统的线下订餐流程转移到线上&#xff0c;为医护人员、患者及家属提供便捷的订餐服务&#xff0c;同时优化医院食堂的后台管理流程。在医…

📰

代币流通率0.53%真相:穿透销毁幻觉的链上经济审计

1. 项目概述&#xff1a;一场被严重误读的“销毁”实验&#xff0c;本质是链上经济模型的压力测试“我烧了22亿Token&#xff0c;真正‘生成’出来的只有0.53%”——这句话乍看像极了某位加密项目方在社交媒体上发布的悲壮宣言&#xff0c;带着强烈的戏剧张力和数据冲击感。但如…

📰

宠物猫交易网站模板改造:HTML5+jQuery实现静态页面到询价闭环

简介&#xff1a;这是一份面向宠物猫交易场景的HTML5 PC端静态网站模板&#xff0c;适合个人卖家、小型宠物店或初创团队快速搭建线上展示与咨询平台&#xff0c;无需编程经验即可上手。压缩包共41个文件&#xff0c;包括6个HTML页面、3个CSS样式表、4个JS交互脚本以及20余张JP…

📰

身份证翻译件去哪里弄?手把手教你3步搞定盖章翻译件

很多人办理签证、留学、移民的时候都需要身份证翻译件&#xff0c;这里提醒大家&#xff0c;单纯依靠翻译软件自己整理出来的译文大多没法直接使用&#xff0c;不少涉外机构办理业务时&#xff0c;一般会要求翻译文件带有翻译专用章、译员签名以及对应的翻译声明。大家可以试试…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬