
简介这份资源是面向高校计算机相关专业学生的Java Web课程设计完整方案基于ServletJSPMysql技术栈实现学生成绩管理系统适合作为期末大作业、课程设计或Java Web入门练手项目。压缩包共188个文件约11.29MB包含24个java源文件、42个jsp页面、72个class编译文件、12个xml配置、9个jar依赖及6个sql脚本另附项目报告docx文档源码经本地编译可运行评审分达98分难度适中。系统涵盖学生信息管理、成绩录入查询、管理员与登录权限等核心模块分层结构清晰便于理解MVC设计思路与数据库交互流程。目前已有223人学习下载可帮助读者快速搭建可运行环境、对照报告梳理设计文档并在此基础上完成二次开发与答辩准备。1. 从一份「能跑起来」的学生成绩管理系统说起很多 Java Web 初学者卡在同一个地方语法学完了Servlet 生命周期背得滚瓜烂熟JSP 九个内置对象也能默写但真让自己从零搭一个学生成绩管理系统还是不知道从哪下手。这个标题对应的正是这样一套东西——用原生 Java Servlet JSP MySQL 实现的学生成绩管理系统附带源码和报告。它不依赖 Spring Boot不依赖 MyBatis纯手工写 JDBC、手写 DAO、手写分页是理解 Java Web 底层请求流转最直接的一条路。它适合三类人一是课程设计或毕业设计需要交完整系统的大学生二是准备 Java 开发工程师面试、想把 Servlet 请求链路讲清楚的人三是刚学完 JSP 个人信息展示页面、想找个综合项目练手的自学者。下面我按「环境怎么搭 → 表怎么设计 → 请求怎么流转 → 成绩怎么算 → 坑在哪」的顺序把整套东西拆开讲一遍能直接照着复现。2. 环境搭建与项目骨架别在第一步就翻车2.1 JDK、Tomcat、MySQL 的版本搭配这套技术栈最怕版本乱配。Servlet 和 JSP 的 API 由 Tomcat 提供JDK 版本决定编译级别MySQL 驱动版本决定连接串写法。我一般用下面这套组合稳定且资料多组件推荐版本说明JDK8 或 11JDK 8 兼容性最好JDK 11 需注意模块化Tomcat8.5 或 9.0Tomcat 10 把包名从 javax 改成 jakarta老代码直接报错MySQL5.7 或 8.08.0 驱动类名和时区参数有变化MySQL 驱动5.1.x 配 5.78.0.x 配 8.0版本不匹配会报No suitable driverIDEIDEA 或 EclipseIDEA 需手动配置 Artifact 和 Tomcat这里有个高频翻车点Tomcat 10 之后javax.servlet.*全部变成jakarta.servlet.*网上大部分学生成绩管理系统源码还是javax写法导入 Tomcat 10 会满屏红线。新手直接选 Tomcat 9.0 最省事。2.2 用 Maven 建 Web 项目的最小 pom不建 Maven 也能跑但依赖管理会很难受。用 Maven 的war打包方式把 Servlet API 和 MySQL 驱动声明清楚project xmlnshttp://maven.apache.org/POM/4.0.0 modelVersion4.0.0/modelVersion groupIdcom.example/groupId artifactIdscore-system/artifactId version1.0/version packagingwar/packaging properties maven.compiler.source8/maven.compiler.source maven.compiler.target8/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties dependencies !-- Servlet API由 Tomcat 提供scope 必须是 provided -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- JSP API同样 provided -- dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency !-- JSTL 标签库用于 JSP 页面循环和判断 -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency !-- MySQL 8.0 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependency /dependencies /projectscope写provided是关键Servlet 和 JSP 的 jar 包 Tomcat 自带如果打进 war 包运行时会出现类加载冲突典型现象是启动报ClassCastException或者过滤器不生效。MySQL 驱动则必须打进WEB-INF/lib否则连接数据库时报找不到驱动。2.3 目录结构定下来再写代码标准 Maven Web 目录长这样别自己乱建score-system/ ├── pom.xml └── src/main/ ├── java/com/example/ │ ├── dao/ // 数据访问层 │ ├── entity/ // 实体类 │ ├── servlet/ // 控制层 │ └── util/ // 数据库工具类 └── webapp/ ├── WEB-INF/ │ ├── web.xml │ └── lib/ ├── css/ ├── js/ └── *.jsp // 视图层WEB-INF下的 JSP 不能被浏览器直接访问必须通过 Servlet 转发这是做权限控制的基础。登录校验的过滤器就靠这个特性拦住未登录用户。3. 数据库设计成绩表为什么不能只存一个分数3.1 四张核心表与字段类型学生成绩管理系统的数据模型不复杂但字段类型选错后面全是坑。常见做法是四张表学生表、课程表、成绩表、用户表。CREATE DATABASE score_db DEFAULT CHARACTER SET utf8mb4; -- 学生表 CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, sno VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, name VARCHAR(50) NOT NULL, gender CHAR(2) DEFAULT 男, class_name VARCHAR(50), phone VARCHAR(20) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 课程表 CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, cno VARCHAR(20) NOT NULL UNIQUE COMMENT 课程号, cname VARCHAR(50) NOT NULL, credit DECIMAL(3,1) DEFAULT 0 COMMENT 学分 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 成绩表学生和课程是多对多必须用中间表 CREATE TABLE score ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,2) DEFAULT 0, exam_time DATE, UNIQUE KEY uk_stu_course (student_id, course_id), FOREIGN KEY (student_id) REFERENCES student(id), FOREIGN KEY (course_id) REFERENCES course(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 用户表 CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role VARCHAR(10) DEFAULT student COMMENT admin/teacher/student ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;成绩表用student_id course_id做联合唯一键防止同一个学生同一门课录入两条成绩。分数用DECIMAL(5,2)而不是FLOAT因为浮点数在求和、求平均时会出现89.99999这种玄学结果展示到页面上很难看。3.2 连接串里三个必调参数MySQL 8.0 的连接串和 5.7 不一样写错直接连不上// util/DBUtil.java public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/score_db ?useUnicodetrue // 开启 Unicode中文不乱码 characterEncodingutf8 // 字符集配合库表 utf8mb4 useSSLfalse // 本地开发关掉 SSL否则报 SSL 连接错误 serverTimezoneAsia/Shanghai // 8.0 必须指定时区否则时间差 8 小时 allowPublicKeyRetrievaltrue; // 8.0 密码插件需要 private static final String USER root; private static final String PWD 你的密码; static { try { Class.forName(com.mysql.cj.jdbc.Driver); // 8.0 驱动类名带 cj } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL 驱动加载失败, e); } } public static Connection getConn() throws SQLException { return DriverManager.getConnection(URL, USER, PWD); } }serverTimezone不写插入的exam_time会莫名其妙差 8 小时useSSLfalse不写MySQL 8.0 默认要求 SSL本地没配证书就报SSL connection error。这两个是新手最常踩的坑。驱动类名 5.7 是com.mysql.jdbc.Driver8.0 是com.mysql.cj.jdbc.Driver写错报ClassNotFoundException。3.3 用 JDBC 写一个通用查询DAO 层不要每个方法都重复写连接、关闭。抽一个模板方法出来// dao/BaseDao.java public abstract class BaseDao { // 通用增删改 protected int update(String sql, Object... params) { try (Connection conn DBUtil.getConn(); PreparedStatement ps conn.prepareStatement(sql)) { for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); // 参数下标从 1 开始 } return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException(执行失败: sql, e); } } // 通用查询由子类实现行映射 protected T ListT query(String sql, RowMapperT mapper, Object... params) { ListT list new ArrayList(); try (Connection conn DBUtil.getConn(); PreparedStatement ps conn.prepareStatement(sql)) { for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } try (ResultSet rs ps.executeQuery()) { while (rs.next()) { list.add(mapper.map(rs)); } } } catch (SQLException e) { throw new RuntimeException(查询失败: sql, e); } return list; } public interface RowMapperT { T map(ResultSet rs) throws SQLException; } }用try-with-resources自动关闭连接避免忘记close()导致连接池耗尽。PreparedStatement用?占位符既防 SQL 注入又省去拼接字符串的麻烦。参数下标从 1 开始这是 JDBC 的历史遗留写 0 会报Parameter index out of range。4. Servlet 与 JSP 的请求流转一次成绩录入到底经过了什么4.1 从表单提交到数据库落库的完整链路以「录入成绩」为例一次请求要穿过五层。理解这条链路Servlet 和 JSP 的关系就通了。第一步JSP 页面渲染表单!-- score_add.jsp -- form action${pageContext.request.contextPath}/score?actionadd methodpost 学生 select namestudentId c:forEach items${students} vars option value${s.id}${s.name}${s.sno}/option /c:forEach /select 课程 select namecourseId c:forEach items${courses} varc option value${c.id}${c.cname}/option /c:forEach /select 分数input typenumber namescore step0.5 min0 max100/ button typesubmit提交/button /form${pageContext.request.contextPath}拿到项目上下文路径避免部署路径变化后链接失效。c:forEach是 JSTL 标签比 JSP 脚本片段% %更干净也更容易维护。第二步Servlet 接收并分发// servlet/ScoreServlet.java WebServlet(/score) public class ScoreServlet extends HttpServlet { private final ScoreDao scoreDao new ScoreDao(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action req.getParameter(action); if (list.equals(action)) { req.setAttribute(scores, scoreDao.findAll()); req.getRequestDispatcher(/score_list.jsp).forward(req, resp); } else if (toAdd.equals(action)) { // 预加载下拉框数据 req.setAttribute(students, new StudentDao().findAll()); req.setAttribute(courses, new CourseDao().findAll()); req.getRequestDispatcher(/score_add.jsp).forward(req, resp); } } Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); // POST 中文必须设且要在取参数前 int studentId Integer.parseInt(req.getParameter(studentId)); int courseId Integer.parseInt(req.getParameter(courseId)); double score Double.parseDouble(req.getParameter(score)); scoreDao.insert(studentId, courseId, score); resp.sendRedirect(req.getContextPath() /score?actionlist); } }doGet负责查询和跳转doPost负责写数据。写完之后用sendRedirect重定向而不是forward这是「提交后刷新页面重复提交」的标准解法——重定向让浏览器发起新请求表单数据不会二次提交。第三步DAO 落库// dao/ScoreDao.java public class ScoreDao extends BaseDao { public void insert(int studentId, int courseId, double score) { String sql INSERT INTO score(student_id, course_id, score, exam_time) VALUES(?, ?, ?, CURDATE()) ON DUPLICATE KEY UPDATE score VALUES(score); update(sql, studentId, courseId, score); } public ListScoreVO findAll() { String sql SELECT s.id, st.name AS studentName, c.cname AS courseName, s.score, s.exam_time FROM score s JOIN student st ON s.student_id st.id JOIN course c ON s.course_id c.id ORDER BY s.id DESC; return query(sql, rs - { ScoreVO vo new ScoreVO(); vo.setId(rs.getInt(id)); vo.setStudentName(rs.getString(studentName)); vo.setCourseName(rs.getString(courseName)); vo.setScore(rs.getDouble(score)); vo.setExamTime(rs.getDate(exam_time)); return vo; }); } }ON DUPLICATE KEY UPDATE配合联合唯一键实现「同一学生同一课程重复录入时更新而非报错」。三表 JOIN 把学生名和课程名一起查出来避免在 JSP 里再查一次数据库。4.2 分页查询的两个关键参数成绩列表数据一多必须分页。MySQL 用LIMIT offset, size两个参数要算对public ListScoreVO findByPage(int pageNo, int pageSize) { int offset (pageNo - 1) * pageSize; // 第 1 页 offset 为 0 String sql SELECT ... FROM score s JOIN student st ON s.student_id st.id JOIN course c ON s.course_id c.id ORDER BY s.id DESC LIMIT ?, ?; return query(sql, this::mapRow, offset, pageSize); } public int countAll() { String sql SELECT COUNT(*) FROM score; return query(sql, rs - rs.getInt(1)).get(0); }pageNo从 1 开始offset (pageNo - 1) * pageSize。如果pageNo传 0offset变成负数MySQL 直接报语法错误。总页数用(total pageSize - 1) / pageSize向上取整别用total / pageSize否则最后一页数据会被吞掉。4.3 过滤器统一处理编码和登录校验每个 Servlet 都写setCharacterEncoding太啰嗦用 Filter 统一处理WebFilter(/*) public class EncodingFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); String uri request.getRequestURI(); // 登录页和登录接口放行 if (uri.endsWith(login.jsp) || uri.contains(/login)) { chain.doFilter(req, resp); return; } // 其余请求校验 session if (request.getSession().getAttribute(user) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }WebFilter(/*)拦截所有请求。放行登录相关路径其余检查 session 里有没有用户对象。注意setCharacterEncoding只对 POST 有效GET 请求的中文参数要在 Tomcat 的server.xml里配URIEncodingUTF-8。5. 避坑与排查那些让我加班到凌晨的问题5.1 中文乱码现象、原因、解决现象表单提交后数据库里存的是????或者页面显示æŽå¼。原因三个环节的编码不一致——JSP 页面编码、请求编码、数据库编码。任何一环掉链子都会乱码。解决JSP 顶部写% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %Filter 里request.setCharacterEncoding(UTF-8)数据库和表用utf8mb4连接串加useUnicodetruecharacterEncodingutf8。四处对齐乱码消失。5.2 空指针getParameter返回 null 的连锁反应现象提交表单报NullPointerException堆栈指向Integer.parseInt。原因表单字段名和getParameter里的名字不一致或者复选框未勾选时根本不提交该字段getParameter返回null。解决取参数后先判空再转换。写个工具方法parseInt(req.getParameter(x), 0)默认值兜底。表单name属性和后端参数名逐一核对这是最笨也最有效的办法。5.3 连接泄漏Tomcat 跑一会儿就卡死现象系统刚启动正常操作几十次后所有请求卡住日志报Too many connections。原因某个 DAO 方法里Connection没关或者异常路径下close()没执行。连接池被占满后续请求全部排队。解决全部改用try-with-resources让 JVM 自动关闭。检查所有getConnection()的调用点确保每个都有对应的关闭逻辑。用SHOW PROCESSLIST能看到堆积的睡眠连接。5.4 JSP 页面刷新重复提交现象录入成绩后按 F5同一条数据被插入两次。原因表单提交后直接forward到结果页浏览器地址栏还是 POST 请求刷新就重发。解决写操作完成后一律sendRedirect让浏览器发起新的 GET 请求。这就是 PRG 模式Post-Redirect-Get是 Web 开发的基本功。5.5 Tomcat 启动报 404 或 ClassNotFound现象项目部署后访问任何页面都是 404或者启动日志报ClassNotFoundException: javax.servlet.http.HttpServlet。原因war 包里缺WEB-INF/classes或WEB-INF/lib或者 Servlet API 被打进了 lib 导致冲突。解决IDEA 里检查 Project Structure 的 Artifact 配置确保compile output和依赖库都正确加入。Servlet API 的 scope 必须是provided不能打进 war。6. 成绩统计与导出把系统从「能用」做到「好用」系统能增删改查只是及格线真正拉开差距的是统计和导出。成绩管理最常被要求的功能是按课程算平均分、按学生算总分、导出 Excel。这里讲两个我常用的技巧。第一个是 SQL 层面直接算统计别在 Java 里循环累加。数据库做聚合比应用层快得多-- 每门课程的平均分、最高分、最低分、及格率 SELECT c.cname, ROUND(AVG(s.score), 2) AS avg_score, MAX(s.score) AS max_score, MIN(s.score) AS min_score, ROUND(SUM(CASE WHEN s.score 60 THEN 1 ELSE 0 END) / COUNT(*) * 100, 1) AS pass_rate FROM score s JOIN course c ON s.course_id c.id GROUP BY c.id, c.cname ORDER BY avg_score DESC;CASE WHEN配合SUM算及格人数再除以总数得到及格率。ROUND保留两位小数避免页面上出现一长串小数。这个查询直接映射到一个 VO 类JSP 用c:forEach渲染成表格即可。第二个是导出 Excel。不引第三方库也能做用 CSV 格式Excel 能直接打开protected void export(HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.setContentType(text/csv;charsetUTF-8); resp.setHeader(Content-Disposition, attachment; filenamescores_ System.currentTimeMillis() .csv); // 写 BOM否则 Excel 打开中文乱码 resp.getOutputStream().write(new byte[]{(byte)0xEF, (byte)0xBB, (byte)0xBF}); PrintWriter out resp.getWriter(); out.println(学号,姓名,课程,分数,考试日期); for (ScoreVO vo : new ScoreDao().findAll()) { out.printf(%s,%s,%s,%.1f,%s%n, vo.getSno(), vo.getStudentName(), vo.getCourseName(), vo.getScore(), vo.getExamTime()); } out.flush(); }关键在开头写 UTF-8 BOM 那三个字节不写的话 Excel 打开中文全是乱码这是血泪经验。Content-Disposition的attachment让浏览器弹下载框而不是直接打开。文件名带时间戳避免多次导出覆盖。验证统计结果对不对我有个习惯拿一门课手动算一遍平均分和 SQL 结果对一下。曾经因为AVG把缺考记录分数为 NULL也算进分母导致平均分偏低查了半天才发现要加WHERE score IS NOT NULL。这种问题不手动验一遍根本发现不了。最后说个习惯这套系统写完我会把每个 Servlet 的请求路径、参数、返回页面列一张表贴在项目 README 里。过两周回头看代码这张表比任何注释都管用。原生 Servlet JSP 的项目结构清晰但跳转关系散落在各处不整理迟早迷路。希望帮到你。本文还有配套的精品资源点击获取