ARTICLE DETAIL

资讯详情

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

JSP+Servlet成绩管理系统实战:Servlet生命周期、Session与过滤链全解析

JSP+Servlet成绩管理系统实战:Servlet生命周期、Session与过滤链全解析 简介一套基于JSP与Servlet的Web成绩管理系统面向Java Web课程设计学生及入门开发者。系统按学生、教师、管理员三种角色划分模块边界清晰学生可查询与修改个人信息、查看成绩教师可录入和调整学生成绩管理员则负责学生、教师、班级、课程及用户权限的全面管理覆盖成绩管理系统的典型业务闭环。压缩包共870个文件约39.64MB以Java源码、JSP页面、JS/CSS前端资源为主并配有SQL数据库脚本、配置文件和说明文档方便导入开发工具后直接运行或二次修改。目前已有173人学习下载。项目中整合了Bootstrap、Summernote等前端组件页面交互完整通过梳理各模块实现能快速理解Servlet控制层、JSP视图层与MySQL数据层之间的协作同时学习表单处理、数据校验、权限管理等关键知识点适合毕业设计、课程设计或自学练手时参考使用。1. 这套JSPServlet成绩管理系统到底解决了什么问题很多同学第一次接触Java Web开发都会选“成绩管理系统”作为毕设或课程设计题目。这个系统听起来简单但当你真正上手去做会发现它几乎覆盖了Servlet生命周期的全部核心知识点请求怎么进来、参数怎么接、Session怎么维持用户状态、JDBC怎么连数据库、JSP怎么把数据渲染成页面。那些只给个前端页面却没有任何业务逻辑的“假系统”和基于JspServlet实现、能跑通增删改查和登录认证的真系统中间隔着一整层Java Web知识。这篇文章从一个一线开发者的视角带你把这个项目从零搭起来。你需要会点Java基础知道JDBC是什么能装好IDEA、Maven、MySQL和Tomcat。做完你会得到一个能部署到真实服务器上的WAR包而不是只能在本地IDE里点着玩的黑匣子。这里不涉及Spring全家桶正因为如此你才能看清楚Servlet容器、过滤器、监听器、JSP、Session这些底层组件是怎么协作完成一次完整的Web请求的。2. 项目骨架与数据库设计成绩管理系统的表该怎么建2.1 用户、学生、课程、成绩四张表的关系能拆多细成绩管理系统的数据模型最常见的是三角关系学生、课程、成绩。再加上一个管理员登录账号用于保护整个后台。这套结构放在JSPServlet项目里最大的好处是表少、关联清晰而且能让你练习到一对多和复合主键这两种基础SQL设计。我建议先建这四张表。学生表存储学号、姓名、班级、性别、入学年份课程表存储课程编号、课程名、学分成绩表是关键它不单放一个成绩字段而是把学生ID和课程ID作为联合主键这样才能保证同一个学生对同一门课只有一条成绩记录管理员表单独存放账号密码。CREATE DATABASE IF NOT EXISTS score_system DEFAULT CHARSET utf8mb4; USE score_system; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role VARCHAR(20) DEFAULT admin ) ENGINEInnoDB; CREATE TABLE t_student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, gender CHAR(1), class_name VARCHAR(50), enroll_year VARCHAR(10) ) ENGINEInnoDB; CREATE TABLE t_course ( id INT PRIMARY KEY AUTO_INCREMENT, course_no VARCHAR(20) NOT NULL UNIQUE, course_name VARCHAR(50) NOT NULL, credit DECIMAL(3,1) ) ENGINEInnoDB; CREATE TABLE t_score ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,2), exam_date DATE, CONSTRAINT fk_score_student FOREIGN KEY (student_id) REFERENCES t_student(id), CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES t_course(id), CONSTRAINT uk_student_course UNIQUE (student_id, course_id) ) ENGINEInnoDB;这里我把t_score的联合唯一约束建成了uk_student_course。这样做的直接好处是业务层插入成绩时不需要先查一遍“这个学生这门课是不是已存在”数据库会在约束层面把重复数据挡住。如果后期想让同一个学生可以补考多次、保留多条历史记录再把这条唯一约束去掉改为前端控制查询最近一次考试成绩。密码字段建议存SHA-256摘要而非明文。你没看错登录功能在毕设里经常被直接明文存储但一条合格的Java Web课程设计应该加上密码加密。数据库表设计时就把它当一条安全红线后面写Servlet登录逻辑时才不会偷懒。2.2 三层包结构与Servlet职责划分跳开纯JSP页面的坑传统JSP项目最常见的翻车是让JSP页面直接访问数据库把% Connection conn ...写在页面顶部。这样项目确实能跑但后续扩展成本极高页面一多改个数据库密码得全局搜索。正确的做法是复用MVC经典分层Model层放DAO和实体类View层放JSPController层放Servlet。包结构我这样划分com.score.entity —— Student、Course、Score、User 实体类 com.score.dao —— 每个实体对应一个DAO接口和实现类 com.score.service —— 业务逻辑层处理密码校验、成绩平均分计算 com.score.servlet —— 接收请求、调用Service、跳转或转发 com.score.filter —— 登录状态过滤器 com.score.util —— DBUtil、MD5工具类Servlet在这套结构里只做三件事取参数、调Service、定跳转。不要在Servlet里写一行SQL更不要在Servlet里做复杂业务计算。曾有同学把所有代码塞进一个doPost里六百多行最终连自己都分不清哪个功能对应哪个方法。2.3 从JSP页面反向推Servlet请求映射设计Servlet映射时不要用/servlet/StudentAdd这种风格。推荐直接用伪静态风格映射/student/add、/student/list、/score/input。这样页面地址看起来清晰后期如果换成SpringMVC控制器的URL基本不用大改。每个模块遵循同一套约定list对应列表查询走GET请求add对应跳转新增页面和提交新增前者GET后者POSTupdate对应修改先GET到编辑页回显再POST提交新数据delete对应删除走POST请求删除接口不要用GET。有些同学图省事写一个a href/student/delete?id3就完事了。这等于把删除操作塞进浏览器地址栏爬虫也能触达的入口传参不够安全。3. 用Servlet处理成绩增删改查接口设计与参数校验3.1 写一个BaseServlet统一分发请求四个模块如果每个都建四个Servlet项目会平白多出一堆类。更常见的做法是写一个BaseServlet重写service方法通过action参数自动调用对应方法。这样控制层的代码量大幅缩小也符合大多数JSPServlet课程设计的要求。package com.score.servlet; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.lang.reflect.Method; public class BaseServlet extends HttpServlet { Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action req.getParameter(action); if (action null || action.trim().isEmpty()) { throw new ServletException(缺少 action 参数无法分发请求); } try { Method method this.getClass().getDeclaredMethod(action, HttpServletRequest.class, HttpServletResponse.class); method.setAccessible(true); method.invoke(this, req, resp); } catch (Exception e) { throw new ServletException(请求处理方法 action 调用失败, e); } } }这段代码用反射机制在service方法里把URL参数action映射为当前Servlet里的同名方法。例如请求/course?actionlist就会自动调用CourseServlet类的list(HttpServletRequest, HttpServletResponse)方法。好处是新增一个功能只需要写一个方法不用修改web.xml和映射配置。使用反射要注意参数顺序不能写反HttpServletRequest必须放在第一个参数位置否则方法调用时会报参数类型不匹配。另外反射在并发量很大时性能不如直接分支判断但这种校内课程设计级别的QPS完全不需要考虑性能劣化的问题。3.2 成绩录入接口参数校验为什么不能只依赖前端成绩录入是整个系统的核心入口。前端JSP页面会用input typenumber限制输入范围但后端Servlet必须再做一次完整校验。前端的限制只是提升用户操作体验后端校验才是数据安全的底线因为一个心怀恶意的人完全可以用Postman绕过页面直接向你的接口提交score999或score-1。Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String studentIdStr req.getParameter(studentId); String courseIdStr req.getParameter(courseId); String scoreStr req.getParameter(score); // 防止看漏了 null 与空字符串的区分 if (studentIdStr null || studentIdStr.trim().isEmpty() || courseIdStr null || courseIdStr.trim().isEmpty() || scoreStr null || scoreStr.trim().isEmpty()) { req.setAttribute(errorMsg, 学生、课程、成绩均不能为空); req.getRequestDispatcher(/error.jsp).forward(req, resp); return; } int studentId 0; int courseId 0; double score 0.0; try { studentId Integer.parseInt(studentIdStr.trim()); courseId Integer.parseInt(courseIdStr.trim()); score Double.parseDouble(scoreStr.trim()); } catch (NumberFormatException e) { req.setAttribute(errorMsg, 参数格式错误必须为数字); req.getRequestDispatcher(/error.jsp).forward(req, resp); return; } if (score 0 || score 100) { req.setAttribute(errorMsg, 成绩必须在0到100分之间); req.getRequestDispatcher(/error.jsp).forward(req, resp); return; } ScoreService scoreService new ScoreService(); boolean result scoreService.addScore(studentId, courseId, score); if (result) { resp.sendRedirect(req.getContextPath() /course?actionscoreList); } else { req.setAttribute(errorMsg, 该学生此课程的成绩已存在不能重复录入); req.getRequestDispatcher(/error.jsp).forward(req, resp); } }这段代码体现了三层校验逻辑先判空再转换类型最后判断业务范围。我把数值转换单独包在try/catch里就是为了区分“参数缺失”和“参数格式错”这两种不同的错误场景方便页面上给用户展示不同的提示信息。这里有个容易被忽视的点请求成功的跳转和请求失败的跳转方式不一样。成功后我用了resp.sendRedirect做重定向失败后用req.getRequestDispatcher().forward做请求转发。原因是重定向会让浏览器发起一次全新的请求地址栏会变成列表页URL用户刷新时只会重复刷新列表不会导致成绩重复提交。而转发是服务端内部跳转地址栏不变化用户按F5会把刚才的POST请求再提交一遍很容易产生重复数据。初次做Java Web项目时很多人在这两类跳转的区别上栽过跟头。3.3 成绩列表查询与JSP渲染不要忘了空数据判断成绩列表页面是整个系统最常被查看的页面。它要把成绩表和学生表、课程表联查出来然后在JSP里用表格逐行输出。查询逻辑写在DAO层Servlet只负责把ListScore传给JSP。JSP拿到集合后必须处理集合为空的情况。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html head title成绩列表/title /head body h2成绩列表/h2 c:if test${empty scoreList} p当前没有成绩记录请先录入。/p /c:if c:if test${not empty scoreList} table border1 cellpadding6 cellspacing0 tr th学号/th th学生姓名/th th课程名/th th成绩/th th考试日期/th th操作/th /tr c:forEach items${scoreList} varscore tr td${score.studentNo}/td td${score.studentName}/td td${score.courseName}/td td${score.score}/td td${score.examDate}/td td a href${pageContext.request.contextPath}/score?actioneditid${score.id}编辑/a a hrefjavascript:void(0) onclickconfirmDelete(${score.id})删除/a /td /tr /c:forEach /table /c:if script function confirmDelete(scoreId) { if (confirm(确定要删除这条成绩记录吗删除后不可恢复。)) { var form document.createElement(form); form.method post; form.action %request.getContextPath()%/score?actiondelete; var input document.createElement(input); input.type hidden; input.name id; input.value scoreId; form.appendChild(input); document.body.appendChild(form); form.submit(); } } /script /body /html我用JSTL的c:forEach循环输出而不是脚本片段式JSP里的for(int i0; ilist.size(); i)。原因有两个一是JSTL能在页面上强制不写Java代码页面维护起来清爽得多二是${score.score}的输出自动调用toString方法不会因为空指针导致整个页面500。删除操作必须用POST。代码里我用JavaScript动态创建了一个表单把删除ID包装成隐藏字段提交。这种写法虽然比直接a href删多一点代码但它避免了GET请求删除数据的安全隐患也演示了在实际开发里如何在页面上安全地提交一个POST请求。4. 登录认证与过滤链Session与Filter的价值所在4.1 加密存储与登录校验DAO层的防呆设计登录功能是成绩管理系统区别于纯静态Web页面的关键标志。没有登录的话任何人都可以直接访问/score/list接口看到所有成绩。一个正经的JSPServlet项目一定要有一个登录页面、一个登录Servlet和一个逻辑判断用户Session是否存在的过滤器。用户表的密码密文存储我用的方案是SHA-256加盐。加盐是指在原始密码后面拼接一个固定字符串再计算哈希。这样做是为了防止恶意用户用彩虹表逆推出常见密码比如123456的SHA-256哈希值在互联网上一搜就能找到。public class MD5Util { private static final String SALT ScoreSystem2024; public static String encrypt(String password) { String target password SALT; try { MessageDigest md MessageDigest.getInstance(SHA-256); byte[] digest md.digest(target.getBytes(StandardCharsets.UTF_8)); StringBuilder sb new StringBuilder(); for (byte b : digest) { String hex Integer.toHexString(b 0xff); if (hex.length() 1) { sb.append(0); } sb.append(hex); } return sb.toString(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(SHA-256 算法不可用, e); } } }盐值不要放在代码里写死。更常见的做法是放在db.properties配置文件中部署时按环境替换。把盐写在工具类里虽然不优雅但对一个课程设计来说已经能挡住绝大多数初级的直接查库破解。如果以后要做到生产级别推荐用BCryptPasswordEncoder这类每次生成随机盐的算法。登录Servlet拿到用户上传的明文密码后调用MD5Util.encrypt得到密文再与数据库里的密文字段比对。数据库里不要存password原始明文这是底线。有些老系统甚至把密码明文直接放在前端页面的注释里展示给用户那已经是重大安全事故了。4.2 过滤器拦截未登录用户每个受保护站点的护城河登录验证只做在单个Servlet里远远不够。成绩管理系统的/course/list、/score/add、/student/update这些地址用户手动在浏览器里输入URL就能直接访问完全绕过了登录页面。要阻止这种未授权访问必须给一组URL加一个过滤器。package com.score.filter; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; public class LoginFilter implements Filter { private String excludedUrls; Override public void init(FilterConfig filterConfig) throws ServletException { this.excludedUrls filterConfig.getInitParameter(excludedUrls); } Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; String requestURI req.getRequestURI(); String contextPath req.getContextPath(); String path requestURI.substring(contextPath.length()); if (excludedUrls ! null excludedUrls.contains(path)) { chain.doFilter(request, response); return; } HttpSession session req.getSession(false); if (session ! null session.getAttribute(loginUser) ! null) { chain.doFilter(request, response); } else { // 未登录则重定向到登录页 String loginPage req.getContextPath() /login.jsp; resp.sendRedirect(loginPage); } } Override public void destroy() { } }req.getSession(false)这个方法传false表示如果当前请求里没有Session对象就直接返回null不会自动创建一个空Session。很多教学代码里直接写req.getSession()如果此时用户还没有Session容器会为他合成一个新Session结果过滤器把新建的Session也当成无效处理白白浪费了资源。判断登录状态时用getSession(false)配合getAttribute才是标准姿势。排除列表里的访问路径至少要包括/login.jsp、/user?actionlogin和静态资源CSS、JS文件。如果忘记排除静态资源用户打开登录页面时会发现浏览器报404或者样式全丢了因为CSS请求也被拦截并重定向到登录页。4.3 退出登录小心Session没有真正失效业务里容易被忽视的细节就是退出登录。最常见的写法是点击“退出”按钮后在LoginServlet里写一行session.invalidate()。但这还不够安全因为Session失效后浏览器地址栏里的JSESSIONID这个Cookie还残留在客户端。正确的做法是把会话Cookie一并清除。protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session req.getSession(false); if (session ! null) { session.invalidate(); } Cookie[] cookies req.getCookies(); if (cookies ! null) { for (Cookie cookie : cookies) { if (JSESSIONID.equals(cookie.getName())) { cookie.setMaxAge(0); cookie.setPath(req.getContextPath()); resp.addCookie(cookie); break; } } } resp.sendRedirect(req.getContextPath() /login.jsp); }调session.invalidate()之后Servlet容器会把服务端的Session对象标记为失效但客户端浏览器里存放的JSESSIONID Cookie仍然存在。Cookie的清理方式是把它的maxAge设置为0再通过resp.addCookie重新写回浏览器浏览器收到后会立刻把同名Cookie删掉。完整的退出登录这两步缺一不可。5. 成绩管理系统常见翻车点现象、原因、解决一条条说清5.1 JSP界面修改后浏览器一直显示旧页面现象在IDEA里改了student_list.jsp上的文字或样式刷新浏览器页面没有任何变化强制刷新也一样。原因Tomcat默认对JSP的修改检测有时间间隔。当项目使用Exploded方式部署时IDEA把编译后的文件直接输出到target目录而JSP文件在src\main\webapp目录下修改后有时没有同步到target目录。另一个原因是浏览器缓存了旧页面。解决在IDEA中将Tomcat配置里的On Update Action和On Frame Deactivation都设置为Update resources这样JSP修改后IDE会自动同步文件到部署目录。如果还没变化关掉Tomcat手动删除target目录后用Maven的clean命令重启项目。提示修改web.xml、Filter类的init方法等容器级配置后Update resources不会生效必须Redeploy或重启Tomcat。5.2 表单提交中文乱码现象从JSP页面往Servlet提交中文姓名后Servlet里接收到的参数值变成一堆问号。原因HTML页面的charset设置是UTF-8但Servlet端在读取请求参数前没有设置request.setCharacterEncoding(UTF-8)。Tomcat 8及以上版本对POST请求默认编码也是UTF-8但实际项目里经常出现过滤器设置不及时的问题。解决在Servlet的doPost方法第一行加上req.setCharacterEncoding(UTF-8)。如果项目里已经有Filter在doFilter里加上同样代码。另外检查JSP页面顶部的contentType是否有charsetUTF-8、数据库连接串是否带了characterEncodingutf8、MySQL表本身是否用的utf8mb4字符集。四层编码全部统一后乱码基本能解决。5.3 连接数据库报ClassNotFoundException现象启动Tomcat后访问/student/list页面报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因mysql-connector-java依赖没有打包到WEB-INF/lib目录下。使用IDEA的Artifacts打包WAR时依赖库有时候没有被包含进去。另一种原因是用了旧版驱动包但代码里写的是新版驱动类名。解决使用Maven管理项目时检查pom.xml里有没有mysql-connector-java依赖。在IDEA的Project Structure - Artifacts里把Available Elements里的依赖右键选择Put into /WEB-INF/lib。如果代码里写的是com.mysql.jdbc.Driver把驱动包升级后改成com.mysql.cj.jdbc.Driver。5.4 删除成绩后列表页还显示旧数据现象点击删除按钮页面提示已删除但列表页还是能看到刚被删除的那条记录。原因浏览器的地址栏里显示的列表地址是/score?actionlist删除逻辑里用的是req.getRequestDispatcher(/score?actionlist).forward(...)而不是sendRedirect。浏览器地址栏没有变化点击F5后重新提交了一次POST请求于是又被当作删除操作返回了。解决删除成功后的跳转必须使用resp.sendRedirect到列表页地址。转发只能用于请求失败或数据校验失败成功操作一律重定向。这样JSP页面上看到的是一条全新的GET请求而不是上一次POST请求的回显。5.5 部署到服务器后登录失败、页面打不开现象本地IDEA里一切正常打成WAR包部署到Linux服务器后Tomcat访问/login.jsp可以打开但提交登录信息后一直进不去或提示404。原因项目Context Path没对上。本地部署时IDEA配置的Application context是/score_war_exploded打成WAR包后如果直接放在webapps目录访问地址变成了http://ip:8080/score_war/。代码里的跳转地址如果全部写成了绝对路径而没有带request.getContextPath()就会跳到一个不存在的路径上。解决所有resp.sendRedirect和req.getRequestDispatcher的路径拼接里必须首先包含req.getContextPath()作为前缀。代码里凡是出现以/开头的跳转地址检查是否漏了contextPath。这是JSPServlet项目部署到真实服务器时最常踩的坑之一。6. 进阶用EL表达式和JSTL重构页面再用单元测试兜底6.1 重构思路把Java代码从JSP页面里彻底赶出去一个可以打80分的JSPServlet成绩管理系统除了功能能跑通还要看代码的可维护性。我常见的做法是功能完成后花半小时把页面里所有的%...%脚本片段替换成EL表达式和JSTL标签。替换后页面文件里不再出现import语句、不再出现for循环、不再出现类型强转。以前遍历成绩列表新手写的JSP是下面这个样子% ListScore list (ListScore) request.getAttribute(scoreList); for (Score s : list) { % tr td% s.getStudentNo() %/td /tr % } %改成JSTL后一个c:forEach就完成了同样的事。EL表达式自动从PageContext、Request、Session、Application四个作用域里寻找变量名。唯一要注意的是Servlet里setAttribute的名字必须与${}里的名字完全一致大小写都不能错否则页面上就会原样显示一个空字符串不报错但很难排查。6.2 配置Tomcat里的数据源比每次手动创建Connection更可靠毕业设计阶段使用DRIVER_MANAGER.getConnection获取数据库连接没有问题。但如果你打算把这个项目当作后续SpringMVC、SpringBoot学习的基础理解连接池的概念会更少吃苦头。在Tomcat的context.xml中配置JNDI DataSource然后Servlet通过Context.lookup获取数据源这个过程值得手动做一遍。Context Resource namejdbc/scoreDB authContainer typejavax.sql.DataSource maxTotal20 maxIdle10 maxWaitMillis10000 usernameroot password123456 driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/score_system?useUnicodetrueamp;characterEncodingutf8amp;useSSLfalseamp;serverTimezoneAsia/Shanghai/ /Context配置里的maxTotal20表示连接池最多同时保持20个物理连接maxWaitMillis10000表示当连接用尽时调用方最多等10秒。DBUtil获取连接时不再创建新连接而是从连接池里借一个。项目关停时Tomcat会自动把连接收回并关闭省去了在DAO里手动conn.close()可能遗漏的风险。6.3 写一个不带页面的Servlet单元测试验证成绩计算的边界成绩管理系统除了增删改查还经常要输出“学生平均分”和“课程最高分”这部分计算逻辑值得单独提取到Service层再用JUnit写一个脱离容器的单元测试。这样不仅验证了算法还为后续重构提供了回归测试保障。public class ScoreCalculatorTest { Test public void testCalculateAverage_EmptyList() { ScoreCalculator calculator new ScoreCalculator(); ListDouble scores new ArrayList(); double avg calculator.calculateAverage(scores); assertEquals(0.0, avg, 0.001); } Test public void testCalculateAverage_NormalScores() { ScoreCalculator calculator new ScoreCalculator(); ListDouble scores Arrays.asList(80.0, 90.0, 70.0); double avg calculator.calculateAverage(scores); assertEquals(80.0, avg, 0.001); } Test public void testCalculateAverage_NullValueShouldBeIgnored() { ScoreCalculator calculator new ScoreCalculator(); ListDouble scores Arrays.asList(80.0, null, 100.0); double avg calculator.calculateAverage(scores); assertEquals(90.0, avg, 0.001); } }单元测试的价值在于当前台页面要求成绩列表页显示平均分时你不必部署整个Tomcat跑到页面上看结果本地直接跑JUnit就能判断计算逻辑对不对。空集合返回0、正常集合求均值、集合中含有null值时跳过不参与计算这三个边界条件如果不在测试里定清楚将来在页面上遇到就会变成一行行报错的焦虑。我最早做成绩管理系统时也经历过把脏数据写进库里、忘记重定向导致重复提交、改完JSP看不到变化的痛苦。后来形成一套习惯每次改动前先跑一遍所有单元测试再把项目部署到本地Tomcat走一遍“登录-列表-新增-编辑-删除”完整流程。这套操作看起来朴素但能确保系统在答辩演示时不翻车。JSPServlet这门技术虽然老但它足够简单恰好让你把Web开发的每一个环节都看破说透希望帮到你。本文还有配套的精品资源点击获取
返回列表