
简介这是一份基于Java与MySQL开发的学生成绩管理分析系统项目源码面向计算机相关专业课程设计、毕业设计及想了解Java桌面应用开发的初学者。系统涵盖学生信息管理、成绩录入查询、统计分析与预警通知等核心模块演示了Swing界面与数据库交互的完整流程有助于理解业务分层和基础CRUD设计。资源压缩包共18个文件、约62KB主体为Java源码及工程配置包括xml界面与模块配置、iml工程模块文件、gradle构建脚本、properties配置文件、jar依赖包以及启动用bat脚本结构精简便于导入IDE直接运行。通过阅读源码可掌握成绩录入、统计报表生成等典型功能的实现思路并参考其模块划分方式优化自己的项目结构。目前已有29人浏览学习适合快速获取一个可运行的学生成绩管理项目雏形在此基础上可进一步扩展报表、权限和移动端功能。1. Java MySQL 学生成绩管理系统的工程结构拆解当你在网盘里下载到一份“学生成绩管理系统.zip”先别急着解压跑起来。这个基于Java与MySQL的实现我打开后发现目录里有java0323 work1.iml、src/EnterGui.java、.idea/uiDesigner.xml以及完整的Gradle构建文件。它没有用Spring Boot也没有Web页面而是纯Java Swing做界面JDBC访问MySQL这正是很多Java基础学习者当下需要的那类可运行源码。它要解决的问题很直接把学生档案、课程成绩、统计分析、预警通知塞进一个桌面工具里让管理员能录成绩、让教师能看排名、让系统能算平均分。适合拿来改课设也适合一步步看懂“Java对象—JDBC—MySQL”之间到底是怎么协作的。不过要注意这个包的代码风格比较早期IDE直接导入可能遇到编码或Gradle版本问题后文会展开讲怎么处理。2. 从 MySQL 建表开始成绩模型设计与数据约束2.1 MySQL 本地安装与连接参数无论你手头这份学生成绩管理分析系统是哪个版本先保证MySQL可用。我用的是MySQL 8.0官网下载地址去MySQL官网找就行。如果你更习惯轻量环境按mysql免安装版教程里的方法操作即可但要注意8.0和5.7的初始化命令有区别。重点是把root密码和认证方式设好免得后面Java连不上去。执行下面这组命令完成初始化与密码修改# 以管理员身份打开CMD或bash mysqld --initialize-insecure mysqld --install MySQL8 net start MySQL8 mysql -u root -p进入MySQL后执行ALTER USER rootlocalhost IDENTIFIED BY 123456; FLUSH PRIVILEGES;如果连接驱动是5.x老版本还需要把认证方式改成兼容的ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 123456; FLUSH PRIVILEGES;这里的逻辑是初始化时生成一个空密码方便首次登录随后立刻修改密码使用mysql_native_password是给老驱动兜底而新版JDBC驱动用默认的caching_sha2_password即可。连接串里要带时区和SSL参数这一步放到第6章细说。我一般把连接信息放在一个db.properties文件里而不是硬编码到Java类中避免换库时到处改。2.2 三张核心表学生、课程、成绩一个完整的成绩管理分析系统最忌讳的是把“语文成绩”“数学成绩”做成student表里的字段。一旦新增一门课就要改表结构非常痛苦。我的建议是按第三范式拆成student、course、score三张表让成绩成为学生与课程之间的关联表。建表SQL如下CREATE DATABASE IF NOT EXISTS stu_gradle DEFAULT CHARACTER SET utf8mb4; USE stu_gradle; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, stu_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, stu_name VARCHAR(30) NOT NULL, sex CHAR(1) DEFAULT 男, age INT, class_name VARCHAR(30) ) ENGINEInnoDB; CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(50) NOT NULL ) ENGINEInnoDB; CREATE TABLE score ( id INT PRIMARY KEY AUTO_INCREMENT, stu_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,2), exam_time DATETIME, UNIQUE KEY uk_stu_course_time (stu_id, course_id, exam_time), CONSTRAINT fk_score_stu FOREIGN KEY (stu_id) REFERENCES student(id) ON DELETE CASCADE, CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES course(id) ON DELETE CASCADE ) ENGINEInnoDB;score表里的unique key很关键它限制了同一个学生在同一次考试中同一门课只能有一条成绩避免重复录入。DECIMAL(5,2)表示成绩最多两位小数范围最大999.99对0100分已经足够。外键使用ON DELETE CASCADE删除学生时级联删除成绩不会留下垃圾数据。实际项目里如果担心成绩历史需要审计可以把外键改成RESTRICT禁止删除已存在成绩的学生。2.3 字符集与引擎选择建表统一使用utf8mb4而不是utf8因为utf8mb4能覆盖emoji等四字节字符姓名备注里偶尔会出现特殊符号用utf8会报错。引擎选InnoDB是因为它支持事务和外键后文的批量录成绩会用事务保证数据一致性。可以用下面命令确认库字符集SHOW CREATE DATABASE stu_gradle;如果默认不是utf8mb4可以在MySQL配置文件里加[mysqld] character-set-serverutf8mb4 collation-serverutf8mb4_unicode_ci这是经验做法客户端连接时还需要在JDBC URL里加characterEncodingUTF-8否则Java插入中文会乱码。记住数据库、表、连接串、Java文件编码四个层面的字符集要一致。很多从网上下载的学生成绩管理系统源码一运行就乱码八成是这四个层面不一致导致的并不是代码本身有错。问题现象常见原因排查方向中文插入后乱码JDBC URL缺少characterEncodingUTF-8连接串加参数建表报错或Emoji丢失使用utf8而非utf8mb4库表字符集改为utf8mb4插入重复成绩score表缺少唯一键增加uk_stu_course_time唯一键环境准备和数据模型都搞定后下一步就该写JDBC持久层了。3. 基于 JDBC 的成绩持久化连接池、PreparedStatement 与事务3.1 从 DriverManager 到 HikariCP网上很多学生成绩系统的JDBC工具类长这样Class.forName(com.mysql.jdbc.Driver)然后每次操作都DriverManager.getConnection()。这在单机课设里没有明显问题但如果你把系统部署给多个教务老师同时用频繁创建连接会让数据库负载很高。我的做法是直接引入HikariCP连接池它速度快、配置简单。在build.gradle里加上两个依赖dependencies { implementation mysql:mysql-connector-java:8.0.33 implementation com.zaxxer:HikariCP:5.0.1 }注意mysql-connector-java的groupId是mysql但8.0.31以后更推荐用com.mysql:mysql-connector-j。不过基于这个项目已有的JDBC代码风格用mysql:mysql-connector-java最稳妥。然后准备一份db.propertiesjdbcUrljdbc:mysql://localhost:3306/stu_gradle?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingUTF-8 usernameroot password123456 maximumPoolSize10 connectionTimeout3000这段配置里serverTimezone是必须的MySQL 8.0默认时区与JDBC不一致时连接池初始化会直接报错。useSSLfalse是因为本地开发没有证书避免SSL握手警告。为了方便老代码复用我仍然写一个DBUtil类但内部换成HikariDataSourceimport com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import java.io.InputStream; import java.sql.Connection; import java.sql.SQLException; import java.util.Properties; public class DBUtil { private static final HikariDataSource dataSource; static { try (InputStream in DBUtil.class.getClassLoader() .getResourceAsStream(db.properties)) { Properties props new Properties(); props.load(in); HikariConfig config new HikariConfig(); config.setJdbcUrl(props.getProperty(jdbcUrl)); config.setUsername(props.getProperty(username)); config.setPassword(props.getProperty(password)); config.setMaximumPoolSize(Integer.parseInt( props.getProperty(maximumPoolSize))); dataSource new HikariDataSource(config); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }逻辑说明静态块在类加载时执行一次读取db.properties并创建连接池后续每次获取连接都是从池中借出用完调用close()会归还连接而不是关闭物理连接。这样在长时间运行的成绩管理界面里不会出现连接耗尽。3.2 PreparedStatement 完成成绩录入在UI层拿到表单里的学号、课程名、分数后不要用字符串拼接SQL因为存在SQL注入风险也会导致SQL解析变慢。正确做法是用PreparedStatement的?占位符。下面是一段录入学生成绩的代码public boolean insertScore(String stuNo, String courseName, double score) throws SQLException { String sql INSERT INTO score (stu_id, course_id, score, exam_time) SELECT s.id, c.id, ?, NOW() FROM student s, course c WHERE s.stu_no ? AND c.course_name ? ; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setDouble(1, score); ps.setString(2, stuNo); ps.setString(3, courseName); return ps.executeUpdate() 1; } }说明SELECT子查询把字符串学号和课程名转成student.id和course.id避免Java层再做一次查询。这里用try-with-resourcesps和conn都会自动关闭。setDouble和setString是按类型绑定比拼接字符串安全。如果executeUpdate返回1说明插入成功如果返回0大概率是学号或课程不存在需要给出提示。3.3 批处理与事务一次考试成绩批量导入成绩录入不是一条一条手工敲经常是Excel整理好后批量导入。逐条插入在1000条左右时还能接受再多就要用事务和批处理。关键逻辑是先关闭自动提交插入全部成功后统一提交有异常就回滚避免一批数据只写入一半。代码如下public void batchInsertScores(ListScoreVo list) { String sql INSERT INTO score (stu_id, course_id, score, exam_time) VALUES ((SELECT id FROM student WHERE stu_no ?), (SELECT id FROM course WHERE course_name ?), ?, NOW()) ; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { conn.setAutoCommit(false); for (ScoreVo vo : list) { ps.setString(1, vo.getStuNo()); ps.setString(2, vo.getCourseName()); ps.setDouble(3, vo.getScore()); ps.addBatch(); } ps.executeBatch(); conn.commit(); } catch (SQLException e) { // 生产环境应记录日志并抛出业务异常 try (Connection conn DBUtil.getConnection()) { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } }这里的细节是预备语句中SELECT子查询虽然每次执行都会被MySQL解析但配合预处理后性能影响不大真正快的方案是把学生和课程ID先缓存到Map里再直接拼数值但那样代码会复杂很多。对于成绩管理分析系统的体量这个写法足够。注意addBatch不会立即执行executeBatch才批量发送setAutoCommit(false)之后MySQL会把所有语句放在一个事务里全部成功才commit。异常类出现场景处理方式SQLException: No suitable driver缺少JDBC依赖或Class.forName未加载检查Gradle依赖确认URL前缀jdbc:mysqlCommunications link failureMySQL服务未启动或端口被防火墙拦截telnet localhost 3306确认连通Unknown character set index 255JDBC连接字符集与MySQL不一致连接串追加characterEncodingUTF-8持久层搭好之后焦点就该转移到查询和分析了。4. 成绩统计与分析聚合 SQL JTable 报表渲染4.1 聚合函数与分组统计成绩统计是整个学生成绩管理分析系统的核心卖点。老师最关心的是班级平均分、最高分、及格率。用一条GROUP BY SQL就能算出来SELECT st.class_name, COUNT(DISTINCT st.id) AS student_cnt, ROUND(AVG(sc.score), 2) AS avg_score, MAX(sc.score) AS max_score, MIN(sc.score) AS min_score, ROUND(SUM(CASE WHEN sc.score 60 THEN 1 ELSE 0 END) / COUNT(*), 2) AS pass_rate FROM score sc JOIN student st ON sc.stu_id st.id GROUP BY st.class_name ORDER BY avg_score DESC;这段SQL的执行逻辑先通过JOIN把student表里的class_name和score表关联然后按班级分组。AVG计算平均分ROUND保留两位小数。pass_rate用CASE WHEN统计大于等于60分的人数占比避免多次扫描同一张表。如果还需要统计单科成绩则再加一个GROUP BY course_id或与course表JOIN。在Swing里展示时我一般会把ResultSet转成ListMapString, Object再填充到JTable的TableModel中。4.2 成绩排名RANK 与 DENSE_RANKMySQL 8.0开始支持窗口函数排名语句写起来比早先的row_number方式清晰得多。按单科成绩排名可以用如下SQLSELECT st.stu_no, st.stu_name, c.course_name, sc.score, RANK() OVER (PARTITION BY c.course_name ORDER BY sc.score DESC) AS rank_no FROM score sc JOIN student st ON sc.stu_id st.id JOIN course c ON sc.course_id c.id ORDER BY c.course_name, rank_no;RANK()和DENSE_RANK()的区别是两个人并列第1时RANK会接着排第3而DENSE_RANK会排第2。学生成绩排名通常用DENSE_RANK更合理因为并列第1之后还是要给第2名。如果你用的是MySQL 5.7没有窗口函数只能通过变量模拟代码长且容易出错这也是我建议使用MySQL 8.0的另一个原因。给界面查询时还可以用LIMIT配合排名来实现“班级前10名”。需求SQL要点适用场景班级平均分GROUP BY class_name AVG(score)期中期末整体分析单科排名RANK() OVER(PARTITION BY course_id)学科竞赛或奖学金成绩分布CASE WHEN分桶 COUNT生成柱状图/饼图4.3 在 Swing 中渲染结果集拿到统计结果后不能直接在事件线程里执行耗时SQL否则界面会转圈半天没响应。常见做法是用SwingWorker把查询放到后台线程查到结果再回调EDT刷新JTable。下面是一个最小可运行示例JTable table new JTable(new DefaultTableModel()); new SwingWorkerListMapString, Object, Void() { Override protected ListMapString, Object doInBackground() { return scoreDao.queryAvgByClass(); } Override protected void done() { try { ListMapString, Object rows get(); DefaultTableModel model (DefaultTableModel) table.getModel(); model.setRowCount(0); String[] columns {班级, 人数, 平均分, 最高分, 最低分, 及格率}; model.setColumnIdentifiers(columns); for (MapString, Object row : rows) { model.addRow(new Object[]{ row.get(class_name), row.get(student_cnt), row.get(avg_score), row.get(max_score), row.get(min_score), row.get(pass_rate) }); } } catch (Exception e) { JOptionPane.showMessageDialog(null, 查询失败 e.getMessage()); } } }.execute();这段代码背后的线程模型SwingWorker的doInBackground在后台线程执行done方法回到EDT。DefaultTableModel在EDT上修改Table不会触发并发问题。如果你图省事直接在MouseListener里查数据库界面会卡住这是Swing程序最常见的低效写法。4.4 生成导出报表统计表算完通常还要导出成Excel或CSV。这个项目源码没有引入POI我一般用CSV做轻量导出用Java标准库就能写try (PrintWriter out new PrintWriter(new FileWriter(score_report.csv, StandardCharsets.UTF_8))) { out.write(\uFEFF); // BOM防止Excel打开中文乱码 out.println(班级,平均分,及格率); for (MapString, Object row : rows) { out.printf(%s,%.2f,%.2f%%%n, row.get(class_name), row.get(avg_score), (double) row.get(pass_rate) * 100); } }加上\uFEFF的原因Excel识别无BOM的UTF-8文件时会按ANSI解析中文标题会乱码。这一个技巧在导出报表时很实用也经常作为Java基础面试八股文中的“为什么文件开头要加BOM”来考。5. 成绩预警通知与数据接口的落地实现5.1 预警阈值配置预警功能听起来高级其实就是定期跑一条SQL找出成绩低于阈值的学生。阈值不应该写死在Java里而是放到一个config表里这样管理员可以在系统管理模块里修改。建表如下CREATE TABLE sys_config ( id INT PRIMARY KEY AUTO_INCREMENT, config_key VARCHAR(50) NOT NULL UNIQUE, config_value VARCHAR(200) ); INSERT INTO sys_config(config_key, config_value) VALUES (warn_score, 60);5.2 定时扫描与通知使用java.util.Timer类定时扫描每30分钟执行一次。一个可用的SQLSELECT st.stu_no, st.stu_name, c.course_name, sc.score, CONCAT(你的, c.course_name, 成绩为, sc.score, 分低于预警线) AS msg FROM score sc JOIN student st ON sc.stu_id st.id JOIN course c ON sc.course_id c.id WHERE sc.score (SELECT config_value FROM sys_config WHERE config_key warn_score) AND DATE(sc.exam_time) CURDATE();然后在Java里把msg展示到通知面板即可。真正的短信或邮件发送需要额外接入阿里云SMS或JavaMail不是这个课设包的重点。这个模块给学生看能展示系统“会思考”给老师看能用来做学习状态干预。预警类型触发条件通知方式单科预警score warn_score窗口弹窗/站内信总分下滑两次考试平均分下降超过10%短信接口缺考提醒成绩表无记录且考试日期已过邮件通知5.3 提供 HTTP 接口给移动端原包是Swing桌面端如果想兼容题目摘要里提到的移动端访问不需要引入Spring Boot一个轻量的JDK自带HttpServer就能提供成绩查询接口import com.sun.net.httpserver.HttpServer; import java.net.InetSocketAddress; public class ApiServer { public static void start(int port) throws Exception { HttpServer server HttpServer.create(new InetSocketAddress(port), 0); server.createContext(/api/scores/rank, exchange - { String json buildJsonFromDb(); // 调用上面的排名SQL byte[] data json.getBytes(UTF-8); exchange.getResponseHeaders().add(Content-Type, application/json; charsetUTF-8); exchange.sendResponseHeaders(200, data.length); exchange.getResponseBody().write(data); exchange.close(); }); server.setExecutor(null); server.start(); } }说明HttpServer是JDK内置模块jdk.httpserver的一部分不需要额外依赖。用它暴露/api/scores/rank接口后手机端浏览器就能访问到排名JSON。注意生产环境要加鉴权不能在接口里直接返回所有学生成绩否则任何人都能通过班级参数拉全表成绩。这个简单接口设计面试时可以讲出来说明你理解“数据接口模块”和跨端访问。6. 调试技巧Swing 界面卡顿、字符集与性能优化6.1 让数据库操作离开 EDTSwing所有UI组件必须运行在事件分发线程上但网络请求和SQL查询不能放在EDT。很多从网上下载的Java课设都犯这个错点击查询按钮后界面直接“假死”Windows标题栏出现“未响应”。解决方法是把耗时操作放到SwingWorker或ExecutorService里。我通常把查询方法统一封装成一个后台任务类UI层只负责传递参数和接收结果这样代码清晰也方便单元测试。6.2 JDBC URL 参数与 MySQL 8.0 兼容MySQL 8.0的JDBC驱动对时区和SSL要求严格连接串至少要有String url jdbc:mysql://localhost:3306/stu_gradle ?useSSLfalse serverTimezoneAsia/Shanghai characterEncodingUTF-8 rewriteBatchedStatementstrue;这里的rewriteBatchedStatementstrue很关键开启后PreparedStatement的executeBatch会真正合并多条INSERT语句写入性能提升几倍。如果你在日志中看到Unknown initial character set index 255一般是连接串里的characterEncoding写成了UTF-8但MySQL初始化成了latin1。要逐个排查。6.3 查询慢时先看 EXPLAIN当学生表数据量增长到几万条时成绩排名查询明显变慢。先在SQL前加上EXPLAINEXPLAIN SELECT ... FROM score ... JOIN student ...;如果type列显示ALL说明全表扫描通常是因为查询字段没有索引。InnoDB会自动为主键和外键建索引但student.stu_no这种查询字段需要手动加唯一索引。在建表语句里已经用了UNIQUE KEY查一下SHOW INDEX FROM student确认即可。另一个调试技巧是给这个学生成绩管理分析系统包里的src目录导入IDEA时如果运行出现ClassNotFound先检查build.gradle里的sourceCompatibility是否设成你本机的JDK版本如果是JDK 17就不要用老旧的JDK 1.8配置。然后刷新Gradle并清理build目录问题大多能解决。本文还有配套的精品资源点击获取