尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

JSP+Servlet学生信息管理系统:从Servlet生命周期到war部署全解析

发布时间:2026/9/29 1:53:16

资讯中心
01
ARTICLE

JSP+Servlet学生信息管理系统:从Servlet生命周期到war部署全解析

JSP+Servlet学生信息管理系统:从Servlet生命周期到war部署全解析
简介面向 Java Web 入门学习者与课程设计者这是一份基于 JSP 和 Servlet 实现的学生信息管理系统完整项目资源。项目覆盖用户登录验证、学生信息增删改查、年级与成绩管理等典型业务模块配合 db_studentinfo.sql 数据库初始化脚本可直接在 Tomcat 容器中部署运行。通过阅读和调试源码可以清晰理解 JSP 动态页面生成、Servlet 请求分发处理、JDBC 数据库连接与事务管理以及 MVC 分层设计思想等 Java Web 核心知识点。资源共 576 个文件压缩包仅 2.75MB包含 java 源码、jsp 页面、sql 脚本、class 编译文件、jar 依赖库以及大量 html、css、js 前端资源与配套 png/gif 图片项目目录结构完整便于按模块对照学习。目前已有 996 人学习使用对于需要完成课程设计、毕业设计或希望系统掌握传统 Java Web 开发流程的读者项目提供了从页面展示到业务处理再到数据持久化的完整链路可直接复用或改造是很实用的参考项目。1. 为什么“老掉牙”的 JSPServlet 选题反而是 Java Web 能力的分水岭打开任何一个毕设选题列表用 jsp、servlet实现学生信息管理系统 这个组合几乎永远占着一个名额。我见过不少人觉得它“过时”上手就用 Spring Boot 重写结果卡在环境配置上也见过老老实实把 JSP Servlet 做完的人反而把请求生命周期、会话状态、JDBC 这些基本功摸得透透的。这个题目真正的价值不是 CRUD 本身而是用最少的框架封装把一次 HTTP 请求从浏览器穿透到数据库再回到页面的完整链路暴露在你面前。这篇笔记就按我实际做项目的顺序从技术选型、表设计、登录与列表、增删改查、分页模糊查询一路讲到 war 包部署和常见翻车现场。适合正在做课设或毕设的学生也适合想快速补 Java Web 底子的从业者。2. 技术骨架先行Servlet 生命周期、MVC 边界与数据库表设计2.1 Servlet 生命周期对象创建一次服务多次销毁一次Servlet 生命周期是理解整个学生信息管理系统的钥匙。很多新手以为一个请求来了就 new 一个 Servlet实际恰好相反。Servlet 是单实例多线程容器启动或第一次被请求时创建实例之后所有请求共用同一个对象走init() → service() → destroy()三个阶段。init()只执行一次适合做读取配置、初始化连接池这类开销大的工作。service()每次请求都会执行doGet/doPost由它按请求方法分发。destroy()在容器关闭或应用卸载时执行一次用来释放资源。因为 Servlet 是单例成员变量就是高风险区多个请求同时读写同一个成员变量会造成数据错乱我一般只把init()里真正需要复用且线程安全的配置放成员变量其余请求数据全部用方法内局部变量。load-on-startup 参数也值得留意。如果你希望在 Tomcat 启动时就初始化 Servlet而不是等第一个请求进来才创建可以在注解里写WebServlet(value /login, loadOnStartup 1)数字越小越先加载。学生管理系统里这个参数不是必须的但用了可以避免第一次访问时明显的卡顿感。2.2 MVC 边界JSP 只做展示Servlet 只做调度学生信息管理系统的功能通常是登录、学生列表、新增、编辑、删除、模糊查询、分页。如果把这些逻辑全塞进 JSP页面 Java 代码会膨胀到难以维护如果全塞进 Servlet又会输出大量 HTML 拼接字符串。正确做法是严格按 MVC 拆分这也是这个项目能被答辩老师认可的关键。我一般这样分JSP 只负责渲染页面里用 JSTL 和 EL 表达式取数据不写 JDBC、不写业务判断Servlet 只做参数接收、调用业务对象、决定跳转哪个页面JavaBean/POJO 承载数据Student、User、PageBeanDao 类负责数据库访问如果业务复杂中间再加一层 Service。对于课设层面的学生管理系统Dao 直接暴露给 Servlet 够用但我会预留 Service 包名避免后续扩展时大改。每个类的职责可以用下面这张表说清楚这也是我写项目前先画出来的分工类名包名职责关键方法Studentcom.student.entity学生实体对应 t_student 表getter/setterUsercom.student.entity登录用户实体getter/setterStudentDaocom.student.dao学生表增删改查、分页、搜索findByPage/search/add/update/deleteUserDaocom.student.dao登录校验findByUsernameAndPasswordLoginServletcom.student.servlet登录控制与跳转doPostStudentServletcom.student.servlet学生列表、增删改查入口doGet/doPostEncodingFiltercom.student.filter统一字符编码doFilter2.3 数据库设计两张核心表与字段命名约定学生信息管理系统最少两张表t_user 存登录账号t_student 存学生信息。班级信息我倾向直接作为 t_student 的一个字段class_name而不是单独建班级表。如果你做的是带班级管理的毕设可以再加 t_class 表但课设阶段两张表足够单独建表反而让增删改查页面多一层联动时间成本翻倍。字段设计上要注意几个约定。学生学号用student_no而不是no避免和 SQL 关键字冲突性别gender用CHAR(2)存“男/女”不要用TINYINT(1)因为这个项目没有枚举转换层字符串直出到页面最省事。建表 SQL 如下字符集统一 utf8mb4避免中文乱码和表情符号报错CREATE DATABASE IF NOT EXISTS student_db DEFAULT CHARACTER SET utf8mb4; USE student_db; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT 密码不加密仅课设演示, real_name VARCHAR(50) COMMENT 真实姓名 ) ENGINEInnoDB; CREATE TABLE t_student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, name VARCHAR(50) NOT NULL COMMENT 姓名, gender CHAR(2) DEFAULT 男 COMMENT 性别, age INT COMMENT 年龄, class_name VARCHAR(50) COMMENT 班级, phone VARCHAR(20) COMMENT 手机号, email VARCHAR(50) COMMENT 邮箱, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 录入时间 ) ENGINEInnoDB;注意password字段我特别标注了“仅课设演示”。真实项目一定不能明文存密码至少加 MD5 加盐或 bcrypt但大部分学生信息管理系统的登录模块为了给答辩演示方便会直接明文比对。我的建议是课设阶段你可以明文论文里必须写“生产环境应使用加密算法”一句话就能堵住答辩老师追问。3. 从零跑通最小系统IDEA 新建 JSP 项目到登录页面3.1 IDEA 新建 JSP 项目注意包名是 javax 还是 jakarta打开 IntelliJ IDEANew Project 里选择 Jakarta EE 或 Java Enterprise 一栏再选 Web Application。这里有个非常容易翻车的点IDEA 新版默认创建的是 Jakarta EE 9 项目Servlet 包名是jakarta.servlet而网上大量老教程写的是import javax.servlet。项目创建时如果提示选择 Web Server通常会自动配置 Tomcat。如果没配就在 Run/Debug Configurations 里新增 Tomcat Server Local把 Application context 设置成未来访问路径。我习惯设成/student这样本地访问地址就是http://localhost:8080/student/。创建完成后目录结构如下这是所有后续文件放置的基础student-system/ ├── src/ │ ├── main/ │ │ ├── java/ # 放 entity、dao、servlet、filter │ │ └── webapp/ # 放 JSP 页面 │ │ ├── WEB-INF/ │ │ │ ├── web.xml │ │ │ └── lib/ # 手动放 mysql-connector、jstl 等 jar │ │ ├── login.jsp │ │ └── profile.jsp ├── pom.xml # 用 Maven 构建时才有普通 Web 项目无此文件如果你用 Maven 骨架创建依赖里要区分两个时代。Tomcat 9 及以下用javax.servlet-apiTomcat 10 及以上用jakarta.servlet-api。我建议课设环境统一用 Tomcat 9因为绝大多数教材、博客、实训指导书都基于 javax 包名你复制代码时不会产生大量编译错误。这属于“先跑通再升级”的思路不要为了用新版给自己挖坑。3.2 建库建表与 JDBC 工具类连接参数直接影响运行结果数据库建好后下一步就是让 Java 代码连上它。连接参数是第一个容易踩坑的地方。MySQL 8.x 驱动需要带时区参数否则报The server time zone value错误连接地址里还要显式指定 utf8 字符集。把连接信息抽到一个 db.properties不写死在代码里jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/student_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password123456各参数含义useUnicodetruecharacterEncodingutf8保证中文以 UTF-8 传输useSSLfalse关闭 SSL 警告serverTimezoneAsia/Shanghai解决驱动与数据库所在时区不一致allowPublicKeyRetrievaltrue避免 MySQL 8 在首次连接时因公钥获取策略报错。对应地写一个 JdbcUtils 工具类负责加载驱动、获取连接、释放资源。注意驱动类注册放在静态代码块里保证类加载一次就注册一次不会随每次 getConnection 重复触发package com.student.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; /** * JDBC 工具类只负责连接与释放连接的样板代码。 */ public class JdbcUtils { static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(MySQL驱动加载失败请检查 jar 是否放入 WEB-INF/lib); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection( jdbc:mysql://localhost:3306/student_db ?useUnicodetruecharacterEncodingutf8 useSSLfalseserverTimezoneAsia/Shanghai allowPublicKeyRetrievaltrue, root, 123456); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException ignored) {} } if (ps ! null) { try { ps.close(); } catch (SQLException ignored) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } } }这个类里的连接参数目前是硬编码没读 db.properties课设能跑。如果你想更规范可以在 getConnection 里用Properties.load()读取类路径下的配置文件。注意关闭资源的顺序是 ResultSet → PreparedStatement → Connection顺序反了可能导致连接未完全释放。这里用空 catch 忽略关闭异常是常见做法因为资源本身已经关闭失败了再抛异常只会干扰主流程。3.3 登录页到 LoginServletSession 会话与页面转发的选择最小系统从登录开始。先写一个最简 login.jsp表单提交到login这个 URL。这里 action 建议写login而不是绝对路径配合 Servlet 的 urlPatterns 在部署时更灵活。% page contentTypetext/html;charsetUTF-8 languagejava % html head title学生信息管理系统 - 登录/title /head body h2学生信息管理系统/h2 form actionlogin methodpost p用户名input typetext nameusername placeholder管理员账号/p p密 码input typepassword namepassword placeholder登录密码/p p stylecolor:red${error}/p button typesubmit登录/button /form /body /html对应 LoginServlet处理 POST 请求校验成功后把当前用户放进 Session再重定向到学生列表失败则转发回登录页并带上错误提示。重定向与转发的区别在这里体现得很直接登录成功后必须用sendRedirect否则浏览器刷新页面会重复提交表单校验失败可以用forward因为用户还在同一个页面流程里。package com.student.servlet; import com.student.dao.UserDao; import com.student.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 final UserDao userDao new UserDao(); Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String username request.getParameter(username); String password request.getParameter(password); User user userDao.findByUsernameAndPassword(username, password); if (user ! null) { // 登录成功写入 session重定向到列表页 HttpSession session request.getSession(); session.setAttribute(loginUser, user); response.sendRedirect(request.getContextPath() /student/list); } else { // 登录失败转发回登录页error 属性会被 EL 表达式读取 request.setAttribute(error, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); } } }这段代码里有几个细节需要解释。request.setCharacterEncoding(UTF-8)只对 POST 请求体有效GET 请求的中文参数依赖 Tomcat 的 URIEncoding后面过滤器章节会统一处理。request.getParameter接收的是字符串如果参数缺失返回 null不要直接Integer.parseInt否则空指针或 NumberFormatException 会打到页面上。getContextPath()返回项目部署路径这里返回/student用它的好处是以后改 Application context 不用逐个修改跳转地址。UserDao 的登录方法很简单用 PreparedStatement 防止 SQL 注入。这里特别强调一点不要用字符串拼接 SQL哪怕只是课设答辩老师会专门看这一处。package com.student.dao; import com.student.entity.User; import com.student.util.JdbcUtils; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class UserDao { public User findByUsernameAndPassword(String username, String password) { String sql SELECT id, username, real_name FROM t_user WHERE username ? AND password ?; try (Connection conn JdbcUtils.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { User user new User(); user.setId(rs.getInt(id)); user.setUsername(rs.getString(username)); user.setRealName(rs.getString(real_name)); return user; } } } catch (SQLException e) { e.printStackTrace(); } return null; } }try-with-resources语法从 Java 7 开始支持使用后可以省略手动 close 连接。但要注意用 try-with-resources 时 JdbcUtils 的 close 方法就不再需要了两套写法不要混用。上面代码先取 username 和 password 做条件查询结果只返回必要字段没有查密码字段这样就算 User 对象序列化后不慎暴露也不会把密码带出去。4. 学生列表与增删改查分页、模糊查询、个人信息展示页4.1 StudentDao用 PreparedStatement 把增删改查收敛到一处StudentDao 是整个系统的核心数据访问类。这里我按“列表 分页 搜索” “新增/更新/删除”两组来组织避免一个类里代码堆成山。先看最关键的分页加模糊查询方法因为多数学生管理系统翻车都翻在这里package com.student.dao; import com.student.entity.Student; import com.student.util.JdbcUtils; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; public class StudentDao { public ListStudent findStudents(String keyword, int offset, int pageSize) { String sql SELECT id, student_no, name, gender, age, class_name, phone, email FROM t_student WHERE student_no LIKE ? OR name LIKE ? ORDER BY id DESC LIMIT ?, ?; ListStudent students new ArrayList(); try (Connection conn JdbcUtils.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { String like % keyword %; ps.setString(1, like); ps.setString(2, like); ps.setInt(3, offset); // 起始行从第几条开始取 ps.setInt(4, pageSize); // 取多少条 try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Student stu new Student(); stu.setId(rs.getInt(id)); stu.setStudentNo(rs.getString(student_no)); stu.setName(rs.getString(name)); stu.setGender(rs.getString(gender)); stu.setAge(rs.getInt(age)); stu.setClassName(rs.getString(class_name)); stu.setPhone(rs.getString(phone)); stu.setEmail(rs.getString(email)); students.add(stu); } } } catch (SQLException e) { e.printStackTrace(); } return students; } }注意几个参数问题。offset是跳过的行数而不是页码。第 1 页第 1 条记录对应 offset0第 2 页对应 offsetpageSize所以 Servlet 层计算时用(pageNo - 1) * pageSize。LIMIT ?, ?的参数是 int不要传字符串进去MySQL JDBC 驱动对类型校验很严格。查询字段全部显式列出不要用SELECT *表面看省事实际上与结果集索引耦合一旦表加字段、不同表拼接查询时会让代码变成黑匣子。增删改方法逻辑相似举一个删除为例。删除通常只需要一个 id 参数public boolean deleteStudentById(int id) { String sql DELETE FROM t_student WHERE id ?; try (Connection conn JdbcUtils.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, id); return ps.executeUpdate() 0; } catch (SQLException e) { e.printStackTrace(); return false; } }executeUpdate()返回受影响行数返回 0 说明 id 不存在返回 1 说明删除成功。这个返回值会被 Servlet 用来决定是跳转到成功提示页还是留在当前页显示错误。4.2 列表页 JSP 渲染与个人信息展示页面列表页 JSP 是整个系统里最长的页面但原则仍然是页面上不写 Java 脚本片段只用 JSTL 和 EL。循环渲染和搜索条件回显都在这里完成。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html head title学生列表/title /head body h2学生信息列表/h2 form actionlist methodget input typetext namekeyword value${param.keyword} placeholder学号或姓名 button typesubmit查询/button /form table border1 cellpadding6 cellspacing0 tr th学号/thth姓名/thth性别/thth年龄/th th班级/thth手机号/thth邮箱/thth操作/th /tr c:forEach items${pageBean.list} varstu tr td${stu.studentNo}/td td${stu.name}/td td${stu.gender}/td td${stu.age}/td td${stu.className}/td td${stu.phone}/td td${stu.email}/td td a hrefedit?id${stu.id}编辑/a a hrefdelete?id${stu.id} onclickreturn confirm(确定删除该学生吗);删除/a /td /tr /c:forEach /table /body /html页面里value${param.keyword}是从请求参数取 keyword 值并回显到输入框用户翻页或刷新后搜索词不会丢。${pageBean.list}对应 Servlet 放入 request 域的 PageBean 对象EL 表达式会自动调用 getter。需要注意 JSTL 核心标签库的 uri 在旧版是http://java.sun.com/jsp/jstl/core如果项目是 Tomcat 10 Jakarta EE要改成jakarta.tags.core否则直接报 500 状态码。再要单独提一下热搜词里出现频率很高的“个人信息展示页面”。这块需求通常是登录后显示当前用户资料我一般放在一个 profile.jsp 里。它不需要访问数据库直接从 session 取登录时放进去的 user 对象% page contentTypetext/html;charsetUTF-8 languagejava % html headtitle个人信息/title/head body h2个人信息/h2 p用户名${sessionScope.loginUser.username}/p p姓名${sessionScope.loginUser.realName}/p pa hreflogout退出登录/a/p /body /htmlsessionScope.loginUser直接定位到 Session 里的属性不再去 request 域里找。这个页面同时提供了一个退出登录入口对应 Servlet 里session.invalidate()后重定向到登录页等于把会话生命周期完整走了一遍。4.3 分页参数pageNo、pageSize、总页数与 LIMIT 边界分页是最容易写出边界 bug 的地方。先写一个 PageBean它不关心具体业务只承载分页状态package com.student.entity; import java.util.List; public class PageBeanT { private int pageNo; // 当前页码从1开始 private int pageSize; // 每页条数 private int totalCount; // 总记录数 private int totalPages; // 总页数 private ListT list; // 当前页数据 public int getTotalPages() { return (int) Math.ceil(totalCount * 1.0 / pageSize); } // getter 和 setter 省略这里只列出关键方法 }Servlet 端接收 pageNo 和 keyword 两个参数。要注意的是用户可能在地址栏手动输入pageNo999或者pageNo-1这些非正常输入不能交给 SQL 直接执行int pageNo 1; int pageSize 5; String pageNoStr request.getParameter(pageNo); if (pageNoStr ! null !pageNoStr.isEmpty()) { try { pageNo Integer.parseInt(pageNoStr); if (pageNo 1) { pageNo 1; // 小于1按第一页处理 } } catch (NumberFormatException e) { pageNo 1; // 非数字按第一页处理 } } int offset (pageNo - 1) * pageSize;分页条在页面上需要计算前三页和后三页这个逻辑属于展示逻辑放 JSP 里会很长放 Servlet 里又不合适。我一般会在 PageBean 里额外提供hasPrev、hasNext这种布尔判断方法JSP 只做判断输出不参与计算。模糊查询的坑在于分页 SQL 带了LIMIT但统计总记录数的 SQL 也带了搜索条件两者条件必须完全一致。很多项目列表页和搜索页分页各写一套搜索后点第 2 页发现搜索条件丢失就是因为在 Servlet 里拼接 SQL 时总条数查询漏掉了 LIKE 条件。4.4 字符编码过滤器一条 Filter 解决整站中文乱码从 login.jsp 提交中文姓名、新增学生时输入中文班级名任何一个环节漏了编码设置数据就会变成问号或乱码。与其在每个 Servlet 里写一行request.setCharacterEncoding不如写一个过滤器配置成拦截所有请求一次解决。package com.student.filter; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.annotation.WebFilter; import java.io.IOException; WebFilter(/*) public class EncodingFilter implements Filter { Override public void init(FilterConfig filterConfig) throws ServletException { // 过滤器初始化可以读 web.xml 里的 init-param这里留为空实现 } Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding(UTF-8); response.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); chain.doFilter(request, response); } Override public void destroy() { // 清理资源 } }WebFilter(/*)表示匹配所有路径包括登录页静态资源和 Servlet。注意过滤器顺序如果项目里还有登录校验过滤器要确保编码过滤器在它之前执行否则校验过滤器里读到的请求参数可能已经是乱码。response.setContentType放在这里统一设置后各 JSP 页面里可以不再重复写但如果某页面想输出 JSON需要单独response.getWriter().write()时再覆盖 ContentType。5. JSPServlet 项目避坑五个常见翻车现场与排查思路5.1 部署类坑404、找不到 javax.servlet 类、初始化参数失效现象一Tomcat 启动正常但访问http://localhost:8080/student/student/list返回 404。查 Tomcat 控制台没有报错。原因Application context 配置不正确或者 IDEA 的 Deployment 里没有把项目的 artifact 添加进去。常见的情况是 Tomcat 已经运行但 Web 应用没有部署任何 war exploded 产物所以只有 Tomcat 默认页面项目路径下全是 404。解决打开 Edit Configurations选中 Tomcat Server 这一条切到 Deployment 标签页确认里面有名为student-system:war exploded的部署项Application context 设置为/student。改完保存后重启 Tomcat。如果用的是外部 Tomcat 而不是 IDEA 集成就把 war / 文件夹直接放进 webapps 下再启动。现象二项目编译时提示程序包 javax.servlet 不存在或者启动时抛NoClassDefFoundError: javax/servlet/ServletException。原因Tomcat 版本和依赖版本不匹配。从 Tomcat 10 开始Servlet API 从javax.servlet改名为jakarta.servlet老代码用旧的包名会直接编译失败反过来新代码用了jakarta包Tomcat 9 也带不了。解决课设项目统一回退到 Tomcat 9 编译期引入javax.servlet-api依赖如果坚持用 Tomcat 10把所有import javax.servlet批量替换为import jakarta.servlet且 JSTL 标签库也要换新版。取决于你手边的教程和实训资料是哪个时代按它们的版本来匹配环境不要混用。现象三在 Servletinit()里读配置第一次访问时配置生效重启 Tomcat 后配置丢失或报空指针。原因init()只有在 Servlet 第一次被请求或者 load-on-startup 触发时才执行。如果代码里把业务初始化的逻辑放在init()而这期间容器还没有完成全局资源加载就会因为依赖的 Context 参数为空而失败。解决把配置读取改为在ServletContextListener.contextInitialized()中完成或者直接用 loadOnStartup 预加载。学生管理系统里最典型的就是数据库连接池初始化用 ServletContextListener 更符合“应用级生命周期”这个语义。5.2 请求与页面类坑中文乱码、重复删除、JSTL 不显示现象四POST 表单提交中文到数据库变成问号但数据库表的字符集已经是 utf8mb4。原因三层编码链路只有两层做对了。常见情况是数据库连接 URL 少了characterEncodingutf8或者 JSP 页面本身的pageEncoding没设置导致浏览器提交的字节流与服务器解析的字符集不一致。解决三层一起控制。第一层JSP 页面头部写全contentTypetext/html;charsetUTF-8第二层过滤器统一设置request.setCharacterEncoding(UTF-8)第三层JDBC URL 带useUnicodetruecharacterEncodingutf8。如果做了这三层还有问题查看 MySQL 连接侧的实际字符集SHOW VARIABLES LIKE character_set%;把 database 和 table 字段确认一遍。现象五列表页点“删除”后页面可能不提示任何错误但刷新浏览器时同一个delete?id27被重复执行数据连续被删两次。原因删除功能用的是 GET 链接a hrefdelete?id27浏览器会把请求留在历史记录里刷新页面时自动重放一遍。GET 请求不该修改服务器资源这是 HTTP 语义要求也是面试和答辩常问的题目。解决删除改为表单 POST 提交或者用 JavaScript 动态创建表单再 submit。学生管理系统里我一般保留一个隐藏表单点击“删除”时先弹确认框确认后把 id 写入隐藏域再提交 POST。同时Servlet 侧删除成功后用response.sendRedirect(list)把最后一步变成 GET 请求刷新时只重载列表不会再触发删除。现象六页面写了c:forEach但浏览器上原样输出标签或者直接报 500 状态码提示找不到 JSTL 标签库描述文件。原因项目缺少 JSTL 依赖。Tomcat 自带的 servlet-api 和 jsp-api 只是基础JSTL 是另一个独立的库文件需要把jstl-1.2.jar和standard.jar老版本放进 WEB-INF/lib。解决Maven 项目在 pom.xml 里加依赖dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency普通 Web 项目手动下载 jstl-1.2.jar 放入 WEB-INF/lib 后重新部署。如果你用的是 Tomcat 10需要的是jakarta.servlet.jsp.jstl-api和jakarta.servlet.jsp.jstl两组依赖不要和 javax 版本混装。6. 打包 war 部署到 Tomcat最后一公里与验证清单6.1 用 war 包完成传统部署IDEA 打包与 Tomcat 目录经过上面几章IDE 里点运行能跑通了但答辩或交给别人运行时不可能每个人都配一套 IDEA 环境。传统 JSP 项目打包 war 就是把整个 Web 应用打成标准 Web 归档文件直接丢到 Tomcat 的 webapps 目录下启动。这是个通用的部署方式。IDEA 里打包的路径是 Build → Build Artifacts如果之前创建项目时配置过 Web Artifact这里会出现 war 选项。选择 Build 后war 文件生成在项目的out/artifacts/下。没有配置过的话Project Structure → Artifacts 里新增一个 Web Application Archive来源选择已有的 war exploded再 Build。war 部署后要记住一点Tomcat 会把 war 自动解压成同名目录。比如复制student-system.war到 webapps启动后会出现webapps/student-system/访问路径也变成http://localhost:8080/student-system/而不是你之前在 IDEA 里配的/student。如果希望用根路径访问可以把 war 改名为ROOT.war这会覆盖 Tomcat 默认首页。这个命名陷阱我踩过提醒一下。6.2 重启后的五项验证与 nginx 是否支持 JSP 的边界部署完成后不要急着交工。我会按下面顺序验证一遍这套清单也是答辩演示时的操作顺序登录页能打开输入错误密码会提示错误输入正确密码能跳转列表页新增一个学生页面跳转后列表第一行能看到它编辑该学生改完年龄和班级刷新后数据不变删除该学生确认弹窗关掉后列表少了一条地址栏带keyword搜索中文姓名能搜出结果翻页后搜索词还在。其中第 5 项最容易出问题。如果搜索词是中文先确认浏览器地址栏显示的 keyword 不是乱码是乱码的话单独检查请求 URL 的编码层。Tomcat 9 的URIEncodingUTF-8配置在 server.xml 的 Connector 节点默认值是 UTF-8老版本需要手动加。有读者问过“nginx 支持 jsp 吗”。准确答案是 nginx 不认识 JSP它是静态服务器和反向代理遇到.jsp后缀不知道交给谁解析。生产环境常见做法是 nginx 只托管静态资源把包含.jsp的动态请求以proxy_pass转发给后端 TomcatTomcat 解析完生成 HTML 再返回给 nginx。课设阶段不需要 nginx但你要清楚这套边界war 包部署是给 Tomcat 的nginx 只是前面的门户JSP 的解析必须落在 Tomcat 上。最后讲一个我做这类项目养成的习惯每次对 JSP 或 Web 配置改动后不只刷新浏览器还要看 Tomcat 日志里的Redeploy或Reload记录确认服务端确实重新加载了新 class而不是浏览器缓存骗了眼睛。这个问题在项目接近交付时最容易让人抓狂。用 war 包部署能有效避开这种玄学问题因为包一次就是一次完整构建丢进 webapps 的就是最终交付物。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。