简介:这是一份面向Java Web初学者与高校课程设计学生的完整学生信息管理系统实战项目,基于JSP+Servlet技术栈实现MVC分层架构,覆盖动态网页开发、HTTP请求处理、JDBC数据库操作及Tomcat容器部署等核心知识点。资源包共576个文件,包含4个JSP页面(负责视图展示)、20个Java源文件与20个Class字节码(含StudentListServlet、LoginServlet等关键控制器)、156个HTML与79个JS/CSS前端资源(构建交互界面),以及1个db_studentinfo.sql数据库脚本,整体压缩包仅2.75MB,轻量易部署。已有998人学习下载,项目结构规范,类命名清晰(如GradeDeleteServlet、StudentDao),便于理解请求流转逻辑与DAO层封装思想;配套SQL脚本与完整编译产物(class文件)可直接导入运行,省去环境配置与编译调试环节,是掌握Java Web基础开发流程的高实用性入门范例。
1. 学生信息管理系统:为什么还在用 JSP/Servlet?不是守旧,是教学闭环里最扎实的“第一块砖”
很多学生拿到毕设题目“学生信息管理系统”时第一反应是:都2024年了,还写JSP?是不是太老?但真正带过三届课设、改过两百多份答辩PPT的工程师会告诉你:这不是技术选型滞后,而是教学场景下不可替代的“能力锚点”。它不追求高并发或微服务架构,而是强制你亲手串起HTTP请求生命周期——从浏览器表单提交(GET/POST)、Servlet接收参数、JDBC连接池配置、SQL预编译防注入、到JSP页面动态渲染数据,每一步都暴露在明处。没有Spring Boot自动装配的黑匣子,没有Vue响应式数据的魔法,所有流转逻辑必须自己画流程图、写try-catch、查request.getParameter()返回null的坑。这套系统适合两类人:一是Java Web入门者,需要把“Web开发”从概念变成手指肌肉记忆;二是课程设计/毕设指导老师,它结构清晰、边界明确、验收标准可量化——增删改查是否完整?数据库字段是否与实体类严格对应?分页是否支持跳转?这些都能一行行代码对得上。如果你正被导师要求“用传统Java Web技术实现”,别焦虑,这恰恰是你第一次真正看懂“用户点击按钮”和“数据库记录变化”之间那条真实链路的机会。
2. 技术栈与环境约束:JDK 8 + Tomcat 9 + MySQL 5.7 的组合不是妥协,是可控性的底线
2.1 为什么锁定 JDK 8 而非 JDK 17?
这不是怀旧,是规避教学环境中的“玄学翻车”。某高校实验室批量部署的开发机仍运行Windows 7 SP1,其默认JRE为1.8.0_202;而JDK 11+引入的模块化系统(JPMS)会让初学者在配置CLASSPATH时陷入无解循环——比如java.lang.NoClassDefFoundError: javax/servlet/Servlet这种报错,本质是JDK 11移除了Java EE模块,但Tomcat 9的lib目录下servlet-api.jar仍依赖javax.*包路径。JDK 8则天然兼容:javac -encoding UTF-8 *.java编译无警告,java -cp ".;lib/*" Main运行零异常。更重要的是,教材《Java Web编程基础》第3章示例代码全部基于JDK 8语法,学生抄作业时复制粘贴就能跑通。若强行升级,光是解决module-info.java冲突和--add-modules java.se.ee参数问题,就会吃掉三天调试时间——而这三天本该用来理解MVC分层。
2.2 Tomcat 9 的核心配置项必须手改的三个位置
Tomcat不是装完就完事,以下配置直接影响系统能否通过答辩验收:
提示:所有修改必须在
conf/server.xml中进行,禁止使用IDE内置Tomcat(如IntelliJ的Artifacts配置),因其会覆盖web.xml的servlet-mapping规则,导致/student/list请求404。
<!-- conf/server.xml 第69行附近 --> <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" /> <!-- 关键:URIEncoding="UTF-8" 解决中文路径参数乱码 --><!-- conf/web.xml 第120行,添加全局编码过滤器 --> <filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.apache.catalina.filters.SetCharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping><!-- conf/context.xml 第15行,配置JNDI数据源(替代硬编码DB连接) --> <Resource name="jdbc/studentDB" auth="Container" type="javax.sql.DataSource" maxTotal="20" maxIdle="10" minIdle="5" username="root" password="123456" driverClassName="com.mysql.cj.jdbc.Driver" url="jdbc:mysql://localhost:3306/studentdb?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"/>设计说明:
URIEncoding="UTF-8"解决GET请求中文参数(如?name=张三)在Servlet中request.getParameter("name")返回乱码的问题;SetCharacterEncodingFilter确保POST表单提交的中文内容(如学生姓名、班级)不因ISO-8859-1默认编码而损坏;- JNDI数据源将数据库连接信息从Java代码剥离,符合“配置与代码分离”原则,且Tomcat启动时自动初始化连接池,避免每次请求新建Connection导致MySQL报
Too many connections。
2.3 MySQL 5.7 建库脚本的关键细节
学生常犯的错误是直接执行CREATE DATABASE studentdb;,结果在插入中文时触发Incorrect string value: '\xE5\xBC\xA0\xE4\xB8\x89'。正确建库语句必须显式声明字符集:
-- 创建数据库(注意:必须用utf8mb4,不是utf8!) CREATE DATABASE studentdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 创建学生表(字段长度按实际需求预留,避免varchar(255)滥用) USE studentdb; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, stu_id VARCHAR(12) NOT NULL UNIQUE COMMENT '学号,如20230001', name VARCHAR(20) NOT NULL COMMENT '姓名', gender ENUM('男','女') NOT NULL DEFAULT '男', age TINYINT UNSIGNED COMMENT '年龄,0-127', class_name VARCHAR(30) NOT NULL COMMENT '班级名称', phone CHAR(11) COMMENT '手机号,固定11位', email VARCHAR(50) COMMENT '邮箱', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;参数说明:
utf8mb4支持emoji和四字节Unicode(如生僻汉字),而MySQL旧版utf8仅支持三字节,会导致部分中文存入后显示为?;ENUM('男','女')比VARCHAR(2)更安全,数据库层强制校验性别值,避免前端传入'未知'导致业务逻辑混乱;TINYINT UNSIGNED存储年龄比INT节省3字节空间,且UNSIGNED防止负数录入(如age=-5);ON UPDATE CURRENT_TIMESTAMP让update_time字段在UPDATE语句执行时自动更新,无需在Servlet中手动设置。
3. 总体架构设计:三层结构与四个核心Servlet的职责划分
3.1 MVC分层落地:为什么Controller层必须拆成四个独立Servlet?
新手常把所有逻辑塞进一个StudentServlet,用?action=add参数区分操作,结果导致方法臃肿、事务难管理、单元测试无法覆盖。合格方案必须按HTTP动词和业务动作拆分:
| Servlet类名 | 映射URL | 核心职责 | 关键设计点 |
|---|---|---|---|
StudentListServlet | /student/list | 查询列表(含分页) | 接收pageNum=1&pageSize=10参数,调用DAO的findPage()方法,将PageBean<Student>放入request域 |
StudentAddServlet | /student/add | 处理新增表单提交 | 验证stu_id唯一性(查库),失败则重定向回add.jsp并携带错误信息 |
StudentEditServlet | /student/edit | 加载编辑页数据 | 根据id查单条记录,将Student对象存入request,转发至edit.jsp |
StudentDeleteServlet | /student/delete | 执行物理删除 | 接收id参数,调用DAO的deleteById(),成功后重定向至/student/list |
设计说明:
- 每个Servlet只做一件事:
StudentListServlet不处理新增,StudentAddServlet不渲染页面,严格遵循单一职责; - 所有Servlet继承自
HttpServlet,doGet()处理页面跳转(如/student/add加载空白表单),doPost()处理数据提交(如/student/add接收表单); - 重定向(
response.sendRedirect())用于防止表单重复提交(F5刷新不会再次POST),而请求转发(request.getRequestDispatcher().forward())用于页面间数据传递(如查出学生信息后跳转到编辑页)。
3.2 DAO层实现:JDBC工具类封装与SQL注入防护
学生最容易在DAO层写出致命漏洞。以下StudentDao关键代码展示如何用预编译语句(PreparedStatement)杜绝SQL注入:
// src/dao/StudentDao.java public class StudentDao { // 使用JNDI获取DataSource,而非new DriverManagerDataSource() private DataSource dataSource = null; public StudentDao() { try { Context initCtx = new InitialContext(); Context envCtx = (Context) initCtx.lookup("java:comp/env"); this.dataSource = (DataSource) envCtx.lookup("jdbc/studentDB"); } catch (Exception e) { throw new RuntimeException("JNDI DataSource lookup failed", e); } } // 安全的查询:使用?占位符,参数由setString()注入 public List<Student> findAll() throws SQLException { String sql = "SELECT id, stu_id, name, gender, age, class_name, phone, email FROM student ORDER BY id DESC"; try (Connection conn = dataSource.getConnection(); PreparedStatement stmt = conn.prepareStatement(sql); ResultSet rs = stmt.executeQuery()) { List<Student> list = new ArrayList<>(); while (rs.next()) { Student s = new Student(); s.setId(rs.getInt("id")); s.setStuId(rs.getString("stu_id")); s.setName(rs.getString("name")); s.setGender(rs.getString("gender")); s.setAge(rs.getByte("age")); // TINYINT映射为byte s.setClassName(rs.getString("class_name")); s.setPhone(rs.getString("phone")); s.setEmail(rs.getString("email")); list.add(s); } return list; } } // 防注入的新增:用户输入的name、class_name等全部走setXxx() public void add(Student student) throws SQLException { String sql = "INSERT INTO student (stu_id, name, gender, age, class_name, phone, email) VALUES (?, ?, ?, ?, ?, ?, ?)"; try (Connection conn = dataSource.getConnection(); PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { stmt.setString(1, student.getStuId()); stmt.setString(2, student.getName()); stmt.setString(3, student.getGender()); stmt.setByte(4, student.getAge()); // 注意:age是TINYINT,用setByte() stmt.setString(5, student.getClassName()); stmt.setString(6, student.getPhone()); stmt.setString(7, student.getEmail()); int rows = stmt.executeUpdate(); if (rows != 1) { throw new SQLException("Insert failed: " + rows + " rows affected"); } // 获取自增主键ID(供后续日志记录) try (ResultSet generatedKeys = stmt.getGeneratedKeys()) { if (generatedKeys.next()) { student.setId(generatedKeys.getInt(1)); } } } } }关键设计点:
dataSource.getConnection()从Tomcat连接池取连接,避免DriverManager.getConnection()创建新连接导致资源耗尽;PreparedStatement的?占位符彻底隔离SQL结构与用户数据,即使传入name="张三'; DROP TABLE student; --",最终执行的也是VALUES ('张三''; DROP TABLE student; --', ...),字符串被转义;setByte(4, student.getAge())严格匹配数据库TINYINT类型,若误用setInt()可能触发隐式转换异常;Statement.RETURN_GENERATED_KEYS参数让executeUpdate()后能获取自增ID,避免二次查询。
3.3 JSP页面层:EL表达式与JSTL标签的最小必要集
学生常堆砌<% %>脚本片段,导致JSP与Java逻辑混杂。合格方案只用EL(Expression Language)和JSTL核心标签:
<%-- list.jsp --%> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head><title>学生列表</title></head> <body> <h2>学生信息列表</h2> <!-- 分页导航栏 --> <div> <c:if test="${not empty pageBean}"> 共${pageBean.totalCount}条记录,第${pageBean.pageNum}页/共${pageBean.totalPage}页 <a href="student/list?pageNum=1">首页</a> <c:if test="${pageBean.pageNum > 1}"> <a href="student/list?pageNum=${pageBean.pageNum - 1}">上一页</a> </c:if> <c:if test="${pageBean.pageNum < pageBean.totalPage}"> <a href="student/list?pageNum=${pageBean.pageNum + 1}">下一页</a> </c:if> <a href="student/list?pageNum=${pageBean.totalPage}">末页</a> </c:if> </div> <!-- 学生表格 --> <table border="1"> <tr> <th>学号</th><th>姓名</th><th>性别</th><th>年龄</th> <th>班级</th><th>电话</th><th>邮箱</th><th>操作</th> </tr> <c:forEach items="${pageBean.list}" var="s"> <tr> <td>${s.stuId}</td> <td>${s.name}</td> <td>${s.gender}</td> <td>${s.age}</td> <td>${s.className}</td> <td>${s.phone}</td> <td>${s.email}</td> <td> <a href="student/edit?id=${s.id}">编辑</a> | <a href="student/delete?id=${s.id}" onclick="return confirm('确定删除学生【${s.name}】?')">删除</a> </td> </tr> </c:forEach> </table> <a href="student/add">添加新学生</a> </body> </html>设计说明:
<c:forEach>替代<% for(int i=0; i<list.size(); i++){ %>,避免脚本片段破坏页面可读性;${pageBean.list}直接访问request域中的PageBean对象,EL自动调用getter方法(getPageBean().getList());onclick="return confirm(...)"在删除前弹窗确认,防止误操作——这是答辩老师必问的“安全性设计”得分点;- 所有URL路径用相对路径(
student/list),不写http://localhost:8080/project/student/list,保证部署到不同上下文路径(如/myapp)时仍可用。
4. 关键模块实现:分页功能与数据校验的硬核落地
4.1 自定义PageBean:为什么不能直接用ArrayList?
学生常把List<Student>直接传给JSP,结果分页逻辑散落在Servlet中,导致代码复用率低。PageBean是解耦分页的核心:
// src/bean/PageBean.java public class PageBean<T> { private List<T> list; // 当前页数据 private int pageNum; // 当前页码(从1开始) private int pageSize; // 每页条数 private int totalCount; // 总记录数 private int totalPage; // 总页数 // 构造方法:根据当前页码、每页条数、总记录数计算总页数 public PageBean(int pageNum, int pageSize, int totalCount) { this.pageNum = pageNum; this.pageSize = pageSize; this.totalCount = totalCount; // 计算总页数:向上取整(totalCount=25, pageSize=10 → totalPage=3) this.totalPage = (int) Math.ceil((double) totalCount / pageSize); // 修正pageNum越界:若pageNum > totalPage,则设为totalPage if (pageNum > totalPage && totalPage > 0) { this.pageNum = totalPage; } // 若pageNum < 1,设为1 if (pageNum < 1) { this.pageNum = 1; } } // getter/setter 省略... }设计说明:
Math.ceil()确保totalCount=1时totalPage=1,而非0;pageNum越界校验防止/student/list?pageNum=999导致空页面或500错误;list字段在Servlet中通过DAO查询填充,PageBean本身不负责数据获取,只承载分页元数据。
4.2 StudentListServlet:分页查询的完整链路
// src/servlet/StudentListServlet.java public class StudentListServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取分页参数(带默认值) String pageNumStr = request.getParameter("pageNum"); String pageSizeStr = request.getParameter("pageSize"); int pageNum = StringUtils.isNumeric(pageNumStr) ? Integer.parseInt(pageNumStr) : 1; int pageSize = StringUtils.isNumeric(pageSizeStr) ? Integer.parseInt(pageSizeStr) : 10; // 2. 调用DAO获取总记录数(用于计算总页数) StudentDao dao = new StudentDao(); int totalCount; try { totalCount = dao.getTotalCount(); // SELECT COUNT(*) FROM student } catch (SQLException e) { request.setAttribute("errorMsg", "查询总数失败:" + e.getMessage()); request.getRequestDispatcher("/error.jsp").forward(request, response); return; } // 3. 创建PageBean(此时已知totalCount,可计算totalPage) PageBean<Student> pageBean = new PageBean<>(pageNum, pageSize, totalCount); // 4. 查询当前页数据(LIMIT offset, size) try { List<Student> currentPageList = dao.findPage(pageBean.getPageNum(), pageBean.getPageSize()); pageBean.setList(currentPageList); } catch (SQLException e) { request.setAttribute("errorMsg", "查询当前页失败:" + e.getMessage()); request.getRequestDispatcher("/error.jsp").forward(request, response); return; } // 5. 将PageBean存入request域,转发至JSP request.setAttribute("pageBean", pageBean); request.getRequestDispatcher("/list.jsp").forward(request, response); } }关键参数说明:
StringUtils.isNumeric()来自Apache Commons Lang,避免Integer.parseInt(null)抛NumberFormatException;dao.getTotalCount()和dao.findPage()必须在同一个事务中?不,分页场景下无需事务,但需保证两次查询间数据一致性(MySQL默认RR隔离级别已足够);findPage()方法内部SQL为SELECT ... FROM student LIMIT ?, ?,offset = (pageNum-1)*pageSize,size = pageSize。
4.3 数据校验:前后端双重防护的落地代码
学生常忽略校验,导致数据库报错。以下为StudentAddServlet中的关键校验:
// src/servlet/StudentAddServlet.java @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取表单参数 String stuId = request.getParameter("stuId"); String name = request.getParameter("name"); String gender = request.getParameter("gender"); String ageStr = request.getParameter("age"); String className = request.getParameter("className"); String phone = request.getParameter("phone"); String email = request.getParameter("email"); // 2. 后端基础校验(必须!) List<String> errors = new ArrayList<>(); if (StringUtils.isBlank(stuId) || stuId.length() > 12) { errors.add("学号不能为空且长度不超过12位"); } if (StringUtils.isBlank(name) || name.length() > 20) { errors.add("姓名不能为空且长度不超过20位"); } if (!"男".equals(gender) && !"女".equals(gender)) { errors.add("性别只能是'男'或'女'"); } Byte age = null; if (StringUtils.isNumeric(ageStr)) { int temp = Integer.parseInt(ageStr); if (temp >= 0 && temp <= 127) { age = (byte) temp; } else { errors.add("年龄必须在0-127之间"); } } else { errors.add("年龄必须是数字"); } // 3. 数据库唯一性校验(学号不能重复) StudentDao dao = new StudentDao(); try { if (dao.existsByStuId(stuId)) { errors.add("学号【" + stuId + "】已存在,请更换"); } } catch (SQLException e) { errors.add("数据库校验失败:" + e.getMessage()); } // 4. 校验失败则返回错误信息 if (!errors.isEmpty()) { request.setAttribute("errors", errors); request.setAttribute("formData", new Student(stuId, name, gender, age, className, phone, email)); request.getRequestDispatcher("/add.jsp").forward(request, response); return; } // 5. 校验通过,构建Student对象并保存 Student student = new Student(); student.setStuId(stuId); student.setName(name); student.setGender(gender); student.setAge(age); student.setClassName(className); student.setPhone(phone); student.setEmail(email); try { dao.add(student); response.sendRedirect(request.getContextPath() + "/student/list"); } catch (SQLException e) { request.setAttribute("errorMsg", "保存失败:" + e.getMessage()); request.getRequestDispatcher("/error.jsp").forward(request, response); } }设计说明:
StringUtils.isBlank()同时检查null、空字符串、纯空白字符(如" "),比== null || "".equals()更健壮;- 学号唯一性校验必须在
add()之前执行,否则INSERT时触发MySQLDuplicate entry异常,属于未预期错误; request.setAttribute("formData", ...)将用户刚填的数据回显到add.jsp,避免填写10个字段后因一个错误导致全部重填——这是答辩时老师会夸的“用户体验细节”。
5. 避坑指南:学生最容易翻车的五个致命点及血泪解决方案
5.1 现象:JSP页面中文显示为“???”
原因:
- Tomcat
server.xml未配置URIEncoding="UTF-8",导致GET请求参数(如?name=张三)在Servlet中getParameter()返回乱码; web.xml未配置SetCharacterEncodingFilter,导致POST表单提交的中文内容被ISO-8859-1解码;- JSP文件本身未声明
<%@ page contentType="text/html;charset=UTF-8" %>,浏览器用默认编码解析HTML。
解决:
三者缺一不可!按顺序检查:
conf/server.xml中<Connector>标签是否有URIEncoding="UTF-8";conf/web.xml中是否配置了SetCharacterEncodingFilter且<url-pattern>/*</url-pattern>;- 每个JSP文件首行是否包含
<%@ page contentType="text/html;charset=UTF-8" language="java" %>。
注意:若用IDEA创建JSP,默认编码可能是GBK,需在
File -> Settings -> Editor -> File Encodings中将Global Encoding和Project Encoding均设为UTF-8。
5.2 现象:java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver
原因:
- MySQL 5.7+驱动jar包(
mysql-connector-java-8.0.x.jar)未放入WEB-INF/lib/目录; - 或放入了但jar包名含
-sources.jar(源码包)或-javadoc.jar(文档包),非可执行jar; - 或Tomcat
lib/目录下存在旧版驱动(如mysql-connector-java-5.1.x.jar),与JDK 8不兼容。
解决:
- 下载官方驱动: https://dev.mysql.com/downloads/connector/j/ ,选择
Platform Independent的ZIP; - 解压后取
mysql-connector-java-8.0.33.jar(版本号以实际为准),直接复制到项目WEB-INF/lib/目录(不是Tomcat的lib/); - 在IDE中右键项目 →
Properties → Deployment Assembly→ 确认WEB-INF/lib/下的jar已勾选为Deploy Path。
提示:若用Maven,
pom.xml中应为<scope>runtime</scope>,否则编译时找不到类。
5.3 现象:HTTP Status 404 – Not Found,访问/student/list报错
原因:
web.xml中<servlet-mapping>的<url-pattern>与浏览器地址栏URL不匹配(如配置了/student/list但实际访问/studentList);- Servlet类未正确继承
HttpServlet或未重写doGet()/doPost(); - Tomcat未识别
web.xml(因<web-app>根标签的version属性不匹配)。
解决:
- 检查
web.xml头声明:必须为<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">; - 核对
<servlet-mapping>:
<servlet-mapping> <servlet-name>StudentListServlet</servlet-name> <url-pattern>/student/list</url-pattern> <!-- 必须与浏览器输入一致 --> </servlet-mapping>- 确认Servlet类:
public class StudentListServlet extends HttpServlet { // 必须继承 @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) // 必须重写 throws ServletException, IOException { // ... } }5.4 现象:新增学生后数据库有记录,但列表页不显示
原因:
StudentListServlet中doGet()方法未调用dao.findPage()查询数据,或查询结果未存入request.setAttribute("pageBean", pageBean);- JSP中
<c:forEach>遍历的变量名与request.setAttribute()的key不一致(如存"students"却遍历${studentList}); PageBean的list字段未被赋值,导致<c:forEach>遍历空集合。
解决:
- 在
StudentListServlet.doGet()末尾加日志:
System.out.println("PageBean.list size: " + pageBean.getList().size()); // 查看是否为0 System.out.println("Request attribute key: " + request.getAttribute("pageBean")); // 查看是否为null- JSP中检查EL表达式:
<c:forEach items="${pageBean.list}" var="s"> <!-- items值必须与setAttribute的key一致 -->- 确保
PageBean构造后调用setList():
pageBean.setList(dao.findPage(...)); // 这行不能漏!5.5 现象:删除学生时,点击“确定”后页面空白或报500
原因:
StudentDeleteServlet中未处理id参数为空或非数字的情况,Integer.parseInt(null)抛NullPointerException;- 删除后未重定向(
sendRedirect),而是forward到list.jsp,导致浏览器地址栏仍是/student/delete?id=5,F5刷新会再次删除; - DAO层
deleteById()方法未关闭PreparedStatement和Connection,连接池耗尽。
解决:
StudentDeleteServlet.doGet()开头加健壮校验:
String idStr = request.getParameter("id"); if (StringUtils.isBlank(idStr) || !StringUtils.isNumeric(idStr)) { response.sendError(HttpServletResponse.SC_BAD_REQUEST, "非法ID参数"); return; } int id = Integer.parseInt(idStr);- 删除成功后必须重定向:
try { dao.deleteById(id); response.sendRedirect(request.getContextPath() + "/student/list"); // 关键! } catch (SQLException e) { request.setAttribute("errorMsg", "删除失败:" + e.getMessage()); request.getRequestDispatcher("/error.jsp").forward(request, response); }- DAO层用try-with-resources确保资源释放:
public void deleteById(int id) throws SQLException { String sql = "DELETE FROM student WHERE id = ?"; try (Connection conn = dataSource.getConnection(); PreparedStatement stmt = conn.prepareStatement(sql)) { stmt.setInt(1, id); stmt.executeUpdate(); } // 自动关闭conn和stmt }6. 答辩验收 checklist:从代码到演示的六步通关法
6.1 代码层面:答辩老师必查的三个“死亡问题”预演
答辩时老师不会逐行读代码,但会精准打击三个高频漏洞点。提前模拟回答,能瞬间建立专业感:
| 问题 | 你的标准答案(背下来) | 为什么这样答 |
|---|---|---|
| Q1:为什么DAO层不用Hibernate/MyBatis,而坚持手写JDBC? | “因为本项目定位是Java Web基础教学,目标是让学生透彻理解HTTP请求-Servlet-JDBC-MySQL的完整链路。框架会隐藏Connection获取、SQL拼接、结果集映射等关键步骤,而手写JDBC强制暴露每个环节——比如PreparedStatement如何防SQL注入,ResultSet如何逐行映射对象。这就像学开车先练离合,而不是直接开自动挡。” | 把“技术落后”转化为“教学目的明确”,展现设计意图 |
Q2:分页查询用LIMIT ?, ?,大数据量时性能会下降,你怎么优化? | “当前设计已满足课程要求(学生表<1万条)。若未来扩展,我会:① 添加复合索引ALTER TABLE student ADD INDEX idx_class_age (class_name, age)加速条件查询;② 改用游标分页(WHERE id > ? ORDER BY id LIMIT ?)替代OFFSET,避免深度分页扫描;③ 对热点查询加Redis缓存。但所有优化都建立在‘先让基础链路跑通’的前提下。” | 承认局限,但给出可落地的演进路径,体现工程思维 |
Q3:web.xml里Servlet配置这么多,Spring Boot一个注解就搞定,为什么不用? | “@WebServlet注解确实简洁,但本项目要求显式配置web.xml,这是为了让学生掌握Web容器的标准部署契约。比如<load-on-startup>1</load-on-startup>控制Servlet初始化顺序,<security-constraint>定义资源访问权限——这些企业级配置,在注解模式下反而需要额外学习web.xml的等价写法。” | 将“规定动作”升华为“标准契约认知”,超越学生视角 |
6.2 演示环节:五分钟高光时刻的脚本化设计
答辩演示不是功能罗列,而是讲一个“问题-解决-验证”的故事。按此脚本走,老师会主动点头:
开场(30秒):
“老师好,我演示的是基于JSP/Servlet的学生信息管理系统。它解决了课程设计中‘Web开发链路不透明’的问题——所有HTTP交互、数据流转、页面渲染都可见、可调试。”核心亮点演示(3分钟):
- 第一步:展示防注入
在新增页面输入姓名:张三'; DROP TABLE student; --→ 提交 → 页面显示“姓名不能为空且长度不超过20位”(后端校验拦截)→ 查看数据库,student表完好。
台词:“这里演示了双重防护:前端JS校验是体验层,后端PreparedStatement是安全底线。” - 第二步:展示分页健壮性
访问/student/list?pageNum=999→ 页面正常显示最后一页,无报错 → 查看URL自动变为/student/list?pageNum=3(PageBean越界修正)。
台词:“分页组件内置了越界保护,避免恶意参数导致系统异常。” - 第三步:展示数据一致性
新增学生A → 立即在列表页看到A → 编辑A的班级 → 保存 → 列表页班级实时更新 → 删除A → 列表页A消失,且无残留。
台词:“所有操作都经过事务校验(新增时学号唯一性检查),确保数据库状态始终与页面一致。”
- 第一步:展示防注入
收尾(1分钟):
“整个系统共12个核心文件:4个Servlet、3个DAO、2个JSP、1个PageBean、1个工具类。所有代码均通过了Tomcat 9
本文还有配套的精品资源,点击获取