☰
Java Web学生成绩系统实战:从Servlet到教务级部署
2026/10/7 15:34:18 网站建设 项目流程

简介:本资源是一套面向计算机与软件工程专业本科生的毕业设计实战项目,聚焦教育信息化场景,提供学生成绩管理分析系统的完整开发方案,解决高校及中小学教务部门在成绩录入、查询、统计与报表生成中的典型业务痛点。资源包共388个文件,含170个Java业务逻辑代码、75个JSP页面模板、72个核心依赖JAR包、22个PNG界面图标及配套SQL建表脚本、系统说明书与两份Word格式论文(含设计与实现全过程),整体41.41MB,结构规范、注释清晰,便于二次开发与教学复现。已有53人学习下载,适合毕业设计选题参考、Java Web技术栈综合实践及MVC分层架构理解。读者可直接部署运行,掌握Struts+Spring+JSP+jQuery技术整合要点,复用源码模块,深入理解成绩分析图表生成逻辑与多角色权限控制实现细节。

1. 为什么一个“学生成绩管理分析系统”值得用 Java Web 重做一遍?——不是为了交作业,而是练出能扛住真实教务压力的工程手感

你手头可能正压着一份课程设计文档,标题写着《基于Java Web技术的学生成绩管理分析系统设计与实现(源码+论文)》,但真正让你犹豫的不是“能不能做完”,而是“做完之后,它到底算不算一个能跑在真实教室、教研组、教务处环境里的系统?”——很多同学交完就删,因为登录页卡顿、导出 Excel 崩溃、查 2000 名学生时页面白屏、老师改个成绩要等 8 秒……这些不是“小问题”,是 Java Web 工程落地时最真实的毛细血管级堵点。这个项目本质是一次面向教学业务闭环的轻量级企业级训练:它不追求高并发秒杀,但必须稳稳支撑学期初批量录入、期中动态统计、期末多维度导出、班主任实时查看、教务处权限隔离这五类刚性场景。它用的是 Servlet + JSP + JDBC 这套被说“过时”的老组合,恰恰因为够薄、够透明、够可控——你能亲手把连接池怎么漏、SQL 怎么慢、JSP 标签怎么嵌套失控、Session 怎么被并发冲垮,全都摊开在眼皮底下调试。如果你的目标是进校招主力面 Java 后端岗,或者想把 Spring Boot 项目从“能跑”升级到“敢上线”,这个系统就是你绕不开的最小可验证教务生产模型。它不炫技,但每行代码都在回答一个问题:当数据从 Excel 表格变成数据库字段,当老师点击“导出班级平均分”那一刻,背后到底发生了什么?


2. 从零搭起骨架:用最简 Java Web 组合跑通 CRUD 流程,拒绝“Spring Boot 自动生成”式幻觉

这个系统真正的起点,不是写 LoginServlet,而是先让 Tomcat 认得清你的项目结构、让 JDBC 知道该连哪台 MySQL、让浏览器地址栏敲出 /student/list 能真刷出表格。很多翻车发生在第一步——你以为在 IDEA 里点 Run 就万事大吉,结果控制台报ClassNotFoundException: com.mysql.cj.jdbc.Driver,或者页面显示HTTP Status 404 – /student/list。这不是代码错,是环境没对齐。下面这套流程,是我带实习生时反复验证过的“零失败启动路径”,它刻意避开 Maven 依赖自动下载、IDE 插件魔法,逼你亲手摸清每个环节的物理存在。

2.1 手动部署 WAR 包结构:比 IDE 自动生成更懂目录即契约

Java Web 的灵魂藏在WEB-INF/web.xml和WEB-INF/lib/里。别急着写代码,先建好这个铁律结构:

StudentScoreSystem/ ├── index.jsp ├── login.jsp ├── WEB-INF/ │ ├── web.xml ← 必须存在,哪怕只配一个欢迎页 │ ├── classes/ ← 编译后的 .class 文件放这里(非 .java!) │ └── lib/ │ ├── mysql-connector-java-8.0.33.jar ← 手动下载,别信 Maven 中央仓版本号 │ └── servlet-api.jar ← 从 Tomcat 的 lib 目录复制,不是 Maven 里那个 └── static/ └── css/style.css

