Java学生成绩管理系统源码解析:从环境搭建到JDBC实战
简介:这是一套面向高校计算机相关专业学生与Java初学者的学生成绩管理系统完整实践资料,以Java语言结合GUI界面与数据库实现,可作为期末课程设计、Java综合实验或自学练手项目使用。压缩包共80个文件,约5.74MB,包含28个java源文件、42个class编译文件、2个jar依赖包,以及docx实验报告、xls成绩数据表和界面图片等配套素材,源码与文档齐备,导入后即可运行调试。系统功能覆盖登录验证、学生与教师注册、成绩的添加修改查询删除、文件导入成绩、学生账号管理及密码修改等模块,结构完整、逻辑清晰。目前已有9124人学习下载,适合需要参考完整项目架构、理解数据库连接与GUI交互实现、并借助实验报告梳理设计思路的读者,也可作为课程作业的对照范本。
1. 从一份能跑起来的 Java 成绩管理源码说起
课程设计季一到,后台问得最多的就是「有没有能直接跑、带数据库、还能交实验报告的 Java 学生成绩管理系统」。我拆的这份资源正好对上:一套 Java 实现的学生成绩管理系统,带 GUI 界面、带数据库脚本、附实验报告文档。它不是那种只有增删改查空壳的 Demo,而是把学生信息、课程、成绩录入、查询统计、报表导出串成一条完整业务线,适合三类人:赶课程设计的学生、想拿一个完整 CRUD 项目练手的初级开发者、需要给学生演示「桌面端 + 数据库」分层结构的讲师。下面我按「它是什么 → 怎么跑起来 → 表怎么设计 → 坑在哪 → 怎么改造成自己的」这条线拆,能抄的代码和参数我都给到位。
2. 环境搭建与首次运行:JDK、数据库、GUI 三件套怎么配
这套系统的技术栈很朴素:Java SE 做主体,Swing 做 GUI,JDBC 直连数据库,没有 Spring 那套重框架。好处是依赖少、启动快、答辩时讲得清;代价是所有连接管理、事务、异常都得自己写。先把运行环境对齐,不然第一次跑必翻车。
2.1 版本与依赖的选型理由
JDK 建议 8 或 11,别一上来就 17+。原因很实际:Swing 在 8/11 上最稳,老项目里常见的sun.*内部类调用、javax.swing的默认外观在 17 上会有警告甚至行为差异;而 JDBC 驱动版本又和数据库绑定。数据库二选一:MySQL 5.7/8.0 或 SQLite。课程设计场景我一般推荐 MySQL,因为实验报告里写「客户端-服务器架构」更像回事,答辩也好讲;如果只是本地演示、不想装服务,SQLite 单文件更省事,改一下驱动和 URL 即可。
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 8 或 11 | Swing 兼容性最好 |
| 数据库 | MySQL 8.0 / SQLite 3 | 二选一,驱动不同 |
| JDBC 驱动 | mysql-connector-java 8.x / sqlite-jdbc | 与数据库版本匹配 |
| 构建 | 纯 javac 或 Maven | 源码包一般给的是普通工程 |
依赖只需要一个 JDBC 驱动 jar。用 Maven 的话在pom.xml里加:
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency>不用 Maven 就把 jar 丢进lib/目录,编译和运行时都加进 classpath。这一步看着简单,但驱动版本和数据库大版本不匹配是后面「连不上库」的头号原因,先对齐再往下走。
2.2 建库建表与初始化数据
源码包里通常带一个.sql脚本,先执行它。核心表一般四张:学生表、课程表、成绩表、用户表(登录用)。下面是我按这类系统常见结构整理的最小可用建表语句,字段名可按你拿到的脚本微调:
CREATE DATABASE IF NOT EXISTS score_mgr DEFAULT CHARSET utf8mb4; USE score_mgr; CREATE TABLE student ( sno VARCHAR(20) PRIMARY KEY, -- 学号 sname VARCHAR(50) NOT NULL, -- 姓名 gender CHAR(2), -- 性别 major VARCHAR(50), -- 专业 class_no VARCHAR(30) -- 班级 ); CREATE TABLE course ( cno VARCHAR(20) PRIMARY KEY, -- 课程号 cname VARCHAR(50) NOT NULL, -- 课程名 credit DECIMAL(3,1) -- 学分 ); CREATE TABLE score ( id INT AUTO_INCREMENT PRIMARY KEY, sno VARCHAR(20), cno VARCHAR(20), score DECIMAL(5,2), term VARCHAR(20), -- 学期 CONSTRAINT fk_sno FOREIGN KEY (sno) REFERENCES student(sno), CONSTRAINT fk_cno FOREIGN KEY (cno) REFERENCES course(cno), UNIQUE KEY uk_sno_cno_term (sno, cno, term) ); CREATE TABLE sys_user ( username VARCHAR(30) PRIMARY KEY, password VARCHAR(64) NOT NULL, -- 存哈希,别存明文 role VARCHAR(10) -- admin / teacher );逻辑说明:score表用(sno, cno, term)做唯一键,防止同一学生同一课程同一学期重复录入,这是成绩系统最容易出的脏数据;外键保证成绩不会挂到不存在的学生或课程上。参数上,score用DECIMAL(5,2)而不是FLOAT,避免 59.999 显示成 60 这种玄学问题;password留 64 位是给 SHA-256 十六进制串用的。
2.3 连接配置与启动顺序
数据库连接参数一般集中在一个db.properties或常量类里,找到它改成你自己的:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/score_mgr?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 jdbc.username=root jdbc.password=你的密码三个参数值得单独说:serverTimezone不写,MySQL 8 驱动会报时区异常;characterEncoding=utf8不写,中文姓名和课程名可能变问号;useSSL=false是本地演示省掉证书配置,生产别这么干。启动顺序固定为:先起数据库服务 → 执行 SQL 脚本 → 确认db.properties指向正确库 → 编译运行主类。主类通常是带main的登录窗口或主窗体,运行命令:
javac -encoding UTF-8 -cp "lib/*" -d out src/**/*.java java -cp "out:lib/*" com.example.MainWindows 下 classpath 分隔符是;不是:,这是新手第一个翻车点。编译报「找不到符号」多半是没把驱动或某个包加进-cp;运行报No suitable driver则是驱动类名写错或 jar 没进 classpath。
3. 成绩录入与查询的核心链路:JDBC 怎么写才不埋雷
GUI 只是壳,真正决定这套系统能不能用的是 JDBC 那几段代码。成绩录入、按条件查询、统计平均分,本质都是「拼 SQL → 设参数 → 执行 → 映射结果」。写得好不好,差别在连接管理和 SQL 注入防护上。
3.1 用 PreparedStatement 堵住注入与类型错
成绩查询界面一般有「学号、课程、学期」几个输入框,很多人图省事直接字符串拼 SQL,结果输入' OR '1'='1就能把全表拉出来。正确写法是占位符:
public List<ScoreVO> queryScore(String sno, String cno, String term) throws SQLException { StringBuilder sql = new StringBuilder( "SELECT s.sno, s.sname, c.cname, sc.score, sc.term " + "FROM score sc JOIN student s ON sc.sno = s.sno " + "JOIN course c ON sc.cno = c.cno WHERE 1=1 "); List<Object> params = new ArrayList<>(); if (sno != null && !sno.isEmpty()) { sql.append("AND sc.sno = ? "); params.add(sno); } if (cno != null && !cno.isEmpty()) { sql.append("AND sc.cno = ? "); params.add(cno); } if (term != null && !term.isEmpty()) { sql.append("AND sc.term = ? "); params.add(term); } List<ScoreVO> list = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql.toString())) { for (int i = 0; i < params.size(); i++) { ps.setObject(i + 1, params.get(i)); // 参数从 1 开始 } try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { ScoreVO vo = new ScoreVO(); vo.setSno(rs.getString("sno")); vo.setSname(rs.getString("sname")); vo.setCname(rs.getString("cname")); vo.setScore(rs.getBigDecimal("score")); vo.setTerm(rs.getString("term")); list.add(vo); } } } return list; }逻辑说明:WHERE 1=1是为了后面统一用AND追加条件,省去判断「是不是第一个条件」的麻烦;params列表和占位符顺序严格对应,setObject的下标从 1 开始,这是 JDBC 的硬规矩,写成 0 会直接抛异常。try-with-resources保证连接、语句、结果集按逆序关闭,漏关连接是桌面程序跑久了「卡死」的常见原因。参数上,getBigDecimal读成绩比getDouble更稳,避免浮点误差在界面上显示成 88.99999。
3.2 连接管理与事务:别让一条坏 SQL 拖垮整个界面
Swing 是单线程模型,所有数据库操作都在事件线程里同步执行,一旦某条 SQL 慢或者连接泄漏,界面直接假死。常见做法是把连接获取收敛到一个工具类,并给录入操作加事务:
public class DBUtil { private static final Properties P = new Properties(); static { try (InputStream in = DBUtil.class.getResourceAsStream("/db.properties")) { P.load(in); Class.forName(P.getProperty("jdbc.driver")); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection( P.getProperty("jdbc.url"), P.getProperty("jdbc.username"), P.getProperty("jdbc.password")); } }批量导入成绩时,一个学生一条记录,中途失败不能只写一半:
try (Connection conn = DBUtil.getConnection()) { conn.setAutoCommit(false); // 关闭自动提交 try (PreparedStatement ps = conn.prepareStatement( "INSERT INTO score(sno,cno,score,term) VALUES(?,?,?,?)")) { for (ScoreRow row : rows) { ps.setString(1, row.getSno()); ps.setString(2, row.getCno()); ps.setBigDecimal(3, row.getScore()); ps.setString(4, row.getTerm()); ps.addBatch(); } ps.executeBatch(); conn.commit(); // 全部成功才提交 } catch (SQLException e) { conn.rollback(); // 任一失败整体回滚 throw e; } }参数说明:setAutoCommit(false)是事务的前提;addBatch/executeBatch把 N 条插入合并成一次网络往返,几百条成绩导入从「转圈十几秒」降到「秒级」;rollback放在 catch 里,保证要么全进要么全不进。这里有个边界:MySQL 的executeBatch遇到唯一键冲突会抛BatchUpdateException,但前面已成功的批次在部分驱动下可能已生效,所以回滚必须显式写,不能指望自动。
3.3 统计查询与结果映射
平均分、及格率、排名这类统计,交给 SQL 做比在 Java 里循环快得多:
SELECT c.cname, COUNT(*) AS total, ROUND(AVG(sc.score), 2) AS avg_score, SUM(CASE WHEN sc.score >= 60 THEN 1 ELSE 0 END) / COUNT(*) AS pass_rate FROM score sc JOIN course c ON sc.cno = c.cno WHERE sc.term = ? GROUP BY c.cno, c.cname ORDER BY avg_score DESC;CASE WHEN算及格人数是标准写法,比在 Java 里再查一遍省一次往返;ROUND(...,2)控制小数位,避免界面显示一长串。映射到 Java 时用rs.getDouble("avg_score")即可,注意列别名要和getXxx的字符串完全一致,大小写敏感在部分数据库上会咬人。
4. 表结构与业务规则的避坑清单:五个真实会翻车的点
这套系统跑通不难,难在数据不出错、界面不假死、答辩不被问倒。下面五条是我拆这类项目时反复见到的坑,按「现象 → 原因 → 解决」记。
4.1 中文变问号或乱码
现象:学生姓名、课程名在界面和数据库里显示成???。原因:建库时没指定utf8mb4,或 JDBC URL 缺characterEncoding=utf8,或javac编译时没加-encoding UTF-8。解决:三处一起改——建库语句带DEFAULT CHARSET utf8mb4,URL 加编码参数,编译命令加-encoding UTF-8。只改一处往往还是乱,因为链路任何一环掉链子都白搭。
4.2 登录成功但主界面空白
现象:输入账号密码能进,主窗体一片灰。原因:Swing 组件在非事件调度线程(EDT)里创建,或者setVisible(true)之前没pack()。解决:主窗体启动包一层SwingUtilities.invokeLater(() -> new MainFrame().setVisible(true)),布局用BorderLayout/GridBagLayout而不是绝对坐标,窗口大小交给pack()算。绝对坐标在换分辨率后必错位,这是血泪经验。
4.3 删除学生时报外键约束失败
现象:删学生提示Cannot delete or update a parent row。原因:score表还有该学号的成绩记录,外键挡住了。解决:两种策略二选一——业务上先删成绩再删学生(软删除更稳,加status字段标记),或者建外键时加ON DELETE CASCADE。课程设计里我一般选软删除,因为答辩时「删除即物理消失」容易被追问数据可追溯性。
4.4 成绩录入重复不报错
现象:同一学生同一课程录了两次,统计平均分被拉低。原因:score表没建唯一键,或者建了但代码里用INSERT没捕获冲突。解决:加UNIQUE(sno,cno,term),代码里捕获SQLIntegrityConstraintViolationException后提示「该成绩已存在,请修改」。别用「先查再插」,并发下照样重复,唯一键才是最后一道闸。
4.5 打包后连不上数据库
现象:IDE 里跑得好好的,打成 jar 双击就报驱动找不到。原因:驱动 jar 没一起打进去,或db.properties用绝对路径读不到。解决:用 Maven 的maven-shade-plugin打 fat jar,配置文件放src/main/resources用getResourceAsStream读;命令行跑时确认-cp包含驱动。这一条在交作业前一定要在「干净目录」里验一遍,别只在 IDE 里自测。
5. 从能跑到能改:把这份源码变成你自己的东西
拿到源码只是起点,真正拉开差距的是你能不能按自己的题目改。我给一个具体的改造路径,顺便说怎么验证改对了。
第一步,换业务字段。比如题目要求加「平时成绩、期末成绩、总评」三列,那就改score表加两列,把原来的单列score拆成regular_score、final_score,总评用生成列或查询时算:ROUND(regular_score*0.3 + final_score*0.7, 2)。改表后同步改ScoreVO、录入界面、查询 SQL 三处,漏一处就出现「界面有、库里没有」的错位。
第二步,加一个统计报表导出。用Apache POI把统计结果写成 Excel,核心就几行:
try (XSSFWorkbook wb = new XSSFWorkbook()) { Sheet sheet = wb.createSheet("成绩统计"); Row header = sheet.createRow(0); String[] cols = {"课程", "人数", "平均分", "及格率"}; for (int i = 0; i < cols.length; i++) header.createCell(i).setCellValue(cols[i]); int r = 1; for (StatVO vo : stats) { Row row = sheet.createRow(r++); row.createCell(0).setCellValue(vo.getCname()); row.createCell(1).setCellValue(vo.getTotal()); row.createCell(2).setCellValue(vo.getAvgScore().doubleValue()); row.createCell(3).setCellValue(vo.getPassRate().doubleValue()); } try (FileOutputStream out = new FileOutputStream("成绩统计.xlsx")) { wb.write(out); } }参数上注意setCellValue(double)和setCellValue(String)是重载,传错类型编译能过但导出内容会怪;及格率存的是 0~1 的小数,要在单元格样式里设成百分比格式才好看。
第三步,验证。改完别只看界面,跑三条自检 SQL:SELECT COUNT(*) FROM score对总数、SELECT sno,cno,term,COUNT(*) FROM score GROUP BY 1,2,3 HAVING COUNT(*)>1应返回空(查重复)、随机抽一个学生手算总评和系统显示比对。这三条过了,基本能交差。
我自己的习惯是:每次改完表结构,先把「增删改查 + 统计 + 导出」六条主路径各点一遍,再跑上面三条 SQL,最后在干净目录打一次包双击验证。这套流程救过我两次——一次是忘了同步 VO 字段导致导出空列,一次是打包漏了配置文件。从那以后我每次交付前都强制走一遍这个清单,省得答辩现场翻车。希望帮到你。
本文还有配套的精品资源,点击获取