
简介这是一套基于ServletJspMySQL实现的学生成绩管理系统完整项目主要面向计算机相关专业毕业设计学生以及需进行Java Web项目实战的开发者。系统将用户划分为管理员、教师、学生三类覆盖学生信息管理、教师信息管理、课程信息管理、开设课程、成绩提交、课程选择与成绩查询等核心功能可直接作为毕业设计或课程设计参考。资源包共166个文件压缩后仅1.53MB包含jsp页面、Java源码、编译后的class文件、数据库脚本以及部分配置与样式文件开发者可借助该项目快速理解三层架构与Servlet/JSP开发流程。目前已有539人学习下载适合正在搭建Web项目、需要完整源码及数据库脚本的Java初学者参考。除核心代码外资源还提供了数据库表设计与关联关系帮助读者梳理业务数据模型便于二次开发与功能扩展。1. 为什么这个“老技术栈”项目依然是学生成绩管理系统的最佳练手选择如果你在搜索引擎里敲下这个标题大概率是正在为课程设计或毕业设计发愁。Servlet、JSP、MySQL这套组合在Spring Boot大行其道的今天确实显得“老气”但正是因为它足够简单、足够直接才成为无数计算机专业学生的第一套完整JavaWeb项目。一个学生成绩管理系统核心就是登录认证、学生信息管理、成绩录入与查询技术边界清晰业务逻辑直白。选择这个方向最大的价值在于你能在一周内真正跑通一个从前端页面到数据库落盘的完整闭环而不是在一堆框架配置里迷失。当你亲手写出一个Servlet处理表单请求、用JSP渲染成绩列表、把数据写进MySQL表时你对JavaWeb的理解会从“背面试题”变成“有手感”。这篇笔记就沿着这个标题的技术点把项目拆开来讲清楚架构怎么搭、数据库怎么建、代码怎么写、部署踩哪些坑以及如何让它从“能跑”变成“能答辩”。2. 从请求到页面ServletJSPMySQL 如何串起一个成绩管理闭环2.1 为什么是这套组合Servlet的定位与JSP的分工逻辑很多新手第一次看这个项目时会被三种技术搞晕Servlet、JSP、HTML到底谁负责什么我的经验是用一句话记住——Servlet管“事”JSP管“看”。浏览器发来一个请求Tomcat根据web.xml或注解找到对应的ServletServlet里写业务逻辑比如从数据库查成绩然后把结果放进request对象里最后转发给JSP页面JSP用Java代码或EL表达式把数据渲染成HTML返回给浏览器。这套分工决定了项目的结构边界。常见做法是一个模块对应一个ServletServlet只做三件事收参数、调DAO、定跳转。至于页面长什么样、表格怎么排那是JSP的事别在Servlet里拼HTML字符串。我看到太多初学者在一个Servlet里既查数据库又拼HTML表格最后代码烂成一锅粥排错时根本分不清是逻辑错误还是标签写错。记住Servlet里出现大段HTML拼接就是设计气味不对的信号。JSP里也不应该出现复杂的业务计算或JDBC代码只保留循环和取值。2.2 三层结构从JSP到Dao到数据库的调用链把这个项目拆成三层来看整个系统的脉络就清晰了。最上层是JSP页面负责展示和收集数据中间层是Servlet扮演控制器角色底层是Dao类和工具类负责JDBC操作MySQL。这也是为什么这个项目特别适合当作入门的原因——它天然符合MVC的思想但又没有Spring MVC那套复杂的前端控制器机制每个组件都在你眼皮底下工作。我一般会给这个项目设计这样一个包结构com.sms.servlet放所有Servlet类com.sms.dao放数据库操作类com.sms.bean放实体类com.sms.util放DBUtil等公共工具类。实体类对应数据库表比如Student类有stuId、stuName、stuClass字段Dao类里写findAll()、findById()、insert()等方法Servlet根据请求参数调用不同Dao方法。这样当你需要加一个新功能比如“按班级查平均分”就知道改哪里——加一个Dao方法加一个Servlet分支再加一个JSP页面不会动到无关代码。2.3 一个请求的完整生命周期以查询成绩为例用查询某个学生的成绩来走一遍完整流程你会对这套技术组合有直观的体感。用户在浏览器地址栏输入/findScore?stuId2024001Tomcat将这个请求交给配好的ScoreServlet。Servlet里先通过request.getParameter(stuId)取出参数然后调用ScoreDao.findScoresByStudentId(stuId)这个方法内部用JDBC去MySQL查询成绩表的记录返回一个ListScore。Servlet把集合放进request作用域再request.getRequestDispatcher(scoreList.jsp).forward(request, response)转发出去。JSP页面里用c:forEach或Java脚本遍历集合将每行成绩渲染成表格的tr。这套链路里新手最容易忽略的是“转发”和“重定向”的区别。转发是服务器内部行为浏览器地址栏不变request里带的数据能直接传给JSP重定向是浏览器重新发一次请求request里的数据会丢失。查询和展示场景一定用转发而登录成功后跳转到主页更合适的是重定向防止刷新页面时重复提交表单。这一条在答辩时被老师问到的概率极高值得提前吃透。3. 数据库先行建库建表与 JDBC 连接配置附可直接执行的 SQL3.1 成绩管理系统的数据模型四张表怎么设计才合理设计数据库是这个项目里最不能跳步的环节。学生成绩管理系统的核心实体是学生、课程、成绩外加登录用户。常见做法是把学生和课程拆成两张基础表成绩表用学生ID加课程ID做联合主键避免一条记录被重复录入。用户表单独放不跟学生表混在一起因为管理员和学生的性质不一样。我见过很多翻车案例把课程名直接写死在成绩表里导致要给某门课改名时得UPDATE几十条数据或者学生表里放了班级名称而不是班级ID导致班级改名时全表遭殃。这种设计缺陷在答辩时很容易被老师追问“第三范式是什么你的表达到第三范式了吗”。所以成绩表里只存student_id和course_id两个外键具体名称通过JOIN去查。这是最稳妥的设计也是面试里常考的主外键关联场景。下面给出可直接执行的建表SQL数据库名用student_grade编码统一用utf8mb4避免中文乱码问题CREATE DATABASE IF NOT EXISTS student_grade DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE student_grade; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role VARCHAR(20) DEFAULT admin ) ENGINEInnoDB COMMENT登录用户表; CREATE TABLE t_student ( id INT PRIMARY KEY AUTO_INCREMENT, stu_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, stu_name VARCHAR(50) NOT NULL, stu_class VARCHAR(50) NOT NULL COMMENT 班级, gender VARCHAR(4) DEFAULT 男 ) ENGINEInnoDB COMMENT学生表; CREATE TABLE t_course ( id INT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(50) NOT NULL UNIQUE, credit INT DEFAULT 0 COMMENT 学分 ) ENGINEInnoDB COMMENT课程表; CREATE TABLE t_score ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,2) NOT NULL, exam_date DATE DEFAULT NULL COMMENT 考试时间, CONSTRAINT fk_score_student FOREIGN KEY (student_id) REFERENCES t_student(id), CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES t_course(id), UNIQUE KEY uk_student_course (student_id, course_id) ) ENGINEInnoDB COMMENT成绩表;3.2 JDBC 连接工具类从 DriverManager 到连接池的演进有了表结构下一步是让Java代码能连上MySQL。常见做法是封装一个DBUtil类把数据库连接参数集中管理所有Dao类都从它这里拿Connection。这样做的好处是将来换数据库端口或密码只用改一处不用满项目搜索DriverManager.getConnection。一个容易踩的坑是驱动类名的版本差异。早期MySQL用的驱动类是com.mysql.jdbc.DriverMySQL 5.7和8.x的驱动包已经换成了com.mysql.cj.jdbc.Driver如果还写老的Tomcat启动时就会报ClassNotFoundException。下面这个工具类配的是JDBC 8.x的写法同时把数据库url里必要的参数也写全了package com.sms.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/student_grade?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USERNAME root; private static final String PASSWORD 你的数据库密码; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USERNAME, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (stmt ! null) { try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn ! null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }这个工具类里有几个参数值得说明。serverTimezoneAsia/Shanghai在MySQL 8.x下必须配否则会报时区错误characterEncodingutf8配合数据库端的utf8mb4能保证中文和生僻字不乱码。useSSLfalse是关掉安全连接本地开发环境没必要为SSL损耗性能。注意PASSWORD变量需要替换成你自己MySQL的密码这是初学者最容易卡住的地方——代码里密码写错控制台报Access denied半天排查不出来。3.3 必会SQL成绩增删改查和JOIN查询的写法表结构和连接都就绪后要在Dao类里写SQL。许多初学者在这里犯的最大错误是在Java字符串里拼SQL参数比如SELECT * FROM t_student WHERE stu_no stuNo 这不仅容易造成SQL注入连SQL里有单引号的数据都会直接报语法错误。正确做法是使用PreparedStatement的占位符让JDBC帮你处理转义。成绩查询最常见的需求是按学生查成绩单这需要三表关联。以下是ScoreDao里核心方法的实现我加了注释方便你对照着修改public ListMapString, Object findScoresByStudentId(int studentId) { ListMapString, Object list new ArrayList(); String sql SELECT s.stu_no, s.stu_name, c.course_name, t.score FROM t_student s JOIN t_score t ON s.id t.student_id JOIN t_course c ON t.course_id c.id WHERE s.id ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, studentId); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { MapString, Object row new HashMap(); row.put(stu_no, rs.getString(stu_no)); row.put(stu_name, rs.getString(stu_name)); row.put(course_name, rs.getString(course_name)); row.put(score, rs.getDouble(score)); list.add(row); } } } catch (SQLException e) { e.printStackTrace(); } return list; }这段代码用的是JDK 7引入的try-with-resources写法Connection、PreparedStatement、ResultSet都会自动关闭不需要手动写close()减少了很多样板代码。MapString, Object承载一行的结果对新手来说不用为每个查询结果新写一个VO类。参数studentId通过ps.setInt()设置这就是PreparedStatement防止SQL注入的关键机制。4. 把增删改查落到代码登录、成绩录入与查询的核心实现4.1 登录模块Session机制与密码处理的两个注意点几乎每个JavaWeb项目都有登录但学生成绩管理系统的登录有两个地方值得认真处理。第一个是密码存储第二个是登录状态的保持与退出。密码绝对不能明文放在数据库里即使这个项目只是课程设计也要用MD5或SHA-256做摘要。你可以写一个MD5Util工具类前端或Servlet里把用户输入的密码加密后再和数据库里的值比对。这样即使数据库泄露密码也不会直接暴露。保持登录状态主要靠Session。用户登录成功后把用户对象放进sessionsession.setAttribute(currentUser, user)然后在需要权限的页面或Servlet里做一个简单的过滤器检查。这里我用一个Filter来拦截未登录访问这是项目里可加分的点也是区分“能跑”和“完整”的标志。退出登录就是session.invalidate()让session失效简单粗暴有效。4.2 成绩录入表单提交到Servlet再落库的完整链路成绩录入是系统的核心业务代码链路最长也是值得写进答辩稿的部分。常见做法是JSP表单里用下拉框选择学生和课程输入分数后提交到ScoreAddServletServlet里做基本校验分数在0~100之间、必填项不能为空再调用Dao方法写入数据库。下面展示Servlet端的关键逻辑这一段你可以直接复用到自己的项目中WebServlet(/addScore) public class ScoreAddServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String studentIdStr request.getParameter(studentId); String courseIdStr request.getParameter(courseId); String scoreStr request.getParameter(score); if (studentIdStr null || courseIdStr null || scoreStr null || scoreStr.trim().isEmpty()) { request.setAttribute(msg, 请完整填写成绩信息); request.getRequestDispatcher(addScore.jsp).forward(request, response); return; } double score Double.parseDouble(scoreStr); if (score 0 || score 100) { request.setAttribute(msg, 成绩必须在0~100之间); request.getRequestDispatcher(addScore.jsp).forward(request, response); return; } ScoreDao dao new ScoreDao(); boolean result dao.insertScore(Integer.parseInt(studentIdStr), Integer.parseInt(courseIdStr), score); if (result) { response.sendRedirect(scoreList.jsp); } else { request.setAttribute(msg, 录入失败可能该学生该课程成绩已存在); request.getRequestDispatcher(addScore.jsp).forward(request, response); } } }这段代码有三个值得记在笔记里的习惯。request.setCharacterEncoding(UTF-8)必须放在所有获取参数的操作前面否则POST表单里的中文会乱码。校验放在业务操作之前这是一种防御式编程思路让非法数据在入口处就被拦截。这里返回错误用的是转发提示信息能通过request.getAttribute(msg)在JSP页面上显示而成功跳转用的是sendRedirect防止用户按F5刷新导致重复插入成绩。4.3 成绩查询与展示JSP中的JSTL循环与空数据处理查询结果的展示页面是JSP如果只用Java脚本片段写% for(...) %页面会极其难看也不利于后续维护。推荐做法是在JSP顶部引入JSTL标签库用c:forEach遍历成绩集合。JSTL需要额外引入两个jar包到WEB-INF/lib目录——jstl.jar和standard.jar许多初学者忘了这一步导致页面报Unable to find taglib错误。下面是一段成绩列表页的核心代码注意处理了集合为空的场景% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % page contentTypetext/html;charsetUTF-8 languagejava % html headtitle成绩列表/title/head body table border1 tr th学号/thth姓名/thth课程/thth分数/th /tr c:forEach varitem items${scoreList} tr tdc:out value${item.stu_no}//td tdc:out value${item.stu_name}//td tdc:out value${item.course_name}//td tdc:out value${item.score}//td /tr /c:forEach /table c:if test${empty scoreList} p暂无成绩记录请先录入。/p /c:if /body /html这里用${item.stu_no}直接取Map里的键值但如果你的查询结果是JavaBean对象就改成${item.stuNo}对应实体类的getter方法。c:if test${empty scoreList}用来处理空集合避免页面上只出现一个表头而没有一行数据体验很糟糕。用c:out输出有个隐藏好处它自动转义HTML标签如果成绩备注里含特殊字符不会破坏页面结构。5. 部署与避坑从Tomcat启动到浏览器出结果的常见问题排查5.1 运行环境的版本匹配Tomcat、JDK、MySQL的三角关系这是最容易被忽视却最容易翻车的地方。我之前带过一个学弟折腾了两天项目跑不起来最后发现是Tomcat 10和JDK 8不兼容——Tomcat 10默认使用Jakarta命名空间Servlet类的包名从javax.servlet换成了jakarta.servlet旧项目代码里的import全部失败。如果你拿到的源码是javax.servlet开头的请务必使用Tomcat 9或更早版本这是血泪经验。JDK版本同理。很多教材上的项目基于JDK 8编写如果你用JDK 17运行旧代码可能遇到模块化限制报错。建议直接用JDK 8配合Tomcat 9这是JavaWeb课程设计最稳的组合。MySQL版本则建议用5.7或8.0要注意JDBC驱动jar包必须和MySQL版本匹配用mysql-connector-java8.x驱动去连MySQL 5.7通常没问题反之用5.1的驱动连MySQL 8.0则可能报认证方式错误。5.2 常见问题一数据库连接报错 Access denied 或 Communications link failure这个报错出现的频率极高现象是Tomcat控制台打印Access denied for user rootlocalhost或Communications link failure。第一种是因为密码错误或用户权限不足第二种是MySQL服务没启动或URL里的端口写错了。排查顺序很有讲究先打开命令行敲mysql -u root -p确认能正常登录排除账号密码问题然后执行SHOW VARIABLES LIKE port确认MySQL监听端口是3306还是别的最后检查DBUtil类里的URL、USERNAME、PASSWORD三个常量有没有写错。我见过一个案例是某人电脑上装了3306端口被占用MySQL实际跑在3307URL没改导致一直连接失败。MySQL 8.x还有一个容易忽略的坑如果提示Public Key Retrieval is not allowed需要在JDBC URL末尾加上allowPublicKeyRetrievaltrue这是新版驱动对caching_sha2_password认证插件的额外要求。5.3 常见问题二页面中文乱码到底改哪三处中文乱码在JSP项目里的出现概率接近百分之百通常由三个层面的编码不一致导致。第一层是页面层面JSP文件头部的pageEncoding要设为UTF-8IDE的文件编码也要改为UTF-8否则源文件在编译时字符串已经成了乱码。第二层是请求层面POST表单需要request.setCharacterEncoding(UTF-8)在Servlet的doPost第一行强制执行。第三层是数据库层面建表时指定utf8mb4JDBC连接URL带characterEncodingutf8。三层都对了中文才稳稳落库。需要特别注意别在JSP里写%request.setCharacterEncoding(UTF-8);%这种写法虽然能生效但很丑陋也不符合代码分层。注意有时你改了代码仍乱码问题出在浏览器缓存。强制刷新页面CtrlF5后再看如果仍然乱码去Tomcat的conf/server.xml里给Connector加上URIEncodingUTF-8这是针对GET请求参数的编码设置。5.4 常见问题三404、500和页面路径找不到的排查套路页面404分两种情况Servlet映射路径配错导致的404和JSP文件物理路径访问不到导致的404。常用排查方法是在浏览器地址栏直接访问一个不带Controller层的静态JSP比如/login.jsp如果它能打开而/login这个Servlet路径404说明问题出在注解或web.xml配置上。WebServlet注解的urlPatterns要写完整比如/login而不是login漏掉前导斜杠是打包后部署才报404的常见原因。遇到500错误时不要慌Tomcat会打印异常栈最常见的两类是ClassNotFoundExceptionjar包缺失和NullPointerExceptionre.forword时request属性对不上名字。重点看异常栈最后几行Caused by顺着包名和行号定位基本能快速解决。6. 让项目“能答辩”成绩统计SQL、分页与数据校验的进阶改造6.1 平均分与排名一组SQL让你的项目比同学的高半级多数学生的成绩管理系统只做到增删改查如果你的项目能额外展示课程平均分、班级排名答辩时的观感会完全不一样。常见做法是新增一个统计页面用SQL的聚合函数和窗口函数完成计算再在JSP里做可视化展示。拿平均分来说一条SQL就能按课程聚合SELECT c.course_name, COUNT(t.id) AS exam_count, ROUND(AVG(t.score), 2) AS avg_score, MAX(t.score) AS max_score, MIN(t.score) AS min_score FROM t_course c LEFT JOIN t_score t ON c.id t.course_id GROUP BY c.id, c.course_name ORDER BY avg_score DESC;这里用LEFT JOIN而不是INNER JOIN的目的是——即使某门课还没有任何成绩记录也要在统计结果里显示出来平均分显示为NULL。ROUND(...,2)让显示更清爽ORDER BY avg_score DESC让课程按平均分从高到低排。排名则是用窗口函数适合MySQL 8.0和5.7的8.0版本老版本不支持RANK()。如果你用的MySQL是5.6排名就只能用自连接或临时变量模拟折腾劲很大建议直接升级到8.0。6.2 分页查询不要让一百条成绩一次刷出来数据量小的时候一次性查出来没感觉但答辩时如果老师随手录入了上百条测试数据列表页加载会明显变慢这时候分页就派上了用场。算法不复杂先算出总记录数再根据当前页码拼LIMIT偏移量查询当前页数据。以下是Dao里分页方法的两个关键SQL-- 计算总记录数 SELECT COUNT(*) FROM t_score; -- 查询第page页的数据pageSize设为10 SELECT * FROM t_score ORDER BY id DESC LIMIT ?, ?;Servlet端需要接收page和pageSize两个参数默认值分别设为1和10LIMIT的起始位置是(page - 1) * pageSize。JSP页面底部渲染上一页、下一页按钮时要在URL里带上页码参数。这里有个细节坑LIMIT的偏移量计算必须用int类型做运算后再传给PreparedStatement直接在SQL里写LIMIT (page-1)*10, 10是语法错误MySQL不支持在LIMIT里做表达式运算。鄙人当年就在这里翻过车。6.3 数据校验的最后一公里拦截非法分数和重复录入成绩录入模块里我已经写了分数范围校验但那只是前端和后端的第一道防线。一个完整的校验体系应该是三层HTML5的min和max属性做第一层即时提示Servlet做第二层业务校验数据库的约束做最后兜底。其中数据库层的校验最容易被忽略——你在t_score表里建了UNIQUE KEY就算Servlet的逻辑有漏洞重复数据也无法插入。另外建议给成绩录入做一个“二次确认”的交互。录完一条成绩后JSP页面弹出提示“已成功录入XXX的YYY课程成绩XX分”这样能防止连续录入时看错了行。这个小互动成本极低但答辩演示时观感很好让人觉得你确实站在使用者的角度考虑过问题。最后一个隐藏加分项是把系统中的SQL都打印到控制台在log4j或简单的System.out.println(sql)里输出答辩时被问到“这个操作执行了什么SQL”直接指着控制台说“就是这一条”比空口解释有说服力得多。回想我自己第一次做这个项目时最深刻的教训就是“不要一上来就写代码”。先把数据库表结构画清楚再把Servlet转发关系列一遍写代码的速度反而最快。这套学生成绩管理系统真正做完一遍你对Servlet的生命周期、Session机制、JDBC操作和SQL编写都会建立起肌肉记忆而这些恰恰是后续学框架时最需要的地基。希望这篇拆解能帮你把项目从“能跑”推到“能讲”少走我当年走过的弯路。本文还有配套的精品资源点击获取