简介:这是一套基于Java Swing与MySQL数据库实现的学生成绩管理系统完整源码包,主要面向正在准备Java期末大作业、课程设计或需要实战练手的开发者。项目通过Swing组件构建图形化操作界面,结合MySQL完成学生、教师、课程、成绩等信息的录入、修改、删除与统计查询,难度定位适中,且源码已通过评审获得98分,很适合作为计算机相关专业课程设计的参考范例。压缩包内共包含18个文件,核心包括7个Java源文件、4个SQL数据库脚本、PDF设计报告、数据库驱动jar包及说明文档等,整体体积仅3.32MB,非常轻量。其中Java源码覆盖登录、主菜单、表格展示、查询结果返回等关键模块;SQL脚本内含学生、教师、课程、成绩四张数据表的建表语句;PDF报告则可用于撰写课程设计说明书时对照参考;随附的JDBC驱动jar使项目更便于直接导入运行。目前已有83人学习下载。资源还提供了运行效果截图,便于读者在本地快速验证功能。通过把界面事件处理、业务逻辑与数据库访问串联起来,能帮助初学者系统理解Swing桌面开发与MySQL交互的完整流程,是一份高性价比的Java课程设计参考资料。
1. 用Java Swing做学生成绩管理,为什么依然是课程设计的高分路线
在Java课程设计里,学生成绩管理系统是老题目,但也是容易拿高分的题目。Swing界面虽然看起来不现代,组件体系却非常完整,事件监听、表格渲染、布局管理全都能在几十个类里讲明白。搭配MySQL做数据持久化,正好覆盖 JDBC、SQL 设计、登录鉴权、多角色菜单这些常考点。很多刚开始学Java的人以为换用JavaFX或者Web技术更“高级”,实际上Swing项目更看重逻辑完整性,能够在一次答辩中同时讲清楚事件分发线程和数据库连接,就已经超过大部分期末作业了。
这份源码包包含主类、登陆窗口、学生菜单、教师菜单和四个基础SQL文件,整体难度适中,适合做期末大作业或课程设计。如果对手里的现成源码包还不熟悉,先理清文件职责和表结构,比一头扎进代码更有效。下面从表设计、角色分发、驱动配置和辅助类封装四个角度拆解,最后给出提交前需要完成的验证清单,尽量让整套流程可以直接复现。
2. 学生成绩管理的表结构设计与Swing角色分发
2.1 先看student/teacher/course/score四个表的关联
拿到由student.sql、teacher.sql、course.sql、score.sql四个文件组成的源码时,第一件事不是打开Java代码,而是先看表结构。项目里的所有菜单、查询能力,实际上都是从身份关系展开的。常见课程设计版本中,这四张表的字段大致如下。
| 表名 | 核心字段 | 说明 |
|---|---|---|
| student | id, student_no, name, gender, class_name, password | 学生登录账号,一般用学号student_no |
| teacher | id, teacher_no, name, course_id, password | 教师账号,可能关联所教课程 |
| course | id, course_name, credit, teacher_id | 课程,通过teacher_id关联教师 |
| score | id, student_id, course_id, score_value, semester | 成绩表,两个外键分别指向学生和课程 |
CREATE TABLE student ( id INT AUTO_INCREMENT PRIMARY KEY, student_no VARCHAR(20) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, name VARCHAR(50) NOT NULL, gender CHAR(1), class_name VARCHAR(50) ); CREATE TABLE course ( id INT AUTO_INCREMENT PRIMARY KEY, course_name VARCHAR(100) NOT NULL, credit DECIMAL(3,1) DEFAULT 2.0, teacher_id INT ); CREATE TABLE score ( id INT AUTO_INCREMENT PRIMARY KEY, student_id INT NOT NULL, course_id INT NOT NULL, score_value DECIMAL(5,2), semester VARCHAR(20) );建库时建议显式指定字符集,避免后面插入中文姓名时出现乱码。可以在student.sql的开头先执行库创建语句,或者单独在MySQL命令行中执行一次。
CREATE DATABASE IF NOT EXISTS student_db DEFAULT CHARACTER SET utf8mb4; USE student_db;这里有一个容易被忽视的设计点:score表中的student_id和course_id要能对上主表主键。如果课程设计里的成绩表没有外键约束,代码中很容易出现“某个学生查到了别人的成绩”这样的逻辑漏洞。至少要在查询语句中加入WHERE score.student_id = student.id和WHERE course.teacher_id = teacher.id之类的关联条件。
2.2 Login的登录查询逻辑与角色分流
Login.java是整个系统的入口逻辑。任务不复杂:接收用户名和密码,分别到 student 表和 teacher 表中查询,命中后根据角色打开不同菜单。
public void login() { String account = usernameField.getText().trim(); String pwd = new String(passwordField.getPassword()); try (Connection conn = DBUtil.getConnection()) { // 先查学生表 String studentSql = "SELECT id, name FROM student WHERE student_no=? AND password=?"; try (PreparedStatement ps = conn.prepareStatement(studentSql)) { ps.setString(1, account); ps.setString(2, pwd); ResultSet rs = ps.executeQuery(); if (rs.next()) { new StudentMenu(rs.getInt("id"), rs.getString("name")).setVisible(true); dispose(); return; } } // 再查教师表 String teacherSql = "SELECT id, name FROM teacher WHERE teacher_no=? AND password=?"; try (PreparedStatement ps = conn.prepareStatement(teacherSql)) { ps.setString(1, account); ps.setString(2, pwd); ResultSet rs = ps.executeQuery(); if (rs.next()) { new TeacherMenu(rs.getInt("id"), rs.getString("name")).setVisible(true); dispose(); return; } } JOptionPane.showMessageDialog(this, "用户名或密码错误"); } catch (SQLException e) { e.printStackTrace(); } }这段代码的关键是优先使用PreparedStatement参数绑定,而不是用字符串拼接SQL。这样能避免单引号注入,这也是期末答辩时容易被提问的点。另外,rs.next()返回 false 时要继续从方法返回,不能直接跳出后却什么也不执行。登录成功后用dispose()释放登录窗口,防止两个窗口同时存在于任务栏。
在课程设计中,密码明文存储很常见,但可以在报告里注明改进方向,例如用MD5加盐或BCrypt。这些改动不影响现有功能,却能让老师觉得你对安全不是完全无感。
2.3 垂直一体化源码与DAO化重构的取舍
这个项目的SQL大多直接写在菜单类里,没有单独拆分DAO层。好处是代码量少、跟着源码走比较容易;坏处是同一个连接配置可能在Login、StudentMenu、TeacherMenu中重复出现。如果修改了数据库密码,就要逐个类替换,这种体验很差。我一般会先确认源码中是否有DBUtil或类似工具类,没有的话会在本地补一个。
lib/mysql-connector-java-8.0.16.jar src/Main.java src/Login.java src/StudentMenu.java src/TeacherMenu.java src/MyTable.java src/ReturnQueryResult.java src/MyGridBagConstraints.java doc.pdf README.mdMyTable负责表格显示,ReturnQueryResult承担 ResultSet 到界面数据的转化,MyGridBagConstraints是 GridBagLayout 的辅助类。下一章先从数据库运行环境开始,再把启动过程完整走一遍。
3. 从SQL脚本到主界面:驱动引入、数据库初始化和登录窗口启动
3.1 四步完成数据库初始化,避免借错库
直接运行Main.java时,经常第一步就报错,因为MySQL中还没有数据库和表。正确顺序是先建库建表,再插入测试数据。以四个SQL文件命名来看,通常每个文件保存一张表,也可能保存整个工程的备份数据。先在MySQL命令行中执行建库命令更稳妥。
mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS student_db CHARACTER SET utf8mb4;" mysql -u root -p student_db < student.sql mysql -u root -p student_db < teacher.sql mysql -u root -p student_db < course.sql mysql -u root -p student_db < score.sql这里要注意导入顺序:如果score.sql中有外键引用 student 和 course,就必须先导入被依赖的表。如果SQL文件里已经写了USE student_db;,命令行中的student_db可以省略。导入完成后,用SHOW TABLES FROM student_db;确认四个表名和源码中查询语句里的表名一致。我曾经遇到过一个版本把表名改成t_student,导致登录时一直报“找不到表”,这类问题很难通过看代码查出来。
3.2 MySQL 8.0驱动加载,理解驱动类名和连接参数
MySQL 8.0 以后,驱动类名是com.mysql.cj.jdbc.Driver,如果继续使用com.mysql.jdbc.Driver,控制台会输出弃用告警,但不会直接让程序崩溃。课程设计里最好按新驱动类名写,以免老师追问。
Class.forName("com.mysql.cj.jdbc.Driver"); String url = "jdbc:mysql://localhost:3306/student_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; String user = "root"; String password = "123456"; Connection conn = DriverManager.getConnection(url, user, password);连接串中useSSL=false避免MySQL 8默认请求SSL而出现警告;serverTimezone=Asia/Shanghai解决 “The server time zone value '�й���ʱ��' is unrecognized” 的报错;characterEncoding=utf8与建库时的utf8mb4对齐。这三个参数可以记成固定组合,其他Swing+MySQL项目也能直接复用。
IDE中通常通过 Project Structure -> Modules -> Dependencies 添加lib/mysql-connector-java-8.0.16.jar。我更推荐在命令行中先验证classpath,因为IDE有时会隐藏真实的构建问题。
javac -encoding UTF-8 -cp ".:lib/mysql-connector-java-8.0.16.jar" src/*.java java -cp ".:lib/mysql-connector-java-8.0.16.jar:src" MainWindows上需要把冒号换成英文分号。src路径也要放在classpath里,否则编译成功但运行时找不到Main类。-encoding UTF-8是重点,如果省略,在Windows默认编码下很容易出现中文注释乱码。
3.3 Main入口为什么要写在事件分发线程中
Main.java里的写法通常是直接把登录窗口构建放入 Swing 事件分发线程。
import javax.swing.*; public class Main { public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { new Login().setVisible(true); } }); } }Swing组件不是线程安全的。如果在main线程直接创建JFrame,快速开关窗口时可能遇到重绘卡顿。SwingUtilities.invokeLater把窗口构建放到EDT队列中,由事件分发线程串行处理。这个细节看起来不起眼,但答辩时能主动讲出来,就说明你理解Swing的并发模型,而不是只会new JFrame()。
实际上,登录按钮的点击事件本身也运行在EDT线程中,所以在actionPerformed里执行数据库查询不会触发跨线程操作。如果查询耗时较长,可以考虑用SwingWorker,但期末项目不需要过度设计。
3.4 第一次运行失败的定位方法
第一次跑源码失败,大多是环境问题而不是代码问题。我的排查顺序是先确认MySQL是否启动,再确认JDBC连接是否正常,最后才看Swing代码。
# 确认MySQL端口监听 netstat -ano | findstr :3306 # 确认能否用命令行连上数据库 mysql -u root -p -e "SELECT 1"如果MySQL服务没启动,Swing窗口能正常出现,但点击登录后控制台报CommunicationsException。此时即使反复修改密码也没有用,必须先启动服务。常见的Access denied for user 'root'@'localhost'则需要核对密码与DBUtil中配置是否一致。
有些同学的MySQL使用caching_sha2_password插件,用8.0.16驱动可以直接连接。如果你拿到的是旧驱动包,就可能在握手阶段报错,这时需要在连接串后追加allowPublicKeyRetrieval=true。
4. 界面显示与查询封装:MyTable、ReturnQueryResult 和 MyGridBagConstraints 怎么配合
4.1 MyTable 是表格显示的地基
MyTable类负责把查询结果渲染到表格,同时控制单元格是否可编辑。成绩系统里通常不需要用户直接在表格中双击修改,所以isCellEditable返回false是合理默认。
public class MyTable extends AbstractTableModel { private String[] columns; private List<String[]> rows = new ArrayList<>(); public MyTable(String[] columns) { this.columns = columns; } @Override public int getRowCount() { return rows.size(); } @Override public int getColumnCount() { return columns.length; } @Override public Object getValueAt(int rowIndex, int columnIndex) { return rows.get(rowIndex)[columnIndex]; } @Override public boolean isCellEditable(int row, int col) { return false; } public void setRows(List<String[]> data) { this.rows = data; fireTableDataChanged(); } }setRows在填充数据后调用fireTableDataChanged(),告诉JTable表格数据变了,界面需要刷新。如果遗漏这一行,数据库查询到了新数据,表格却仍然显示旧内容,这是最典型的“数据没回显”问题。
在课程设计中使用List<String[]>比使用Vector更接近现代Java习惯。如果在源码中看到Vector,也不用立刻改,明白它只是旧集合类即可。
4.2 ReturnQueryResult 负责把 ResultSet 转成界面数据
ResultSet的生命周期依赖数据库连接。通常在Connection关闭后,ResultSet就不能再读取了。很多隐藏Bug就是连接关闭后访问结果集引起的,所以需要一个独立方法把ResultSet内容提前复制出来。
public static List<String[]> convert(ResultSet rs) throws SQLException { List<String[]> rows = new ArrayList<>(); ResultSetMetaData meta = rs.getMetaData(); int columnCount = meta.getColumnCount(); while (rs.next()) { String[] row = new String[columnCount]; for (int i = 1; i <= columnCount; i++) { row[i - 1] = rs.getString(i) == null ? "" : rs.getString(i); } rows.add(row); } return rows; }最容易出错的是列索引从1开始,而数组下标从0开始。这里用rs.getString(i)读取第 i 列,再存入数组的i - 1位置。如果写错,第一列数据会丢失,后面所有列都会错位。
将 null 转换为空字符串是为了避免 Swing 表格单元格显示 “null”。虽然这不是致命问题,但演示时会影响第一印象。
4.3 用 MyGridBagConstraints 简化 GridBagLayout 参数
GridBagLayout 功能强大,但参数太多。MyGridBagConstraints的目的就是让调用方写代码时更短,在一行内把位置和填充行为表达清楚。
public MyGridBagConstraints set(int gridx, int gridy, int span, int fill) { this.gridx = gridx; this.gridy = gridy; this.gridwidth = span; this.fill = fill; return this; }在Login界面中,使用方式如下:
add(new JLabel("学号"), new MyGridBagConstraints().set(0, 0, 1, GridBagConstraints.NONE)); add(jtfAccount, new MyGridBagConstraints().set(1, 0, 2, GridBagConstraints.HORIZONTAL)); add(new JButton("登录"), new MyGridBagConstraints().set(1, 3, 1, GridBagConstraints.NONE));fill=HORIZONTAL表示文本框在水平方向拉伸,占满列宽。span=2表示输入框跨两列。按钮如果也用HORIZONTAL,会被拉得很宽,不美观,所以按钮用NONE。insets参数如果不在封装中暴露,组件间的空隙会很小。我一般会增加一个inset(int top, int left, int bottom, int right)方法,它可以直接设置内外边距。
| 参数名 | 作用 | 课程设计常用值 |
|---|---|---|
| gridx | 组件所在列 | 0、1、2 |
| gridy | 组件所在行 | 0、1、2、3 |
| gridwidth | 组件横向跨越列数 | 1 或 2 |
| fill | 拉伸方向 | NONE、HORIZONTAL |
| insets | 外边距 | 5、5、5、5 |
在答辩时,这个封装是对Swing布局深入理解的体现。只需说明“这个类把设置参数的过程收敛成一次方法调用,让布局意图更清晰”,老师就会认为你是自己写过的,而不是简单靠模板拼界面。
4.4 学生菜单和教师菜单的功能边界
学生菜单的典型入口是“查询成绩”“修改密码”;教师菜单是“录入成绩”“维护学生”“查看课程”。由于项目同时存在StudentMenu.java和TeacherMenu.java,它们都有一个共同设计:构造方法接收主键参数。
private int studentId; private String studentName; public StudentMenu(int studentId, String studentName) { this.studentId = studentId; this.studentName = studentName; }后续查询成绩时使用WHERE student_id=?,避免让学生手工输入学号,同时防止越权查询。教师录入成绩时会根据登录教师ID关联查询可教课程,这样就不会出现一位英语老师去录入数学成绩的情况。
这两个菜单不一定需要抽象父类,课程设计里保持简单即可。关键是登录后的身份与菜单入口严格对应,学生身份看不到教师管理按钮。演示时用两个不同账号分别登录截图,比在一个界面里隐藏按钮更能体现角色设计。
5. 提交前自检清单:从数据回显及时区报错到打包内容
提交前要把时间花在“验证”和“打包”上。评分老师通常不会逐行阅读源码,而是会直接运行。所以要保证机器上第一次导入四个SQL文件后,登录、查询、回显完整链路都能走通。
先准备两个测试账号,一个学生账号,一个教师账号。学生登录后,界面标题或欢迎语应该显示学生姓名;教师登录后,能看到进入录入窗口的按钮。如果两个角色的菜单标题一致,说明登录分支没有正确读取身份,需要回到Login类检查dispose()和setVisible()的执行顺序。测试时,我一般会让教师新增一条成绩,再退回去重新查询,观察表格是否刷新。若没有刷新,手动调用一次查询方法,并重新执行setRows。fireTableDataChanged是整个回显问题的核心入口。
MySQL时区问题、中文乱码和驱动加载问题是最常遇到的三个运行问题,对应的解决方式可以整理成一个简单检查表。
| 检查项 | 现象 | 处理方式 |
|---|---|---|
| MySQL 8 时区提示 | The server time zone value ... is unrecognized | jdbc url 加serverTimezone=Asia/Shanghai |
| 中文显示乱码 | 表数据和界面出现乱码 | 建库使用utf8mb4,url 加characterEncoding=utf8 |
| 登录按钮点击无响应 | 控制台显示ClassNotFoundException | 确认lib/mysql-connector-java-8.0.16.jar已加入运行时 classpath |
如果你的环境是MySQL 8.0.16配套驱动,一般不需要改代码。但别人拿到源码后如果使用MySQL 5.7,数据库驱动兼容但密码加密方式可能有差异,会在DriverManager.getConnection处抛出认证错误。此时在README里写清楚驱动版本,避免同组同学用默认配置运行失败。
打包时至少包含src、lib、doc.pdf和README.md。很多项目只把源码交给老师,老师本地没有lib目录就无法编译,最后直接被扣分。如果README.assets中有运行截图,也一并放进附件目录,方便在报告中用相对路径引用。我自己在最后一遍自检时一定会执行以下命令,而不是只依赖IDE运行。
javac -encoding UTF-8 -cp ".:lib/mysql-connector-java-8.0.16.jar" src/*.java java -cp ".:lib/mysql-connector-java-8.0.16.jar:src" Main命令行能跑通,说明classpath和编码没有隐藏问题。最后一件事是检查ReturnQueryResult返回的表头顺序与界面中columns数组是否一致。表头顺序对不上时,表格虽然能显示,但列名和数据会错位,这种情况下最值得怀疑的是ResultSetMetaData的列索引顺序与查询语句中的列顺序不同。
本文还有配套的精品资源,点击获取