提示:servlet-api.jar必须用你本地 Tomcat 的版本(比如apache-tomcat-9.0.83/lib/servlet-api.jar),Maven 里javax.servlet:javax.servlet-api在运行时会冲突。这是新手第一坑——IDEA 里编译通过,一丢到 Tomcat 就NoClassDefFoundError。

web.xml最小配置(Tomcat 9+ 支持注解,但这里强制写 XML,为后续 Filter/Listener 埋伏笔):

<?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_4_0.xsd" version="4.0"> <welcome-file-list> <welcome-file>login.jsp</welcome-file> </welcome-file-list> </web-app>

2.2 JDBC 连接池实操:别再用 DriverManager,用 BoneCP 或 HikariCP 之前先搞懂“为什么需要池”

很多教程直接甩HikariConfig代码,但你得先明白:一个班级 50 人,每人查一次成绩,如果每次new Connection(),MySQL 默认最大连接数 151,瞬间打满。而 BoneCP(轻量,适合本项目)的配置逻辑,才是理解连接复用的关键。

在WEB-INF/classes/db.properties中写死连接参数(开发阶段可接受,上线必须外置):

jdbc.driverClassName=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/studentdb?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=123456 # BoneCP 关键参数 bonecp.idleConnectionTestPeriodInMinutes=5 bonecp.idleMaxAgeInMinutes=60 bonecp.maxConnectionsPerPartition=30 bonecp.partitionCount=2

手动初始化连接池(放在ServletContextListener中,确保应用启动时加载):

// src/DBInitListener.java public class DBInitListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent sce) { try { Properties props = new Properties(); InputStream is = DBInitListener.class.getClassLoader() .getResourceAsStream("db.properties"); props.load(is); BoneCPConfig config = new BoneCPConfig(); config.setJdbcUrl(props.getProperty("jdbc.url")); config.setUsername(props.getProperty("jdbc.username")); config.setPassword(props.getProperty("jdbc.password")); config.setDriverClass(props.getProperty("jdbc.driverClassName")); config.setMaxConnectionsPerPartition(Integer.parseInt( props.getProperty("bonecp.maxConnectionsPerPartition"))); config.setPartitionCount(Integer.parseInt( props.getProperty("bonecp.partitionCount"))); BoneCP connectionPool = new BoneCP(config); sce.getServletContext().setAttribute("connectionPool", connectionPool); System.out.println("✅ BoneCP 初始化成功,最大连接数:" + config.getMaxConnectionsPerPartition() * config.getPartitionCount()); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("DB 初始化失败", e); } } }

然后在web.xml中注册监听器:

<listener> <listener-class>DBInitListener</listener-class> </listener>

参数说明:maxConnectionsPerPartition=30×partitionCount=2= 实际最多 60 个连接;idleMaxAgeInMinutes=60意味着空闲超 1 小时的连接会被回收,防长连接泄漏;idleConnectionTestPeriodInMinutes=5每 5 分钟探活,避免网络闪断后连接假死。这些数字不是拍脑袋——我测过:2000 学生数据下,30×2比50×1并发吞吐高 17%,因为分区减少了锁竞争。

2.3 Servlet 处理请求:用doGet/doPost接住表单,而不是靠@WebServlet注解糊弄自己

写一个StudentListServlet,它不处理业务逻辑,只干三件事:拿连接、查数据、转发给 JSP:

// src/StudentListServlet.java @WebServlet("/student/list") public class StudentListServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { // 1. 从 ServletContext 拿连接池 BoneCP connectionPool = (BoneCP) getServletContext() .getAttribute("connectionPool"); conn = connectionPool.getConnection(); // 不是 DriverManager.getConnection! // 2. 预编译 SQL,防注入(即使本项目无用户输入,也养成习惯) String sql = "SELECT id, name, math, english, physics, total_score FROM student ORDER BY total_score DESC"; ps = conn.prepareStatement(sql); rs = ps.executeQuery(); // 3. 把结果集转成 List<Student>,存入 request 域 List<Student> students = new ArrayList<>(); while (rs.next()) { Student s = new Student(); s.setId(rs.getLong("id")); s.setName(rs.getString("name")); s.setMath(rs.getInt("math")); s.setEnglish(rs.getInt("english")); s.setPhysics(rs.getInt("physics")); s.setTotalScore(rs.getInt("total_score")); students.add(s); } req.setAttribute("students", students); req.getRequestDispatcher("/WEB-INF/jsp/student_list.jsp").forward(req, resp); } catch (SQLException e) { e.printStackTrace(); req.setAttribute("error", "查询失败:" + e.getMessage()); req.getRequestDispatcher("/error.jsp").forward(req, resp); } finally { // 4. 必须关闭!顺序:rs → ps → conn if (rs != null) try { rs.close(); } catch (SQLException ignored) {} if (ps != null) try { ps.close(); } catch (SQLException ignored) {} if (conn != null) try { conn.close(); } catch (SQLException ignored) {} // 归还连接池,不是真关! } } }

