ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java Web学生成绩系统实战:从Servlet到教务级部署

Java Web学生成绩系统实战:从Servlet到教务级部署 简介本资源是一套面向计算机与软件工程专业本科生的毕业设计实战项目聚焦教育信息化场景提供学生成绩管理分析系统的完整开发方案解决高校及中小学教务部门在成绩录入、查询、统计与报表生成中的典型业务痛点。资源包共388个文件含170个Java业务逻辑代码、75个JSP页面模板、72个核心依赖JAR包、22个PNG界面图标及配套SQL建表脚本、系统说明书与两份Word格式论文含设计与实现全过程整体41.41MB结构规范、注释清晰便于二次开发与教学复现。已有53人学习下载适合毕业设计选题参考、Java Web技术栈综合实践及MVC分层架构理解。读者可直接部署运行掌握StrutsSpringJSPjQuery技术整合要点复用源码模块深入理解成绩分析图表生成逻辑与多角色权限控制实现细节。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.jarMaven 里javax.servlet:javax.servlet-api在运行时会冲突。这是新手第一坑——IDEA 里编译通过一丢到 Tomcat 就NoClassDefFoundError。web.xml最小配置Tomcat 9 支持注解但这里强制写 XML为后续 Filter/Listener 埋伏笔?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 welcome-file-list welcome-filelogin.jsp/welcome-file /welcome-file-list /web-app2.2 JDBC 连接池实操别再用 DriverManager用 BoneCP 或 HikariCP 之前先搞懂“为什么需要池”很多教程直接甩HikariConfig代码但你得先明白一个班级 50 人每人查一次成绩如果每次new Connection()MySQL 默认最大连接数 151瞬间打满。而 BoneCP轻量适合本项目的配置逻辑才是理解连接复用的关键。在WEB-INF/classes/db.properties中写死连接参数开发阶段可接受上线必须外置jdbc.driverClassNamecom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/studentdb?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password123456 # BoneCP 关键参数 bonecp.idleConnectionTestPeriodInMinutes5 bonecp.idleMaxAgeInMinutes60 bonecp.maxConnectionsPerPartition30 bonecp.partitionCount2手动初始化连接池放在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-classDBInitListener/listener-class /listener参数说明maxConnectionsPerPartition30×partitionCount2 实际最多 60 个连接idleMaxAgeInMinutes60意味着空闲超 1 小时的连接会被回收防长连接泄漏idleConnectionTestPeriodInMinutes5每 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. 把结果集转成 ListStudent存入 request 域 ListStudent 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. 让数据“活”起来成绩分析模块不是画饼而是用 JDBCJSP 实现可交互的统计看板“分析系统”四个字不能只停留在论文里写“采用 SPSS 进行回归分析”。在 Java Web 场景下真正的分析能力体现在老师选一个班级3 秒内看到该班各科平均分、及格率、分数段分布柱状图、与年级对比的雷达图。这些不需要 ECharts 复杂配置用原生 HTMLCSS少量 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 contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://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 classchart-row pstrong班级人数/strong${summary.studentCount} 人/p pstrong平均分/strong数学 ${summary.avgMath} | 英语 ${summary.avgEnglish} | 物理 ${summary.avgPhysics} | 总分 ${summary.avgTotal}/p pstrong及格率/strong数学 ${summary.mathPassRate}% | 英语 ${summary.englishPassRate}% | 物理 ${summary.physicsPassRate}%/p /div !-- 分数段分布柱状图 -- div classchart-row h3总分分数段分布/h3 c:set vartotalStudents value${summary.studentCount} / c:forEach items${summary.distribution} vardist div classbar-label${dist.key}/div div classbar stylewidth: ${dist.value * 100 / totalStudents}%;/div span${dist.value} 人 (${fmt:formatNumber(dist.value * 100 / totalStudents, 0.0)}%)/spanbr /c:forEach /div !-- 与年级对比假设年级平均分已传入 -- c:if test${not empty gradeSummary} div classchart-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%柱状图宽度用stylewidth: ${dist.value * 100 / totalStudents}%动态计算100% 宽度对应全班人数“与年级对比”用文字箭头↑/↓和差值替代复杂雷达图信息密度不输且零 JS 依赖所有样式内联不引入外部 CSS 文件避免部署时路径错乱。3.3 导出 Excel用 Apache POI 生成 .xlsx而非 response.getOutputStream 写 CSVCSV 看似简单但中文乱码、Excel 兼容性、公式支持全是坑。POI 是 Java Web 导出事实标准// 在 ExportExcelServlet.java 中 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Long classId Long.parseLong(req.getParameter(classId)); ListStudent 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; filenameclass_ classId _score.xlsx); workbook.write(resp.getOutputStream()); workbook.close(); }避坑点必须用XSSFWorkbook.xlsxHSSFWorkbook.xls不支持 65536 行以上而一个年级可能超 3000 人autoSizeColumn()要在write()之前调用否则无效Content-Disposition的filename用英文或 URL 编码中文否则 IE 下乱码workbook.close()必须调用否则内存泄漏——POI 的 Workbook 对象很重。4. 权限与安全不是加个 login.jsp 就叫“权限管理”而是用 Filter Session 参数校验守住教务数据底线很多“成绩管理系统”最大的漏洞不在密码强度而在URL 地址栏里手动改?id123就能看别人成绩。真正的权限控制是让http://localhost:8080/student/detail?id123这个请求在到达StudentDetailServlet之前就被拦下来。Filter 是 Java Web 的守门员它不关心业务只做三件事检查登录态、校验资源归属、记录操作日志。4.1 登录状态 Filter拦截未登录访问且区分管理员/教师/学生角色建AuthFilter.java注册到web.xmlfilter filter-nameAuthFilter/filter-name filter-classAuthFilter/filter-class /filter filter-mapping filter-nameAuthFilter/filter-name url-pattern/student/*/url-pattern url-pattern/teacher/*/url-pattern url-pattern/admin/*/url-pattern /filter-mappingFilter 实现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 防止了字符串拼接注入但?idabc或?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 8Whitelist仍有效。别用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.shLinux或bin/catalina.batWindows在JAVA_OPTS中加入-XX:PermSize128M -XX:MaxPermSize256M更治本的方法禁用 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 文件尤其含公式、样式很容易超限本文还有配套的精品资源点击获取
返回列表