简介这是一套基于JavaWeb与MySql的学生成绩管理系统完整项目适用于计算机、通信、人工智能、自动化等专业的课程设计、期末大作业或毕业设计。项目中包含前端JSP页面、后端Java控制层与业务层代码、数据库SQL脚本及项目配置文件覆盖了学生、教师、课程、成绩、班级、授课等多模块的增删改查与关联操作整体结构清晰适合JavaWeb入门学习者对照调试也便于有基础者二次扩展功能。资源为zip压缩包共101个文件主要包括32个Java源码、19个JSP页面、3个jar依赖库、MySql建库脚本及相关配置文档压缩包大小约11.57MB解压后即可导入IDE配合MySql运行。该资源来自个人毕设项目答辩评审分达95分代码经过调试确保可运行具备较高的学习借鉴价值。目前已有1074人浏览学习欢迎下载交流适合希望快速掌握SSM或ServletJSP开发模式、以及需要完成成绩管理类课设项目的读者。1. JavaWeb成绩课设拆解先看清这套系统的边界这套基于JavaWebMySQL的学生成绩管理系统代码量不大但链路完整是典型的课设项目形态没有Spring、没有MyBatis用传统的Servlet处理请求、Repository类访问数据库、MySQL存数据。千万别小看这个组合正因为它没有框架帮你把细节藏起来从浏览器发请求到Tomcat分配Servlet、再到JDBC查询成绩数据返回页面每一步都能亲眼看到。项目源码中包含了Grade、Student、Class、Course、Teacher、Teaching六个核心实体及对应的Repository和Servlet覆盖了成绩录入、查询、修改、按条件筛选等课设常见功能。这个资源适合三类人正在选JavaWeb课设题目的大三学生想做毕设又不想用框架堆代码的本科毕业生以及需要一套完整可运行Demo做二次开发基础的程序员。项目自带SQL文件导入数据库、配置连接、丢进Tomcat就能跑。下面按数据层、控制层、部署、优化四个方向拆解重点说清楚每个类该改哪里、每段代码为什么这么写、答辩时怎么往深处讲。2. 数据层设计表结构、Repository与JDBC参数取舍2.1 六张核心表的字段与外键关系成绩管理系统的数据模型核心围绕「学生—课程—成绩」三元关系展开。项目里的六个Repository类正好对应六张表student、class、course、teacher、teaching、grade。这个建模方式很常规但很严谨class和teacher是基础档案teaching表充当班级和教师的关联grade表用外键同时关联student和course构成成绩记录。CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) UNIQUE NOT NULL, name VARCHAR(50) NOT NULL, class_id INT, gender CHAR(2), phone VARCHAR(20) ); CREATE TABLE grade ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT, course_id INT, score DECIMAL(5,2), exam_date DATE, FOREIGN KEY (student_id) REFERENCES student(id), FOREIGN KEY (course_id) REFERENCES course(id) );这段建表语句把最核心的两张表立起来了。DECIMAL(5,2)用于成绩字段最大可以存999.99覆盖百分制和小数分场景UNIQUE约束加在student_no上防止学号重复外键让成绩表必须引用真实存在的学生和课程。可以看到表结构上没有做复杂的三级联动设计班级学生、课程授课都通过外键关联这对课设来说冗余度刚好查询时多表JOIN的复杂度可控。2.2 Repository类的JDBC连接封装项目里既然出现了ClassRepository、StudentRepository这类类名采用的必然是Repository模式的变体——每个类负责一张表的增删改查。在原生JDBC开发中这个角色的核心职责是两件事拿到数据库连接、执行SQL并返回实体对象。我一般在课设代码里用属性文件保存连接参数而不是硬编码在Java代码中这样换环境只需要改一个文件jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/grade_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456连接参数里最值得留意的是serverTimezone和characterEncoding两个参数。前者解决MySQL 8与JDBC驱动之间的时区类型不匹配问题不写会直接报The server time zone value错误后者配合数据库端utf8mb4字符集保证页面录入中文成绩备注时不会变成问号。useSSLfalse是为了跳过安全证书校验单机课设环境不需要加密通道省去一堆SSL警告日志。有了配置项接下来是GradeRepository的典型查询方法实现public class GradeRepository { private String url, user, password; public ListGrade findByStudentId(int studentId) { String sql SELECT * FROM grade WHERE student_id ?; ListGrade list new ArrayList(); try (Connection conn DriverManager.getConnection(url, user, password); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, studentId); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Grade grade new Grade(); grade.setId(rs.getInt(id)); grade.setScore(rs.getBigDecimal(score)); list.add(grade); } } } catch (SQLException e) { e.printStackTrace(); } return list; } }这个方法实现的是「按学号查成绩单」——课设里成绩列表页最常调用的数据接口。代码里最关键的细节是?占位符配合ps.setInt(1, studentId)这比字符串拼接SQL更安全能直接避免传入1 OR 11这类参数时的注入风险。整个方法用try-with-resources包起来连接、预处理对象、结果集在退出时自动关闭不会出现连接泄漏。注意这个类需要它有构造函数从属性文件读取连接配置保证GradeServlet能直接new GradeRepository()使用。2.3 连接复用与查询性能的权衡课设项目通常不需要连接池但如果答辩老师问「为什么不用C3P0或Druid」你要能说出个所以然来。单用户、低并发场景下DriverManager每次请求都建立新连接性能损耗在毫秒级别而引入连接池要配置依赖、管理空闲连接反而增加复杂度。不过有一个中间方案值得考虑用ThreadLocal在单个请求线程内复用连接。即在Repository外面套一层过滤器在请求开始时打开Connection存入ThreadLocal请求结束时关闭并移除。这样同一个Servlet里多次调用Repository不会反复建连代码结构比连接池简单得多。这个方向到第5章再展开讲课设答辩时讲清楚这一层比背「MVC三层架构」有说服力得多。3. Servlet路由与成绩流转URL到Repository的参数链3.1 Servlet路由表注解映射与请求分发项目中的GradeServlet、StudentServlet、TeachingServlet三个控制类承担了所有业务流量的入口分发。在JavaWeb阶段控制层不叫Controller而叫Servlet本质工作是一样的接收请求参数、调用数据层方法、决定跳转哪个页面。用Servlet 3.0的注解方式配置路由比web.xml简洁也是当前课设的主流写法WebServlet(/grade/*) public class GradeServlet extends HttpServlet { private GradeRepository gradeRepository new GradeRepository(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String pathInfo req.getPathInfo(); if (/list.equals(pathInfo)) { String keyword req.getParameter(keyword); req.setAttribute(gradeList, gradeRepository.findByKeyword(keyword)); req.getRequestDispatcher(/grade_list.jsp).forward(req, resp); } else if (/detail.equals(pathInfo)) { int id Integer.parseInt(req.getParameter(id)); req.setAttribute(grade, gradeRepository.findById(id)); req.getRequestDispatcher(/grade_detail.jsp).forward(req, resp); } } }这个路由设计遵循的约定是/grade开头的一级路径标识业务模块/grade/*后面的细节路径标识操作动作。getPathInfo()拿到的正是星号匹配的部分所以/grade/list进入列表逻辑、/grade/detail进入详情逻辑。注意这里把keyword参数直接传给了Repository实际上应该先做非空判断课设可以不那么严谨但要清楚这个位置是参数校验的边界。主要路由映射关系整理如下URL模式Servlet方法功能/student/listStudentServletdoGet学生列表分页查询/student/addStudentServletdoPost新增学生信息/course/listCourseRepository对应的ServletdoGet课程列表展示/grade/listGradeServletdoGet成绩列表与关键字过滤/grade/saveGradeServletdoPost新增或更新成绩记录/teaching/listTeachingServletdoGet授课关系查询3.2 成绩录入链路从表单到Grade表成绩录入是这套系统里最有代表性的业务它穿过完整的「表单→Servlet→Repository→数据库」链路。前端成绩录入页通常是一个表格每行填写学生ID、课程ID和分数提交时通过AJAX或普通form表单POST到/grade/save。Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); int studentId Integer.parseInt(req.getParameter(studentId)); int courseId Integer.parseInt(req.getParameter(courseId)); BigDecimal score new BigDecimal(req.getParameter(score)); Grade grade new Grade(); grade.setStudentId(studentId); grade.setCourseId(courseId); grade.setScore(score); boolean success gradeRepository.save(grade); if (success) { resp.sendRedirect(req.getContextPath() /grade/list); } else { req.setAttribute(error, 成绩保存失败请检查学生和课程是否存在); req.getRequestDispatcher(/grade_edit.jsp).forward(req, resp); } }req.setCharacterEncoding(UTF-8)必须在读任何参数之前调用这是POST中文乱码的经典原因。sendRedirect做重定向浏览器地址栏变成/grade/list用户刷新也不会重复提交表单而forward是服务端跳转地址栏不变表单可能被重复提交。这是课设答辩的高频追问点务必要自己在代码里实测一遍两种行为的差异。3.3 Repository是否存在真正的事务边界这个项目的Repository类各管一张表但跨表操作时事务就容易出问题。比如TeacherServlet在更新任课关系时先删旧记录再插新记录这两步必须在一个事务里完成。原生JDBC的写法是在Connection上做手脚conn.setAutoCommit(false); try { teachingRepository.deleteByClassId(classId); teachingRepository.insertBatch(classId, teacherIds); conn.commit(); } catch (SQLException e) { conn.rollback(); }由于每个Repository私建Connection这个写法只能保证单个Repository内部的原子性。如果不同表的操作分散在多个Repository里就要引入前面提到的ThreadLocal方案来共享同一个连接。课设里最常见的做法还是把跨表写在Servlet里用上述代码硬编码虽然不够优雅但逻辑直白。真正要杜绝的是delete成功后insert抛异常导致数据丢失——现在看只要rollback写在catch块里这一步就守住了。4. 课设跑通MySQL建库、Tomcat部署与典型报错排查4.1 MySQL 8与驱动版本的匹配问题拿到项目源码后第一个坑往往出在数据库环节。源码里自带的SQL脚本可以直接导入但导入前后需要核对MySQL版本。如果你的机器装的是MySQL 8.x驱动类名必须用com.mysql.cj.jdbc.Driver如果是MySQL 5.7及以下则用com.mysql.jdbc.Driver。不少课设代码写的是旧驱动类名在MySQL 8上会直接报ClassNotFoundException或者驱动自动注册失败。版本匹配的正确做法是确认WEB-INF/lib下的mysql-connector-java.jar版本以它为准调整驱动类名。常见的版本对应关系如下MySQL版本推荐连接器JAR驱动类名5.5 / 5.6mysql-connector-java-5.1.47com.mysql.jdbc.Driver5.75.1.49或8.0.xcom.mysql.jdbc.Driver或cj.Driver8.0mysql-connector-java-8.0.30com.mysql.cj.jdbc.DriverMySQL 8还默认使用caching_sha2_password认证插件老版本驱动连不上时会报Unable to load authentication plugin。最省事的办法是建库时指定账号使用mysql_native_passwordALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;这条SQL把root账号的认证方式改回传统密码校验兼容旧连接器。前提是你对root有权限很多课设环境忘了这步Java报错和MySQL客户端正常连接的表现完全不一致查起来极其迷惑。4.2 SQL文件导入的两种正确姿势项目压缩包里的SQL文件通常包含建库、建表、插入测试数据三部分。导入之前先确认文件编码Windows下用记事本另存为UTF-8格式否则表里中文数据会变成乱码。推荐在MySQL命令行客户端里直接执行批量导入mysql -u root -p grade_system.sql命令敲完后输入密码脚本自动执行。符号把SQL文件内容重定向给mysql客户端所有语句顺序执行遇到CREATE DATABASE IF NOT EXISTS会自动建库。如果想只导入到已有库中先执行use grade_system;再sourcesource /path/to/grade_system.sql;我把source命令单独提出来是因为它在Navicat、DataGrip等GUI中对应「运行SQL文件」是图形化工具用户最常用的方式。导入完成后执行SHOW TABLES;确认六张表都出现再SELECT COUNT(*) FROM student;验证测试数据量符合预期。如果表存在但数据为空说明脚本里INSERT语句被某种原因跳过优先检查编码和分号是否有缺失。另外项目代码里写死了数据库名grade_system的话SQL文件里的CREATE DATABASE语句就必须与之一致。这里最容易踩的坑是改了数据库名但忘记改JDBC连接串中的库名导致Unknown database报错。4.3 Tomcat部署与目录结构核对这个项目既然是JavaWeb课设部署环境应该是Tomcat 8.5或9.x。拿到源码后先检查项目目录是否标准src目录放Java源码web目录下要有WEB-INF/web.xml和JSP页面WEB-INF/lib放mysql驱动JAR包。IDE中部署时选择Artifact为war exploded模式这样修改JSP无需重启Tomcat改Java代码也只是热重载调试效率提升明显。回到IDEA中配置Tomcat的步骤我的建议是Run Configuration里新增Tomcat Server LocalApplication Server指向Tomcat安装目录Deployment页签添加项目ArtifactApplication context填/grade_system。启动后访问http://localhost:8080/grade_system/如果看到登录页或首页就说明部署成功。如果URL打不开优先看Tomcat的catalina.out日志重点搜SEVERE和Exception两个关键词。4.4 三个必考的报错排查场景场景一ClassNotFoundException: com.mysql.cj.jdbc.Driver驱动JAR没有正确放进WEB-INF/lib目录。检查项目构建输出路径里是否有classes和lib确认Artifact的WEB-INF下能看到mysql-connector-java.jar。IDEA中右键JAR选择Add as Library不够还要把这个库关联到Web Artifact的输出里。场景二Could not create connection to database server这个报错信息下藏着多种原因连接串IP或端口写错、MySQL服务没启动、防火墙拦截3306端口。用telnet 127.0.0.1 3306先验证MySQL端口是否可通再检查URL里主机名是否写成localhost——注意JDBC URL中的localhost与MySQL授权表中的localhost是两回事如果连接串用了127.0.0.1而授权表只允许localhost也会被拒。场景三页面中文乱码Web容器层和数据库层都要看。Tomcat 8.5之后URIEncoding默认就是UTF-8问题常出在两层JSP页面顶部缺pageEncodingUTF-8或者JDBC连接串缺characterEncodingutf8。排查时按「页面级别→请求级别→存储级别」逐层确认前两处没问题就进MySQL执行SHOW VARIABLES LIKE character_set%;看数据库字符集是否为utf8mb4。提示把排查思路固定为「先验证环境再查代码」比反复改代码重试高效得多。每个报错先回答三个问题驱动在不在、连接通不通、SQL语句对不对。5. 给成绩模块加分的查询技巧统计SQL与分页改造5.1 一条SQL搞定班级平均分与排名答辩时如果只能讲一个亮点我会选成绩统计分析。基础教育表结构已经包含了学生、班级、课程、成绩四个维度写一条聚合SQL就能把班级排名、课程平均分、最高最低分一次性查出来SELECT c.class_name, co.course_name, COUNT(g.id) AS exam_count, ROUND(AVG(g.score), 2) AS avg_score, MAX(g.score) AS max_score, MIN(g.score) AS min_score FROM grade g JOIN student s ON g.student_id s.id JOIN class c ON s.class_id c.id JOIN course co ON g.course_id co.id GROUP BY c.id, co.id ORDER BY avg_score DESC;这个查询做了三张表JOIN再分组返回结果中的每个班级、每门课程一行汇总数据。ROUND(AVG(g.score), 2)控制小数位成绩展示页面直接显示两位小数而不需要前端处理。COUNT(g.id)统计的是有效成绩记录数如果一个学生缺考成绩表里根本没有这条记录统计时不会计入分母这个口径要了然于心。5.2 及格率与分数段分布的聚合写法平均分之外课设里另一个高频考核点是及格率分析。与其在Java里循环逐条判断不如让MySQL一次算完用SUM(CASE WHEN...)做条件计数SELECT c.class_name, COUNT(*) AS total_count, SUM(CASE WHEN g.score 60 THEN 1 ELSE 0 END) AS pass_count, CONCAT(ROUND(SUM(CASE WHEN g.score 60 THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 2), %) AS pass_rate, SUM(CASE WHEN g.score 90 THEN 1 ELSE 0 END) AS excellent_count FROM grade g JOIN student s ON g.student_id s.id JOIN class c ON s.class_id c.id GROUP BY c.id;这里的CASE WHEN在聚合函数内部逐行判断只对满足条件的行累加1最终得到及格人数和优秀人数。pass_rate字段的算法里把分子乘以100.0再除以总数注意一定要写100.0而不是100否则MySQL的整数除法会直接丢弃小数位及格率永远显示0%。整条SQL不需要改动任何Java代码就能在列表页右侧展示统计面板属于投入产出比极高的加分项。5.3 分页查询参数与前端页码回传学生列表和成绩列表的数据量稍大后一次性SELECT * FROM student会把几百条数据全塞进页面性能和观感双输。分页的常规写法是LIMIT offset, size配合页面参数计算偏移量。int page Integer.parseInt(req.getParameter(page)); // 当前页码 int size 10; // 每页条数 int offset (page - 1) * size; String sql SELECT * FROM grade ORDER BY id DESC LIMIT ?, ?; PreparedStatement ps conn.prepareStatement(sql); ps.setInt(1, offset); ps.setInt(2, size);分页的SQL并不复杂真正容易出错的是前端页码回传与总页数计算。总页数需要先执行SELECT COUNT(*) FROM grade得到记录总数再用(total size - 1) / size向上取整得出。页面底部的下一页链接要携带当前页码参数点击上一页时要注意边界处理第一页时禁用上一页按钮最后一页时禁用下一页。如果课设要求加分可以考虑把这套逻辑抽成一个PageUtil类统一接收总记录数、当前页、每页条数输出起始偏移和页数列表——这也正好衔接了第2章提到的ThreadLocal连接复用的改造方向两者合起来就是一套非常完整的面试讲解素材。本文还有配套的精品资源点击获取