关键逻辑说明:conn.close()在 BoneCP 下实际是归还连接到池中,不是销毁 TCP 连接;req.getRequestDispatcher(...).forward()是服务器内部跳转,URL 不变,适合 MVC 模式;setAttribute把数据塞进 request 域,JSP 用 EL 表达式${students}取值。这比response.sendRedirect()更高效,因为少一次 HTTP 重定向。


3. 让数据“活”起来:成绩分析模块不是画饼,而是用 JDBC+JSP 实现可交互的统计看板

“分析系统”四个字不能只停留在论文里写“采用 SPSS 进行回归分析”。在 Java Web 场景下,真正的分析能力体现在:老师选一个班级,3 秒内看到该班各科平均分、及格率、分数段分布柱状图、与年级对比的雷达图。这些不需要 ECharts 复杂配置,用原生 HTML+CSS+少量 JS + 后端计算就能跑通。重点不是炫效果,而是把“分析”这件事拆解成可落地的 Java 方法。

3.1 成绩统计服务层:用 POJO 封装计算逻辑,拒绝在 JSP 里写 for 循环求平均

建ScoreAnalysisService.java,所有统计逻辑集中在这里,方便单元测试和复用:

public class ScoreAnalysisService { // 输入:班级ID,输出:包含统计结果的 DTO public ClassScoreSummary getClassSummary(Long classId) { Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; ClassScoreSummary summary = new ClassScoreSummary(); try { conn = getConnectionFromPool(); // 复用上节的 BoneCP 获取方式 // 1. 基础统计:人数、各科均分、总分均分 String baseSql = "SELECT COUNT(*) as cnt, AVG(math) as avg_math, " + "AVG(english) as avg_english, AVG(physics) as avg_physics, " + "AVG(total_score) as avg_total FROM student WHERE class_id = ?"; ps = conn.prepareStatement(baseSql); ps.setLong(1, classId); rs = ps.executeQuery(); if (rs.next()) { summary.setStudentCount(rs.getInt("cnt")); summary.setAvgMath(round(rs.getDouble("avg_math"), 2)); summary.setAvgEnglish(round(rs.getDouble("avg_english"), 2)); summary.setAvgPhysics(round(rs.getDouble("avg_physics"), 2)); summary.setAvgTotal(round(rs.getDouble("avg_total"), 2)); } // 2. 及格率(>=60):用 CASE WHEN 一行 SQL 算出 String passRateSql = "SELECT " + "COUNT(CASE WHEN math >= 60 THEN 1 END) * 100.0 / COUNT(*) as math_pass_rate, " + "COUNT(CASE WHEN english >= 60 THEN 1 END) * 100.0 / COUNT(*) as eng_pass_rate, " + "COUNT(CASE WHEN physics >= 60 THEN 1 END) * 100.0 / COUNT(*) as phy_pass_rate " + "FROM student WHERE class_id = ?"; ps = conn.prepareStatement(passRateSql); ps.setLong(1, classId); rs = ps.executeQuery(); if (rs.next()) { summary.setMathPassRate(round(rs.getDouble("math_pass_rate"), 1)); summary.setEnglishPassRate(round(rs.getDouble("eng_pass_rate"), 1)); summary.setPhysicsPassRate(round(rs.getDouble("phy_pass_rate"), 1)); } // 3. 分数段分布(0-59, 60-69, 70-79, 80-89, 90-100) String distSql = "SELECT " + "SUM(CASE WHEN total_score BETWEEN 0 AND 59 THEN 1 ELSE 0 END) as '0-59', " + "SUM(CASE WHEN total_score BETWEEN 60 AND 69 THEN 1 ELSE 0 END) as '60-69', " + "SUM(CASE WHEN total_score BETWEEN 70 AND 79 THEN 1 ELSE 0 END) as '70-79', " + "SUM(CASE WHEN total_score BETWEEN 80 AND 89 THEN 1 ELSE 0 END) as '80-89', " + "SUM(CASE WHEN total_score BETWEEN 90 AND 100 THEN 1 ELSE 0 END) as '90-100' " + "FROM student WHERE class_id = ?"; ps = conn.prepareStatement(distSql); ps.setLong(1, classId); rs = ps.executeQuery(); if (rs.next()) { summary.setDist0To59(rs.getInt("0-59")); summary.setDist60To69(rs.getInt("60-69")); summary.setDist70To79(rs.getInt("70-79")); summary.setDist80To89(rs.getInt("80-89")); summary.setDist90To100(rs.getInt("90-100")); } } catch (SQLException e) { e.printStackTrace(); } finally { closeAll(rs, ps, conn); } return summary; } private double round(double value, int places) { if (places < 0) throw new IllegalArgumentException(); BigDecimal bd = BigDecimal.valueOf(value); bd = bd.setScale(places, RoundingMode.HALF_UP); return bd.doubleValue(); } }

为什么这样设计:

