资讯详情 Servlet+JSP+Bootstrap+MySQL学生信息管理系统实战讲解
📅 2026/10/9 16:07:11
简介一份基于Java ServletJSPBootstrapMySQL的学生信息管理系统源码与说明文档面向Java Web初学者、期末大作业及课程设计人群。系统涵盖登录注册、验证码、学生信息增删改查、教师与管理员权限等典型模块并附有可直接导入的student_manager.sql数据库脚本整体难度适中评审达98分源码经本地编译可运行适合学习分层架构与前端页面整合。压缩包共82个文件约12.03MB包含21个class编译文件、20个java源码、15个jsp页面、6个css样式、5个js脚本、3个jar依赖包以及sql、md说明等方便对照Java后台逻辑、JSP视图层和Bootstrap响应式界面进行完整项目复盘。目前已有157人学习下载对于需要快速完成课设或巩固Servlet/DAO/JSP开发流程的读者是一份结构清晰、可直接演练的实践素材。1. 学生信息管理系统怎么选型ServletJSPBootstrap这套组合的定位与价值每年毕业季和课程设计季都会有一批人对着JavaServletJSPBootstrapMySQL学生信息管理系统这个标题犹豫这套技术栈是不是过时了Spring Boot都到3.x了为什么还要碰JSP我的判断是如果你要交的是一个能跑通、能讲清楚、敢被老师问原理的JavaWeb项目这套组合恰恰是最稳的答案。它绕开了Spring Boot里大量自动配置的黑匣子让你用最少的封装看清HTTP请求从浏览器到数据库再返回的完整链路。做这个项目的人通常是三类正在学JavaWeb的在校生、需要补一个完整实战案例的转行者、以及想快速搭一个内部管理系统原型的从业者。本文按我实际交付这类项目的顺序来写先立数据库再搭工程骨架然后写Servlet业务与JSP页面最后给出我踩过的坑和值得做的进阶收尾。2. MySQL数据库设计先行学生、班级、用户三张表的结构与初始化数据2.1 为什么先设计表而不是先写代码我见过太多人打开IDE就写Servlet写到查询学生列表时才发现缺字段、缺关联、连启动数据都没有最后调试时间全耗在改表结构上。数据库是这套系统的地基表一旦定下来JavaBean、Servlet、JSP页面全都是围绕字段名在转。学生信息管理系统的核心需求通常就三块学生的增删改查、按班级或关键字筛选、以及登录后才能操作。因此三张表足够用户表存登录账号、班级表存班级名、学生表存学生档案。如果题目里要求选课再加一张课程表和中间表但那是后话别在第一版就铺开。设计时要注意几个基本功主键用自增int不要用业务字段当主键字符串字段给合理长度姓名给30足够别一上来就varchar(255)时间字段用datetime毕业设计里不需要折腾timestamp的时区问题。编码统一utf8mb4避免中文乱码——这一步比任何代码都能省时间。2.2 初始化SQL脚本建库、建表、插入启动数据以下是我通常会直接用的脚本。注意我故意把班级和学生拆开这样页面上可以做下拉筛选而不是让学生表里直接存一个中文字符串。-- 创建数据库指定字符集 CREATE DATABASE IF NOT EXISTS student_manager DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE student_manager; -- 用户表管理员登录 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, real_name VARCHAR(30) DEFAULT ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 班级表独立成表便于扩展和筛选 CREATE TABLE t_class ( id INT PRIMARY KEY AUTO_INCREMENT, class_name VARCHAR(50) NOT NULL UNIQUE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 学生表通过class_id关联班级 CREATE TABLE t_student ( id INT PRIMARY KEY AUTO_INCREMENT, stu_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, stu_name VARCHAR(30) NOT NULL COMMENT 姓名, gender TINYINT DEFAULT 1 COMMENT 1男 0女, age INT DEFAULT 0, class_id INT COMMENT 关联t_class.id, phone VARCHAR(20) DEFAULT , email VARCHAR(50) DEFAULT , create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_stu_class FOREIGN KEY (class_id) REFERENCES t_class(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 初始化数据登录账号 / 两个班级 / 若干学生 INSERT INTO t_user (username, password, real_name) VALUES (admin, 123456, 系统管理员); INSERT INTO t_class (class_name) VALUES (计算机2101班), (软件2102班); INSERT INTO t_student (stu_no, stu_name, gender, age, class_id, phone, email) VALUES (2021001, 张伟, 1, 20, 1, 13800001111, zhangweiexample.com), (2021002, 李娜, 0, 21, 1, 13800002222, linaexample.com), (2021003, 王强, 1, 22, 2, 13800003333, wangqiangexample.com);这段脚本有三个地方要留意。第一t_student的stu_no加了UNIQUE约束业务上学号不能重复必须让数据库兜底而不是只在Java里判断。第二外键fk_stu_class我保留着虽然有人嫌它影响插入性能但在这个量级的系统里外键能有效防止你删掉一个还在被引用的班级。第三初始化数据里我特意只插了三条学生记录方便你第一眼看到列表页效果后再自己动手加几条测试。提示如果你用的MySQL是8.0以上版本脚本里的DEFAULT CHARSET写法完全兼容如果是5.7同样没问题。千万别用utf8字符集它在MySQL里是坑utf8mb4才是完整的中文支持方案。2.3 连接数据库前的必做检查表建好之后先用MySQL命令行或Navicat执行一遍脚本确认没有报错然后跑一条最简单的查询USE student_manager; SELECT s.stu_no, s.stu_name, c.class_name FROM t_student s LEFT JOIN t_class c ON s.class_id c.id;这条SQL能出数据说明表和关联都没问题。接下来再去写Java代码。很多人跳过这个验证步骤直接去配JDBC结果分不清是SQL写错还是Java代码写错这是典型的调试顺序问题。另外记好你的连接参数地址通常是jdbc:mysql://localhost:3306/student_manager账号root密码是你本机安装时设的。MySQL 8.0的驱动类名是com.mysql.cj.jdbc.Driver5.7则用com.mysql.jdbc.Driver这两者在pom里选版本时要对应好。3. 搭好Maven Web项目骨架pom依赖、Java三层目录与Tomcat运行方式3.1 为什么推荐Maven而不是直接建Dynamic Web ProjectEclipse里直接建Dynamic Web Project也能写但管理jar包会把人逼疯——你要手动去网上下载Servlet API、JSTL、MySQL驱动版本还可能彼此冲突。Maven把这些事全部接管pom.xml里声明依赖它会自动下载并传递依赖。更关键的是Maven项目结构是行业通用规范你拿去给任何人看都能马上定位到代码位置。用Maven搭骨架本质上是在模仿真实公司的工程布局。3.2 pom.xml核心依赖与版本搭配下面这个pom.xml是能直接用的我按当前最稳妥的版本组合给你?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion groupIdcom.example/groupId artifactIdstudent-manager/artifactId version1.0-SNAPSHOT/version packagingwar/packaging properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties dependencies !-- Servlet APITomcat运行时提供实现 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- JSP API -- dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency !-- JSTL 标签库JSP里做循环和判断 -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency !-- MySQL 驱动8.0.33 兼容 MySQL 5.7 和 8.x -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency /dependencies build finalNamestudent-manager/finalName plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-compiler-plugin/artifactId version3.11.0/version /plugin /plugins /build /project这里有两个细节要注意。第一Servlet和JSP的依赖scope是provided意思是编译时需要、但运行时由Tomcat提供如果你写错成默认的compile部署时可能出现jar包冲突。第二finalName指定了打war包的名字这样部署后访问路径是http://localhost:8080/student-manager短一点不容易手滑写错。3.3 目录结构与第一个Java类的编写规范Maven Web项目的标准目录长这样src/main/java Java源码按包分层 src/main/resources 配置文件如db.properties src/main/webapp JSP、CSS、JS、静态资源 src/main/webapp/WEB-INF/web.xml Servlet配置Java后端按三层结构分包这是这套系统能不能讲清楚的关键。我习惯建四个包——entity放数据库表对应的JavaBeandao放操作数据库的JDBC代码service放业务逻辑servlet或controller放接收请求和转发页面的控制层。别把所有代码塞在一个包里否则答辩时老师问你分层体现在哪你拿不出来。先写最简单的实体类作为示例package com.example.entity; public class Student { private Integer id; private String stuNo; private String stuName; private Integer gender; private Integer age; private Integer classId; private String phone; private String email; private String className; // 关联查询出的班级名不属于t_student表字段 // 此处省略所有getter和setter // 建议手写或用IDE生成不要用Lombok——部分环境不支持注解处理器 }注意className这个字段它对应的是联表查询的结果。实体类可以比表多字段这是JavaWeb开发里非常常见的做法——对象不只是表结构的一对一映射还要承载页面展示所需的数据。为什么要手写getter/setter而不依赖Lombok因为你可能在答辩现场换一台机器演示如果IDE没配好Lombok插件整个项目直接编译失败这种风险不值得冒。3.4 用Tomcat跑起来的两种方式项目骨架搭好后运行方式有两种。第一种是在IDEA里配置Tomcat ServerDeployment选student-manager:war explodedApplication context填/student-manager点Debug启动。第二种是命令行方式先mvn clean package打出war包把它拷贝到Tomcat的webapps目录然后启动Tomcat的bin/startup.bat。新手我建议直接用IDEA方式因为改完代码热部署方便但要记住类路径和Tomcat的编码设置必须确认是UTF-8否则JSP页面提交中文表单时会乱码。4. Servlet写核心业务登录校验、学生列表与增删改查的请求流转4.1 BaseServlet用反射减少Servlet类的数量如果每个功能都写一个Servlet这个项目会有LoginServlet、StudentListServlet、StudentAddServlet、StudentDeleteServlet……太多类而且每个类里都是重复的doGet/doPost转发代码。我的做法是写一个BaseServlet基类通过反射把请求分发到子类的具体方法上。这是当前JavaWeb教学里最常见、也最容易被答辩老师认可的封装方式。package com.example.servlet; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.lang.reflect.Method; public class BaseServlet extends HttpServlet { Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, java.io.IOException { // 例如请求URL为 /student/listmethod为list String methodName req.getParameter(method); if (methodName null || methodName.isEmpty()) { methodName list; // 默认执行列表方法 } try { // 反射调用子类中同名方法方法签名统一为 (HttpServletRequest, HttpServletResponse) Method method this.getClass().getMethod(methodName, HttpServletRequest.class, HttpServletResponse.class); method.invoke(this, req, resp); } catch (Exception e) { throw new ServletException(反射调用方法失败: methodName, e); } } }这个类的逻辑非常直观前端提交methodadd服务器就调用子类的add(HttpServletRequest, HttpServletResponse)方法。参数method成了请求的路由标识。这样做的好处是后续每增加一个功能只需在子类里加一个同名方法不需要改web.xml、不需要新增Servlet类代码量直接少一半。我在实际项目里一直用这个套路它本质上是用一个反射入口替代了多个Servlet映射。4.2 登录校验从JSP页面到Servlet再到Session用户输入账号密码提交到ServletServlet校验数据库成功后把用户信息存进session。这个流程是JavaWeb的经典考题。先看登录的Servlet方法package com.example.servlet; import com.example.dao.UserDao; import com.example.entity.User; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class UserServlet extends BaseServlet { private UserDao userDao new UserDao(); // 处理登录请求 public void login(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 1. 接收页面参数注意UTF-8编码 req.setCharacterEncoding(UTF-8); String username req.getParameter(username); String password req.getParameter(password); // 2. 空值校验不要等到SQL执行才发现参数为空 if (username null || password null || username.trim().isEmpty() || password.trim().isEmpty()) { req.setAttribute(errorMsg, 用户名和密码不能为空); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } // 3. 调用DAO查询数据库 User user userDao.findByUsernameAndPassword(username, password); // 4. 判断结果成功进主页失败回登录页 if (user ! null) { req.getSession().setAttribute(loginUser, user); resp.sendRedirect(req.getContextPath() /student?methodlist); } else { req.setAttribute(errorMsg, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); } } }这段代码有三个点值得讲给新手听。第一空值校验放在SQL查询之前既节省了数据库连接资源又能给出更友好的错误提示。第二登录成功后我用的是sendRedirect而不是forward这是有意的——重定向会改变浏览器地址栏URL用户刷新页面时不会重复提交登录表单。第三如果登录失败用forward转发回login.jsp同时通过request.setAttribute带一个errorMsg回去页面里用EL表达式显示。这个转发带参数、重定向防重复提交的选择是JavaWeb里一个经典的细节考点。4.3 学生列表JDBC查询、分页参数与条件拼接学生列表是系统的核心页面普通版直接SELECT *就能做但要达到高分项目的标准至少要支持多条件查询和分页。分页不能靠前端做必须用SQL的LIMIT在数据库层面完成。下面这段DAO代码是列表查询的完整版package com.example.dao; import com.example.entity.Student; import java.sql.*; import java.util.ArrayList; import java.util.List; public class StudentDao { // 查询学生列表支持关键字和班级筛选支持分页 public ListStudent findList(String keyword, Integer classId, int page, int pageSize) { ListStudent list new ArrayList(); StringBuilder sql new StringBuilder(); sql.append(SELECT s.*, c.class_name FROM t_student s ); sql.append(LEFT JOIN t_class c ON s.class_id c.id WHERE 11 ); // 动态拼接查询条件 if (keyword ! null !keyword.trim().isEmpty()) { sql.append(AND (s.stu_name LIKE ? OR s.stu_no LIKE ?) ); } if (classId ! null) { sql.append(AND s.class_id ? ); } sql.append(ORDER BY s.id DESC LIMIT ?, ?); try (Connection conn DbUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql.toString())) { int index 1; if (keyword ! null !keyword.trim().isEmpty()) { String like % keyword %; ps.setString(index, like); ps.setString(index, like); } if (classId ! null) { ps.setInt(index, classId); } ps.setInt(index, (page - 1) * pageSize); ps.setInt(index, pageSize); ResultSet rs ps.executeQuery(); while (rs.next()) { Student stu new Student(); stu.setId(rs.getInt(id)); stu.setStuNo(rs.getString(stu_no)); stu.setStuName(rs.getString(stu_name)); stu.setGender(rs.getInt(gender)); stu.setAge(rs.getInt(age)); stu.setClassId(rs.getInt(class_id)); stu.setPhone(rs.getString(phone)); stu.setEmail(rs.getString(email)); stu.setClassName(rs.getString(class_name)); list.add(stu); } } catch (SQLException e) { e.printStackTrace(); } return list; } // 统计符合条件的记录数用于分页计算总页数 public int count(String keyword, Integer classId) { // 实现思路与findList类似SQL换成 SELECT COUNT(*) return 0; } }为什么要写WHERE 11而不是拼条件时管前管后因为动态SQL的表单查询里11让所有后续条件都能用AND直接拼代码结构干净很多。另一个关键是LIMIT ?, ?——第一个问号是偏移量第二个是每页条数偏移量必须在Java里算(page - 1) * pageSize。新手最容易在这里翻车把page直接传给LIMIT结果第二页就查不到数据了。提示这套系统的数据量通常只有几百条分页用LIMIT足够。但如果未来数据量上来需要加ORDER BY索引优化那是另一层面的问题现阶段别过度设计。4.4 新增和修改同一个表单页面的两种处理路径学生的新增和修改共用一个表单页面这个页面根据id参数判断是空表单还是回显数据。对应的Servlet里写两个方法。新增比较简单接收参数、封装成Student对象、调用DAO的insert方法。修改则要先按id查出来回显用户提交后再执行update。我把修改方法完整写出来因为它涵盖了数据回显更新双重点public void edit(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); // 接收表单参数 String idStr req.getParameter(id); String stuNo req.getParameter(stuNo); String stuName req.getParameter(stuName); String genderStr req.getParameter(gender); String ageStr req.getParameter(age); String classIdStr req.getParameter(classId); String phone req.getParameter(phone); String email req.getParameter(email); // 手动转int并对空值做兜底 Integer id Integer.valueOf(idStr); int gender 女.equals(genderStr) ? 0 : 1; int age ageStr null || ageStr.isEmpty() ? 0 : Integer.parseInt(ageStr); int classId Integer.parseInt(classIdStr); // 封装对象 Student stu new Student(); stu.setId(id); stu.setStuNo(stuNo); stu.setStuName(stuName); stu.setGender(gender); stu.setAge(age); stu.setClassId(classId); stu.setPhone(phone); stu.setEmail(email); // 执行更新 new StudentDao().update(stu); // 操作完成后重定向到列表页避免刷新重复提交 resp.sendRedirect(req.getContextPath() /student?methodlist); }这里有个容易忽略的细节性别在数据库里是TINYINT0和1但页面上用户看到的是男/女两个radio。因此Servlet接收的是字符串男或女需要转换成数字再存库反过来在回显时又要根据数字回填radio的选中状态。这种前后端数据形态不一致的处理是JavaWeb开发里的常态一定要养成页面参数→Java类型→数据库类型逐层转换的意识。页面上那些成绩、年龄字段用户可能留空不填所以Integer.parseInt之前一定要做空字符串判断否则直接NumberFormatException。4.5 删除操作为什么建议用逻辑删除而不是物理删除删除学生看似简单DELETE FROM t_student WHERE id?一行SQL的事但真实管理系统里很少做物理删除。原因很简单删错了没有后悔药。我一般会建议加一个status字段0表示正常1表示已删除列表查询时默认过滤status0。如果题目没有明确要求可以不做但如果你想让项目达到高分水平这是加分项。逻辑删除的实现成本很低——查询SQL加一个条件删除方法改成UPDATE t_student SET status1 WHERE id?前端按钮颜色从红色改为灰色提示已停用。这个改动花不了十分钟但能在答辩时展示你对数据安全的思考比背概念有用得多。5. JSPBootstrap渲染前端登录页、列表页、表单页的页面实现与交互5.1 为什么JSP里不写Java脚本片段很多教材里的JSP还在用% for(int i0; ilist.size(); i) %这种脚本片段但真实开发中这种做法已经被淘汰了。JSP的最佳实践是页面里只放HTML、EL表达式和JSTL标签Java逻辑全部留在Servlet里。这样做的好处是页面结构清晰、改样式不碰逻辑、JSP引擎解析也更高效。下面的列表页面就是标准写法——${list}是Servlet通过request.setAttribute(list, list)传过来的集合JSTL的c:forEach负责循环输出。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % !DOCTYPE html html langzh-CN head meta charsetUTF-8 title学生列表/title link relstylesheet href${pageContext.request.contextPath}/css/bootstrap.min.css /head body div classcontainer h3 classmt-3学生信息列表/h3 div classmb-2 a href${pageContext.request.contextPath}/student?methodtoAdd classbtn btn-primary新增学生/a /div table classtable table-bordered table-hover thead classthead-light tr th学号/thth姓名/thth性别/thth年龄/th th班级/thth联系电话/thth操作/th /tr /thead tbody c:forEach items${list} varstu tr td${stu.stuNo}/td td${stu.stuName}/td td${stu.gender 1 ? 男 : 女}/td td${stu.age}/td td${stu.className}/td td${stu.phone}/td td a href${pageContext.request.contextPath}/student?methodtoEditid${stu.id} classbtn btn-sm btn-warning编辑/a a href${pageContext.request.contextPath}/student?methoddeleteid${stu.id} classbtn btn-sm btn-danger onclickreturn confirm(确定删除该学生吗)删除/a /td /tr /c:forEach /tbody /table /div /body /html这个页面用到了Bootstrap的表格样式、按钮样式和栅格容器。这里能看到三个关键点第一所有动态数据都用EL表达式取比如${stu.stuName}就是调用Student类的getStuName()方法属性名和getter方法名要严格对应第二URL里手动拼接methodtoEdit和id${stu.id}这对应BaseServlet里的反射路由第三删除确认框用onclickreturn confirm(...)如果用户取消浏览器的跳转会被拦截。5.2 列表页的搜索表单与分页条前后端参数对接列表页不能只展示数据还要有查询条件。搜索框和分页条的参数需要和Servlet端接收的参数名完全一致。下面这段是搜索加分页的完整前端片段form classform-inline mb-3 methodget action${pageContext.request.contextPath}/student input typehidden namemethod valuelist input typetext namekeyword value${keyword} classform-control mr-2 placeholder姓名或学号 select nameclassId classform-control mr-2 option value全部班级/option c:forEach items${classList} varclazz option value${clazz.id} ${classId clazz.id ? selected : } ${clazz.className} /option /c:forEach /select button typesubmit classbtn btn-info查询/button /form nav ul classpagination li classpage-item ${page 1 ? disabled : } a classpage-link href${pageContext.request.contextPath}/student?methodlistkeyword${keyword}classId${classId}page${page - 1}上一页/a /li li classpage-item disabled span classpage-link第 ${page} / ${totalPage} 页/span /li li classpage-item ${page totalPage ? disabled : } a classpage-link href${pageContext.request.contextPath}/student?methodlistkeyword${keyword}classId${classId}page${page 1}下一页/a /li /ul /nav这个片段的细节在于keyword、classId、page这三个参数既要在Servlet端通过request.getParameter接收又要在JSP端通过EL表达式取出并回显到表单里还要在分页链接里再次拼接传回Servlet。如果某一处漏了就会出现点击下一页后搜索条件丢失的问题。解决办法是在Servlet处理列表方法时把这些参数原封不动地放回request域req.setAttribute(keyword, keyword); req.setAttribute(classId, classId); req.setAttribute(page, page); req.setAttribute(totalPage, totalPage);这样JSP才能拿到它们。这个参数三处同步的思路是JavaWeb分页功能最常见的实现套路也是新手容易忽略的坑。5.3 表单校验前端用JavaScript快速拦后端用Java严格拦新增和修改共用的表单页面我通常会在前端加一段JavaScript校验拦截空学号、空姓名等低级错误但要注意前端校验只能提升用户体验真正的安全防线在后端Servlet里。因为用户可以绕过浏览器直接构造请求。所以我的习惯是两层都做前端用onsubmit拦后端用Java再查一遍。前端JS示例function checkForm() { var stuNo document.getElementById(stuNo).value.trim(); var stuName document.getElementById(stuName).value.trim(); if (stuNo ) { alert(学号不能为空); return false; } if (stuName ) { alert(姓名不能为空); return false; } return true; }这段代码的逻辑不需要解释但要强调的是岗位习惯前端校验是给用户看的后端校验才是给系统用的。后端校验即使只是判空也绝对不能省因为遗漏一个空值就可能让SQL拼接出错或数字转换崩溃直接暴露500错误页。5.4 登录页的session判断与权限拦截登录页面做好之后还有一个必修课——未登录用户不应该访问列表页。最直接的实现方式是在JSP页面顶部加判断但这会污染每个页面更好的方式是写一个过滤器拦截所有以/student开头的请求检查session里有没有loginUser。过滤器的代码其实很短package com.example.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 { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; HttpSession session req.getSession(false); // 未登录则跳回login.jsp if (session null || session.getAttribute(loginUser) null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; } // 已登录则继续执行后续请求 chain.doFilter(request, response); } }注意几个细节getSession(false)里的false表示——如果当前没有session就返回null而不是自动创建一个新session这样未登录用户不会被无意义地创建session对象。未登录时用重定向而不是转发因为要改变浏览器地址栏让用户明确看到回到登录页了。这个过滤器写完后要去web.xml里注册映射到需要的URL路径。如果你用注解WebFilter也可以但web.xml方式更直观、更适合答辩讲解。6. 避坑排查从404到中文乱码这套项目最常见的5个运行时问题6.1 登录提交后404Servlet类的Annotation和web.xml映射都没生效现象表单action写了student?methodlogin提交后浏览器显示404Tomcat控制台没有任何报错日志。原因Servlet类上没有加WebServlet(/student)注解web.xml里也没有配置对应的servlet-mapping所以Tomcat根本不知道请求该交给谁。解决二选一。推荐用注解在StudentServlet类名上方加WebServlet(/student)。如果用web.xml则要在web-app标签内配置servlet和servlet-mapping两块。检查WEB-INF/classes目录下有没有StudentServlet.class文件确认编译产物存在再排查路由配置。6.2 JSP页面点击删除后整页变成红叉报错现象删除按钮点击确认后页面报错javax.servlet.ServletException: java.lang.NumberFormatException控制台提示For input string: 。原因删除请求的URL是student?methoddeleteid...在JSP里${stu.id}取不到值时拼接出id后端Integer.parseInt()直接抛异常。解决找到数据为空的总根源——列表查询结果中没有把id正确封装。检查StudentDao里是否写了stu.setId(rs.getInt(id))以及ResultSet列名是否与表字段一致。这是对象封装遗漏而不是删除方法本身的问题。养成每次写完DAO查询后先在Servlet里System.out.println(list.size())验证数据的习惯。6.3 查询界面显示中文正常但往数据库里插入中文变成问号现象页面上新增一条带中文的学生记录数据库中显示????或直接乱码。原因三个环节至少有一个编码不对——数据库连接URL缺少characterEncodingutf-8参数、JSP页面contentType没写UTF-8、Servlet没有调用req.setCharacterEncoding(UTF-8)。解决按序排查。第一JDBC连接串改成jdbc:mysql://localhost:3306/student_manager?useUnicodetruecharacterEncodingutf-8useSSLfalseserverTimezoneAsia/Shanghai这个最常被忽略。第二JSP顶部确认% page contentTypetext/html;charsetUTF-8 %。第三所有Servlet接收参数之前调用req.setCharacterEncoding(UTF-8)。此外检查表结构是否DEFAULT CHARSETutf8mb4。这四个位置全部对齐后乱码问题必然消失。6.4 修改学生信息时班级下拉框没有选中当前班级现象编辑页面打开了学生信息都回显了但班级下拉框默认停在请选择没有自动选中他所在的班级。原因option标签里的selected判断写错了。很多人写成${classId clazz.id}但classId从request取出来是字符串clazz.id是整数用比较永远不相等必须转成相同类型。解决使用EL表达式的eq代替写成${classId eq clazz.id}或者${classId clazz.id ? selected : }但前提是Servlet放入request域的classId也为Integer类型。我一般做法是在Servlet端就直接转好类型再setAttribute而不是在JSP里强转。JSP里做类型转换是下策会降低页面可读性。6.5 Tomcat 10和Tomcat 9的兼容性问题javax还是jakarta现象项目在本机能跑换到一台新装的Tomcat 10上启动直接报错提示ClassNotFoundException: javax.servlet.ServletException。原因Tomcat 10把所有Servlet API包名从javax.*改成了jakarta.*你的代码里全是javax.servlet在Tomcat 10下加载不到。这是版本断层导致的兼容问题。解决最稳妥的办法是用Tomcat 9.x版本它对应javax命名空间和课程里教的保持一致。如果你坚持用Tomcat 10需要把所有import javax.servlet改成import jakarta.servlet依赖里也换成对应版本。对于学生项目我强烈建议直接用Tomcat 9没必要在这个细节上浪费时间。7. 验证与进阶把连接池、防SQL注入和日志补上让项目达到答辩水平基础功能跑通后这个项目大概处于能交差的60分水平。想让它成为真正能拿高分、也敢写进简历的项目我建议按三个方向做进阶。这三个方向不需要重构都是在现有代码上叠加每完成一个都能在答辩时讲出一段有深度的内容。第一个方向是引入数据库连接池。现在你的DbUtil.getConnection()大概率是DriverManager.getConnection()直连这在演示环境能用但并发的场景下会频繁创建和销毁连接。换成Druid或HikariCP本质上是把连接生命周期交给池管理。以Druid为例在pom里引入com.alibaba:druid:1.2.20然后用一个配置文件初始化连接池把原来getConnection()方法改成从池中取。答辩老师看到这个会认为你了解生产环境的资源管理而不是只会在本地搭个玩具。哪怕你不深入源码只要讲清楚复用连接、减少创建开销这一条就够了。第二个方向是防SQL注入的确认与演示。如果你全程用的是PreparedStatement那么恭喜你已经具备基础的防注入能力——但你要能在答辩时证明这一点。把原来用Statement拼接字符串的代码找出来对比讲或者现场演示输入 OR 11这样的字符串让老师看到查询结果不受影响。还有一个冷门但重要的检查点排序字段如果是从前端传参数的比如?orderByage你如果直接拼接进SQL这里就是注入点。安全的做法是做一个白名单映射只允许传入预定义好的字段名。第三个方向是加一行日志输出。不要用System.out.println哪怕你只有时间加一个log4j2或Slf4j也算进阶。配置很简单pom加依赖、resources放一个log4j2.xml、DAO和Servlet里把e.printStackTrace()换成log.error(查询学生列表失败, e)。这一条在实际工作中是强制规范在课程设计里是明显的加分项因为它体现了问题追踪意识。做完这三个方向之后我强烈建议你做一个验收测试——对照需求列表逐条点一遍登录是否校验空密码、新建学生时学号重复是否被拦截、修改后列表是否立即刷新、删除前是否有确认提示、分页翻到第3页后条件还在不在。把测试过程录一段视频存下来答辩现场如果机器出问题直接放视频这是最实在的后悔药。我在交付这个方向的方案时每次都先跑测试再碰代码。最后说一句掏心窝的话这个技术栈确实不是最前沿的但它把你放在了一个很有利的位置——没有任何框架帮你隐藏细节每一行请求流转你都看得懂。我的教训是与其急着去写Spring Boot的starter不如先把这套不靠自动化、不靠黑匣子的系统彻底吃透。希望帮到你。本文还有配套的精品资源点击获取