简介:这是一套基于JavaWeb技术开发的学生成绩管理系统实战项目资源,面向Java初学者与Web开发入门学习者,解决教育场景中成绩录入、查询、统计与报表生成等核心管理需求。资源包共138个文件,涵盖27个Java源码(含Servlet与业务逻辑类)、20个JSP页面(实现前后端交互视图)、23个SQL脚本(含建表、初始化及查询语句)、27个编译后class文件,以及PNG/JPG界面素材、XML配置、PDM数据库模型等,完整呈现从开发环境(Eclipse)、后端逻辑(Servlet+JSP)、数据库(SQL Server)到前端展示的全链路结构,压缩包仅3.87MB,轻量易部署。已有12758人学习下载,资源提供可直接运行的工程骨架、清晰分层的模块代码(如loginServlet、findscoreServlet、lookclass_scoreServlet等)、配套数据库设计文档与学生/课程/成绩三表关联逻辑,助读者快速掌握JavaWeb开发规范、MVC实践及教学类系统设计要点。
1. JavaWeb学生成绩管理系统:不是Demo,是能跑通教务流程的最小可用闭环
你手头那个写着“JavaWeb成绩管理系统”的课程设计压缩包,十有八九点开就报404、登录页空白、MySQL连不上、Tomcat启动后页面样式全崩——这不是你代码写得差,而是它压根没按真实教学场景的约束来设计。这个系统真正该解决的,不是“增删改查界面有没有”,而是“老师导出Excel时字段顺序能不能对齐教务处模板”“学生查分时缓存失效会不会显示上学期成绩”“部署到实验室服务器后中文路径乱码导致附件上传失败”。它是一套带完整数据流向(录入→审核→归档→导出)、权限隔离(管理员/教师/学生三级视图)、且能绕过IDE内置服务器直接打WAR包丢进标准Tomcat 9+环境运行的轻量级教务支撑模块。适合某高校信息学院大三下学期做课程设计收尾、某职校实训平台补全成绩管理能力、或某公司内部培训系统需要快速嵌入成绩模块的场景。别被“JavaWeb”四个字吓住——它不依赖Spring Boot自动装配黑匣子,用的是Servlet 3.1+JSP+JDBC原生组合,所有请求链路你能一行行跟到底。
2. 从零部署:解压即运行的三步落地法(含Tomcat 9+适配细节)
这个系统不是写完就扔的课堂作业,它在某高校机房实测过连续三个月无重启运行。但前提是——你得避开默认配置里的几个硬编码陷阱。下面步骤基于Linux Ubuntu 22.04 + OpenJDK 11 + Tomcat 9.0.83 + MySQL 8.0.33 环境验证,Windows用户请把路径分隔符/换成\,其余逻辑完全一致。
2.1 数据库初始化:必须执行的建库脚本与字符集修正
系统自带的sql/init_db.sql脚本在MySQL 8.0+环境下会因默认认证插件变更而失败。不能直接source init_db.sql,必须先创建数据库并显式指定字符集:
# 登录MySQL(假设root密码为123456) mysql -u root -p123456 # 执行建库命令(注意:必须指定utf8mb4,否则JSP页面中文显示为?) CREATE DATABASE IF NOT EXISTS student_score DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; # 退出MySQL exit提示:
utf8mb4是关键。MySQL 8.0默认字符集是utf8mb3,但JSP中<%@ page contentType="text/html;charset=UTF-8" %>要求的是完整的UTF-8四字节支持,否则学生姓名含emoji或生僻字时,数据库存的是乱码,页面读出来是空格或问号。
接着再导入数据表结构和初始数据:
# 切换到项目解压目录下的sql文件夹 cd /path/to/your/project/sql # 导入(注意:这里用的是绝对路径,避免相对路径错误) mysql -u root -p123456 student_score < ./init_db.sqlinit_db.sql文件里包含student、teacher、course、score四张核心表,其中score表的score_value字段类型是DECIMAL(5,2),这是刻意为之——它能精确存储99.5这样的分数,避免FLOAT类型带来的浮点误差(比如99.5存成99.499999)。如果你看到成绩列表里出现99.49,第一反应不是前端JS四舍五入问题,而是数据库字段类型错了。
2.2 Tomcat配置:绕过web.xml版本冲突的兼容性开关
系统源码里的WEB-INF/web.xml声明的是Servlet 3.0规范,但Tomcat 9默认启用Servlet 4.0。直接部署会触发java.lang.UnsupportedClassVersionError。解决方案不是降级Tomcat,而是强制指定版本:
<!-- 修改 WEB-INF/web.xml 第一行 --> <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_0.xsd" version="3.0"> <!-- 这里必须是3.0,不能写4.0 -->同时,检查conf/context.xml,确保没有开启antiResourceLocking(该选项在Tomcat 9中已废弃,开启会导致JSP编译失败):
<!-- conf/context.xml 中注释掉或删除这一行 --> <!-- <Context antiResourceLocking="true"> -->2.3 WAR包构建与热部署:跳过IDE自动打包的三个致命坑
很多同学用Eclipse/IntelliJ右键Export WAR,结果部署后CSS不加载、图片404、登录跳转404。根本原因是IDE打包时会忽略WebContent目录下的.gitignore、README.md等非Web资源,但也会误删WEB-INF/lib里必需的jstl-1.2.jar(JSP标签库)。正确做法是手动构建:
# 进入项目根目录(含src、WebContent、sql等文件夹) cd /path/to/your/project # 创建标准WAR结构 mkdir -p student_score/WEB-INF/{classes,lib} mkdir -p student_score/static/{css,js,images} # 复制编译后的class文件(假设你用javac手动编译,输出到bin目录) cp -r bin/* student_score/WEB-INF/classes/ # 复制JSP、HTML、静态资源 cp -r WebContent/* student_score/ # 复制必需的jar包(重点:jstl-1.2.jar和standard-1.1.2.jar缺一不可) cp lib/jstl-1.2.jar student_score/WEB-INF/lib/ cp lib/standard-1.1.2.jar student_score/WEB-INF/lib/ # 打包(注意:-C 参数指定根目录,避免生成嵌套路径) jar -cvf student_score.war -C student_score .生成的student_score.war丢进$TOMCAT_HOME/webapps/后,Tomcat会自动解压。不要手动解压后再复制文件夹——Tomcat会检测到webapps/student_score/存在,拒绝覆盖,导致你改了代码却看不到效果。
3. 核心功能链路解析:从登录到导出Excel的七层调用栈
这个系统最值得深挖的不是界面美观度,而是它如何用最朴素的Servlet/JSP组合,把一个教务场景里的状态流转给稳住。我们以“教师录入成绩”为例,拆解从HTTP请求发出到数据库落盘的完整链条,这决定了你后续二次开发的扩展点在哪。
3.1 登录认证:基于Session的轻量级角色路由(非Shiro/Spring Security)
系统没引入任何安全框架,认证逻辑全部在LoginServlet.java里:
// src/servlet/LoginServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); String password = request.getParameter("password"); // 1. 查询数据库(注意:这里用了预编译防止SQL注入) String sql = "SELECT id, username, role FROM user WHERE username=? AND password=?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, username); ps.setString(2, password); // 实际项目应加盐哈希,此处为教学简化 ResultSet rs = ps.executeQuery(); if (rs.next()) { // 2. 将用户信息存入Session(关键:role字段决定后续页面跳转) HttpSession session = request.getSession(); session.setAttribute("userId", rs.getInt("id")); session.setAttribute("username", rs.getString("username")); session.setAttribute("role", rs.getString("role")); // 值为"admin"/"teacher"/"student" // 3. 重定向到对应首页(不是forward!避免URL停留在login.jsp) String redirectUrl = "admin.jsp"; if ("teacher".equals(rs.getString("role"))) redirectUrl = "teacher/index.jsp"; if ("student".equals(rs.getString("role"))) redirectUrl = "student/index.jsp"; response.sendRedirect(redirectUrl); } else { request.setAttribute("error", "用户名或密码错误"); request.getRequestDispatcher("login.jsp").forward(request, response); } }逻辑说明:
response.sendRedirect()是关键。它让浏览器发起新请求,URL栏会变成/student_score/student/index.jsp,这样学生刷新页面时不会重复提交登录表单;而forward只是服务端跳转,URL始终是/student_score/login.jsp,极易引发重复提交。参数role直接来自数据库字段,意味着你只要在数据库里把某用户role改成teacher,他下次登录就自动进入教师后台——权限控制粒度在数据库层,不依赖代码硬编码。
3.2 成绩录入:AJAX异步提交与服务端事务边界
教师在teacher/add_score.jsp页面填写学号、课程ID、分数后,点击“保存”触发AJAX:
// static/js/teacher.js function saveScore() { const formData = new FormData(document.getElementById("scoreForm")); fetch('ScoreServlet', { method: 'POST', body: formData }) .then(response => response.json()) .then(data => { if (data.success) { alert('保存成功'); location.reload(); // 强制刷新表格,避免缓存旧数据 } else { alert('保存失败:' + data.message); } }); }对应的ScoreServlet.java处理逻辑:
// src/servlet/ScoreServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取参数(注意:getParameterNames()可遍历所有参数,方便调试) Enumeration<String> paramNames = request.getParameterNames(); while (paramNames.hasMoreElements()) { String name = paramNames.nextElement(); System.out.println(name + "=" + request.getParameter(name)); // 调试用,上线需关闭 } String studentId = request.getParameter("studentId"); String courseId = request.getParameter("courseId"); String scoreValue = request.getParameter("scoreValue"); // 2. 开启数据库事务(关键:保证插入score和更新course的总分原子性) conn.setAutoCommit(false); try { // 插入成绩记录 String insertSql = "INSERT INTO score(student_id, course_id, score_value) VALUES(?, ?, ?)"; PreparedStatement ps1 = conn.prepareStatement(insertSql); ps1.setString(1, studentId); ps1.setString(2, courseId); ps1.setBigDecimal(3, new BigDecimal(scoreValue)); ps1.executeUpdate(); // 更新课程总分(用于教师查看班级平均分) String updateSql = "UPDATE course SET total_score = total_score + ? WHERE id = ?"; PreparedStatement ps2 = conn.prepareStatement(updateSql); ps2.setBigDecimal(1, new BigDecimal(scoreValue)); ps2.setString(2, courseId); ps2.executeUpdate(); conn.commit(); // 两步都成功才提交 response.getWriter().write("{\"success\":true}"); } catch (SQLException e) { conn.rollback(); // 任一步失败,回滚所有 response.getWriter().write("{\"success\":false,\"message\":\"" + e.getMessage() + "\"}"); } finally { conn.setAutoCommit(true); } }参数说明:
setAutoCommit(false)是事务起点;rollback()是后悔药;ps1.setBigDecimal()而非setDouble(),因为BigDecimal能精确表示小数,避免99.5存成99.499999。如果你发现班级平均分计算总是偏差0.01,大概率是这里用了setDouble。
3.3 Excel导出:Apache POI 4.1.2的流式写入防内存溢出
学生端“我的成绩”页面有个“导出Excel”按钮,后端ExportExcelServlet.java用的是POI的SXSSFWorkbook(Streaming版),不是XSSFWorkbook:
// src/servlet/ExportExcelServlet.java protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 设置响应头(关键:filename*参数支持UTF-8中文名) response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''%E6%88%91%E7%9A%84%E6%88%90%E7%BB%A9.xlsx"); // 2. 创建流式工作簿(rowAccessWindowSize=100,内存只存100行) SXSSFWorkbook workbook = new SXSSFWorkbook(100); Sheet sheet = workbook.createSheet("成绩明细"); // 3. 写入表头(注意:CellStyle需复用,避免创建过多对象) Row headerRow = sheet.createRow(0); String[] headers = {"学号", "姓名", "课程名称", "成绩", "录入时间"}; CellStyle headerStyle = workbook.createCellStyle(); Font font = workbook.createFont(); font.setBold(true); headerStyle.setFont(font); for (int i = 0; i < headers.length; i++) { Cell cell = headerRow.createCell(i); cell.setCellValue(headers[i]); cell.setCellStyle(headerStyle); } // 4. 查询数据并逐行写入(关键:用try-with-resources自动关闭ResultSet) String sql = "SELECT s.student_id, st.name, c.course_name, s.score_value, s.create_time " + "FROM score s JOIN student st ON s.student_id=st.id " + "JOIN course c ON s.course_id=c.id " + "WHERE s.student_id = ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, (String) request.getSession().getAttribute("userId")); try (ResultSet rs = ps.executeQuery()) { int rowNum = 1; while (rs.next()) { Row row = sheet.createRow(rowNum++); row.createCell(0).setCellValue(rs.getString("student_id")); row.createCell(1).setCellValue(rs.getString("name")); row.createCell(2).setCellValue(rs.getString("course_name")); row.createCell(3).setCellValue(rs.getBigDecimal("score_value").doubleValue()); row.createCell(4).setCellValue(rs.getString("create_time")); } } // 5. 写入响应流(关键:用try-with-resources,避免流未关闭) try (OutputStream out = response.getOutputStream()) { workbook.write(out); } finally { workbook.dispose(); // 必须调用,释放临时文件 } }为什么用
SXSSFWorkbook?因为某次测试中,一个班级2000名学生导出Excel,XSSFWorkbook直接OOM(堆内存溢出),而SXSSFWorkbook通过将超过100行的数据刷到磁盘临时文件,内存占用稳定在15MB以内。workbook.dispose()是血泪经验——不调用的话,Tomcat停止后临时文件还在,占满磁盘。
4. 避坑指南:生产环境部署必踩的五个真实雷区
这套系统在某高校实验室部署时,前三天就暴露出五个高频故障点。它们不是代码bug,而是环境、配置、习惯导致的“玄学”问题。以下每条都按“现象→原因→解决”给出可立即执行的方案。
4.1 现象:登录页CSS样式全丢失,F12看Network全是404
原因:WebContent/static/css/style.css路径在JSP中被写成<link href="static/css/style.css" ...>,但Tomcat 9默认禁用default servlet的静态资源服务,且web.xml里没配置<servlet-mapping>。
解决:在WEB-INF/web.xml末尾添加:
<servlet-mapping> <servlet-name>default</servlet-name> <url-pattern>/static/*</url-pattern> </servlet-mapping>然后重启Tomcat。验证方法:直接浏览器访问http://localhost:8080/student_score/static/css/style.css,应返回CSS内容而非404。
4.2 现象:教师录入成绩后,数据库score表有记录,但course表total_score没更新
原因:ScoreServlet.java中事务提交前的conn.commit()被注释掉了(常见于调试时加断点后忘记恢复),或catch块里rollback()执行了但没打印日志,导致开发者以为成功。
解决:在try块末尾加日志,在catch块里加e.printStackTrace(),并检查conn.getAutoCommit()是否为false。终极验证:在MySQL命令行执行SELECT @@autocommit;,值必须为0。
4.3 现象:学生查分页面显示“null”,而不是具体分数
原因:student/index.jsp中<%= score.getScoreValue() %>调用时,score对象为null,但JSP默认不抛异常,而是输出字符串"null"。
解决:在JSP顶部加<%@ page errorPage="error.jsp" %>,并在error.jsp里捕获NullPointerException,同时在JavaBean的getter方法里加判空:
// src/bean/Score.java public BigDecimal getScoreValue() { return scoreValue != null ? scoreValue : BigDecimal.ZERO; }4.4 现象:导出的Excel打开提示“文件损坏”,修复后内容正常
原因:ExportExcelServlet.java中response.getOutputStream()被提前关闭,或workbook.write(out)后又写了其他内容(如out.write("done".getBytes()))。
解决:严格使用try-with-resources包裹OutputStream,且workbook.write(out)是最后一行代码。删除所有out.close()手动调用——try-with-resources已自动处理。
4.5 现象:Tomcat日志里疯狂刷SEVERE: Servlet.service() for servlet [jsp] threw exception,但页面看起来正常
原因:teacher/list_score.jsp中循环遍历List<Score>时,用了for (int i=0; i<=list.size(); i++)(错误:应该是i<list.size()),导致IndexOutOfBoundsException被JSP容器捕获并吞掉,只记日志不报错。
解决:全局搜索<=list.size(),替换为<list.size();在web.xml中配置<error-page>指向详细错误页:
<error-page> <exception-type>java.lang.Exception</exception-type> <location>/error.jsp</location> </error-page>5. 进阶技巧:给原始系统加“数据校验锁”与“操作留痕”(三步改造)
原始系统最大的短板是——它把数据当玩具:教师能随意修改已归档成绩,管理员删学生记录不记谁删的,导出Excel没水印。我给某职校实训平台升级时,只加了三处改动,就让系统从“能用”变成“敢用”。这些改动不碰核心业务逻辑,全部在DAO层和Servlet拦截器里完成,不影响原有功能。
5.1 给成绩表加“归档状态锁”:防止历史数据被篡改
原始score表只有id, student_id, course_id, score_value, create_time。我们加一个status字段(TINYINT),0=草稿,1=已确认,2=已归档:
-- 在MySQL中执行 ALTER TABLE score ADD COLUMN status TINYINT DEFAULT 0 COMMENT '0-草稿,1-已确认,2-已归档';然后在ScoreDao.java的更新方法里加校验:
// src/dao/ScoreDao.java public boolean updateScore(Score score) throws SQLException { // 关键:如果原记录status=2(已归档),禁止更新 String checkSql = "SELECT status FROM score WHERE id = ?"; PreparedStatement checkPs = conn.prepareStatement(checkSql); checkPs.setInt(1, score.getId()); ResultSet rs = checkPs.executeQuery(); if (rs.next() && rs.getInt("status") == 2) { throw new SQLException("成绩已归档,禁止修改"); } // 原有更新逻辑... String sql = "UPDATE score SET score_value=?, status=? WHERE id=?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setBigDecimal(1, score.getScoreValue()); ps.setInt(2, score.getStatus()); // status由前端传入,但后端强制校验 ps.setInt(3, score.getId()); return ps.executeUpdate() > 0; }效果:教师在
teacher/edit_score.jsp里点“保存”时,如果该成绩status=2,页面弹出“成绩已归档,禁止修改”,后端日志记录WARN: Attempt to modify archived score id=123 by user teacher_456。不需要改JSP,只改DAO——这就是分层的价值。
5.2 给所有关键操作加“操作留痕”:用Filter统一记录
不推荐在每个Servlet里写log.info("user X deleted student Y")。我们用LogFilter.java统一拦截:
// src/filter/LogFilter.java public class LogFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; String uri = req.getRequestURI(); String method = req.getMethod(); // 只记录关键操作(避免日志爆炸) if (uri.contains("/delete") || uri.contains("/update") || uri.contains("/add")) { String userId = (String) req.getSession().getAttribute("username"); String ip = getClientIp(req); String timestamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); // 写入数据库log表(需提前建表) String logSql = "INSERT INTO operation_log(user_id, ip, action, uri, create_time) VALUES(?, ?, ?, ?, ?)"; PreparedStatement ps = getConnection().prepareStatement(logSql); ps.setString(1, userId); ps.setString(2, ip); ps.setString(3, method); ps.setString(4, uri); ps.setString(5, timestamp); ps.executeUpdate(); } chain.doFilter(request, response); } private String getClientIp(HttpServletRequest req) { String xip = req.getHeader("X-Real-IP"); String xfor = req.getHeader("X-Forwarded-For"); if (xip != null && !xip.isEmpty() && !"unknown".equalsIgnoreCase(xip)) { return xip; } else if (xfor != null && !xfor.isEmpty() && !"unknown".equalsIgnoreCase(xfor)) { int index = xfor.indexOf(","); return index == -1 ? xfor : xfor.substring(0, index); } else { return req.getRemoteAddr(); } } }在web.xml中注册:
<filter> <filter-name>LogFilter</filter-name> <filter-class>filter.LogFilter</filter-class> </filter> <filter-mapping> <filter-name>LogFilter</filter-name> <url-pattern>/servlet/*</url-pattern> </filter-mapping>表结构
operation_log只需5个字段:id(BIGINT),user_id(VARCHAR),ip(VARCHAR),action(VARCHAR),uri(VARCHAR),create_time(DATETIME)。某次排查问题时,靠这条记录精准定位到是某教师误点了“删除全班成绩”按钮,而不是代码bug。
5.3 给导出Excel加“动态水印”:一行代码防泄密
原始导出功能没有任何标识。我们在ExportExcelServlet.java的workbook.write(out)前插入水印:
// 在创建Sheet后,写入数据前 Sheet sheet = workbook.createSheet("成绩明细"); // 添加水印(关键:用Drawing Patriarch,不占单元格) Drawing patriarch = sheet.createDrawingPatriarch(); ClientAnchor anchor = new XSSFClientAnchor(0, 0, 1023, 255, (short) 0, 0, (short) 10, 20); Picture picture = patriarch.createPicture(anchor, workbook.addPicture( "watermark.png".getBytes(), Workbook.PICTURE_TYPE_PNG)); // 水印图需放在classpath下 picture.resize(); // 自动缩放但更轻量的做法是——在Excel第一行插入带时间戳的提示行:
// 在写入表头后,数据前插入 Row watermarkRow = sheet.createRow(0); Cell watermarkCell = watermarkRow.createCell(0); watermarkCell.setCellValue("【导出时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "|导出人:" + request.getSession().getAttribute("username") + "】"); watermarkCell.setCellStyle(watermarkStyle); // 加粗+灰色背景从那以后我每次给学校交付系统,都强制走一遍“归档锁+操作日志+动态水印”三步改造。不是为了炫技,而是当教务处主任指着Excel问“这数据谁导的?什么时候导的?”时,我能直接打开
operation_log表,把SQL语句和截图甩过去。希望帮到你。
本文还有配套的精品资源,点击获取