简介:这是一套面向高校计算机相关专业学生与Java初学者的课程设计/期末作业级实战项目,围绕学生成绩管理场景,采用Java语言结合GUI界面与数据库实现,可用于课程设计参考、Java综合练习或小型信息管理系统入门学习。压缩包共80个文件,约5.74MB,包含28个java源文件与42个class编译文件,另有jar依赖包、docx实验报告、xls成绩数据表及jpg界面背景图等,源码与文档配套齐全,导入后即可运行调试。资源已积累9124人学习下载,具备一定的参考基础。功能层面覆盖登录验证、学生与教师注册、成绩的添加修改查询删除、文件导入成绩、学生账号管理及密码修改等模块,实验报告可辅助梳理系统设计与实现思路,适合需要完整项目案例与文档参考的读者对照学习、二次改造与查漏补缺。
1. 从一份能跑起来的 Java 成绩管理项目说起
很多同学做课程设计时,最头疼的不是写不出增删改查,而是拼不出一个「能演示、能交差、还能讲清楚」的完整系统。这份 Java 学生成绩管理系统正好卡在这个痛点上:它带 GUI 界面、带数据库脚本、还附了实验报告,拿到手就能跑,跑完还能照着改。我拆过不少同类项目,大多数要么只有控制台、要么数据库是内存模拟的,这份的完整度算比较实在。适合谁?正在做 Java 课程设计的学生、想练手 Swing + JDBC 组合的初学者,以及需要一份可二次开发底座的开发者。下面我按「它是什么 → 怎么跑起来 → 核心代码怎么写的 → 怎么改」的顺序,把这份源码拆开讲。
2. 项目定位与技术栈:Swing + JDBC 的经典三层结构
2.1 这套系统到底管了什么
从功能上看,这是一个典型的学生成绩管理场景,围绕三张核心表展开:学生信息、课程信息、成绩记录。系统要解决的事情很具体——录入学生、维护课程、登记成绩、按条件查询、统计分数。GUI 界面把这些操作包起来,让非技术用户也能点着用。
我一般会把这类系统的功能拆成四块来看:
- 学生管理:学号、姓名、性别、班级、联系方式等基础信息的增删改查
- 课程管理:课程编号、课程名称、学分、任课教师
- 成绩管理:把学生和课程关联起来,录入分数,支持修改和删除
- 查询统计:按学号查个人成绩、按课程查成绩单、算平均分和及格率
这四块对应到代码里,就是四组 DAO 方法加四个界面面板。理解了这个映射关系,后面看目录结构就不会迷路。
2.2 技术选型为什么是 Swing 而不是 JavaFX
项目用的是 Swing 做界面。这个选择在课程设计场景里非常合理:Swing 是 JDK 自带的,不需要额外引入 UI 依赖,javac编译完直接java就能跑,省掉了一堆环境配置的麻烦。JavaFX 从 JDK 11 之后被移出标准库,要单独配模块,对新手不友好。
数据库这边用的是 JDBC 直连,没有上 MyBatis 或 Hibernate。原因也简单——成绩管理的 SQL 就那么几条,手写 JDBC 反而透明,出了问题能一眼看到是哪条 SQL 挂了。常见做法是把连接信息抽到一个配置类里,避免散落在各个 DAO 中。
整体是「界面层 → 业务层 → 数据访问层 → 数据库」的四层走向,虽然项目规模不大,但分层是清晰的。这种结构对二次开发很友好:想换数据库只动 DAO,想改界面只动 UI 层,互不牵连。
2.3 目录结构长什么样
拿到源码后,先别急着编译,花两分钟把目录扫一遍。典型的工程结构大致是这样:
StudentScoreSystem/ ├── src/ │ ├── com/score/ │ │ ├── ui/ # 界面层:登录、主窗口、各管理面板 │ │ ├── dao/ # 数据访问层:StudentDao、CourseDao、ScoreDao │ │ ├── model/ # 实体类:Student、Course、Score │ │ ├── util/ # 工具类:数据库连接、字符串校验 │ │ └── Main.java # 程序入口 ├── sql/ │ └── init.sql # 建库建表 + 初始数据 ├── lib/ │ └── mysql-connector-java.jar # JDBC 驱动 └── 实验报告.doc / .pdf # 设计文档ui包里通常一个面板一个类,dao包里一个实体对应一个类,model包就是纯 POJO。这种「一个职责一个类」的划分,是判断一份课程设计代码是否值得二次开发的重要信号——如果所有逻辑都塞在一个Main.java里,改起来会很痛苦。
提示:不同版本的源码包目录名可能略有差异,比如
dao写成db、model写成entity,但分层逻辑是一致的,按类名找就行。
3. 跑起来:环境准备、建库与启动顺序
3.1 环境清单与版本建议
在动手之前,先把环境对齐。这份项目对版本不算挑剔,但有几个点要注意:
| 组件 | 建议版本 | 说明 |
|---|---|---|
| JDK | 8 或 11 | Swing 在两者上都稳定,8 兼容性最好 |
| MySQL | 5.7 或 8.0 | 8.0 注意驱动包版本要匹配 |
| JDBC 驱动 | mysql-connector-java 5.1.x / 8.0.x | 与 MySQL 版本对应 |
| IDE | IntelliJ IDEA / Eclipse | 任一即可,能配 jar 依赖 |
JDK 和 MySQL 的安装这里不展开,重点说容易翻车的地方:MySQL 8.0 的驱动类名是com.mysql.cj.jdbc.Driver,而 5.x 是com.mysql.jdbc.Driver,连接串里还要带时区参数。这两点搞错,程序启动就会报ClassNotFoundException或时区异常。
3.2 建库建表:先跑 init.sql
数据库是这套系统的地基,顺序不能乱。打开 MySQL 客户端,先执行sql/init.sql:
-- 创建数据库,字符集用 utf8mb4 避免中文乱码 CREATE DATABASE IF NOT EXISTS score_db DEFAULT CHARACTER SET utf8mb4; USE score_db; -- 学生表 CREATE TABLE student ( stu_id VARCHAR(20) PRIMARY KEY, -- 学号 name VARCHAR(50) NOT NULL, -- 姓名 gender CHAR(2), -- 性别 class_name VARCHAR(50), -- 班级 phone VARCHAR(20) -- 联系方式 ); -- 课程表 CREATE TABLE course ( course_id VARCHAR(20) PRIMARY KEY, -- 课程编号 course_name VARCHAR(50) NOT NULL, -- 课程名称 credit DECIMAL(3,1), -- 学分 teacher VARCHAR(50) -- 任课教师 ); -- 成绩表,学生和课程多对多关联 CREATE TABLE score ( id INT AUTO_INCREMENT PRIMARY KEY, stu_id VARCHAR(20), course_id VARCHAR(20), score DECIMAL(5,1), -- 分数,保留一位小数 FOREIGN KEY (stu_id) REFERENCES student(stu_id), FOREIGN KEY (course_id) REFERENCES course(course_id) );这段 SQL 的关键在于三张表的关系设计。score表是典型的关联表,用stu_id和course_id两个外键把学生和课程连起来,一条记录代表「某个学生某门课的成绩」。分数用DECIMAL(5,1)而不是INT,是为了支持 89.5 这种带小数的成绩,这个细节在统计平均分时很重要。
外键约束建议保留。虽然有些教程为了省事会去掉外键,但保留它能防止录入不存在的学号或课程号,数据一致性有保障。如果导入初始数据时报外键错误,先检查插入顺序——必须先插student和course,再插score。
3.3 配置数据库连接并启动
建完库,找到项目里的数据库连接工具类,通常是util/DBUtil.java或类似名字,把连接参数改成自己的:
public class DBUtil { // MySQL 8.0 用 com.mysql.cj.jdbc.Driver private static final String DRIVER = "com.mysql.cj.jdbc.Driver"; // 连接串带上时区和字符集参数,避免中文乱码和时区报错 private static final String URL = "jdbc:mysql://localhost:3306/score_db" + "?useUnicode=true&characterEncoding=utf8" + "&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PASSWORD = "你的密码"; public static Connection getConnection() throws Exception { Class.forName(DRIVER); // 加载驱动 return DriverManager.getConnection(URL, USER, PASSWORD); } }参数说明:useUnicode=true&characterEncoding=utf8保证中文姓名和课程名不乱码;serverTimezone是 MySQL 8.0 的必填项,不写会抛The server time zone value is unrecognized。USER和PASSWORD换成自己本机的账号密码。
配置好之后,把 JDBC 驱动 jar 加到项目的 classpath 里。IDEA 里的做法是File → Project Structure → Libraries → +选中 jar 包;Eclipse 是右键项目Build Path → Add External Archives。这一步漏了,编译能过但运行必挂。
最后运行Main.java。如果一切正常,会先弹出登录窗口(常见默认账号是admin/admin,具体看源码里的判断逻辑),登录后进入主界面。启动顺序总结成一句话:先建库 → 再配连接 → 加驱动 → 跑 Main,顺序错了排查起来会很绕。
4. 核心模块拆解:从登录校验到成绩统计的完整链路
4.1 实体类与 DAO:一条 SQL 背后的封装
先看实体类,它决定了数据在内存里怎么表示。以Student为例:
public class Student { private String stuId; private String name; private String gender; private String className; private String phone; // 无参构造 + 全参构造 + getter/setter 省略 public String getStuId() { return stuId; } public void setStuId(String stuId) { this.stuId = stuId; } // ... 其余字段同理 }实体类没什么花活,就是字段加 getter/setter。它的价值在于给 DAO 层提供一个类型安全的载体——查询结果从ResultSet映射成Student对象,界面层拿到的就是对象而不是一堆字符串。
DAO 层是真正干活的地方。以学生查询为例:
public class StudentDao { // 按学号查询单个学生 public Student findByStuId(String stuId) { String sql = "SELECT * FROM student WHERE stu_id = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, stuId); // 参数化,防 SQL 注入 try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { Student s = new Student(); s.setStuId(rs.getString("stu_id")); s.setName(rs.getString("name")); s.setGender(rs.getString("gender")); s.setClassName(rs.getString("class_name")); s.setPhone(rs.getString("phone")); return s; } } } catch (Exception e) { e.printStackTrace(); // 实际项目应记录日志 } return null; } }这段代码有几个值得说的点。第一,用PreparedStatement加?占位符,而不是字符串拼接 SQL,这是防注入的基本功,成绩系统里学号是用户输入的,拼接就是给自己埋雷。第二,try-with-resources自动关闭Connection、PreparedStatement、ResultSet,避免连接泄漏——这类系统连接数不多,但泄漏积累久了照样把数据库连接池耗光。第三,ResultSet的列名要和建表时的字段名对上,class_name对应实体里的className,映射错了就是空值。
增删改的方法结构类似,只是把executeQuery换成executeUpdate,返回受影响行数判断成功与否。批量导入成绩时,我一般会用addBatch()+executeBatch(),比循环单条插入快一个数量级。
4.2 登录与权限校验:一个容易被忽略的入口
登录界面是系统的第一道门。很多课程设计在这里偷懒,把账号密码硬编码在代码里比较:
// 登录按钮的点击事件 loginBtn.addActionListener(e -> { String user = userField.getText().trim(); String pwd = new String(pwdField.getPassword()); if (user.isEmpty() || pwd.isEmpty()) { JOptionPane.showMessageDialog(this, "账号和密码不能为空"); return; } // 常见做法:查 user 表校验,或与配置的默认账号比对 if (userDao.checkLogin(user, pwd)) { new MainFrame().setVisible(true); // 进入主界面 this.dispose(); // 关闭登录窗 } else { JOptionPane.showMessageDialog(this, "账号或密码错误"); } });这里pwdField.getPassword()返回的是char[]而不是String,Swing 这么设计是出于安全考虑,用完可以手动清零。校验逻辑走userDao.checkLogin,内部同样是一条参数化查询。如果项目里没有独立的用户表,而是硬编码了admin/123456,二次开发时建议补一张user表,把账号管理做成可维护的——这是从「能演示」到「能交付」的关键一步。
登录成功后new MainFrame().setVisible(true)打开主窗口,同时dispose()释放登录窗资源。这个细节处理得好,不会出现登录窗残留在后台的情况。
4.3 成绩录入与统计:业务逻辑最集中的地方
成绩模块是整套系统的核心,涉及录入、修改、查询、统计四类操作。录入时最关键的是校验——分数必须在 0 到 100 之间,学号和课程号必须存在:
public boolean addScore(String stuId, String courseId, double score) { // 业务校验:分数范围 if (score < 0 || score > 100) { throw new IllegalArgumentException("分数必须在 0-100 之间"); } // 校验学生和课程是否存在,避免外键报错 if (studentDao.findByStuId(stuId) == null) { throw new IllegalArgumentException("学号不存在:" + stuId); } String sql = "INSERT INTO score(stu_id, course_id, score) VALUES(?,?,?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, stuId); ps.setString(2, courseId); ps.setDouble(3, score); return ps.executeUpdate() > 0; } catch (Exception e) { e.printStackTrace(); return false; } }校验放在 DAO 之前,是为了给用户友好提示,而不是让数据库抛一个看不懂的外键异常。这种「先校验再落库」的顺序,是业务层该做的事。
统计功能通常是一条聚合 SQL 搞定:
// 统计某门课程的平均分、最高分、及格率 public Map<String, Object> statByCourse(String courseId) { String sql = "SELECT AVG(score) avg_score, MAX(score) max_score, " + "SUM(CASE WHEN score >= 60 THEN 1 ELSE 0 END) / COUNT(*) pass_rate " + "FROM score WHERE course_id = ?"; // 执行查询并封装结果,略 }SUM(CASE WHEN ...)是算及格率的常用写法,比查出来在 Java 里循环统计更高效。AVG和MAX直接交给数据库算,减少数据传输。这类聚合查询在实验报告里往往是加分项,因为它体现了对 SQL 的掌握程度。
4.4 界面层组织:面板切换与事件绑定
Swing 界面层的组织方式,决定了代码好不好维护。常见做法是用JTabbedPane把学生、课程、成绩分成三个标签页,每个标签页一个独立的JPanel子类:
public class MainFrame extends JFrame { public MainFrame() { setTitle("学生成绩管理系统"); setSize(900, 600); setLocationRelativeTo(null); // 居中显示 setDefaultCloseOperation(EXIT_ON_CLOSE); JTabbedPane tabs = new JTabbedPane(); tabs.addTab("学生管理", new StudentPanel()); tabs.addTab("课程管理", new CoursePanel()); tabs.addTab("成绩管理", new ScorePanel()); add(tabs); } }每个 Panel 内部自己管自己的表格、按钮和事件,主窗口只负责组装。这种划分让「改一个模块不影响其他模块」成为可能。表格用JTable加DefaultTableModel,查询结果填充到 model 里,界面自动刷新。
事件绑定统一用addActionListener的 lambda 写法,比匿名内部类简洁。按钮点击后先做输入校验,再调 DAO,最后刷新表格——这个「校验 → 操作 → 刷新」的三段式,是每个按钮事件的标准骨架,照着套就行。
5. 避坑与排查:启动失败、乱码、外键报错的真实记录
5.1 启动报 ClassNotFoundException: com.mysql.cj.jdbc.Driver
现象:程序一运行就抛驱动类找不到的异常,堆栈指向Class.forName。
原因:两种情况——要么 JDBC 驱动 jar 没加到 classpath,要么驱动类名和 MySQL 版本不匹配(5.x 的包用了 8.0 的类名,或反过来)。
解决:先确认lib目录下的 jar 已加入项目依赖;再核对版本,MySQL 8.0 用com.mysql.cj.jdbc.Driver,5.7 用com.mysql.jdbc.Driver。改完重新编译,别只改代码不重新构建。
5.2 中文姓名存进去变成问号
现象:录入「张三」,数据库里显示???,或者查询出来是乱码。
原因:字符集链路没对齐。建库时没指定utf8mb4,或者连接串缺characterEncoding=utf8,任何一环掉链子都会乱码。
解决:三处一起查——建库语句带DEFAULT CHARACTER SET utf8mb4、连接串带useUnicode=true&characterEncoding=utf8、MySQL 服务端字符集配置正确。改完最好重建库再测,旧数据已经乱码的救不回来。
5.3 插入成绩时报外键约束失败
现象:Cannot add or update a child row: a foreign key constraint fails。
原因:往score表插数据时,stu_id或course_id在对应的主表里不存在。常见于手工测试时随手编了个学号。
解决:插入前先确认学生和课程已存在,代码里就是 4.3 节那段前置校验。如果是导入初始数据报错,检查init.sql里的插入顺序,必须先主表后关联表。
5.4 界面能打开但表格一片空白
现象:登录进主界面,表格区域什么都没有,也不报错。
原因:查询结果没填进DefaultTableModel,或者ResultSet的列名和getString的参数对不上,取出来全是 null。
解决:在 DAO 里打印一下rs的行数,确认 SQL 确实查到了数据;再核对列名,class_name别写成className。表格 model 的列名数组要和填充数据的顺序一致,错位了数据就串行。
5.5 关闭窗口后进程不退出
现象:点了关闭按钮,窗口没了,但任务管理器里 Java 进程还在。
原因:JFrame的默认关闭行为是HIDE_ON_CLOSE,只隐藏不退出;或者有非守护线程(比如数据库连接池)还在跑。
解决:主窗口设setDefaultCloseOperation(EXIT_ON_CLOSE),确保关闭即退出。如果用了连接池,在windowClosing事件里手动关闭池。这类问题不影响功能,但演示时进程残留会显得不专业。
6. 二次开发:把课程设计改成能交付的小系统
6.1 从硬编码到可配置:先动连接和账号
拿到源码后,我建议第一刀砍向硬编码。数据库连接信息、默认登录账号这些散落在代码里的常量,全部抽到config.properties:
db.url=jdbc:mysql://localhost:3306/score_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai db.user=root db.password=123456然后用Properties类加载,DBUtil只读配置不写死。这样换台机器部署,改一个文件就行,不用重新编译。账号体系同理,补一张user表,密码存 MD5 或 BCrypt 哈希,登录走查表校验——这一步做完,系统就从「演示品」往「能交付」迈了一大截。
6.2 加功能的标准姿势:以「按班级统计」为例
二次开发最常见的需求是加统计维度。以「按班级算平均分」为例,改动路径很清晰:
ScoreDao加一个方法,SQL 用JOIN把score和student连起来,按class_name分组ScorePanel加一个按钮和对应的结果表格- 按钮事件里调新方法,把结果填进表格
public List<Object[]> avgByClass() { String sql = "SELECT s.class_name, AVG(sc.score) avg_score " + "FROM score sc JOIN student s ON sc.stu_id = s.stu_id " + "GROUP BY s.class_name ORDER BY avg_score DESC"; List<Object[]> result = new ArrayList<>(); // 执行查询,每行封装成 Object[]{班级, 平均分},略 return result; }这个套路可以复制到任何新统计需求上:DAO 加查询 → 界面加控件 → 事件里串联。三层各改一处,互不干扰,这就是分层带来的好处。改完记得在实验报告里补一段设计说明,答辩时能讲清楚「为什么这么加」。
6.3 验证改动是否真的生效
改完代码别急着说「应该没问题了」,按这个顺序验一遍:先单独跑 DAO 方法(写个main打印结果),确认 SQL 对;再启动界面点按钮,确认数据填对了;最后造几条边界数据——分数 0、分数 100、不存在的学号——看校验有没有拦住。我见过太多「界面能点但数据是错的」的情况,根子都在跳过了 DAO 单测这一步。
注意:改数据库结构后,记得同步更新
init.sql和实验报告里的表设计章节,否则下次重建库又对不上。
6.4 一个我踩过的坑
早些年我改这类项目,习惯直接在MainFrame里加逻辑,图省事。结果功能越加越多,一个类膨胀到上千行,后来想换个表格样式都不敢动,牵一发动全身。从那以后我每次动源码前,都强制先看一眼分层边界——该进 DAO 的 SQL 绝不写在界面里,该在业务层做的校验绝不下放到按钮事件。这个习惯让后续每次改造都省心不少。希望这份拆解能帮你少走点弯路,把这份成绩管理系统真正跑起来、改下去。
本文还有配套的精品资源,点击获取