尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

从零构建Java Web学生管理系统:Servlet+JSP+JDBC+MySQL全流程解析

从零构建Java Web学生管理系统:Servlet+JSP+JDBC+MySQL全流程解析 简介一份面向Java Web学习者的学生管理系统完整项目涵盖学生信息录入、查询、修改、删除以及成绩管理、课程安排等核心业务适合课程设计、毕业设计或入门实战参考。压缩包共154个文件包含54个Java源码与对应class文件、14个JSP页面、7个jar依赖、1个SQL数据库脚本以及多张PNG/JPG运行截图总大小约8.7MB目录结构清晰便于对照学习前后端分层与MVC设计模式。项目涉及Servlet、JSP、JSTL等Java Web基础知识结合MySQL数据库表设计并融入了Spring Boot、MyBatis等框架思想前端使用HTML5、CSS3、JavaScript及Vue.js进行交互展示同时展示了Shiro/Spring Security权限控制、Tomcat部署等真实项目中的常见环节。已有1010人学习下载通过源码加数据库脚本读者可快速搭建环境运行系统并从中掌握从Dao、Service到Controller的完整代码组织方式为独立开发Web应用积累扎实经验。1. 学生管理系统为什么是 Java Web 入门的第一道坎学生管理系统大概是 Java Web 领域里被复现次数最多的项目甚至没有之一。任何一家培训机构的结业项目、任何一届计算机专业的数据库课程设计都绕不开它。但恰恰是这个看起来只有增删改查的小系统卡住了大量从 Java SE 过渡到 Java Web 的开发者——框架越用越熟反而说不清一个 HTTP 请求从浏览器出发到数据库返回结果中间到底经过了几层、每层在干什么。这个标题里的“源码数据库截图”三个词实际上对应的是三个不同的交付物源码对应工程结构和分层设计数据库对应建库脚本和表关系设计截图对应页面交互和功能完整度。你拿到这样的压缩包目的无非是两个要么在课程设计或毕业设计里提交一套能跑通的系统要么把这份代码当作阅读材料弄懂 Servlet、JSP、JDBC、MySQL 这几样东西是怎么协同工作的。本文就从这三个交付物出发把学生管理系统从环境搭建到部署验证的完整路径拆开讲中间会涉及数据表怎么设计、DAO 层要不要写接口、分页查询的 SQL 怎么写、以及部署时那些让你怀疑人生的路径问题。适合正在做课程设计的学生也适合打算用这个项目复习 Java Web 基础、准备 java 面试题里关于 MVC 和数据库连接池问题的开发者。2. 建库脚本与数据表设计学生管理系统的地基2.1 先想清楚这张 ER 图怎么画学生管理系统虽然叫“学生管理”但如果你只建一张 student 表后面的扩展会很痛苦。最常见的需求是三角色模型学生、教师或管理员、课程外加一张选课关系表。这种设计对应数据库课程设计里常说的三范式核心是避免数据冗余——学生的姓名、学号只存一次选课关系单独抽出来修改课程名称时不需要批量更新学生表。表结构设计的产物直接对应压缩包里那个.sql文件通常叫student_ms.sql或db_student.sql。打开这个文件你应该能看到CREATE DATABASE、USE、CREATE TABLE、INSERT INTO这几类语句。没有这些语句的“源码数据库”包是不完整的因为光有 Java 代码没有建库脚本换一台机器根本跑不起来。2.2 最小可用的建库脚本长什么样学生管理系统的标准表结构至少要包含以下四张表对应关系如下表所示表名关键字段说明t_userid, username, password, role登录账号表role 区分管理员和学生t_studentid, stu_no, name, gender, age, class_name学生基本信息表t_courseid, course_no, course_name, credit课程信息表t_scoreid, student_id, course_id, score选课和成绩表关联学生与课程这份表结构用一段完整的 SQL 来表达常见做法是把下面的内容直接保存为student_ms.sqlCREATE DATABASE IF NOT EXISTS student_ms DEFAULT CHARACTER SET utf8mb4; USE student_ms; -- 用户表登录认证用 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role TINYINT NOT NULL DEFAULT 0 COMMENT 0-管理员 1-学生 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 学生表核心业务表 CREATE TABLE t_student ( id INT PRIMARY KEY AUTO_INCREMENT, stu_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, name VARCHAR(32) NOT NULL, gender CHAR(1) DEFAULT 男, age INT DEFAULT 0, class_name VARCHAR(64) DEFAULT COMMENT 班级 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 课程表 CREATE TABLE t_course ( id INT PRIMARY KEY AUTO_INCREMENT, course_no VARCHAR(20) NOT NULL UNIQUE, course_name VARCHAR(64) NOT NULL, credit DECIMAL(3,1) DEFAULT 0 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 成绩表多对多关系的中间表 CREATE TABLE t_score ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,2) DEFAULT 0, CONSTRAINT fk_score_student FOREIGN KEY (student_id) REFERENCES t_student(id), CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES t_course(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; INSERT INTO t_user(username, password, role) VALUES(admin, admin123, 0);这里有个常见的妥协方案为了降低课程设计的实现难度很多人会把成绩字段直接放进学生表或者完全省略课程表只保留前两张表。这样做代码量会少一些但如果答辩老师问起“为什么不拆表”你很难给出合理答案。只要不是时间紧到只剩一天我建议保留完整的四表结构同时准备两三条INSERT INTO t_student的初始数据方便系统部署后立刻验证效果。2.3 外键约束和 InnoDB 选型的边界建表脚本里每个表都指定了ENGINEInnoDB这在学生管理系统里是一个必须被理解的选择。InnoDB 支持事务和外键约束t_score表里那两个CONSTRAINT ... FOREIGN KEY的作用是确保不会出现一条成绩记录指向一张不存在的学生记录。如果只用默认的 MyISAM外键会被静默忽略而且不支持行级锁并发稍高时容易出问题。外键约束也有它麻烦的一面——删除学生时要先删成绩记录否则会报外键约束错误。你在写删除功能时如果遇到Cannot delete or update a parent row这个报错不用怀疑数据库配置就是子表里有引用数据没清理。常见做法是在删除学生的 Service 层方法里先调用成绩 DAO 的删除方法再删除学生记录顺序不能反。utf8mb4 字符集的选择同样值得在答辩时主动提一句MySQL 的 utf8 是 utf8mb3 的别名存不了 emoji 和生僻字utf8mb4 才是真正的四字节 UTF-8。学生姓名里如果出现生僻字用旧字符集就会报Incorrect string value错误。从选型理由到 SQL 落地这一步是整篇数据库课程设计的基础建议先用 Navicat 或命令行把脚本执行一遍确认库和表都能正常创建再继续写 Java 代码。3. 工程结构与 MVC 分层拿到源码后先看哪里3.1 为什么学生管理系统必须分包你解压源码包后看到的第一个目录就是工程结构。如果是一个标准的 Java Web 工程目录结构应该是这样的src/ ├── com/student/ms/ │ ├── dao/ # 数据访问层负责 JDBC 操作 │ │ ├── StudentDao.java │ │ └── impl/ │ │ └── StudentDaoImpl.java │ ├── entity/ # 实体类对应数据表 │ │ ├── Student.java │ │ ├── Course.java │ │ └── Score.java │ ├── service/ # 业务逻辑层 │ │ ├── StudentService.java │ │ └── impl/ │ │ └── StudentServiceImpl.java │ ├── servlet/ # 控制器层接收 HTTP 请求 │ │ ├── StudentListServlet.java │ │ ├── StudentAddServlet.java │ │ └── StudentDeleteServlet.java │ └── util/ # 工具类放 DBUtil、StringUtil │ └── DBUtil.java web/ ├── WEB-INF/ │ ├── web.xml # Servlet 映射配置 │ └── lib/ # 依赖 jar 包 ├── css/ # 样式文件 ├── jsp/ # 视图层页面 │ ├── student_list.jsp │ ├── student_add.jsp │ └── student_update.jsp └── index.jsp # 入口页面这个结构在 java web 项目里几乎是约定俗成的目的就是让每一层各司其职entity 层只装数据不写逻辑dao 层只做 SQL 操作不处理业务判断sevlet 层接收请求后调用 service 层service 层返回结果由 servlet 转发给 JSP 渲染。很多初学者觉得这种分层麻烦把 JDBC 代码直接写在 JSP 页面里系统是能跑但后续加一个“查询成绩”的功能就要改好几个页面出了问题也不知道该排查哪里。判断一份源码质量的标准很简单打开StudentDaoImpl.java如果里面同时出现了request.getParameter()和Statement的字符串拼接 SQL说明分层没做彻底。一份合格的课程设计源码DAO 层里不应该出现任何 Servlet API 的引用。3.2 StudentDaoImpl 里的 JDBC 模板代码DAO 层是学生管理系统里代码最密集的部分全部都是 JDBC 标准操作。以查询所有学生为例常见做法是维护一个 DBUtil 工具类来管理 Connection 的获取避免每个方法里都写一遍注册驱动和建连接// DBUtil.java - 数据库连接工具类 package com.student.ms.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { // 这里的连接信息要和 db.properties 里保持一致 private static final String URL jdbc:mysql://localhost:3306/student_ms?useSSLfalsecharacterEncodingutf8mb4; private static final String USER root; private static final String PASSWORD 123456; static { try { // 加载驱动MySQL 8.0 对应 com.mysql.cj.jdbc.Driver Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { // 每次请求都新建连接课程设计够用生产环境要换连接池 return DriverManager.getConnection(URL, USER, PASSWORD); } }StudentDaoImpl 里的查询方法这样写// StudentDaoImpl.java - 查询学生列表 public ListStudent findAll() { // 定义在方法外finally 块里统一关闭 Connection conn null; PreparedStatement ps null; ResultSet rs null; ListStudent list new ArrayList(); String sql SELECT id, stu_no, name, gender, age, class_name FROM t_student ORDER BY id; try { conn DBUtil.getConnection(); // PreparedStatement 预编译防止 SQL 注入 ps conn.prepareStatement(sql); rs ps.executeQuery(); while (rs.next()) { Student s new Student(); // 按列名取值比按下标更可读也不容易受字段顺序影响 s.setId(rs.getInt(id)); s.setStuNo(rs.getString(stu_no)); s.setName(rs.getString(name)); s.setGender(rs.getString(gender)); s.setAge(rs.getInt(age)); s.setClassName(rs.getString(class_name)); list.add(s); } } catch (SQLException e) { // 课程设计阶段用 e.printStackTrace() 可以但心里要知道应该用日志框架 e.printStackTrace(); } finally { // 释放顺序和获取顺序相反 try { if (rs ! null) rs.close(); } catch (SQLException e) {} try { if (ps ! null) ps.close(); } catch (SQLException e) {} try { if (conn ! null) conn.close(); } catch (SQLException e) {} } return list; }这段代码里最容易出问题的地方是 finally 块中的资源关闭。只关 Connection 不关 ResultSet 和 PreparedStatement在连续查询几十次之后会报Too many connections错误因为连接池默认上限只有 100资源不释放就会把连接耗尽。还有读取 ResultSet 时要按列名取值而不是按下标 1、2、3否则表结构一调整所有取值位置都会错位。这两个细节在 java 面试八股文里经常被问到代码里有没有认真处理是判断这份源码能不能过评审的一个标志。3.3 Service 层要不要配合接口很多课程设计源码里 service 和 dao 都只写一个类不写接口。我个人的观点是学生管理系统这种体量直接写类完全够用不写接口不是错误——但既然标题里带了“源码”说明读者希望看到的是标准一点的写法带接口的效果更好。带接口的好处体现在两个地方一是可以方便地切换实现类比如从 JDBC 实现改成 MyBatis 实现业务层代码完全不用改二是在答辩时老师问“为什么分层要分开”你可以顺着接口设计讲依赖倒置原则。一个典型的 Service 接口写法是public interface StudentService { ListStudent findAll(); boolean addStudent(Student s); boolean updateStudent(Student s); boolean deleteStudent(int id); } public class StudentServiceImpl implements StudentService { // 面向接口编程具体实现类在构造时指定 private StudentDao studentDao new StudentDaoImpl(); Override public boolean addStudent(Student s) { // 校验学号是否被占用 Student exists studentDao.findByStuNo(s.getStuNo()); if (exists ! null) { return false; } return studentDao.insert(s) 0; } }要留意的是 Service 层和 DAO 的返回值约定。切不要用 boolean 表示操作成功与否因为 insert 影响行数可以是 1 也可以大于 1批量插入时用int返回受影响行数更准确。学生管理系统里最常见的是insert方法返回intService 层拿这个返回值判断是否大于 0再决定返回什么样的提示信息给页面。3.4 从 web.xml 到 Servlet 3.0 注解拿到源码后web.xml 文件是判断这个项目技术栈新旧的一个窗口。Servlet 3.0 之前的项目必须在 web.xml 里写servlet-mapping配置每新增一个 Servlet 就要改一次 XML。而 Servlet 3.0 以后Tomcat 7 即支持可以用WebServlet注解直接标记省掉了这一步。两种写法都在大量源码包中存在都能正常跑通。老式 web.xml 写法在源码包里仍然很常见servlet servlet-namestudentListServlet/servlet-name servlet-classcom.student.ms.servlet.StudentListServlet/servlet-class /servlet servlet-mapping servlet-namestudentListServlet/servlet-name url-pattern/student/list/url-pattern /servlet-mapping注解式写法则是在 Servlet 类上方直接加一行WebServlet(/student/list)效果等价。如果看到源码的 web.xml 里是空的或者只有几行欢迎页配置而 Servlet 类上有注解那是新写法如果 web.xml 密密麻麻写满了映射那也不用紧张两种都能在 Tomcat 9 上正常部署。使用 Spring Boot 的项目另当别论它用内嵌 Tomcatweb.xml 根本不存在。学生管理系统用不用 Spring Boot取决于标题里带没带 “spring” 字样。这个标题只写了 java web那 Servlet JSP 就是最切题的方案。4. 从 JSP 表单到 Servlet 控制器增删改查的完整闭环4.1 学生列表页面用 EL 和 JSTL 渲染数据学生管理系统的核心交互页是学生列表页。这次我们用 JSP EL 表达式 JSTL 标签库来渲染数据这也是 java web 项目里最常见的视图层写法。服务端在StudentListServlet里查出数据后用request.setAttribute(studentList, list)放到请求域再forward到student_list.jsp页面展示。写一个带搜索功能的学生列表页% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html head title学生列表 - 学生管理系统/title /head body h3学生列表/h3 !-- 搜索表单查询条件通过 GET 方式提交刷新后 URL 可分享 -- form action${pageContext.request.contextPath}/student/list methodget input typetext namekeyword placeholder输入姓名或学号 value${param.keyword}/ button typesubmit搜索/button /form table border1 cellpadding8 cellspacing0 tr th编号/thth学号/thth姓名/thth性别/thth年龄/thth班级/thth操作/th /tr c:forEach items${studentList} vars tr td${s.id}/td td${s.stuNo}/td td${s.name}/td td${s.gender}/td td${s.age}/td td${s.className}/td td !-- 修改和删除操作把 id 带上删除前在 JS 里二次确认 -- a href${pageContext.request.contextPath}/student/edit?id${s.id}修改/a a hrefjavascript:void(0) onclickconfirmDelete(${s.id}, ${s.name})删除/a /td /tr /c:forEach /table script typetext/javascript function confirmDelete(id, name) { if (confirm(确定要删除学生[ name ]的记录吗此操作不可恢复。)) { location.href ${pageContext.request.contextPath}/student/delete?id id; } } /script /body /html这段 JSP 里值得注意的有三个点。第一个是${pageContext.request.contextPath}它获取当前应用的上下文路径部署时上下文路径是student_ms还是ROOT所有路径拼接都能自动适配。写死路径会造成一个经典问题本地访问http://localhost:8080/student_ms/页面正常部署到服务器后如果改名所有页面都找不到资源。第二个是${param.keyword}用于回显搜索框的内容这个内置对象从请求参数里取值不需要 Servlet 额外往 request 里塞一个 keyword。第三个是删除操作用 GET 方式直接发起请求不是 RESTful 的最佳实践但课程设计阶段大量项目这样处理你要记住的是注意让中间层做删除确认避免误删。4.2 Servlet 里如何承接表单参数并做校验StudentAddServlet接收前端表单 POST 过来的参数做基本校验后调用 Service 层完成插入。这里最关键的是两层校验第一层是前端 HTML 必填项required 属性第二层是 Servlet 里的二次校验。前端校验只解决用户体验问题不能作为安全边界。// StudentAddServlet.java - 添加学生 WebServlet(/student/add) public class StudentAddServlet extends HttpServlet { private StudentService studentService new StudentServiceImpl(); Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 设置编码防止中文乱码 request.setCharacterEncoding(UTF-8); String stuNo request.getParameter(stuNo); String name request.getParameter(name); String gender request.getParameter(gender); String ageStr request.getParameter(age); String className request.getParameter(className); // 必填校验后端同样要做不能依赖前端 if (stuNo null || stuNo.trim().isEmpty() || name null || name.trim().isEmpty()) { request.setAttribute(error, 学号和姓名不能为空); // 转发到新增页面注意是 forward 而不是 redirect request.getRequestDispatcher(/jsp/student_add.jsp).forward(request, response); return; } int age 0; try { age Integer.parseInt(ageStr); } catch (NumberFormatException e) { request.setAttribute(error, 年龄必须是数字); request.getRequestDispatcher(/jsp/student_add.jsp).forward(request, response); return; } // 封装成实体类交给 service 层处理 Student s new Student(); s.setStuNo(stuNo.trim()); s.setName(name.trim()); s.setGender(gender); s.setAge(age); s.setClassName(className null ? : className.trim()); boolean success studentService.addStudent(s); if (success) { // 重定向防止表单重复提交 response.sendRedirect(request.getContextPath() /student/list); } else { request.setAttribute(error, 学号已存在添加失败); request.getRequestDispatcher(/jsp/student_add.jsp).forward(request, response); } } }参数说明stuNo、name这些参数的 name 必须和 JSP 表单里input标签的 name 属性严格一致否则request.getParameter()只能拿到 null校验永远不通过。gender参数建议在 JSP 里用input typeradio namegender value男 checked的形式限定可选值避免用户手填出问题。ageStr先用字符串接收再手动转 int这一步是很多初学者直接Integer.parseInt(request.getParameter(age))导致 500 报错的地方——用户没填年龄时拿到的就是 nullparseInt(null)必然抛 NumberFormatException。4.3 分页查询的 SQL 写法与参数边界当学生记录超过一百条StudentListServlet 就不能一次性把全部数据查出来返回给 JSP 了。分页是学生管理系统扩展功能时绕不开的需求也会作为加分项出现在课程设计的要求里。最常见且容易讲清楚的分页是物理分页SQL 用 MySQL 的LIMIT关键字实现。// StudentDaoImpl.java - 带关键字搜索的分页查询 public ListStudent findPage(String keyword, int pageNo, int pageSize) { ListStudent list new ArrayList(); // LIMIT 的第一个参数是偏移量不是页码 // 所以页码要转成偏移量 (pageNo - 1) * pageSize String sql SELECT id, stu_no, name, gender, age, class_name FROM t_student ; StringBuilder sb new StringBuilder(sql); ListObject params new ArrayList(); // 关键字模糊查询姓名或学号 if (keyword ! null !keyword.trim().isEmpty()) { sb.append(WHERE name LIKE ? OR stu_no LIKE ? ); params.add(% keyword.trim() %); params.add(% keyword.trim() %); } sb.append(ORDER BY id LIMIT ?, ?); params.add((pageNo - 1) * pageSize); params.add(pageSize); // 省略 try-with-resources 的 JDBC 执行过程... return list; } // 查询总记录数用于前端计算总页数 public int count(String keyword) { String sql SELECT COUNT(*) FROM t_student; // 同样的 keyword 拼接逻辑返回 rs.getInt(1) // 省略实现... }分页这里有三个必调参数pageNo当前页码从 1 开始、pageSize每页显示条数通常设 10 或 20、keyword搜索关键字。LIMIT ?, ?两个参数的顺序不能写反前一个是 offset 偏移量后一个是 row count。常见的错误是把页码直接传给第一个占位符结果第一页显示、第二页却什么都查不出来。偏移量的计算是(pageNo - 1) * pageSize这个公式要能默写出来——它是 java web 分页里最容易被问到的点之一。JSP 页面底部分页栏用 JSTL 渲染时注意处理“上一页”“下一页”的边界pageNo 1时禁用上一页pageNo totalPage时禁用下一页同时 URL 上要带原有 keyword 参数否则翻页后搜索条件丢失。处理方式是${pageContext.request.contextPath}/student/list?pageNo${pageNo - 1}keyword${param.keyword}把 keyword 原样传回去。4.4 修改与删除操作里最容易出 bug 的小细节修改操作的流程是先查询原数据回显到表单再提交更新。StudentEditServlet里用studentDao.findById(id)查出记录通过转发把数据带到student_update.jsp页面。注意回显时不能直接写${student.name}如果原对象为 nullJSP 页面数据展示区域会是一大堆空白。判断逻辑应该放在 Servlet 里完成查无此人就重定向到列表页并携带错误提示。// StudentEditServlet.java - 回显数据到编辑页面 WebServlet(/student/edit) public class StudentEditServlet extends HttpServlet { private StudentDao studentDao new StudentDaoImpl(); Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String idStr request.getParameter(id); if (idStr null || idStr.trim().isEmpty()) { response.sendRedirect(request.getContextPath() /student/list); return; } int id Integer.parseInt(idStr); Student s studentDao.findById(id); if (s ! null) { request.setAttribute(student, s); request.getRequestDispatcher(/jsp/student_update.jsp).forward(request, response); } else { response.sendRedirect(request.getContextPath() /student/list); } } }删除操作同理DeleteServlet接收 id 后直接调studentDao.deleteById(id)。这里要从 Service 层处理外键约束的问题如果实体类间有关联关系要按依赖顺序删除否则会一直报外键错误。这个前后顺序问题前面已经说过再强调一次先删t_score表的关联记录再删t_student的记录否则数据库会直接拒绝执行。5. JSP 内置对象、请求转发与重定向学生管理系统的运行机制5.1 把 JSP 的几个关键内置对象用对JSP 页面里能直接用request、response、session、application这些对象不用声明也不用 import。很多初学者不理解为什么 JSP 里可以凭空用这些变量其实 JSP 在第一次被访问时会被 Tomcat 翻译成 Servlet 的 .java 文件翻译过程中 Tomcat 自动生成了这些局部变量。这一点每次 java 面试都可能会被问到属于 java web 基础的八股文。四个作用域对象的使用场景区分如下表内置对象作用域学生管理系统中的典型用途request一次请求列表页查询条件、添加失败的错误提示session一次会话登录状态、当前用户信息application整个应用全局配置、计数器pageContext当前页面获取其他作用域的数据将用户信息放入 session 是在登录功能的 LoginServlet 里做的session.setAttribute(loginUser, user)。其他页面判断是否登录就是查session.getAttribute(loginUser)是否为 null。很多源码里会用过滤器统一做登录检查避免每个 Servlet 里重复写判断逻辑。5.2 forward 和 redirect 到底怎么选学生管理系统里几乎到处都有request.getRequestDispatcher().forward()和response.sendRedirect()的调用这两者的区别经常作为 java 面试题出现。forward 是服务器内部转发浏览器地址栏不变整个过程只有一次请求redirect 是浏览器重新发起一次请求地址栏变成新地址整个过程两次请求。判断该用哪个的关键标准是操作是否会改变服务端数据查询成功后用 forward把数据带给 JSP 渲染添加、修改、删除成功后必须用 redirect否则用户在列表页按 F5 刷新就会重复提交上一次的表单数据。表单重复提交是学生管理系统里被提到最多的坑之一根治它就是在写操作成功后拒绝 forward强制重定向。5.3 登录过滤器的三个必写逻辑过滤器 Filter 是 java web 项目里的标准组件解决的是未登录不能访问学生管理页面这类横切需求。学生管理系统的过滤器写三个逻辑放行登录页和静态资源、检查 session 里有没有登录标记、未通过验证就重定向到登录页。// LoginFilter.java - 登录验证过滤器 WebFilter(/*) // 拦截所有请求 public class LoginFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; // 获取请求路径 String uri request.getRequestURI(); String contextPath request.getContextPath(); String path uri.substring(contextPath.length()); // 放行条件登录页、登录接口、静态资源 if (path.equals(/login.jsp) || path.equals(/login) || path.startsWith(/css/) || path.startsWith(/js/)) { chain.doFilter(req, resp); return; } // 判断是否已登录 Object loginUser request.getSession().getAttribute(loginUser); if (loginUser null) { // 重定向到登录页不能 forward防止刷新时重复提交 response.sendRedirect(request.getContextPath() /login.jsp); return; } // 已登录则放行继续执行后续的 Servlet chain.doFilter(req, resp); } }这段代码在部署时有个容易困惑的细节如果项目上下文路径不是根路径request.getRequestURI()返回的路径会带着上下文前缀比如student_ms/jsp/student_list.jsp所以要先去掉 contextPath 再判断路径。路径判定的startsWith(/css/)是通配放行整个静态资源目录如果资源放在其他目录把前缀改掉就行。登录过滤器放在学生管理系统的任何源码包中的 web.xml 或注解配置里都行但要注意过滤器只解决是否登录的问题不解决登录后越权访问的问题——比如学生角色访问只有管理员能用的新增/删除接口。如果源码里只有登录过滤器没有角色判断这是大多数课程设计项目的安全边界答辩时能主动提出来会加分。6. 使用 curl 做部署后的冒烟测试学生管理系统上线前必过 5 关学生管理系统开发完部署到 Tomcat最后一项是验证整个链路是否真的可用。很多人启动 Tomcat 后看到首页能打开就认为项目没毛病实际上首页只加载了静态资源数据库连接是好是坏完全没验证到。我习惯用 curl 做几个简单的 HTTP 请求直接从外部视角验证系统是否真的具备“增删改查”条件。6.1 五条 curl 命令验证系统基本链路以下命令在项目部署完成后在终端窗口执行。把localhost:8080换成你的实际地址和端口。# 第1关验证项目上下文能正常响应 HTTP 状态码 curl -I http://localhost:8080/student_ms/login.jsp # 第2关验证登录接口能否被访问POST 方式提交登录表单 curl -X POST http://localhost:8080/student_ms/login \ -d usernameadminpasswordadmin123 \ -c cookies.txt -L -v # 第3关携带登录凭证访问学生列表接口 curl -b cookies.txt http://localhost:8080/student_ms/student/list # 第4关验证新增学生的请求处理默认密码在源码中查找 curl -X POST http://localhost:8080/student_ms/student/add \ -b cookies.txt \ -d stuNo2024001name测试用户gender男age20className计算机2301班 # 第5关验证后续页面跳转不带明显错误 curl -b cookies.txt -L http://localhost:8080/student_ms/student/list | grep 测试用户逐条解释逻辑第 1 条用-I发送 HEAD 请求只获取响应头状态码是 200 说明 Tomcat 已正确加载这个应用第 2 条用-c把服务端 Set-Cookie 写入本地 cookies.txt 文件-L跟随重定向第 3 条用-b携带登录凭证访问受保护资源如果响应是登录页 HTML 而不是学生列表说明 Filter 没有正确放行登录用户第 4 条实际提交一条数据验证 DAO 层的 insert 方法第 5 条把返回页面源码做 grep确认数据真正写入了数据库并经 JSP 渲染出来。这套命令跑完数据库连接是否有问题、Servlet 路径映射是否正确、过滤器放行规则是否合理、表单参数名是否对齐全部能得到反馈。如果第 2 条命令失败去看数据库连接配置和 JDBC 驱动的版本兼容性如果第 3 条失败检查 Filter 的拦截规则和 session 写入时机如果第 4 条失败打开 Tomcat 的logs/catalina.out这是排错的第一现场。6.2 修改数据后验证 update 和 delete 的边界最后补两个针对修改与删除操作的高频问题排查点位。一是更新操作只改一个字段时JSP 里没回显的字段会不会变成 null——检查 SQL 语句是不是把所有字段都更新了一遍还是用了动态拼接只更新非空字段二是删除操作时报外键错误时先执行 SQL 语句查询 t_score 表里有没有关联记录SELECT * FROM t_score WHERE student_id 12;有记录就先删除子表数据再删除学生表数据这是学生管理系统与外键约束最常见的交手环节。整套流程跑下来这个项目就能从能打开网页推进到增删改查全链路可验证的状态这也是源码包价值最大的部分。本文还有配套的精品资源点击获取
返回列表