简介:这是一份基于 Java Swing 与 MySQL 实现的学生成绩管理系统源码包,面向 Java 桌面应用初学者及需要课程设计、毕业设计参考的开发者。系统涵盖成绩信息管理、课程管理、学生信息管理、登录与密码修改等核心模块,并通过 JDBC 完成对 MySQL 的增删改查,能够帮助读者掌握界面布局、事件监听与数据库交互的整体开发流程。压缩包为 zip 格式,共 145 个文件,包括 27 个 java 源文件、30 个 form 界面文件、80 个 class 编译文件、4 个 jar 依赖包及 1 个 sql 数据库脚本,资源整体约 746KB,体积小巧但结构完整。已有 5365 人学习/下载,适合用来快速搭建练习环境。通过阅读源码和运行调试,读者可以获得完整的 Eclipse 工程、数据库建表与初始化脚本、Swing 界面设计范式,并在此基础上扩展教师管理、班级管理等新模块,加深对 Java 桌面应用与 MySQL 持久化存储的理解。
1. 用 Swing 写成绩管理系统,为什么还值得认真做一遍
很多经历过实训项目的人看到「java swing 学生成绩管理系统」第一反应是「太老了」。在 Spring Boot + Vue 占主导的今天,Swing 确实不是企业级首选,但它有一个被低估的价值:用最少的框架依赖,把 JDBC、事件监听、JTable 数据绑定、事务控制这些 Java 基础功完整走一遍。学生成绩管理系统的业务足够清楚——学生表、课程表、成绩表,增删改查加统计——非常适合用来验证你对 Java SE 的掌握程度。这篇文章要解决的正是这样一件事:在 Eclipse 下从建库到界面,一步步把系统跑起来,并告诉你成绩表为什么不能简单设计成一行一个学生加一个总分字段,以及在用 MySQL 8.x 时 JDBC 驱动版本不匹配会踩到什么坑。适合正在做课程设计、准备 Java 面试基础题、或者想补桌面应用开发能力的读者。
2. 建库建表前的实体设计:成绩怎么算才是关键
2.1 学生、课程、成绩三张表的 ER 关系
学生成绩管理系统听起来简单,但「成绩」这个业务对象很容易设计错。常见错误是直接在学生表里加chinese_score、math_score、english_score三个字段,这样做当课程数量变动时就会面临改表结构的尴尬,而且无法存储多次考试的成绩。
正确做法是拆成三张核心表:
| 表名 | 用途 | 关键字段 |
|---|---|---|
| student | 学生档案 | stu_id, stu_name, stu_class, stu_phone |
| course | 课程档案 | course_id, course_name, course_credit |
| score | 成绩事实表 | stu_id, course_id, score_value, exam_date |
学生和课程是多对多关系:一个学生选多门课,一门课被多个学生选。成绩表就是这两张表的关联实体,每一行记录「某个学生在某门课上的某次考试成绩」。stu_id和course_id在成绩表里组成联合主键,同时分别作为外键引用学生表和课程表。用 ER 图表示就是学生表中导出一个联系到成绩表,成绩表再连接到课程表。
2.2 MySQL 建库建表 DDL:联合主键和外键约束
在 MySQL 8.0 版本下,我一般会在命令行和 MySQL Workbench 里分别确认一次执行结果。下面这段 DDL 可以直接复制进 MySQL Workbench 或 mysql 命令行工具:
CREATE DATABASE IF NOT EXISTS student_grade DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE student_grade; CREATE TABLE student ( stu_id VARCHAR(20) PRIMARY KEY, stu_name VARCHAR(50) NOT NULL, stu_class VARCHAR(50), stu_phone VARCHAR(20) ) ENGINE=InnoDB; CREATE TABLE course ( course_id VARCHAR(20) PRIMARY KEY, course_name VARCHAR(50) NOT NULL, course_credit DECIMAL(3,1) ) ENGINE=InnoDB; CREATE TABLE score ( stu_id VARCHAR(20) NOT NULL, course_id VARCHAR(20) NOT NULL, score_value DECIMAL(5,2), exam_date DATE, PRIMARY KEY (stu_id, course_id), CONSTRAINT fk_score_student FOREIGN KEY (stu_id) REFERENCES student(stu_id) ON UPDATE CASCADE ON DELETE CASCADE, CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES course(course_id) ON UPDATE CASCADE ) ENGINE=InnoDB;建表时把utf8mb4写进库的默认字符集,是避免中文乱码的第一步。ENGINE=InnoDB是为了让外键约束和后续的事务操作生效。成绩表的联合主键防止了同一个学生同一门课被插入两条互相矛盾的记录。外键上的ON UPDATE CASCADE意味着如果学号因转专业发生变更,成绩表会自动同步,不需要手动更新。
2.3 为什么不推荐把平均分存进成绩表
成绩表里只存原始分,平均分、总分、排名全部通过查询计算。原因有二:第一,存了总分就会产生「总分与明细不一致」的数据一致性问题;第二,当需要按学期筛选成绩时,冗余字段的值维护会很麻烦。查询时用聚合函数实时算即可:
SELECT s.stu_id, s.stu_name, ROUND(AVG(sc.score_value), 2) AS avg_score, COUNT(sc.course_id) AS course_count FROM student s LEFT JOIN score sc ON s.stu_id = sc.stu_id GROUP BY s.stu_id, s.stu_name ORDER BY avg_score DESC;这里用LEFT JOIN是因为有的学生可能还没有成绩记录,内连接会把这类学生直接过滤掉。ROUND(..., 2)控制平均分的小数位数。业务系统里如果需要展示「张三的平均分是 88.50」,这条 SQL 已经足够,不必额外维护一张统计表。
3. Eclipse 环境准备与 JDBC 连接:驱动配好代码才稳
3.1 mysql-connector-j 的 jar 包导入与常见启动异常
在 Eclipse 里新建 Java 项目后,第一件事不是写代码,而是把 MySQL 官方驱动放进classpath。MySQL 8.x 对应的是mysql-connector-j,早期版本叫mysql-connector-java。下载时注意选择平台无关的 jar 包,然后在 Eclipse 项目上右键Build Path -> Add External Archives选中 jar。
这个环节最容易撞上的报错是满屏的ClassNotFoundException: com.mysql.cj.jdbc.Driver或No suitable driver found for jdbc:mysql://localhost:3306/student_grade。前者说明驱动 jar 没有真正加入构建路径,后者通常是因为连接串写得有问题或者驱动类名写错。还有一种出现在项目运行期的异常是eclipse 找不到或无法加载主类,原因是编译输出目录损坏或者配置了错误的 JRE,清理一次项目再重新构建即可。
3.2 封装 DBHelper 工具类:注册驱动、获取连接、关闭资源
连接数据库的代码如果散落在每一个业务方法里,项目会变得很难维护。常见做法是先封装一个DBHelper类,把驱动注册、连接获取、资源释放集中在一起。下面这套代码结构在课程设计和中小型桌面应用里都很通用:
package util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class DBHelper { private static final String DRIVER = "com.mysql.cj.jdbc.Driver"; private static final String URL = "jdbc:mysql://localhost:3306/student_grade?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(ResultSet rs, PreparedStatement ps, Connection conn) { try { if (rs != null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (ps != null) ps.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }Class.forName(DRIVER)在静态块里执行,保证类加载时驱动只注册一次。连接串里的serverTimezone=Asia/Shanghai是 MySQL 8.x 必须配的参数,不配会报CommunicationsException或时区错误。characterEncoding=utf8配合数据库的utf8mb4,是为了让中文在 JDBC 传输层不丢失。close方法里三个参数可以为 null,调用DBHelper.close(rs, ps, conn)时不会因为某个参数为空而失败。
3.3 第一次连库必踩的驱动版本参数对照
老项目里常见的驱动类是com.mysql.jdbc.Driver,MySQL 5.x 时代这么写没问题,但 6.0 以后官方把驱动类迁移到了com.mysql.cj.jdbc.Driver。已经用旧驱动的项目在连接 MySQL 8 时会出现警告或直接拒绝连接。
| MySQL 版本 | JDBC 驱动类 | 推荐的连接参数 |
|---|---|---|
| MySQL 5.x | com.mysql.jdbc.Driver | 无需 serverTimezone |
| MySQL 8.x | com.mysql.cj.jdbc.Driver | serverTimezone 必填 |
| MySQL 8.x 新装 | mysql-connector-j 8.0.33 | 同时建议 useSSL=false |
连接成功后可以先用一个最简单的SELECT 1验证链路:
Connection conn = DBHelper.getConnection(); PreparedStatement ps = conn.prepareStatement("SELECT 1"); ResultSet rs = ps.executeQuery(); System.out.println(rs.next() ? "connect ok" : "connect fail"); DBHelper.close(rs, ps, conn);如果这段在main方法里能打印connect ok,说明环境问题已经全部排除。后续所有 Swing 界面代码都可以放心地基于这套连接工具开发。
4. Swing 界面从登录到成绩录入:JTable 绑定查询结果
4.1 登录窗体与密码校验的常规写法
学生成绩管理系统通常需要一个入口登录。Swing 里最直白的做法是JFrame加两个JLabel、两个JTextField/JPasswordField、一个JButton。点击登录按钮后用ActionListener触发查询,从数据库里SELECT * FROM user WHERE username=? AND password=?。即使只有「管理员/admin」这一个固定用户,也建议存在 MySQL 的用户表里,而非硬编码在 Java 代码中。
JButton loginBtn = new JButton("登录"); loginBtn.addActionListener(e -> { String username = userField.getText().trim(); String password = new String(pwdField.getPassword()); String sql = "SELECT role FROM user WHERE username = ? AND password = ?"; try (Connection conn = DBHelper.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { JOptionPane.showMessageDialog(frame, "登录成功,角色:" + rs.getString("role")); } else { JOptionPane.showMessageDialog(frame, "用户名或密码错误", "错误", JOptionPane.ERROR_MESSAGE); } } } catch (Exception ex) { ex.printStackTrace(); } });userField.getText().trim()会去掉首尾空格,防止用户误输入空格导致查询失败。JPasswordField.getPassword()返回的是字符数组而不是字符串,直接用new String(...)转成字符串,是教学项目里简单可接受的做法,生产系统里还应当加密后再比对。 这里用try-with-resources自动关闭连接和 PreparedStatement。查询结果里如果只有一行,说明账号密码匹配成功。
4.2 JTable + DefaultTableModel 动态刷新成绩数据
主界面一般由一个JTable和一组操作按钮组成。JTable的数据模型推荐使用DefaultTableModel,因为它允许运行时自由增删行。下面的方法封装了「从成绩表联查学生姓名和课程名并填充表格」的逻辑:
import javax.swing.table.DefaultTableModel; public void loadGradeData(DefaultTableModel model) { String sql = "SELECT sc.stu_id, s.stu_name, c.course_name, " + "sc.score_value, sc.exam_date " + "FROM score sc " + "JOIN student s ON sc.stu_id = s.stu_id " + "JOIN course c ON sc.course_id = c.course_id"; try (Connection conn = DBHelper.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { model.setRowCount(0); while (rs.next()) { model.addRow(new Object[]{ rs.getString("stu_id"), rs.getString("stu_name"), rs.getString("course_name"), rs.getBigDecimal("score_value"), rs.getDate("exam_date") }); } } catch (Exception e) { e.printStackTrace(); } }model.setRowCount(0)的作用是先把旧数据清空,再逐条添加新行,这样点击「刷新」按钮时界面不会出现残留。JOIN让成绩表里的学生姓名和课程名称不再需要前端的二次查询,一次 SQL 就能带出所有展示字段。表格初始化时该这样绑定:
String[] columns = {"学号", "姓名", "课程", "成绩", "考试日期"}; DefaultTableModel model = new DefaultTableModel(columns, 0); JTable table = new JTable(model); JScrollPane scrollPane = new JScrollPane(table);DefaultTableModel(columns, 0)表示有 5 列但初始行为 0,后续由addRow动态填充。JTable必须放进JScrollPane,否则表头不会显示,列宽也会异常。
4.3 按学号或姓名模糊查询:PreparedStatement 写法与 % 拼接
在搜索框里输入关键字,点击查询按钮,系统按学号或者姓名对学生成绩做模糊匹配。SQL 中模糊查询使用LIKE,配合%通配符实现包含匹配。注意%是放在参数绑定之前拼进 SQL,还是通过setString传进去——正确做法是拼在传入值两侧:
String keyword = searchField.getText().trim(); String sql = "SELECT s.stu_id, s.stu_name, c.course_name, sc.score_value " + "FROM score sc " + "JOIN student s ON sc.stu_id = s.stu_id " + "JOIN course c ON sc.course_id = c.course_id " + "WHERE s.stu_name LIKE ? OR sc.stu_id LIKE ?"; try (Connection conn = DBHelper.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + keyword + "%"); ps.setString(2, "%" + keyword + "%"); try (ResultSet rs = ps.executeQuery()) { model.setRowCount(0); while (rs.next()) { model.addRow(new Object[]{ rs.getString("stu_id"), rs.getString("stu_name"), rs.getString("course_name"), rs.getBigDecimal("score_value") }); } } } catch (Exception e) { e.printStackTrace(); }传入%+ 关键字 +%后,student_grade库里的LIKE ?会被替换成LIKE '%张%'再执行。 直接用字符串拼接 SQL 的写法很容易被注入攻击,比如在文本框里输入' OR '1'='1就能绕过条件,而PreparedStatement的参数占位符机制从底层把值作为字面量处理,不带任何解释执行。搜索框触发时可以在单击查询按钮的事件里调用以上逻辑。
4.4 成绩录入与事务处理:update 和 delete 的边界
录入或修改成绩时,界面一般弹出一个JDialog,内含课程下拉框、学号文本框、成绩文本框和保存按钮。保存按钮背后的 SQL 分两种情况:成绩记录存在则更新,不存在则插入。如果只是课程设计,可以直接先查再判断;稳妥一点的做法是利用联合主键做INSERT ... ON DUPLICATE KEY UPDATE:
String sql = "INSERT INTO score (stu_id, course_id, score_value, exam_date) " + "VALUES (?, ?, ?, ?) " + "ON DUPLICATE KEY UPDATE score_value = VALUES(score_value), exam_date = VALUES(exam_date)"; try (Connection conn = DBHelper.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, stuId); ps.setString(2, courseId); ps.setBigDecimal(3, new BigDecimal(scoreText)); ps.setDate(4, java.sql.Date.valueOf(examDate)); int rows = ps.executeUpdate(); if (rows > 0) { JOptionPane.showMessageDialog(frame, "保存成功"); } } catch (SQLException e) { if (e.getErrorCode() == 1452) { JOptionPane.showMessageDialog(frame, "学生或课程不存在,请先核对基础档案"); } }ON DUPLICATE KEY UPDATE在联合主键命中时会自动转成更新,避免了「先查询再决定新增或修改」的两步逻辑。executeUpdate的返回值为 1 表示受影响行数为 1。错误码 1452 是外键约束失败,代表录入时填写的学号在学生表中找不到,这个提示对使用者比较友好。成绩录入完成后调用loadGradeData(model)刷新表格,保证界面所见即数据库所有。
5. 进阶打磨:导出 CSV 和成绩统计的边界验证
系统跑通基本功能后,真正体现工程意识的是细节处理。先给成绩列表加一个「导出 CSV」功能,用JFileChooser让用户选择保存路径,把JTable当前的数据写入文本文件。这里要注意 CSV 对中文逗号的处理——如果字段本身包含逗号,需要整体对齐加双引号;学生姓名和课程名一般不含逗号,但成绩字段可能出现79.50这样带点的小数,直接写不会被拆分。核心代码如下:
File file = chooser.getSelectedFile(); try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter( new FileOutputStream(file), StandardCharsets.UTF_8))) { for (int i = 0; i < table.getColumnCount(); i++) { writer.write(table.getColumnName(i) + ","); } writer.newLine(); for (int i = 0; i < table.getRowCount(); i++) { StringBuilder sb = new StringBuilder(); for (int j = 0; j < table.getColumnCount(); j++) { Object val = table.getValueAt(i, j); sb.append(val == null ? "" : val.toString()).append(","); } writer.write(sb.toString()); writer.newLine(); } JOptionPane.showMessageDialog(frame, "导出成功"); }用StandardCharsets.UTF_8而不是平台默认编码,可以避免 Excel 打开 CSV 时出现「锟斤拷」式乱码。 另外值得把「优秀率」加进统计逻辑:SELECT SUM(CASE WHEN score_value >= 90 THEN 1 ELSE 0 END)/COUNT(*) FROM score WHERE course_id = ?,用它判断一个班的单科成绩分布。
验证系统是否可靠,可以在录入界面连输 10 条成绩后去 MySQL Workbench 里核对数据行数,或用SELECT COUNT(*) FROM score检查。最常用于自检的一条 SQL 是EXPLAIN SELECT ... WHERE stu_id='2024001',确认是否用到主键索引。像名单一样把缺考成绩设为NULL而不是 0,对平均值计算的影响完全不同。最后在 Eclipse 里File -> Export -> Runnable JAR file,选择包含驱动 jar 的启动配置,生成可运行的 jar 包后即可在命令行直接执行,但记得运行机器上也装了相同版本的 MySQL 并有对应数据库。
本文还有配套的精品资源,点击获取