
简介一套基于Java技术栈开发的学生成绩管理系统完整源码包适合Java初学者、在校学生及毕业设计开发者用于系统学习Web项目的完整实现流程。系统覆盖用户登录认证、学生信息管理、成绩录入与查询等核心功能依托Servlet、JSP、JDBC及MVC分层架构进行开发并配套界面运行截图与数据库初始化脚本可帮助理解从前端页面交互到后端数据持久化的完整链路。压缩包共148个文件含76个Java源文件、50个编译后的class文件、6张系统运行截图、4个jar依赖包及2个SQL数据库脚本整体约2.66MB目录结构清晰、内容完整。目前已有349人浏览学习。通过学习本资源可掌握实体类与DAO层设计、数据库表创建与JDBC操作、业务逻辑与视图渲染的衔接等关键技能理解异常处理与Session会话管理等知识点是一份兼顾代码阅读与实际运行的实战型学习材料。1. 这份 java 学生成绩管理系统资源先别急着看源码下载完这份 java 学生成绩管理系统你先别急着双击 .bin 里的可运行文件也别一上来就打开源码。先把压缩包里的截图翻一遍再把 .DataBase 里的数据库脚本导入 MySQL 跑通之后源码才有意义。这是一个典型的 Java SE 桌面项目Swing 做窗口JDBC 连数据库核心文件按 .src、.Dialog、.main 分工放好。它适合三类人做 Java 课程设计或毕设、需要一套能交差且能讲清楚逻辑的项目的人刚学完 JDBC 想看看真实工程怎么分层的人以及想拿现成系统改造出成绩管理、教师管理功能的人。如果你原本以为是 ServletJSP 的 Web 系统那是下载说明文案带偏了按桌面项目去跑才顺。2. 环境与目录结构先从 .bin 和 .src 里定位入口2.1 把目录当地图.bin、.DataBase、.Dialog、.main 各自装什么拿到压缩包先别急着解压后乱翻。我一般会先把顶层目录拉一个清单这份资源的清单里最有价值的四个目录是目录里面大概是什么复现时怎么用.bin编译后的字节码和可能的可执行入口跑通环境后用来对比“编译产物和源码是否一致”.DataBase数据库脚本SQL 建表语句和初始化数据第一个要处理的东西直接决定系统能不能连上库.Dialog对话框相关源码Swing 窗口的视图层改界面、调布局时主要看这里.main程序入口负责把登录框或主窗口拉起来定位 main 方法从这儿开始追代码另外压缩包里还有几个以._开头的隐藏文件以及散落在根目录的GlobalSearchDialog.class、GlobalSearchDialog1.class、GlobalSearchDialogT.class。._开头的是 Mac 打包时自动生成的 AppleDouble 元数据和项目运行没关系你直接忽略就行那三个 class 文件是同一个“全局搜索对话框”的三版编译产物说明作者当时在迭代这个功能后面我会专门讲怎么利用它们。很多人一拿到压缩包就双击 .bin 里的东西结果弹个黑框一闪而过或者报“找不到主类”。这不是资源坏了而是执行顺序错了。这个项目的完整链路是先建库导数据再配 JDBC 连接最后启动 main 入口。你跳过了数据库脚本程序连不上库自然起不来。2.2 JDK MySQL 驱动 jar最小运行组合老规矩先把运行环境确认一遍。这个项目是标准 JDBC 工程没有引入 Maven 或 Gradle所以依赖全靠一个mysql-connector-java驱动 jar 撑着。我常用的组合是JDK 1.8 或 11别用太高版本JDK 17 以后有些 Swing 老代码会有模块化访问报错。MySQL 5.7 或 8.0。脚本是哪一版导出来的不确定但 8.0 兼容性更好。驱动 jar 用mysql-connector-java-5.1.49.jar或8.0.28.jar都行按你本机 MySQL 版本选5.7 配 5.1.x8.0 配 8.0.x。连接数据库的代码是这个项目的地基我一般把连接逻辑抽到一个独立类里。下面这段是常见的写法和这份资源里的风格应该是一致的import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class ConnectionUtil { // 三个参数按你的数据库实际情况改 private static final String URL jdbc:mysql://localhost:3306/student_manage ?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD 123456; static { try { // 加载驱动MySQL 8.0 驱动会自动注册老版本必须写这一步 Class.forName(com.mysql.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { // 每次调用都创建新连接资源少可以这样用改造时可以换连接池 return DriverManager.getConnection(URL, USER, PASSWORD); } }这段代码需要注意三点。第一URL 里的useSSLfalse不是可有可无的MySQL 8.0 默认 TLS 行为会让你连不上后面避坑章我会细说。第二serverTimezoneAsia/Shanghai是处理时间类型报错的老驱动不写会直接抛异常。第三Class.forName(com.mysql.jdbc.Driver)在老版本驱动里必需换了 8.0.x 驱动后可以改成com.mysql.cj.jdbc.Driver但写成旧路径也能用因为驱动包保留了兼容入口。2.3 命令行快速验证不依赖 IDEA 也能跑起来我曾经犯过一个错把 src 扔进 IDEA 直接点运行结果报了一堆错以为是源码不完整后来发现是环境问题。所以现在我的习惯是先用命令行做最小验证确认 JDK 和驱动都正常再进 IDE。在压缩包解压目录下打开终端执行两步。如果你的 .bin 里已经是编译好的 class先直接用编译产物验证# Windows 环境路径分隔符是分号 java -cp bin;lib/mysql-connector-java-8.0.28.jar com.student.main.StudentManageSystem # Linux 或 macOS路径分隔符是冒号 java -cp bin:lib/mysql-connector-java-8.0.28.jar com.student.main.StudentManageSystem如果这个命令告诉你“找不到主类”别慌说明 .bin 里的包路径可能和你预期的不一样。用 jar 命令看一眼编译产物的结构jar tf bin/StudentManageSystem.jar | grep -i main这里com.student.main.StudentManageSystem是入口类的全限定名实际以你解压后的目录结构为准。我通常会先用find . -name *.class | head -20扫一遍看包路径长什么样再反推主类位置。确认 class 文件确实存在后-cp参数里的 jar 路径也必须正确否则会报ClassNotFoundException。这一步跑通了后面进 IDEA 就是秒开的事。3. 数据库脚本导入建库、导入、校验三件套3.1 先读脚本再下手从 .DataBase 里判断表结构和版本很多人的习惯是拿到 SQL 直接往 Navicat 里拖回车。我吃过一次亏脚本里有DROP TABLE IF EXISTS直接把我本地一个同名的旧表给清掉了。所以现在我的原则是——先读后导。打开 .DataBase 目录下的 SQL 文件重点看三处建库语句有没有写、表名是什么、初始账号数据在不在。按这类成绩管理系统的常规设计脚本里大概率包含这几张表表名核心字段作用studentid、student_no、name、gender、class_id学生基础信息courseid、course_name、credit、teacher_id课程信息和任课教师gradeid、student_id、course_id、score成绩记录一对多关系在这里体现users 或 user_infoid、username、password、role登录账号密码通常是加密串读脚本的时候顺手确认一下字符集。如果文件头部有SET NAMES utf8mb4或者建表语句里带DEFAULT CHARSETutf8mb4那没问题如果是 gbk 甚至没写后面查中文名铁定乱码。判断完这三点再动手导你的效率会高很多。3.2 导入前处理先建库再建表字符集用 utf8mb4我一般不用 Navicat 的“运行 SQL 文件”直接跑整个脚本而是分成两步先在 MySQL 里建一个空库再执行脚本内容。这样可以绕开脚本里可能写死的库名和你本地库名冲突的问题。-- 先连上 MySQL 服务端执行这条建库名字随你但后面改 JDBC URL 要同步 CREATE DATABASE IF NOT EXISTS student_manage DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; -- 再用这个库 USE student_manage;为什么要手动建库而不是直接跑脚本因为很多祖传脚本的建库语句是CREATE DATABASE IF NOT EXISTS但它的 character set 可能是latin1或者干脆没指定。这时候你后面插入中文姓名、课程名存入数据库就变成问号查出来的也是 ?非常难排查。手动建库就一句话把字符集钉死在 utf8mb4 上一劳永逸。建库完成后再执行脚本文件里的建表和插入语句。命令行方式是这样mysql -u root -p student_manage /解压目录/.DataBase/student_manage.sql如果你更习惯图形界面Navicat 或 IDEA 自带的 Database 面板都可以。以 IDEA 为例右侧 Database 面板新建数据源选 MySQL填上账号密码然后右键你的student_manage库选择Run SQL Script选中那个 sql 文件执行。注意 IDEA 执行脚本时默认是按当前数据源连接的你的 URL 里那串参数不会影响脚本执行但脚本内部如果有USE database语句会切走库执行完留意一下当前选中的库是不是你要的那个。3.3 导入后自检别急着写代码先跑几条 SQL脚本导完别急着关。我习惯立刻做三件事确认数据真的进来了-- 看看每个表有多少条数据心里有个底 SELECT COUNT(*) FROM student; SELECT COUNT(*) FROM course; SELECT COUNT(*) FROM grade; -- 看初始账号能不能查出来注意密码字段是明文还是 MD5 SELECT id, username, password, role FROM users LIMIT 5; -- 看中文内容有没有乱码乱码的话这步直接暴露 SELECT name, class_id FROM student LIMIT 10;这三条 SQL 能覆盖 90% 的导入问题。第一条查数量确认脚本不是空壳第二条看账号表这决定你后面登录时该输什么密码第三条看乱码如果这里出来的是???说明你建库那步的字符集没指定成功或者脚本本身是 gbk 编码需要把脚本文件另存为 utf8 再跑一次。还要注意一点脚本里的账号密码如果是一串 32 位的十六进制字符说明是 MD5 加密后的值。比如 admin 的明文密码是 123456库里存的可能是e10adc3949ba59abbe56e057f20f883e。这种情况下你先确认源码里的密码校验逻辑再看是要改库里的值还是改源码的判断方式。我见过不少人在这一步卡一整天其实就一句话的事。4. 核心代码逻辑实体类、DAO 与登录认证一条线拆完4.1 按 MVC 拆模块Dialog 是视图DAO 是模型main 管流程这个项目没有引入 Spring分层全靠包结构自己约束。Dialog目录里放的是 Swing 界面类相当于 View.src根目录下的实体类和 DAO 类相当于 Modelmain目录里的启动类干的是 Controller 的活。搞清楚这个分工你改需求的时候就知道代码该往哪里放。比如你想给成绩列表加一列“总分”改动路径应该是先改grade表对应的实体类加字段再改 DAO 里查询那条 SQL最后改 Dialog 里的表格模型。你要是直接从界面代码开始改找半天也找不到数据是从哪儿来的。这个项目值得学习的点也在这它没用什么高深框架但把经典分层用在了 Swing 上非常适合用来理解“界面、逻辑、数据”三者如何解耦。4.2 StudentDao 的增删改查模板PreparedStatement 用法值得抠一抠页面里所有增删改查最终都会落到一个 DAO 类上。这类旧工程的 DAO 写法很有代表性先看一段典型的学生新增和查询逻辑import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; public class StudentDao { // 新增学生参数来自 Dialog 层收集的界面输入 public boolean addStudent(Student student) { String sql INSERT INTO student(student_no, name, gender, class_id) VALUES(?,?,?,?); // try-with-resources 写法连接和语句自动关不用手动 finally try (Connection conn ConnectionUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, student.getStudentNo()); ps.setString(2, student.getName()); ps.setString(3, student.getGender()); ps.setInt(4, student.getClassId()); return ps.executeUpdate() 0; } catch (SQLException e) { e.printStackTrace(); return false; } } // 按姓名模糊查询返回列表供 Dialog 层填充 JTable public ListStudent searchByName(String keyword) { String sql SELECT * FROM student WHERE name LIKE ?; ListStudent list new ArrayList(); try (Connection conn ConnectionUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % keyword %); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { list.add(mapRow(rs)); } } } catch (SQLException e) { e.printStackTrace(); } return list; } // 把 ResultSet 当前行映射成实体对象每张表都配一个这样的方法最舒服 private Student mapRow(ResultSet rs) throws SQLException { Student s new Student(); s.setId(rs.getInt(id)); s.setStudentNo(rs.getString(student_no)); s.setName(rs.getString(name)); s.setGender(rs.getString(gender)); s.setClassId(rs.getInt(class_id)); return s; } }这段代码里最值得学习的不是 SQL 本身而是PreparedStatement的占位符用法。真实项目里几乎没人用Statement拼字符串因为 SQL 注入和安全问题全是拼串拼出来的。setString(1, xxx)这类调用是给第一个问号赋值下标从 1 开始不是从 0。如果你加了条件查不出来先看占位符的数量和 set 的顺序对不对我在这上面吃亏的次数比业务逻辑错误多得多。另外try-with-resources是 JDK 7 以后的推荐写法自动调用close()释放连接。老代码里常见的finally { conn.close(); }也能用但如果里面再嵌套一层判断空指针写起来就啰嗦。你现在看源码时如果发现用的是老写法也别急着改能跑就行改成新写法属于重构风险自担。4.3 登录认证怎么做密码比对和用户状态登录是每个管理系统跑通的第一个功能。它的代码逻辑不复杂但有个细节特别容易看走眼。常规做法是把用户输入的密码经过同一个摘要算法处理再和库里存的串比对import java.security.MessageDigest; public class LoginService { // 用户输入的明文密码和库里的 md5 串做比对 public boolean login(String username, String plainPassword) { String sql SELECT password, role FROM users WHERE username ?; try (Connection conn ConnectionUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { String dbPwd rs.getString(password); // 库里的串是 32 位说明是 md5长度不是 32 就按明文处理 if (dbPwd.length() 32) { return md5(plainPassword).equalsIgnoreCase(dbPwd); } return plainPassword.equals(dbPwd); } } } catch (Exception e) { e.printStackTrace(); } return false; } private String md5(String input) throws Exception { MessageDigest md MessageDigest.getInstance(MD5); byte[] bytes md.digest(input.getBytes(UTF-8)); StringBuilder sb new StringBuilder(); for (byte b : bytes) { // 转成十六进制小写注意和库里的值大小写对得上 sb.append(String.format(%02x, b)); } return sb.toString(); } }为什么我特意强调equalsIgnoreCase和大小写问题因为同一串明文在不同系统、不同代码版本里 MD5 出来的十六进制串可能大小写不一致而String.equals是区分大小写的。你登录失败如果报的是“用户名或密码错误”检查方向就是明文密码经过了哪种摘要算法是不是 UTF-8 编码和库里存的串是否同一规则。另外注意这个项目登录后的状态多半是保存在一个静态变量或当前窗口中没有 Session 概念每关一个窗口就要重新登录这是 Swing 应用和 Web 应用的一个明显区别。4.4 成绩排名和统计SQL 聚合是重头戏成绩管理系统的核心卖点在“管理”二字最常被问到的问题就是“排名怎么算的”。这类系统的排名逻辑通常不写在 Java 里而是直接写在 SQL 上。比较典型的是按课程统计平均分和排名-- 按课程统计平均分、最高分、最低分 SELECT course_id, COUNT(*) AS total_count, AVG(score) AS avg_score, MAX(score) AS max_score, MIN(score) AS min_score FROM grade GROUP BY course_id; -- 全年级按总分排名用变量实现行号MySQL 8.0 也可以用 RANK() OVER() SELECT student_no, student_name, SUM(score) AS total_score FROM grade GROUP BY student_id ORDER BY total_score DESC;在源码里找到这类 SQL 后你要改排名规则就改这里。比如想只统计必修课就加一个course_type的条件到 WHERE想按班级排名就在 GROUP BY 里再加一个class_id。这里面的坑主要是GROUP BY和ORDER BY的执行顺序先分组再排序所以ORDER BY里只能用聚合函数或分组字段不能用普通字段。如果打开源码发现排名是在 Java 里用Collections.sort做的那就说明作者图省事把数据全捞到内存里再排。数据量小没感觉等你有几千条成绩时就会明显卡顿。改造思路很清晰把比较逻辑从 Java 挪到 SQL用ORDER BY解决Java 侧只负责展示。5. 避坑排查运行 Java 成绩系统时最容易翻车的五处5.1 现象双击 .bin 或命令行启动黑框一闪而过原因程序抛了未捕获异常直接退出最常见的异常是ClassNotFoundException或SQLException。前者是驱动 jar 没放进 classpath后者是数据库没导入脚本或连接参数不对。解决不要双击运行改为命令行启动让它把异常打印出来或者看同目录下有没有日志文件。先确认 MySQL 服务是否启动再确认ConnectionUtil里的 URL、账号、密码和本地一致。我一般会写一个只有三行代码的测试类只调ConnectionUtil.getConnection()能连上再谈界面。5.2 现象MySQL 8.0 连接报Public Key Retrieval is not allowed原因MySQL 8.0 默认使用caching_sha2_password认证插件JDBC 驱动首次连接时需要获取服务器的公钥而 URL 里没放开这个限制。解决在 JDBC URL 末尾加参数allowPublicKeyRetrievaltrueuseSSLfalse这样驱动会自动拉取公钥完成认证。这个报错特别像“玄学错误”不看异常堆栈根本想不到是连接参数的问题。5.3 现象窗口能弹出来但所有中文显示成乱码原因两层可能性。第一层是数据库表或连接字符集不对查询结果出来就是问号第二层是 Java 文件编译时用了错误的编码Swing 界面上直接显示乱码。解决先查数据库执行SHOW CREATE TABLE student;看DEFAULT CHARSET是不是 utf8mb4。连接 URL 里加上characterEncodingutf8。如果数据库没问题那就是源码编译编码问题IDEA 右下角把文件编码切换到 UTF-8重新编译。记住一条源码、连接、数据库三者字符集必须全链路一致断一环就乱。5.4 现象IDEA 里源码报错GlobalSearchDialog找不到符号原因压缩包根目录下的GlobalSearchDialog1.class和GlobalSearchDialogT.class是历史编译产物和.src里的当前源码版本不对应。IDEA 有时会自动把 class 文件加进编译路径导致新旧 API 冲突。解决在 IDEA 里把根目录散落的.class文件从项目里排除掉只保留.src作为源根。具体操作是右键这些 class 文件选择Mark as Plain Text或者在项目结构里把输出路径清理干净。源码里引用的类名如果找不到去.Dialog目录里找同名.java文件那才是源码本体。5.5 现象登录页输入 admin / 123456 一直提示密码错误原因脚本里的初始密码不是 123456或者是加密后的值而源码里的校验逻辑对不上。还有一种可能是users表里根本没写入初始数据查询返回空集。解决先用命令行连 MySQL 查SELECT username, password FROM users;确认有没有账号。再看密码的长度和格式判断是明文还是 MD5。如果是 MD5自己算一个已知明文的 MD5 值UPDATE users SET password你的md5串 WHERE usernameadmin;改完再登录。这类问题多半是脚本和源码版本不是同一批打包出来的改初始密码是最快的兜底方案。6. 进阶改造把三个搜索对话框合并成通用查询组件拆这个包的时候我注意到一个有意思的细节根目录下躺着GlobalSearchDialog.class、GlobalSearchDialog1.class、GlobalSearchDialogT.class三个编译产物。这说明原作者至少迭代了三版“全局搜索”功能但源码里未必保留了三份完整代码。读这种资源最高效的进阶方式不是改业务而是把这个搜索对话框抽成一个通用的查询弹窗。设计思路是搜索对话框不关心你要查哪张表只负责接收一个关键词和一个查询回调。用 Java 的泛型和函数式接口可以把这个组件做得很干净import javax.swing.*; import java.awt.*; import java.util.List; import java.util.function.Function; /** * 通用搜索对话框传一个查询函数返回匹配结果 * 学生按姓名查课程按课程名查成绩按学号查共用这一个窗口 */ public class GenericSearchDialogT extends JDialog { private JTextField keywordField; private JTable resultTable; private FunctionString, ListT searchFunction; public GenericSearchDialog(Frame owner, String title, FunctionString, ListT searchFunction) { super(owner, title, true); this.searchFunction searchFunction; initUI(); } private void initUI() { setSize(600, 400); setLayout(new BorderLayout()); JPanel top new JPanel(); keywordField new JTextField(15); JButton searchBtn new JButton(搜索); top.add(keywordField); top.add(searchBtn); resultTable new JTable(); add(top, BorderLayout.NORTH); add(new JScrollPane(resultTable), BorderLayout.CENTER); // 点击按钮时把关键词丢给调用方定义的查询逻辑 searchBtn.addActionListener(e - { String keyword keywordField.getText().trim(); ListT rows searchFunction.apply(keyword); // 这里根据 T 的具体类型决定怎么填充表格模型 }); } }这个改造的核心价值是把三版重复代码收敛成一个文件。原来的三个 Dialog 各写各的查询逻辑界面结构基本雷同代码维护成本高三倍。换成泛型组件后调用方只需要传入一个 lambda 表达式即可复用// 查学生 new GenericSearchDialog(parent, 学生搜索, keyword - studentDao.searchByName(keyword)).setVisible(true); // 查课程 new GenericSearchDialog(parent, 课程搜索, keyword - courseDao.searchByKeyword(keyword)).setVisible(true);改造完怎么验证不要只点界面上的按钮我习惯直接在 DAO 层写测试入口。项目没引 JUnit那就写一个带main方法的临时测试类直接调searchByName打印结果确认 SQL 对了再去看界面。如果你想把 JUnit 加进来只要把mysql-connector-java和junit两个 jar 放进 classpath 就能跑这个项目的分层结构对单测很友好DAO 不依赖任何 Swing 组件。最后说个血泪教训以前我拿到这种带.class历史产物的资源总是直接忽略结果有次把旧 class 误当成新功能照葫芦画瓢改了好久才发现源码里根本没有对应类。从那以后每次解压这种课程设计资源我都先拉一遍文件清单把.class、._开头文件和源码分开摆放再动手。这套 java 学生成绩管理系统本质上是个能跑通的完整工程SQL 脚本、界面截图、分层源码都齐照着先导库再改代码的顺序来你半天内就能看到登录窗口。希望帮到你。本文还有配套的精品资源点击获取