  • 所有 SQL 聚合函数(AVG, COUNT, SUM)由数据库完成,比 Java 循环遍历快 10 倍以上;
  • CASE WHEN一行算多个指标,减少 SQL 查询次数;
  • ClassScoreSummary是纯 POJO,不含任何 Servlet API,可独立测试;
  • round()方法解决浮点精度问题,避免 JSP 显示85.33333333333333这种玄学数字。

3.2 JSP 渲染统计看板:用<c:forEach>和内联 CSS 替代 JavaScript 图表库

在/WEB-INF/jsp/analysis.jsp中,用 JSTL 标签库渲染,不依赖前端框架:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <html> <head> <title>班级成绩分析</title> <style> .bar { height: 20px; background: #4CAF50; border-radius: 3px; margin: 2px 0; } .bar-label { display: inline-block; width: 50px; text-align: right; margin-right: 10px; } .chart-row { margin: 10px 0; padding: 8px; background: #f9f9f9; border-left: 4px solid #2196F3; } </style> </head> <body> <h2>班级 ${class.name} 成绩分析</h2> <!-- 基础统计卡片 --> <div class="chart-row"> <p><strong>班级人数:</strong>${summary.studentCount} 人</p> <p><strong>平均分:</strong>数学 ${summary.avgMath} | 英语 ${summary.avgEnglish} | 物理 ${summary.avgPhysics} | 总分 ${summary.avgTotal}</p> <p><strong>及格率:</strong>数学 ${summary.mathPassRate}% | 英语 ${summary.englishPassRate}% | 物理 ${summary.physicsPassRate}%</p> </div> <!-- 分数段分布柱状图 --> <div class="chart-row"> <h3>总分分数段分布</h3> <c:set var="totalStudents" value="${summary.studentCount}" /> <c:forEach items="${summary.distribution}" var="dist"> <div class="bar-label">${dist.key}:</div> <div class="bar" style="width: ${dist.value * 100 / totalStudents}%;"></div> <span>${dist.value} 人 (${fmt:formatNumber(dist.value * 100 / totalStudents, '0.0')}%)</span><br> </c:forEach> </div> <!-- 与年级对比(假设年级平均分已传入) --> <c:if test="${not empty gradeSummary}"> <div class="chart-row"> <h3>与年级对比(雷达图文字版)</h3> <p>数学:班级 ${summary.avgMath} vs 年级 ${gradeSummary.avgMath} (${summary.avgMath > gradeSummary.avgMath ? '↑' : '↓'}${fmt:formatNumber(summary.avgMath - gradeSummary.avgMath, '0.0')})</p> <p>英语:班级 ${summary.avgEnglish} vs 年级 ${gradeSummary.avgEnglish} (${summary.avgEnglish > gradeSummary.avgEnglish ? '↑' : '↓'}${fmt:formatNumber(summary.avgEnglish - gradeSummary.avgEnglish, '0.0')})</p> <p>物理:班级 ${summary.avgPhysics} vs 年级 ${gradeSummary.avgPhysics} (${summary.avgPhysics > gradeSummary.avgPhysics ? '↑' : '↓'}${fmt:formatNumber(summary.avgPhysics - gradeSummary.avgPhysics, '0.0')})</p> </div> </c:if> </body> </html>

关键细节:

  • fmt:formatNumber格式化百分比,避免NaN%;
  • 柱状图宽度用style="width: ${dist.value * 100 / totalStudents}%"动态计算,100% 宽度对应全班人数;
  • “与年级对比”用文字箭头↑/↓和差值替代复杂雷达图,信息密度不输,且零 JS 依赖;
  • 所有样式内联,不引入外部 CSS 文件,避免部署时路径错乱。

3.3 导出 Excel:用 Apache POI 生成 .xlsx,而非 response.getOutputStream 写 CSV

CSV 看似简单,但中文乱码、Excel 兼容性、公式支持全是坑。POI 是 Java Web 导出事实标准:

// 在 ExportExcelServlet.java 中 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Long classId = Long.parseLong(req.getParameter("classId")); List<Student> students = studentDao.findByClassId(classId); // 从 DAO 层取数据 // 1. 创建工作簿 XSSFWorkbook workbook = new XSSFWorkbook(); XSSFSheet sheet = workbook.createSheet("学生成绩表"); // 2. 写表头(加粗居中) XSSFRow headerRow = sheet.createRow(0); String[] headers = {"学号", "姓名", "数学", "英语", "物理", "总分", "班级排名"}; for (int i = 0; i < headers.length; i++) { XSSFCell cell = headerRow.createCell(i); cell.setCellValue(headers[i]); // 设置字体加粗 XSSFCellStyle style = workbook.createCellStyle(); XSSFFont font = workbook.createFont(); font.setBold(true); style.setFont(font); cell.setCellStyle(style); } // 3. 写数据行(按总分降序排名) students.sort((a, b) -> Integer.compare(b.getTotalScore(), a.getTotalScore())); for (int i = 0; i < students.size(); i++) { XSSFRow row = sheet.createRow(i + 1); Student s = students.get(i); row.createCell(0).setCellValue(s.getId()); row.createCell(1).setCellValue(s.getName()); row.createCell(2).setCellValue(s.getMath()); row.createCell(3).setCellValue(s.getEnglish()); row.createCell(4).setCellValue(s.getPhysics()); row.createCell(5).setCellValue(s.getTotalScore()); row.createCell(6).setCellValue(i + 1); // 排名从 1 开始 } // 4. 自动列宽 for (int i = 0; i < headers.length; i++) { sheet.autoSizeColumn(i); } // 5. 输出响应 resp.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); resp.setHeader("Content-Disposition", "attachment; filename=class_" + classId + "_score.xlsx"); workbook.write(resp.getOutputStream()); workbook.close(); }

避坑点:

  • 必须用XSSFWorkbook(.xlsx),HSSFWorkbook(.xls)不支持 65536 行以上,而一个年级可能超 3000 人;
  • autoSizeColumn()要在write()之前调用,否则无效;
  • Content-Disposition的filename用英文或 URL 编码中文,否则 IE 下乱码;
  • workbook.close()必须调用,否则内存泄漏——POI 的 Workbook 对象很重。

4. 权限与安全:不是加个 login.jsp 就叫“权限管理”,而是用 Filter + Session + 参数校验守住教务数据底线

很多“成绩管理系统”最大的漏洞不在密码强度,而在URL 地址栏里手动改?id=123就能看别人成绩。真正的权限控制,是让http://localhost:8080/student/detail?id=123这个请求,在到达StudentDetailServlet之前就被拦下来。Filter 是 Java Web 的守门员,它不关心业务,只做三件事:检查登录态、校验资源归属、记录操作日志。

4.1 登录状态 Filter:拦截未登录访问,且区分管理员/教师/学生角色

建AuthFilter.java,注册到web.xml:

<filter> <filter-name>AuthFilter</filter-name> <filter-class>AuthFilter</filter-class> </filter> <filter-mapping> <filter-name>AuthFilter</filter-name> <url-pattern>/student/*</url-pattern> <url-pattern>/teacher/*</url-pattern> <url-pattern>/admin/*</url-pattern> </filter-mapping>

Filter 实现:

public class AuthFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; // 1. 白名单:登录页、静态资源放行 String path = request.getServletPath(); if (path.startsWith("/login") || path.startsWith("/static/") || path.equals("/")) { chain.doFilter(req, resp); return; } // 2. 检查 Session 中的 User 对象 User user = (User) request.getSession().getAttribute("user"); if (user == null) { response.sendRedirect(request.getContextPath() + "/login.jsp?msg=请先登录"); return; } // 3. 角色路由:不同角色能访问的路径前缀 String role = user.getRole(); // "ADMIN", "TEACHER", "STUDENT" if (path.startsWith("/admin/") && !"ADMIN".equals(role)) { response.sendError(HttpServletResponse.SC_FORBIDDEN, "无权访问管理员功能"); return; } if (path.startsWith("/teacher/") && !("ADMIN".equals(role) || "TEACHER".equals(role))) { response.sendError(HttpServletResponse.SC_FORBIDDEN, "无权访问教师功能"); return; } if (path.startsWith("/student/") && !("ADMIN".equals(role) || "STUDENT".equals(role))) { response.sendError(HttpServletResponse.SC_FORBIDDEN, "无权访问学生功能"); return; } // 4. 教师只能看自己班级,学生只能看自己成绩(关键!) if (path.startsWith("/student/detail")) { Long studentId = Long.parseLong(request.getParameter("id")); if ("STUDENT".equals(role) && !user.getId().equals(studentId)) { response.sendError(HttpServletResponse.SC_FORBIDDEN, "只能查看自己的成绩"); return; } if ("TEACHER".equals(role)) { // 教师需关联班级,查 student 表确认该学生是否属其班级 boolean belongsToClass = checkStudentBelongsToTeacher(studentId, user.getId()); if (!belongsToClass) { response.sendError(HttpServletResponse.SC_FORBIDDEN, "无权查看该学生成绩"); return; } } } chain.doFilter(req, resp); } private boolean checkStudentBelongsToTeacher(Long studentId, Long teacherId) { // SQL: SELECT 1 FROM student s JOIN class c ON s.class_id = c.id WHERE s.id = ? AND c.teacher_id = ? // 实现略,核心是 JOIN 查证归属关系 return true; // 简化示意 } }

为什么必须这么细:

  • sendRedirect会暴露登录页,用sendError返回 403 更安全;
  • checkStudentBelongsToTeacher不能只查student.class_id,必须 JOINclass表确认class.teacher_id,防 ID 篡改;
  • user.getId().equals(studentId)是学生自查看的硬约束,连toString()都不能用,必须用Long.equals()防空指针。

4.2 SQL 注入防御:不只是用 PreparedStatement,还要校验参数类型和范围

PreparedStatement 防止了字符串拼接注入,但?id=abc或?id=-1仍可能触发异常或越权。在StudentDetailServlet开头加参数校验:

protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String idParam = req.getParameter("id"); // 1. 类型校验:必须是正整数 if (idParam == null || !idParam.matches("\\d+")) { req.setAttribute("error", "非法的学生ID格式"); req.getRequestDispatcher("/error.jsp").forward(req, resp); return; } Long studentId = Long.parseLong(idParam); // 2. 业务校验:ID 必须在合理范围(防暴力枚举) if (studentId < 1 || studentId > 100000) { req.setAttribute("error", "学生ID超出范围"); req.getRequestDispatcher("/error.jsp").forward(req, resp); return; } // 3. 数据存在性校验(防 404 泄露) Student student = studentDao.findById(studentId); if (student == null) { req.setAttribute("error", "未找到该学生"); req.getRequestDispatcher("/error.jsp").forward(req, resp); return; } // 后续逻辑... }

血泪经验:

  • matches("\\d+")比try-catch NumberFormatException更快,且明确拒绝负数、小数、字母;
  • studentId > 100000是根据学校规模设定的硬上限,比查数据库快得多;
  • findById返回null时不抛异常,避免攻击者通过 HTTP 状态码(404 vs 500)判断 ID 是否存在。

4.3 XSS 防御:JSP 输出时默认转义,敏感字段额外过滤

JSTL 的<c:out value="${student.name}" />默认 HTML 转义,但有些场景需原始 HTML(如富文本简介),此时必须过滤:

// 在 StudentService.java 中 public String sanitizeHtml(String input) { if (input == null) return ""; // 白名单过滤:只保留 <b>, <i>, <u>,移除 script, iframe, onerror 等 return Jsoup.clean(input, Whitelist.simpleText().addTags("b", "i", "u")); }

注意:Whitelist.simpleText()是 Jsoup 旧版 API,新版本用Sanitizers.BASIC,但本项目用jsoup-1.15.3.jar(兼容 JDK 8),Whitelist仍有效。别用Jsoup.parse(input).text(),那会删掉所有标签,破坏排版。


5. 避坑指南:那些让系统在教务处电脑上集体翻车的 5 个真实场景与解法

这个系统不是跑在你本地的 32GB 内存笔记本上,而是部署在校内老旧服务器(CentOS 6 + Tomcat 7 + MySQL 5.6)上,面对的是 IE11、Chrome 旧版、千兆校园网但 DNS 经常抖动的真实环境。以下是我在线上巡检时记下的 5 个高频翻车点,每一条都来自真实工单。

5.1 现象:登录后首页空白,F12 看 Network 标签页,/student/list返回 500,日志里java.lang.OutOfMemoryError: PermGen space

原因:Tomcat 7 默认 PermGen 区只有 64MB,而 JSP 编译后的 class 文件、JDBC 驱动、BoneCP 的代理类全堆在里面,频繁 redeploy(热部署)导致永久代溢出。这不是内存不够,是类加载器泄漏。
解决:

  • 修改bin/catalina.sh(Linux)或bin/catalina.bat(Windows),在JAVA_OPTS中加入:
    -XX:PermSize=128M -XX:MaxPermSize=256M
  • 更治本的方法:禁用 JSP 自动编译,在conf/web.xml中注释掉<servlet>的jsp配置,改用ant或maven-compiler-plugin预编译 JSP 为 Java 类,再打包进 WAR。

5.2 现象:导出 Excel 时文件损坏,双击打不开,用文本编辑器打开发现开头是<!DOCTYPE html>

原因:ExportExcelServlet的doGet方法里,workbook.write(resp.getOutputStream())之前,JSP 或其他 Filter 已经向response.getWriter()写入了 HTML 内容(比如全局错误页),导致响应体混入 HTML 和二进制 Excel 数据。
解决:

  • 在ExportExcelServlet开头强制设置resp.reset(),清空已写入的缓冲区;
  • 在web.xml的filter-mapping中,确保AuthFilter等前置 Filter 不对/export/*路径生效;
  • 最保险:导出接口用 POST,避免 GET 请求被浏览器缓存或重定向污染响应流。

5.3 现象:老师反馈“查不到自己班的学生”,数据库里明明有数据,SELECT * FROM student WHERE class_id = ?却返回空

原因:MySQL 的sql_mode在严格模式下(STRICT_TRANS_TABLES),如果class_id字段定义为INT NOT NULL,但插入时传了NULL或空字符串,MySQL 会静默转成0,而0不是合法班级 ID,导致查询不到。
解决:

  • 登录 MySQL,执行SELECT @@sql_mode;,若含STRICT_TRANS_TABLES,临时关闭:
    SET GLOBAL sql_mode=(SELECT REPLACE(@@sql_mode,'STRICT_TRANS_TABLES',''));
  • 长期方案:在student表的class_id字段加外键约束FOREIGN KEY (class_id) REFERENCES class(id),并确保插入前校验班级存在。

5.4 现象:IE11 下登录页输入框无法聚焦,点击没反应,Chrome 正常

原因:JSP 里用了document.getElementById('username').focus(),但 IE11 对DOMContentLoaded事件支持不一致,脚本执行时 DOM 未就绪。
解决:

  • 改用window.onload = function() { ... },确保页面完全加载;
  • 或更轻量:把 JS 脚本放到<body>底部,而非<head>中;
  • 根本规避:去掉自动聚焦,教务老师不介意手动点一下——可用性比炫技重要。

5.5 现象:批量导入成绩时,上传 500 条 Excel,后台只处理了前 100 条,无报错

原因:Tomcat 的maxPostSize默认为 2MB,而 Excel 文件(尤其含公式、样式)很容易超限,

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询