Java+JSP+MySQL学生管理系统开发全攻略:从三层架构到部署避坑 简介一份面向Java Web初学者的完整学生信息管理系统项目源码包基于JavaJSPMySQL搭建经典B/S架构实现学生信息增删改查、登录权限管理、条件查询和统计报表等典型功能适用于课程设计、毕业设计或自学练手。压缩包共75个文件主要包括28个JSP页面负责前端展示、11个Java源文件及22个Class文件负责业务逻辑、10个XML配置文件用于框架或环境配置并附带MySQL建表SQL脚本整体仅3.92MB目录规范、便于快速部署运行。通过学习本包可以掌握ServletJSPJavaBean的开发模式、JDBC数据库操作、EL与JSTL简化页面逻辑等核心技能同时参考其数据库设计和分层结构为后续扩展或重构提供基础。目前已有8989人学习下载适合希望从零搭建完整Web管理系统并积累实战经验的开发者。1. 为什么还在用 JSP 写学生管理系统JavaJSPMysql做Web学生信息管理系统这选题听起来像是七八年前的课程设计但放到今天依然是Java Web入门和毕设选题里最稳的一条路。原因是学生信息管理系统的业务核心就是登录、增删改查、分页、条件搜索这一串标准CRUDJSP恰好能把前端页面、Servlet 控制和 MySQL 数据操作摁在同一个 Web 容器里跑学习链路最短答辩时也最好讲。它不适合大厂高并发场景但适合两类人一类是刚学完 Java 基础、想完整跑通一个Web项目的入门者另一类是毕业设计想稳过、又不想碰微服务重装备的学生。做完这套系统你能真正摸清一次浏览器请求从 Servlet 到 DAO 再到 MySQL 的完整路径也能看懂 Tomcat、war 包、会话和过滤器到底在干什么。2. 跑通环境与建库JDK、Tomcat、MySQL 的版本搭配和一份能直接执行的建库脚本2.1 版本搭配为什么不能乱来做传统 JSP 项目环境选型的第一原则是「不要追新」。最常见、踩坑最少的组合是 JDK 8 Tomcat 9 MySQL 5.7 或 8.0 Maven 3.6 左右。这套组合里 JDK 8 对 JSP、Servlet 的兼容最稳Tomcat 9 用的还是 javax.servlet 命名空间和教科书、培训课里的写法完全对得上。组件推荐版本说明JDK8 或 11老项目编译、运行零障碍Tomcat8.5 / 9.x支持 javax.servlet部署 war 直接跑MySQL5.7 / 8.05.7 更省事8.0 需要驱动匹配构建工具Maven管理依赖、打包 war 都比手工拷 jar 可靠不建议用 JDK 17 Tomcat 10 的组合跑老 JSP 项目。Tomcat 10 把 Servlet 规范包名从 javax.servlet 整体改成了 jakarta.servlet老项目里上百个 import 和 web.xml 头全要改第三方标签库也未必跟进折腾一圈的价值为零。我一般见到同学拿 Tomcat 10 部署传统 JSP 项目翻车第一反应都是让他换回 Tomcat 9。IDEA 新建 JSP 项目的常见做法是新建一个 Maven 工程打包方式选 war然后在 Project Structure 里把 Web 模块和 Artifact 配上再在 Run Configuration 里挂一个本地 Tomcat。如果你手头还没有完整的工程骨架也可以先建空的 Java Enterprise 项目勾选 Web Application但后面迟早要补 Maven不如一步到位。2.2 一份能直接执行的建库脚本数据库设计决定后面 DAO 层写得多痛苦。学生信息管理系统通常两张表就够学生表 student 和用户表 sys_user。字段不贪多但类型、默认值、唯一键、字符集这些细节在建表时就要定好别等代码写一半再回头 ALTER。CREATE DATABASE IF NOT EXISTS student_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE student_db; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, stu_no VARCHAR(20) NOT NULL COMMENT 学号, name VARCHAR(50) NOT NULL COMMENT 姓名, gender TINYINT NOT NULL DEFAULT 0 COMMENT 0未知 1男 2女, birthday DATE NULL COMMENT 出生日期, phone VARCHAR(20) NULL COMMENT 手机号, email VARCHAR(100) NULL COMMENT 邮箱, address VARCHAR(200) NULL COMMENT 住址, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, UNIQUE KEY uk_stu_no (stu_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生信息表; CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT 密码先存MD5后续要升级, real_name VARCHAR(50) NULL COMMENT 真实姓名 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT系统用户表; INSERT INTO sys_user (username, password, real_name) VALUES (admin, MD5(admin123), 管理员);这段脚本里有几个决策值得说明。性别用 TINYINT 加默认值 0而不是字符串或枚举因为 MySQL 枚举类型后期加值要 ALTER TABLETINYINT 改起来代价小Java 端做字典映射就行。学号加唯一索引是硬约束防止同一个人被重复录入两次。create_time 用 DEFAULT CURRENT_TIMESTAMP插入语句就不用显式管时间字段了这是很多教程里漏掉的细节。字符集统一 utf8mb4因为 utf8 在 MySQL 里是 utf8mb3 的别名存不了 emoji 和生僻字学生姓名里偶尔会出现这类字符。sys_user 表里密码先存 MD5 是妥协做法因为教科书和多数毕设都这么写。但你要知道 MD5 已经能被彩虹表秒破第 6 章我会说怎么升级成加盐散列这里先保证不是明文落库。2.3 连接 MySQL 的 DBUtil 与三个必写参数数据库连接工具类是每个 DAO 的基础。先把最简版跑通再用连接池替换这条路最稳。下面这个 DBUtil 用 DriverManager 直连适合开发阶段和几百条数据的小项目。package com.demo.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/student_db ?useSSLfalse serverTimezoneAsia/Shanghai characterEncodingutf8; private static final String USER root; private static final String PASSWORD 你自己的密码; static { try { // MySQL 8 驱动类名5.x 驱动是 com.mysql.jdbc.Driver Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(加载数据库驱动失败, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }URL 里三个参数是血泪经验useSSLfalse 避免 MySQL 8 在本地连接时报 SSL 协商警告serverTimezoneAsia/Shanghai 解决日期时间字段读取时差八小时的问题characterEncodingutf8 保证中文写入不乱码。少一个后面排查乱码或连接超时都要浪费半天。注意PASSWORD 里如果包含 、#、 这类特殊字符直接拼在 URL 里会被当成参数分隔符报错时看着完全莫名其妙。最省心的办法是把数据库账号配置放到 jdbc.properties 文件里用 Properties 类读取别硬编码在 Java 类中。3. 三层结构落代码从登录到增删改的 ServletDAOJSP 完整链路3.1 包结构划分与 URL 路由设计拿到项目先别急着写代码把包结构和请求路径定下来后面每加一个功能都是往格子里填东西。学生信息管理系统用三层就够了Servlet 做控制器DAO 做数据访问JSP 只负责渲染。业务逻辑简单的项目不需要再强行拆 Service 层但如果你后面要加事务Service 层还是有必要的——第 6 章会提。包/类职责对应 URLcom.demo.entity.Student数据实体无com.demo.dao.StudentDao增删改查 SQL无com.demo.servlet.LoginServlet登录控制/logincom.demo.servlet.StuListServlet分页列表与搜索/student/listcom.demo.servlet.StuAddServlet新增学生/student/addcom.demo.servlet.StuDeleteServlet删除学生/student/deletecom.demo.servlet.StuUpdateServlet修改学生/student/updatecom.demo.filter.LoginFilter会话校验/*URL 用 /模块/动作 的格式比 /addStudent.do 这类命名清晰也方便 Filter 按前缀拦截。Controller 用 Servlet 而不是在 JSP 里直接写 Java 脚本片段核心原因只有一个维护性。JSP 里写业务逻辑改一次需求要同时动页面和逻辑代码混在一起后期谁接手都头疼。3.2 实体类 Student实体类的字段要和 student 表一一对应数据类型也要匹配。Java 侧日期用 java.time.LocalDate对应 MySQL 的 DATEcreate_time 用 LocalDateTime。JDBC 4.2 之后驱动原生支持 java.time不需要再手动转 java.util.Date。package com.demo.entity; import java.time.LocalDate; import java.time.LocalDateTime; public class Student { private Integer id; private String stuNo; private String name; private Integer gender; // 0未知 1男 2女 private LocalDate birthday; private String phone; private String email; private String address; private LocalDateTime createTime; // 省略 getter/setterIDEA 快捷键 AltInsert 生成 }实体里 gender 用 Integer 不用 int是为了让 DAO 判断条件时能区分「没传」和「传了 0」两种情况。前端下拉框不选性别时传空字符串转成 Integer 就是 null如果字段是基本类型 int默认值 0 会和「未知」语义冲突做条件查询时容易出错。3.3 登录链路DAO 查询 Servlet 会话控制登录是学生管理系统的门面也是后面 Filter 拦截的基础。LoginServlet 的 doPost 做四件事取参数、调 DAO 验证、成功写 session 并重定向、失败回登录页并提示。package com.demo.servlet; import com.demo.dao.UserDao; import com.demo.entity.User; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; WebServlet(/login) public class LoginServlet extends HttpServlet { private UserDao userDao new UserDao(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String username req.getParameter(username); String password req.getParameter(password); if (username null || password null || username.trim().isEmpty() || password.isEmpty()) { req.setAttribute(msg, 用户名和密码不能为空); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } User user userDao.findByUsernameAndPassword( username.trim(), password); if (user null) { req.setAttribute(msg, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } // 登录成功把用户对象放进 session HttpSession session req.getSession(); session.setAttribute(loginUser, user); // 重定向到列表页避免刷新页面重复提交表单 resp.sendRedirect(req.getContextPath() /student/list); } }这里有两个关键设计。失败时用 forward 转发并携带 msg 属性login.jsp 用 EL 表达式${msg}显示错误成功时用 sendRedirect 重定向。为什么成功要用重定向因为如果用 forward浏览器地址栏还停在 /login用户按 F5 刷新会把上一次的登录表单再提交一次数据库重复写入不说体验也奇怪。重定向会让浏览器重新发起 GET 请求到列表页刷新任意次数都是安全的。3.4 新增学生一张表单走通 Servlet、DAO 与重定向新增功能要同时写 JSP 表单、Servlet 和 DAO 的 insert 方法。JSP 表单注意几点action 写绝对路径用${pageContext.request.contextPath}拼别写死相对路径method 用 post性别用下拉框默认选中「未知」。% page contentTypetext/html;charsetUTF-8 languagejava % form action${pageContext.request.contextPath}/student/add methodpost div label学号/label input typetext namestuNo required /div div label姓名/label input typetext namename required /div div label性别/label select namegender option value0未知/option option value1男/option option value2女/option /select /div div label出生日期/label input typedate namebirthday /div div label手机号/label input typetext namephone /div button typesubmit保存/button /form对应的 StuAddServlet 接收参数后组装 Student 对象调用 DAO 插入最后重定向回列表页。package com.demo.servlet; import com.demo.dao.StudentDao; import com.demo.entity.Student; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; import java.time.LocalDate; WebServlet(/student/add) public class StuAddServlet extends HttpServlet { private StudentDao studentDao new StudentDao(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); Student s new Student(); s.setStuNo(req.getParameter(stuNo).trim()); s.setName(req.getParameter(name).trim()); s.setGender(Integer.parseInt(req.getParameter(gender))); String birthday req.getParameter(birthday); if (birthday ! null !birthday.isEmpty()) { s.setBirthday(LocalDate.parse(birthday)); } s.setPhone(req.getParameter(phone)); s.setEmail(req.getParameter(email)); s.setAddress(req.getParameter(address)); studentDao.insert(s); resp.sendRedirect(req.getContextPath() /student/list); } }doPost 开头那行req.setCharacterEncoding(UTF-8)必须放在读取任何参数之前否则中文姓名全变问号。日期字段从前端input typedate传来的是 yyyy-MM-dd 字符串直接 LocalDate.parse 就能解析不需要手动格式化。如果用户没填日期浏览器传过来是空字符串直接 parse 会抛 DateTimeParseException所以要判空。3.5 删除和修改URL 传参的边界检查删除功能最常见的是通过 URL 传 id比如/student/delete?id3。这里有个初学者必踩的坑Integer.parseInt(id)遇到非数字字符串会抛 NumberFormatException页面直接 500。包裹 try/catch 是最低要求。WebServlet(/student/delete) public class StuDeleteServlet extends HttpServlet { private StudentDao studentDao new StudentDao(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String idStr req.getParameter(id); if (idStr null || idStr.trim().isEmpty()) { resp.sendRedirect(req.getContextPath() /student/list); return; } try { int id Integer.parseInt(idStr); studentDao.deleteById(id); } catch (NumberFormatException e) { // id 不合法不处理直接回列表页 } resp.sendRedirect(req.getContextPath() /student/list); } }为什么删除用 doGet课程设计和毕设里普遍这么写因为链接点击就能触发代码量最少。但你要知道正式项目不会用 GET 做删除——搜索引擎爬虫、浏览器预加载都可能导致误删。答辩如果被问到答「生产环境会用 POST 加按钮表单提交或者 AJAX 请求这里为了演示简化」就能圆过去。修改功能的 Servlet 结构和新增几乎一样只是把 DAO 方法换成 updateById表单里加一个隐藏域input typehidden nameid value${student.id}不再展开。4. 学生列表分页与条件查询SQL、参数与 JSP 渲染的一条完整链路4.1 分页 SQL 的写法与 COUNT 统计学生列表是访问最频繁的页面分页是必须的。MySQL 分页用 LIMIT 两个参数第一个是偏移量第二个是每页条数。先写通 SQL 再写 Java逻辑会清晰很多。-- 第 2 页每页 5 条偏移量 (页号-1) * 每页条数 SELECT id, stu_no, name, gender, birthday, phone, email FROM student ORDER BY id DESC LIMIT 5, 5; -- 统计总条数用于计算总页数 SELECT COUNT(*) FROM student;注意 COUNT 查询要和列表查询分开执行不能写SELECT * FROM student LIMIT 0,5然后resultSet.last()拿总数——那样会把所有数据加载到内存数据量上去之后页面响应会肉眼可见地变慢。小系统里虽然只有几千条数据无所谓但要养成好习惯。列表查询建议按 id 倒序新增的学生排在最前面用户操作完能看到立竿见影的效果。4.2 后端参数接收与 PageBean 封装Servlet 端要接收三个参数pageNo 表示当前页pageSize 表示每页条数还有后面条件查询要用的 keyword 和 gender。参数必须做默认值处理否则用户手动改 URL 传pageNo-1或者不传程序就崩了。int pageNo 1; int pageSize 5; String pageNoStr req.getParameter(pageNo); String pageSizeStr req.getParameter(pageSize); if (pageNoStr ! null !pageNoStr.trim().isEmpty()) { pageNo Integer.parseInt(pageNoStr); } if (pageSizeStr ! null !pageSizeStr.trim().isEmpty()) { pageSize Integer.parseInt(pageSizeStr); } // 非法值归位 if (pageNo 1) pageNo 1; if (pageSize 1 || pageSize 50) pageSize 5;PageBean 是一个纯数据容器把分页相关的所有信息打包传给 JSP页面就不用自己算了。字段包括 pageNo、pageSize、total、pageCount、list 数据以及一个getPageCount()计算方法(total pageSize - 1) / pageSize。这个公式比total % pageSize 0 ? total / pageSize : total / pageSize 1简洁也不会出除零错。4.3 DAO 层动态条件拼接关键词搜索与性别筛选学生列表页通常带一个搜索框按姓名或学号模糊查询加上性别下拉筛选。DAO 层不能写死 SQL要用 StringBuilder 动态拼接 where 条件参数统一放进 List 收集最后一次性传给 PreparedStatement。public ListStudent findPage(int pageNo, int pageSize, String keyword, Integer gender) { StringBuilder sql new StringBuilder( SELECT id, stu_no, name, gender, birthday, phone, email FROM student WHERE 11); ListObject params new ArrayList(); if (keyword ! null !keyword.trim().isEmpty()) { sql.append( AND (name LIKE ? OR stu_no LIKE ?)); params.add(% keyword.trim() %); params.add(% keyword.trim() %); } if (gender ! null) { sql.append( AND gender ?); params.add(gender); } sql.append( ORDER BY id DESC LIMIT ?, ?); params.add((pageNo - 1) * pageSize); params.add(pageSize); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql.toString())) { for (int i 0; i params.size(); i) { ps.setObject(i 1, params.get(i)); } ResultSet rs ps.executeQuery(); ListStudent list new ArrayList(); 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.getInt(gender)); // birthday、phone、email 同理 list.add(s); } return list; } catch (SQLException e) { throw new RuntimeException(查询学生列表失败, e); } }参数顺序是这里最隐蔽的坑list.add 的顺序必须和 SQL 里问号出现的顺序完全一致。先加的是 keyword 两次然后是 gender最后才是 LIMIT 的两个参数。一旦顺序错位轻则查不出数据重则 SQLException 报参数类型不匹配。排查这类问题最快的方法是打印完整 SQL 和参数列表不要干瞪眼。注意LIKE 条件里的百分号必须拼进参数值不能写在 SQL 模板里。写成LIKE ?后传%张%是安全的写成LIKE %?%直接报错PreparedStatement 不负责替你拼通配符。WHERE 11这个写法很多人觉得丑但它在动态拼接里确实好用——每个条件都从 AND 开始不用判断是不是第一条。小项目性能影响可以忽略PostgreSQL 和 MySQL 的优化器会直接把它当恒真条件处理不是性能瓶颈。4.4 JSP 渲染列表与分页条JSTL 和 EL 代替脚本片段列表页 JSP 的核心是 JSTL 标签遍历和 EL 表达式取值。页面顶部要引入 JSTL 的 taglib这是新手最容易漏的一步——漏了之后页面直接报错提示找不到循环标签。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table border1 cellpadding6 cellspacing0 tr thID/thth学号/thth姓名/thth性别/th th出生日期/thth手机号/thth操作/th /tr c:forEach items${pageBean.list} varstu tr td${stu.id}/td td${stu.stuNo}/td td${stu.name}/td td c:choose c:when test${stu.gender 1}男/c:when c:when test${stu.gender 2}女/c:when c:otherwise未知/c:otherwise /c:choose /td td${stu.birthday}/td td${stu.phone}/td td a href${pageContext.request.contextPath}/student/delete?id${stu.id} onclickreturn confirm(确定删除吗)删除/a a href${pageContext.request.contextPath}/student/edit?id${stu.id}修改/a /td /tr /c:forEach /tableJSP 里不要写% for (Student s : list) { %脚本片段。EL 和 JSTL 是 JSP 2.0 之后的标准用法脚本片段混杂 Java 代码会让页面变成一团乱麻Tomcat 编译 JSP 时还会把脚本片段里的异常直接抛到页面上排错体验非常差。性别显示用 c:choose 做映射比在 Java 里拼字符串再输出干净。分页条放在表格下方要保留当前的搜索条件。我用 c:url 标签把 keyword 和 gender 参数拼进去这样翻页后搜索条件不会丢。div classpagination span共 ${pageBean.total} 条第 ${pageBean.pageNo}/${pageBean.pageCount} 页/span c:if test${pageBean.pageNo 1} a hrefc:url value/student/list c:param namepageNo value${pageBean.pageNo - 1}/ c:param namekeyword value${keyword}/ /c:url上一页/a /c:if c:if test${pageBean.pageNo pageBean.pageCount} a hrefc:url value/student/list c:param namepageNo value${pageBean.pageNo 1}/ c:param namekeyword value${keyword}/ /c:url下一页/a /c:if /div搜索表单的提交方式用 GET不要用 POST。因为搜索条件应该体现在 URL 上这样用户可以把带条件的列表页链接发给别人刷新页面也不会弹「确认重新提交表单」。Servlet 端用req.getParameter(keyword)一视同仁地接收不需要区分 GET 还是 POST。5. 打包 war 与部署避坑传统 JSP 项目上线必踩的五个坑5.1 用 Maven 把传统 JSP 项目打成 war 并部署到 Tomcat开发环境用 IDEA 的 Tomcat 跑 exploded artifact和最终部署不是一回事。交付或答辩时需要一个独立的 war 包放到 Tomcat 的 webapps 目录下就能跑。Maven 项目直接在终端执行一行命令mvn clean package -DskipTests打包完成后在 target 目录下会生成xxx.war命名通常是 项目名-1.0-SNAPSHOT.war。把 war 复制到 Tomcat 的 webapps 目录启动bin/startup.shWindows 是 startup.batTomcat 会自动解压 war 并部署。访问路径是http://localhost:8080/项目名/后面再拼具体 URL比如登录页就是http://localhost:8080/项目名/login.jsp。war 包的本质是一个 zip但内部结构有要求WEB-INF 下必须有 web.xmlServlet 3.0 后可以用注解代替但 web.xml 存在更稳classes 目录放编译后的 classlib 目录放所有依赖 jar。如果打包后发现部署报 ClassNotFoundException九成是 lib 目录里少了依赖。5.2 坑一部署后 JSP 报错找不到 JSTL 标签现象本地 IDEA 里跑得好好的打成 war 部署到独立 Tomcat 后列表页打开直接 500报org.apache.jasper.JasperException底层是ClassNotFoundException: org.apache.taglibs.standard.tag.rt.core.ForEachTag。原因IDEA 里配置 Tomcat 时JSTL 的 jar 是 IDE 帮你看的打在 lib 下但 Maven 打包时如果依赖是 provided scope 或者根本没声明webapps 下的项目 WEB-INF/lib 里就没有 JSTL。独立 Tomcat 的 lib 目录里默认又不带 JSTL。解决确认 pom.xml 里引入了 JSTL 和 standard 依赖且 scope 不是 provided。老项目直接用 jar 的把 jstl.jar 和 standard.jar 复制进 WEB-INF/lib 再重新打包。检查方式很简单解压 war看 WEB-INF/lib 下有没有这两个 jar。5.3 坑二MySQL 连接报 Access denied 或 Communications link failure现象程序启动后首次查数据库直接抛异常日志里出现Access denied for user rootlocalhost或者Communications link failure有时还带The server time zone value字样。原因分三种。第一种是密码本身错但更隐蔽的是密码含特殊字符在 JDBC URL 里被 URL 编码吃掉第二种是 MySQL 8.0 默认认证插件是 caching_sha2_password而你用的驱动还是老的 mysql-connector-java 5.x握手阶段就失败第三种是 URL 少了 serverTimezone 参数驱动拿 JVM 默认时区去和 MySQL 对比报时区异常。解决先把账号密码用命令行mysql -uroot -p验证一遍能连上再谈代码。接着确认驱动版本MySQL 8 必须用 8.x 的com.mysql.cj.jdbc.Driver5.x 驱动连 8.0 就是不行。最后把 URL 的 useSSL、serverTimezone、characterEncoding 三个参数补齐重启后再看。5.4 坑三中文乱码页面、请求、数据库三层全得统一现象登录 admin 成功后列表页中文姓名变成或者æå这类乱码写入数据库后也是乱码。原因字符集在四个环节里任何一环不一致都会出问题。JSP 文件本身的编码不是 UTF-8Servlet 没调req.setCharacterEncoding(UTF-8)JDBC URL 没带 characterEncoding 参数数据表默认字符集是 latin1。四者里坏一环乱码就来了。解决统一 UTF-8。JSP 第一行写% page contentTypetext/html;charsetUTF-8 languagejava %Servlet 在所有读取参数之前调用req.setCharacterEncoding(UTF-8)JDBC URL 带characterEncodingutf8建表语句用 utf8mb4。Tomcat 8.5 之后默认 URIEncoding 已是 UTF-8如果还乱检查 conf/server.xml 里 Connector 节点有没有被改过 URIEncoding。5.5 坑四不登录直接输入 URL 绕过页面现象用户在登录页不登录直接在地址栏输入http://localhost:8080/项目名/student/list列表数据照样打开登录形同虚设。原因Servlet 和 JSP 默认没有任何鉴权。WebServlet 注解只负责路由不做会话校验JSP 放在 webapp 根目录下Tomcat 允许直接访问。Filter 没写整个应用就裸奔。解决写一个 LoginFilter 拦截所有请求检查 session 里有没有 loginUser没有就重定向到 login.jsp。注意放行登录页本身和静态资源css、js否则登录页的样式也全丢了。更彻底的做法是把所有需要保护的 JSP 放进 WEB-INF 目录下Tomcat 禁止客户端直问 WEB-INF只能通过 Servlet 转发访问从物理上堵死绕过路径。WebFilter(/*) public class LoginFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; String uri req.getRequestURI(); String ctx req.getContextPath(); // 放行登录相关和静态资源 if (uri.equals(ctx /login) || uri.endsWith(.css) || uri.endsWith(.js)) { chain.doFilter(request, response); return; } HttpSession session req.getSession(false); if (session null || session.getAttribute(loginUser) null) { resp.sendRedirect(ctx /login.jsp); return; } chain.doFilter(request, response); } }req.getSession(false)是关键参数为 false 时如果当前没有会话返回 null 而不是新建一个。如果写成getSession()拦截器会把「未登录用户」变成「已登录用户」——因为 Session 被创建出来了虽然里面没有 loginUser但你已经给攻击者构造了一个合法会话对象。这属于安全细节里的黑匣子很多人掉进去出不来。5.6 坑五Tomcat 10 部署老项目javax 包名报编译错误现象把项目 war 丢进 Tomcat 10 的 webapps浏览器访问页面时下方异常信息一长串第一行就是java.lang.ClassNotFoundException: javax.servlet.http.HttpServlet或ServletException: Error instantiating servlet class。原因Tomcat 10 从 Java EE 迁移到 Jakarta EE 9Servlet API 包名从 javax.servlet 变成了 jakarta.servlet。老项目里所有 import javax.servlet 的源代码、web.xml 里的头声明以及第三方依赖对 javax 的引用全部作废。解决最省事的是换 Tomcat 9.x不要去改代码适配 Jakarta。课程设计和毕设的评分标准通常不绑定 Tomcat 版本换回 Tomcat 9 所有代码原样跑通。如果老师环境固定是 Tomcat 10那要把依赖换成 jakarta.servlet-api所有 import 批量替换成 jakarta.servletFilter、Servlet、Listener 的接口名不变工作量取决于代码量。对学习项目来说我一般直接建议换 Tomcat 9。提示部署后看日志比看浏览器页面高效得多。Tomcat 的日志在 logs/catalina.out 和服务名专用日志里报错堆栈前三行基本就定位了问题别在浏览器截图猜原因。6. 用验证清单和生产习惯收尾连接池参数、密码散列与 Spring Boot 迁移参考6.1 自测验证清单系统做完至少过一遍项目写完别急着交付按下面清单走一遍能筛掉大部分低级问题。验证项操作预期结果登录校验先输错密码再输正确密码第一次提示错误第二次进入列表页重复提交登录成功后按 F5 刷新不会重复提交登录表单新增学生填写包含中文姓名的表单列表页出现新记录中文不乱码分页边界翻到最后一页再点下一页页面不变或停留在末页不报错条件查询按姓名搜索后翻页搜索条件不丢翻页结果仍是过滤后的数据鉴权拦截未登录直接访问 /student/list被重定向到登录页删除确认删除一条记录后返回列表记录消失总数减一6.2 连接池参数与学生系统的合适取值DriverManager 直连开发方便但每查询一次就建一次 TCP 连接MySQL 默认连接超时是八小时挂着不动还会报连接失效。换成 Druid 连接池只需要改 DBUtil 的初始化方式DAO 代码不用动。学生管理系统并发低参数取值不用激进参数建议值说明initialSize5启动时建立 5 个连接minIdle5空闲低于 5 个就补maxActive20上限 20够课程设计用maxWait3000拿不到连接最多等 3 秒别无限等validationQuerySELECT 1空闲连接保活maxWait 设 3000 而不是默认的 60000是为了让系统在高并发挤爆连接池时快速失败而不是所有线程都挂在那里等。对学习项目来说主动失败比假死好排查得多。6.3 密码散列别再存明文前面的建表脚本里用了 MD5那只是权宜之计。MD5 没有加盐一样的密码会生成一样的散列值彩虹表一查就还原。哪怕只是毕设也建议升级成 BCrypt每次生成不同的盐散列结果不可逆。改动范围很小注册或修改密码时用 BCrypt 加密存储登录时用BCrypt.checkpw(明文, 散列值)校验。用户表已有的 MD5 数据可以先标记为「弱密码」下次登录时强制升级不用迁移旧数据。6.4 Spring Boot 集成 JSP目录与视图配置如果你的下一站是 Spring Boot集成 JSP 并不复杂但有固定的目录要求。JSP 必须放在src/main/webapp/WEB-INF/jsp下不能放在 classpath 的 templates 目录里因为 Spring Boot 内嵌 Tomcat 默认不扫描 webapp 下的 JSP。视图解析配置在 application.propertiesspring.mvc.view.prefix/WEB-INF/jsp/ spring.mvc.view.suffix.jsp对应的 Controller 方法返回student/list时Spring Boot 会自动拼成/WEB-INF/jsp/student/list.jsp。传统 JSP 项目里 Servlet 的 doGet 改成GetMapping方法原来req.getParameter改成 Controller 方法参数session.setAttribute改成HttpSession参数直接注入迁移成本不算高。要注意的是打包方式需要改成 war 并用SpringBootServletInitializer如果用内置 Tomcat 还要额外引入 jsp 相关依赖否则页面会 404。我最早做学生管理系统时把所有 SQL 直接写进 JSP 脚本片段页面越改越长最后连自己都找不到逻辑在哪里。后来按 Servlet DAO JSTL 重写一遍才体会到分层不是形式主义是给未来的自己留条活路。这套系统规模不大但五脏俱全把分页边界、字符集、会话拦截这几个容易翻车的位置一次做对比多写一百行代码有价值。希望帮到你。本文还有配套的精品资源点击获取