
简介基于JspServlet实现的Web成绩管理系统是一份面向Java初学者与课程设计学生的完整项目源码包。系统按学生、教师、管理员三个角色设计覆盖个人信息维护、成绩录入与查询、课程安排、用户与班级管理等典型业务适合作为高校Java Web课程设计、毕业设计参考或SSH/SSM前置技能练习。压缩包共870个文件包含93个Java源文件、46个JSP页面、212个JavaScript脚本、174个CSS样式表以及大量PNG/JPG/SVG图标图片、SQL脚本、Excel模板和说明文档整体大小约39.64MB代码与静态资源分层存放目录结构清晰便于直接导入IDE运行和学习。目前已有173人学习下载。资源内提供完整数据库脚本与项目配置文件可帮助理解JSPServletMySQL的整合流程也能借助前端样式快速改造出更美观的界面是课程设计与项目实战的实用参考便于二次开发与功能扩展。1. 从毕设标配到生产可用JspServlet 成绩管理系统到底能走多远你身边学长学姐的毕设目录里大概率躺着这个项目基于 JspServlet 的Web成绩管理系统。它看起来像是十年前的老古董但如果你真正动手写过会发现这套技术栈的价值远不止“应付答辩”这么简单。我见过不少人在 SpringBoot 里折腾半天最后连 JSP 怎么渲染都没搞明白转头来补 Servlet 的课才把请求处理、会话管理、过滤器拦截这些东西彻底理清。这个系统的核心任务很朴素教师登录后录入成绩、学生查询成绩、管理员管理课程和学生信息但就是这一套流程能把 Java Web 最底层的运行机制全部串起来。这篇文章不打算带你抄一遍代码交差。我会从技术选型的底层逻辑讲起把环境搭建、分层架构、核心功能实现、部署上线和那些只有写完了才吐血领悟的坑全部拆开给你看。适合的人群有两类一类是正在做 Java Web 课设、毕设想搞明白“为什么这么写”的学生一类是工作后用惯了 Spring 全家桶想回头补 Servlet/JSP 底层原理或者需要接手维护老系统的工程师。先说一个反直觉的结论JspServlet 这套组合如果你配色和布局稍微用心一点做出来的成绩管理系统完全可以直接部署在校园内网给老师用它不丑也不慢。真正让它翻车的往往是开发者自己没搞懂请求生命周期和乱码处理而不是技术栈本身。下面的内容每一步你都可以照着敲我尽量把参数和踩坑都交代清楚。2. 技术选型不是考古为什么 JspServlet 在这个场景反而顺手2.1 Servlet 负责逻辑JSP 负责展示这个分工到底怎么理解很多人第一次接触 Java Web听到“Servlet 是控制器JSP 是视图”这句教科书定义依然一头雾水。我打个比方Servlet 就像餐厅后厨你点菜发 HTTP 请求给服务员Tomcat服务员把菜单送到后厨后厨根据你点的菜决定做什么业务逻辑做好之后服务员端到你桌上返回 HTML 页面。JSP 就是这个装菜的盘子——盘子本身不决定菜是什么但菜最终要靠盘子呈现给你。从代码层面看Servlet 是一个 Java 类继承 HttpServlet重写 doGet 和 doPost 方法。它的任务是从 request 对象里拿参数、调数据库、把结果塞进 request 或 session 域然后 forward 转发给 JSP。JSP 本质上是一个被 Tomcat 编译成 Servlet 的 HTML 模板里面可以写 Java 代码片段脚本元素也可以只用 JSTL 标签和 EL 表达式。WebServlet(/login) public class LoginServlet extends HttpServlet { Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 设置请求和响应的字符编码这一步不做中文百分百乱码 request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); String username request.getParameter(username); String password request.getParameter(password); // 调 UserDao 验证账号密码省略具体实现 User user new UserDao().login(username, password); if (user ! null) { // 登录成功后把用户信息放进 session后续页面才能识别身份 request.getSession().setAttribute(loginUser, user); response.sendRedirect(index.jsp); } else { request.setAttribute(errorMsg, 用户名或密码错误); request.getRequestDispatcher(login.jsp).forward(request, response); } } }这段代码里有三个关键参数需要留意第一request.setCharacterEncoding(UTF-8)必须写在拿参数之前否则 POST 请求里的中文参数会因服务器默认编码Tomcat 8 之前是 ISO-8859-1变成乱码第二response.setContentType要同时设编码和类型不然浏览器不知道拿到的内容是 HTML第三sendRedirect和forward的区别在这里就体现出来了——重定向是浏览器重新发一次请求地址栏会变适合登录成功后跳转转发是服务器内部跳转地址栏不变适合把错误信息带回登录页。2.2 MVC 思想在 JspServlet 里的落地Model、View、Controller 各管一摊这个成绩管理系统的核心设计思路就是严格遵循 MVC 分层。Controller 层全部用 Servlet 充当View 层是 JSP 页面Model 层由 JavaBean 和 DAO 组成。这样分层有一个直接好处JSP 页面里几乎不写 Java 代码只通过 JSTL 标签和 EL 表达式取值页面设计人员和 Java 开发人员可以并行工作。DAO 层Data Access Object是这套架构里最容易写烂的地方。我见过不少同学直接在 Servlet 里写 JDBC 代码连数据库驱动都放在 Servlet 里加载一个登录功能十几个方法挤在一起后期想改表结构简直是灾难。常见的做法是单独建一个com.school.grade.dao包每个实体对应一个 Dao 接口和实现类比如StudentDao接口定义findAll()、findById()、add()、update()、delete()实现类里再处理 JDBC 的PreparedStatement和ResultSet。public class StudentDao { // 数据库连接工具类getConnection 方法内部通过 Druid 连接池获取连接 private QueryRunner runner new QueryRunner(DBUtils.getDataSource()); public ListStudent findAll() throws SQLException { String sql SELECT id, student_no, name, class_name FROM student ORDER BY id; return runner.query(sql, new BeanListHandler(Student.class)); } public int add(Student student) throws SQLException { String sql INSERT INTO student (student_no, name, class_name) VALUES (?, ?, ?); return runner.update(sql, student.getStudentNo(), student.getName(), student.getClassName()); } }这里我用了 Apache Commons DbUtils 的 QueryRunner它比纯手写 JDBC 省掉大量try-catch-finally的样板代码又不是 MyBatis 那样的重量级框架非常适合这种课设级别的项目。参数说明BeanListHandler需要Student类的属性名和表字段名一一对应比如数据库字段student_no如果直接映射到studentNo就会失败最稳妥的做法是给表字段起别名让下划线命名变成驼峰命名的别名。2.3 三大件版本选型JDK 8、Tomcat 8.5、MySQL 5.7 的黄金组合在做版本选型之前你要先搞清楚一件事JspServlet 项目对新版本的兼容性并不是“越新越好”。以我踩过的坑为例用 JDK 17 跑 Tomcat 9 和旧版 JSTL 1.2会因为模块化限制报java.lang.IllegalAccessError折腾半天最后退回到 JDK 8 一切正常。对于这个成绩管理系统我推荐的版本组合是组件推荐版本备注JDK1.8最稳定的 Java Web 运行环境Tomcat8.5.x支持 Servlet 3.1 JSP 2.3MySQL5.7.x支持性好教学资源最多IDEA2021 任意版本社区版就够用Maven3.6.x管理依赖比手工导 jar 包靠谱选择这套组合的理由很实在JDK 8 是 Tomcat 8.5 官方支持最好的版本MySQL 5.7 的utf8mb4字符集能完美存储中文和 emojiIDEA 社区版免费支持 Maven 和 Tomcat 集成。如果你是在校学生学校机房电脑配置普遍不高这套组合比最新的 JDK 21 Tomcat 11 跑起来流畅得多。3. 从零搭建开发环境半小时跑通一个最小可运行的项目骨架3.1 用 IDEA 创建 JavaWeb 项目的标准姿势很多同学第一次在 IDEA 里建 Web 项目会发现自己新建的模块里没有web目录也没法配置 Tomcat。原因是在 IntelliJ IDEA 2022 之后的版本里新建 Maven 项目默认不包含 Web 骨架需要手动添加。我一般这样做先创建一个普通的 Maven 项目然后在src/main目录下手动建webapp目录并补上WEB-INF/web.xml文件。如果你用的是 IDEA 2021 及之前的版本可以直接选择Maven Archetype里的maven-archetype-webapp。项目建好后pom.xml里需要引入这几个关键依赖dependencies !-- Servlet APIscope 必须是 provided因为 Tomcat 自带这些类 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version3.1.0/version scopeprovided/scope /dependency !-- JSP API 同样用 provided -- dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.1/version scopeprovided/scope /dependency !-- JSTL 标签库这个要打包进 WAR注意不能省 -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency !-- MySQL 驱动注意版本要和数据库匹配 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.49/version /dependency !-- Druid 连接池 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.8/version /dependency !-- Apache DBUtils 简化 JDBC 操作 -- dependency groupIdcommons-dbutils/groupId artifactIdcommons-dbutils/artifactId version1.7/version /dependency /dependencies参数说明javax.servlet-api的scope设为provided是最容易忽略的地方。如果设成默认的compile打 WAR 包时会把 Servlet API 类也打进去部署到 Tomcat 后反而可能和 Tomcat 自带的类冲突报各种诡异的ClassCastException。jstl依赖则相反它必须跟着项目走因为 Tomcat 默认不包含 JSTL 实现。3.2 配置 Tomcat 数据源和连接池Druid 的十个关键参数连接池是成绩管理系统的基础设施因为它要支持多个教师同时录入成绩、多个学生同时查询分数。如果每次请求都新建一个数据库连接高并发下 MySQL 分分钟报Too many connections。常见的做法是用 Alibaba Druid 连接池它的配置主要在src/main/resources/druid.properties文件里jdbc.driverClassNamecom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/grade_db?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.passwordyour_password # 连接池初始大小 jdbc.initialSize5 # 最大活跃连接数 jdbc.maxActive20 # 最小空闲连接数 jdbc.minIdle3 # 获取连接超时时间毫秒 jdbc.maxWait60000 # 检测连接有效性的 SQL jdbc.validationQuerySELECT 1 # 从池里拿连接时是否检测有效性 jdbc.testWhileIdletrue # 归还连接时是否检测有效性 jdbc.testOnReturnfalse在这几个参数里最需要上心的是maxWait。如果你的系统一天只有几百个访问量这个值设成 6000060 秒没有感知但如果是选课高峰期几百人同时查成绩maxWait太短会直接抛GetConnectionTimeoutException太长会让用户一直转圈。另外validationQuerySELECT 1这句不要去掉它能保证从池里拿出来的连接一定是活的免得 MySQL 因为wait_timeout把空闲连接断了你还莫名其妙报通讯故障。3.3 数据库设计三张表搞定核心业务这个成绩管理系统的数据模型核心就是学生、课程、成绩三个实体。如果你把用户表拆得五花八门反而给课设答辩增加解释负担。我用的表结构是这样的-- 用户表三种角色通过 type 字段区分 CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT MD5加密后的密码, real_name VARCHAR(30) NOT NULL COMMENT 真实姓名, role TINYINT NOT NULL DEFAULT 3 COMMENT 1管理员 2教师 3学生, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 课程表 CREATE TABLE course ( id INT NOT NULL AUTO_INCREMENT, course_no VARCHAR(20) NOT NULL COMMENT 课程编号, name VARCHAR(50) NOT NULL COMMENT 课程名称, credit DECIMAL(3,1) NOT NULL DEFAULT 2.0 COMMENT 学分, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 成绩表 CREATE TABLE grade ( id INT NOT NULL AUTO_INCREMENT, student_id INT NOT NULL COMMENT 关联user表的id, course_id INT NOT NULL COMMENT 关联course表的id, score DECIMAL(5,2) COMMENT 成绩NULL表示未录入, exam_time VARCHAR(20) COMMENT 考试时间, PRIMARY KEY (id), UNIQUE KEY uk_student_course (student_id, course_id), CONSTRAINT fk_grade_student FOREIGN KEY (student_id) REFERENCES user(id), CONSTRAINT fk_grade_course FOREIGN KEY (course_id) REFERENCES course(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意成绩表里student_id直接复用user表的主键前提是在user表里通过role3区分学生账号。这样省掉一张 student 表逻辑上反而更清晰。uk_student_course这个唯一约束非常关键它保证一个学生同一门课只能有一条成绩记录从数据库层面阻断重复录入。如果你在代码里还写了一遍判断属于双重保险但数据库唯一约束才是最后一道防线。4. 核心功能实战登录鉴权、成绩管理、Excel 导入导出的完整实现4.1 验证码与登录功能用 Kaptcha 生成随机会话验证码登录功能是成绩管理系统的门面。常见的做法是页面加载时先向后端发一个请求获取验证码图片用户输入用户名、密码、验证码后提交后端先校验验证码是否正确再校验账号密码。验证码的作用是防止暴力破解课设里有这个东西答辩时也更有说头。Kaptcha 是一个开源的验证码生成库引入依赖后在web.xml配置一个Servlet就能用。我一般把验证码生成放在一个独立的CaptchaServlet里WebServlet(/captcha) public class CaptchaServlet extends HttpServlet { Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType(image/jpeg); response.setHeader(Cache-Control, no-store, no-cache); // 生成验证码文本长度4只含数字 String captchaText CaptchaUtil.createText(4); // 存入 session登录时比对 request.getSession().setAttribute(captcha, captchaText); // 生成图片并输出 CaptchaUtil.createImage(captchaText, response.getOutputStream()); } }这里有一个细节值得注意response.setHeader(Cache-Control, no-store, no-cache)必须加上否则浏览器可能会缓存验证码图片导致你刷新好几次看到的都是同一张图登录时永远提示验证码错误。这个坑我帮别人排查过好几次最后发现根本不是代码逻辑问题而是响应头没设置。登录逻辑里验证码比对用equalsIgnoreCase并且比对完立即从 session 里移除防止重复使用。4.2 成绩列表的数据流从数据库到 JSP 表格的分页与搜索成绩列表是整个系统里接口最丰富的页面。教师需要按课程查看所有学生的成绩需要支持按学号、姓名搜索还需要分页。这里的分页查询如果不做所有成绩一次性全查出来数据量到几千条时页面肉眼可见地卡顿。WebServlet(/grade/list) public class GradeListServlet extends HttpServlet { Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 分页参数当前页和每页条数 int pageNum Integer.parseInt(request.getParameter(pageNum) null ? 1 : request.getParameter(pageNum)); int pageSize 10; // 搜索条件 String keyword request.getParameter(keyword); int courseId Integer.parseInt(request.getParameter(courseId)); GradeDao dao new GradeDao(); // 查询总数和当前页数据 int total dao.count(courseId, keyword); ListMapString, Object list dao.findPage(courseId, keyword, (pageNum - 1) * pageSize, pageSize); request.setAttribute(total, total); request.setAttribute(list, list); request.setAttribute(pageNum, pageNum); request.setAttribute(totalPages, (int) Math.ceil(total * 1.0 / pageSize)); // 转发到 JSP 页面 request.getRequestDispatcher(/WEB-INF/jsp/grade/list.jsp).forward(request, response); } }在这个 Servlet 里我把 JSP 页面放在了WEB-INF目录下。这个做法有一个立竿见影的好处用户无法通过浏览器直接访问WEB-INF下的文件所有页面必须经过 Servlet 转发才能打开这对权限控制很有帮助。分页参数里pageNum从 1 开始计算 SQL 里的LIMIT偏移量时要减一这是最容易算错的地方。4.3 Excel 批量导入成绩POI 解析与事务处理成绩系统里最怕的是老师拿到一张 Excel 成绩表让你手动一条条录。所以在进阶功能里我用 Apache POI 做了批量导入教师上传 Excel 文件系统解析后把成绩写入数据库。这一步涉及两个核心问题文件解析和事务一致性。public void importGrades(InputStream inputStream, int courseId) throws Exception { Workbook workbook new XSSFWorkbook(inputStream); Sheet sheet workbook.getSheetAt(0); // 开启事务所有成绩要么全部导入成功要么全部回滚 Connection conn DBUtils.getConnection(); conn.setAutoCommit(false); try { GradeDao dao new GradeDao(); for (int i 1; i sheet.getLastRowNum(); i) { Row row sheet.getRow(i); if (row null) { continue; // 跳过空行 } String studentNo getCellValue(row.getCell(0)); double score Double.parseDouble(getCellValue(row.getCell(1))); int studentId findStudentIdByNo(studentNo); if (studentId 0) { dao.insertOrUpdate(conn, studentId, courseId, score); } } conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); workbook.close(); } }这段代码里if (row null) continue;是个不起眼但很实用的边界处理。因为sheet.getLastRowNum()返回的是最后一个有数据的行索引但中间可能有空行。另外getCellValue这个方法要处理单元格类型如果 Excel 里学号是数字格式getNumericCellValue()拿到的是double转字符串时不能直接String.valueOf()否则会出现科学计数法正确的做法是判断CellType.NUMERIC后用BigDecimal转换再转字符串。事务处理是整个导入功能的关键。如果不开启事务Excel 里第 50 行数据格式有误前面 49 行已经写进数据库了老师还得自己核对哪些导入了哪些没有。开了事务之后rollback()会让所有操作集体作废重新改好 Excel 再导一次即可。4.4 导出成绩单用 POI 生成可下载的 xlsx 文件导出是导入的逆向操作。教师查询出了成绩列表后要能一键下载 Excel 到本地。导出时要注意两个问题文件名中文乱码和每次导出生成临时文件的清理。WebServlet(/grade/export) public class GradeExportServlet extends HttpServlet { Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); // 处理中文文件名用 URLEncoder 编码 String fileName URLEncoder.encode(成绩表, UTF-8).replaceAll(\\, %20); response.setHeader(Content-Disposition, attachment;filename fileName .xlsx); // 查询数据写入 WorkBook GradeDao dao new GradeDao(); ListMapString, Object list dao.findAll(); try (Workbook workbook new XSSFWorkbook()) { Sheet sheet workbook.createSheet(成绩单); // 创建表头 Row header sheet.createRow(0); header.createCell(0).setCellValue(学号); header.createCell(1).setCellValue(姓名); header.createCell(2).setCellValue(课程); header.createCell(3).setCellValue(成绩); // 填充数据略 workbook.write(response.getOutputStream()); } } }这里我用了 try-with-resources 语法自动关闭 Workbook一定要养成这个习惯。POI 的 XSSFWorkbook 在写入大量数据时非常吃内存如果数据量超过 65000 行大工作时XSSFWorkbook会爆内存这时候得换SXSSFWorkbook流式写入。对于成绩管理系统的数据量XSSFWorkbook完全够用。5. 编码、会话与部署几个你大概率会翻车的避坑记录5.1 中文乱码从 JSP 到数据库的每一层都要指定 UTF-8乱码问题是 Java Web 里最折磨人的问题因为它不在代码层面报错只在页面上给你显示一堆问号。我总结了三层防线每一层都不能漏。第一层是 JSP 页面本身的编码% page contentTypetext/html;charsetUTF-8 languagejava %这一句必须写同时页面的 meta 标签里也要写charsetUTF-8。第二层是请求和响应编码POST 请求在doPost方法第一行设置request.setCharacterEncoding(UTF-8)这个只对 POST 请求体有效如果是 GET 请求传中文参数还需要在 Tomcat 的server.xml里给 Connector 加URIEncodingUTF-8。第三层是数据库连接 URL必须有characterEncodingutf8mb4。如果以上都做了还是乱码还有一个隐藏很深的坑MySQL 表字段的字符集。建表时如果不指定DEFAULT CHARSETutf8mb4会继承数据库的默认字符集如果数据库本身是latin1你在代码里怎么处理都没用。我血泪经验是建完表后先跑一条ALTER TABLE grade CONVERT TO CHARACTER SET utf8mb4;看看能否修复能用再发起码不会反复改代码。5.2 静态资源配置CSS 和图片加载不出来的真正原因成绩管理系统的页面好看不好看全看 CSS。很多同学写完 JSP 后浏览器 F12 一看样式全没了控制台报 404。原因通常是路径写错了。在 JSP 页面里引用 CSS不能写相对路径css/style.css因为当你在/grade/list这个 Servlet 处理器里得到 request 后页面的基准路径可能是/grade/相对路径会解析成/grade/css/style.css实际文件却在/css/style.css。解决的方案有几种。最简单的是在 JSP 页面顶部加一个${pageContext.request.contextPath}前缀也就是像这样link relstylesheet href${pageContext.request.contextPath}/css/style.css。另一种是配置一个定期清理的过滤器拦截静态资源请求直接放行*.css、*.js、*.png、*.jpgWebFilter(/*) public class EncodingAndStaticResourceFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; String uri request.getRequestURI(); // 静态资源直接放行不经过编码过滤和登录拦截 if (uri.endsWith(.css) || uri.endsWith(.js) || uri.endsWith(.png) || uri.endsWith(.jpg)) { chain.doFilter(req, resp); return; } req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); chain.doFilter(req, resp); } }这个过滤器里我故意把静态资源放行放在最前面是因为如果你先做字符编码设置再做资源分发静态文件的二进制内容可能会被错误地编码处理导致图片损坏。还有一点JSP 页面如果是通过sendRedirect跳转过来的加载 CSS 时路径要以ctxPath前缀为准这算是我踩过最熟的坑。5.3 权限拦截为什么直接访问 JSP 页面会绕过登录拦截成绩管理系统有三种角色管理员、教师、学生。它们能看到的内容不同教师只能操作自己教的课程成绩学生只能查看自己的成绩管理员可以管理全部数据。如果权限控制只依赖页面上的超链接隐藏那用户只要在地址栏输入某个 Servlet 的 URL 就能直接访问完全绕过你的界面。常见的做法是写一个AuthFilter在过滤器里检查 session 里是否有登录用户并校验当前请求路径和用户角色是否匹配WebFilter(/*) public class AuthFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; // 不受保护的路径登录页、登录接口、验证码、静态资源 String path request.getRequestURI().substring(request.getContextPath().length()); if (/login.jsp.equals(path) || /login.equals(path) || /captcha.equals(path) || path.startsWith(/css/) || path.startsWith(/js/) || path.startsWith(/images/)) { chain.doFilter(req, resp); return; } // 检查 session 中的登录状态 HttpSession session request.getSession(); User user (User) session.getAttribute(loginUser); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } // 管理员访问学生接口时拦截 if (/grade/export.equals(path) user.getRole() ! 1) { response.sendError(HttpServletResponse.SC_FORBIDDEN); return; } chain.doFilter(req, resp); } }写这个过滤器时要留意request.getRequestURI()和request.getContextPath()的搭配。如果你在过滤器里直接用完整 URI 判断部署时项目名一旦改动所有路径判断全部失效所以必须先减去contextPath。还有 session 过期问题如果用户长时间不操作session 空了过滤器要把人踢回登录页但登录页本身必须是白名单不然会死循环重定向。5.4 部署到云服务器WAR 包名、Tomcat 端口和路径坑到了部署阶段常见的做法是把项目打成 WAR 包扔进 Tomcat 的webapps目录。但这个过程中有三个坑特别烦人。第一个坑是 WAR 包文件名如果你把包命名为grade_management.war部署后访问路径就是http://ip:8080/grade_management/login.jsp多了一层目录页面里的所有相对路径都要重新计算。想直接通过根路径访问就得把 WAR 包改名为ROOT.war替换掉 Tomcat 自带的 ROOT 目录。第二个坑是端口冲突。云服务器上经常已经跑了 Nginx 或其他服务8080很可能被占。修改 Tomcat 端口的方法是编辑conf/server.xml文件把Connector port8080改成其他端口比如 8081。注意除了这个 HTTP Connector还有两个端口需要留意Server port8005 shutdownSHUTDOWN和 AJP 的8009如果这三个端口没改全启动时报端口占用的错误会让人摸不着头脑。第三个坑是防火墙和安全组过滤。云服务商的安全组规则默认只开放少数端口你要在控制台把8080或你改的端口加入放行列表。做完以上步骤才打开浏览器验证curl -I http://localhost:8080/login.jsp能返回 200再试外网 IP。6. 系统验证与后续优化从“能跑”到“跑得更稳”的三个进阶方向6.1 功能验证的检查清单我和别人交接时做的用例表确认功能开发完成后不能只点两下页面就觉得 OK 了。我一般会用一份检查清单逐项过一遍登录模块要验证正确密码登录、错误密码提示、验证码过期、session 过期后跳转成绩模块要验证空成绩、0 分、满分 100 分、超过 100 分的非法值是否能被拦截Excel 导入要测试空学号、重复记录、非法字符。这一项一项过完再交给导师或同事验收至少不会出现“页面能打开但核心功能是坏的”这种尴尬局面。在验证过程中我发现最容易遗漏的是成绩更新的幂等性。教师第一次导入成绩后又修改了某个学生的分数重新导入这时候系统应该做的是更新而不是插入重复记录。对应的 SQL 就是INSERT ... ON DUPLICATE KEY UPDATE利用成绩表上的唯一索引uk_student_course来实现。这个细节在答辩时很能体现你考虑问题的全面性。6.2 性能体检模拟 100 个并发查询时应该看哪些指标成绩管理系统在提交作业或上线前最好做一次简单的压力测试。Apache JMeter 是最常见的工具我一般会设置 100 个线程同时请求成绩列表接口然后观察两个核心指标响应时间和错误率。如果平均响应时间超过 500ms第一件检查的事是 SQL 有没有走索引。在成绩表上查询条件通常是WHERE student_id ? AND course_id ?这时候如果uk_student_course这个联合索引存在查询就是索引覆盖扫描速度飞快。但如果你的查询条件只有student_id或只有course_id联合索引只能生效一半这时候需要额外给单独字段建索引。另外一个性能优化点是 JSP 页面的 include 指令如果头部导航栏、底部版权栏在每个页面都静态引入编译时会重复生成大量代码改用% include fileheader.jsp %或者 jsp:include 动态引入能显著减少页面体积。6.3 安全加固SQL 注入和 XSS 攻击的防线在哪里成绩系统里有一个典型的 SQL 注入风险点登录功能。如果你在 DAO 里用的是字符串拼接 SQL用户输入 OR 11作为用户名你的登录验证就形同虚设。我开发时统一用 PreparedStatement 预编译它能有效杜绝这种注入。而 XSS 攻击则主要发生在 JSP 页面的数据回显上学生提交的“备注”字段可能包含script标签如果不做转义直接输出别人看到的就是弹窗脚本。在 JSP 里使用 EL 表达式${student.note}输出时JSTL 默认不会自动转义 HTML。要防护 XSS需要配合 JSTL 的c:out value${student.note} /它自带转义功能。另外在过滤器里添加统一的X-Content-Type-Options: nosniff响应头能防止浏览器把非脚本文件当作脚本执行这个加一行设一个头部就够response.setHeader(X-Content-Type-Options, nosniff)。从毕设到现在我前后写过三个不同定制版本的 JspServlet 管理系统体验和从这几个坑里翻出来的经验几乎一模一样。最后说一个我的个人习惯供参考每次部署前都会在本地用干净的环境从 JDK 安装到项目启动完整走一遍把变量配置和端口变化都记在 Notebook 里折腾完再登录服务器验证一遍。真心希望这篇笔记能帮你的成绩管理系统少走几趟弯路——哪怕只帮你躲过一个乱码坑也算没白写了。本文还有配套的精品资源点击获取