news 2026/9/29 2:05:20

Servlet + JSP 实现学生信息管理系统:原理到部署全指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Servlet + JSP 实现学生信息管理系统:原理到部署全指南

简介:JSP与Servlet实现的学生信息管理系统,是一份适合Java Web初学者的完整项目资源,覆盖动态网页、服务器端编程与数据库交互等核心环节。项目基于MVC模式设计,通过JSP构建界面,Servlet处理请求,并借助JDBC操作数据库,可帮助理解HTTP请求响应、会话管理及容器部署等知识。压缩包共576个文件,体积2.75MB,包含JSP页面、Java与class文件、SQL初始化脚本、HTML/CSS/JavaScript前端资源以及PNG/GIF图片等,目录结构清晰,便于本地导入与改造学习。其中附带源码与编译产物,适合对照学习Servlet典型写法。已有996人学习浏览,对入门级读者具有一定参考价值。通过导入数据库脚本并在Tomcat中部署,可直观体验从登录到学生信息增删改查的完整流程,是巩固Java Web基础、积累项目经验的不错选择。

1. 为什么这个 2000 年的技术栈,到现在还是课设和毕设的常青树

如果你在 2024 年打开招聘网站,Java 后端岗位的要求里几乎看不到 JSP 和 Servlet 的身影,但打开任何一届计算机专业的课设题目列表,“基于 JSP、Servlet 的学生信息管理系统”依然是出现频率最高的一档。哪怕 Spring Boot 已经把约定大于配置玩到了极致,哪怕前端早就不再用 JSP 里的<%= %>去渲染数据,每年依然有成千上万的学生和刚转行的开发者,在用一个老技术栈完成自己的第一个完整 Web 项目。

jsp、servlet 实现学生信息管理系统这个标题之所以长盛不衰,是因为它恰好处在一个“能学到本质”和“能跑出结果”的交叉点上:Servlet 让你看到 HTTP 请求如何被接收和分发,JSP 让你看到服务端渲染的数据怎么变成 HTML,JDBC 让你看到数据到底存进了哪张表。不需要框架帮你把一切都藏起来,你能在三天之内从一个空目录写到一个能登录、能增删改查、能部署到 Tomcat 的完整系统。这篇笔记就把这套东西从原理到落地完整讲清楚:技术选型怎么定、项目骨架怎么搭、核心代码怎么写、打包部署怎么弄、坑在哪里、答辩时怎么往上加亮点。

2. 动手前先想清楚:技术选型、运行环境与项目骨架

2.1 Servlet 生命周期与 JSP 的分工:谁在前端谁在后端

很多人在写第一个 JavaWeb 项目时会困惑一个问题:既然 Servlet 能在 Java 代码里直接输出 HTML,为什么还要用 JSP?反过来,既然 JSP 里能写 Java 代码,为什么还需要 Servlet?

这两者从来不是二选一的关系,而是各管一段。Servlet 负责的是“处理请求”这一侧:接收参数、调用数据库、处理业务逻辑、决定跳转到哪一页。JSP 负责的是“展示结果”这一侧:把 Java 对象里的数据填充到 HTML 模板里,交给浏览器渲染。JSP 本质上会被 Tomcat 编译成一个 Servlet 子类,它和 Servlet 共享同一套生命周期:第一次被访问时加载和初始化(init),每次请求到来时执行服务方法(service,实际分发到doGet或doPost),应用关闭或重新加载时销毁(destroy)。

理解这个分工的意义在于,你会自然地拒绝两种极端写法:一种是所有页面都用response.getWriter().println("<html>...")拼字符串,另一种是整个项目干脆不用 Servlet,所有逻辑全写在 JSP 的脚本片段里。前者让 HTML 维护变成噩梦,后者让 Java 编译错误在运行时才暴露。正确做法是前端表单提交给 Servlet,Servlet 处理完业务后把结果放进request或session,再通过RequestDispatcher.forward转发到 JSP 展示。这套模式在你以后学 Spring MVC 时会发现几乎一模一样:DispatcherServlet 承担了所有 Servlet 的活,JSP 变成了 ViewResolver 管理的视图。

2.2 环境准备与 JDK/Tomcat 版本选择的两个注意点

写 JSP/Servlet 项目对环境版本极其敏感,这不是玄学,而是 Tomcat 版本更迭导致的命名空间迁移。我一般会用这个组合:JDK 8 或 JDK 11,Tomcat 9,MySQL 5.7 或 8.0,IDEA Ultimate 或 Eclipse IDE for Enterprise Java。这个组合是老教材、老博客、老示例代码覆盖最全面的版本,遇到问题百度一下基本都能搜到答案。

这里有两个特别容易踩的版本问题。第一,如果你用 Tomcat 10 或更新版本,项目里引入的 Servlet API 的包名从javax.servlet改成了jakarta.servlet,绝大多数老教材和网上的旧代码会直接编译失败。除非你有明确的理由用 Tomcat 10,否则老老实实用 Tomcat 9,不要在环境上给自己加难度。第二,MySQL 8.0 的 JDBC 驱动在连接字符串里要显式加上useSSL=false&serverTimezone=Asia/Shanghai,否则启动时可能报时区错误或 SSL 连接警告,这不是代码逻辑问题,但卡住无数新手半小时起步。

2.3 标准项目目录结构:从 IDEA 新建 JavaWeb 项目开始

用 IDEA 新建 JSP/Servlet 项目的操作路径,不同版本略有差异,常见做法是:New Project 时选择 Jakarta EE 或 Java Enterprise 模板,Web Application 勾选 Struts 之类的框架都不选,保留一个干净的 Web 项目骨架。如果你用的是社区版 IDEA,没有 Jakarta EE 模板,可以直接 New Project 选普通 Java 项目,然后手动在src/main下建java和webapp目录,再右键 webapp 目录选择 “Add Framework Support” 添加 Web 支持。项目建好之后,目录结构应该是这样:

student-manager/ ├── pom.xml # 如果用 Maven 管理依赖(推荐) ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/example/student/ │ │ │ ├── entity/ # 实体类:Student、Admin 等 │ │ │ ├── dao/ # 数据访问层:JDBC 操作 │ │ │ ├── servlet/ # 控制器层:处理请求 │ │ │ └── util/ # 工具类:DBUtil、编码过滤器等 │ │ └── webapp/ │ │ ├── WEB-INF/ │ │ │ ├── web.xml # Servlet 映射、欢迎页配置 │ │ │ └── lib/ # 手动放 jar 包时使用 │ │ ├── css/ │ │ ├── js/ │ │ ├── login.jsp │ │ ├── list.jsp │ │ └── ... │ └── test/ # 测试目录,课设可以忽略 └── sql/ └── init.sql # 建库建表脚本

我强烈建议用 Maven 而不是手动下载 jar 包往WEB-INF/lib里拖。手动导包有两个痛点:一是很容易出现多个版本的 jar 包混在 lib 目录里导致类冲突,二是换一台电脑打开项目时依赖丢失,每次都要重新配置。用 Maven 只需要在pom.xml里声明依赖坐标,IDEA 会自动下载并管理。唯一要注意的是 Maven 项目里 JSP 和 Servlet 依赖的scope要写成provided,因为这个依赖在 Tomcat 里已经存在,打包时不需要带进去,否则 war 包体积变大而且可能和 Tomcat 自带的类冲突。

3. 从登录到学生信息管理:一整套可复现的核心实现

3.1 数据库设计:三张表和一张关联表就够

学生信息管理系统的数据模型不需要设计得很复杂,三张核心表加一张关联表足够支撑课设答辩。学生表存学生信息,管理员表存登录账号,班级表存专业和班级信息,选课表存学生和课程的关联关系。如果你做的选题里有成绩管理,再加一个成绩表,把学生 ID、课程 ID、分数三个字段关联起来即可。

CREATE DATABASE student_db DEFAULT CHARACTER SET utf8mb4; USE student_db; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT '学号', name VARCHAR(50) NOT NULL COMMENT '姓名', gender CHAR(1) DEFAULT '男', age INT, class_name VARCHAR(50) COMMENT '班级', phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); CREATE TABLE admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50) ); CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(100) NOT NULL, credit DECIMAL(3,1) ); CREATE TABLE student_course ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,1), FOREIGN KEY (student_id) REFERENCES student(id), FOREIGN KEY (course_id) REFERENCES course(id) );

注意字符集统一用utf8mb4,而不是utf8。utf8在 MySQL 里实际上是utf8mb3,只能存基本的多语言字符,遇到 emoji 或者生僻字会报Incorrect string value错误。虽然学生管理系统里大概率不会存 emoji,但养成用utf8mb4的习惯可以避免很多不可预知的乱码问题。

时间字段用DATETIME而不是TIMESTAMP。TIMESTAMP 的范围只到 2038 年,而且受数据库时区设置影响,DATETIME 没有这些限制。DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP这两句能让你在插入和更新时免去手动维护时间的麻烦。

3.2 JDBC 工具类与数据库连接:先能连上再说别的

数据库连接是整套系统最基础的能力,每个 DAO 方法里都重复写Class.forName(...)、DriverManager.getConnection(...)会让代码很臃肿。一般做法是抽一个工具类,把连接获取和资源关闭统一收口。

package com.example.student.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/student_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static final String USERNAME = "root"; private static final String PASSWORD = "你的数据库密码"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError("MySQL JDBC 驱动加载失败,检查依赖是否引入"); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USERNAME, PASSWORD); } public static void close(ResultSet rs, PreparedStatement ps, Connection conn) { if (rs != null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (ps != null) { try { ps.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }

几个参数说明一下。连接串里characterEncoding=utf8保证数据库读写都按 UTF-8 传输,serverTimezone=Asia/Shanghai规避 MySQL 8 的时区告警,useSSL=false是因为本地开发没有配置 SSL 证书,不关掉会报一堆握手警告甚至可能失败。Class.forName这一行在 JDBC 4.0 之后其实可以省略,因为驱动 jar 包里的META-INF/services会自动注册驱动,但显式写上有一个好处:类加载失败时你能在异常堆栈里立刻定位到是驱动缺失,而不是连接超时的表象。写静态代码块而不是每次获取连接时都Class.forName,是为了保证类只加载一次,减少不必要的开销。

连接关闭的close方法要同时处理ResultSet、PreparedStatement和Connection三个资源,顺序不能乱:先关结果集,再关语句,最后关连接。如果只关了连接,某些数据库驱动会在池化场景下导致游标未释放,内存慢慢堆满。这里用的是裸DriverManager连接,性能上限不高,但课设和实训项目完全够用,等做到第 6 章的进阶优化时再换连接池。

3.3 登录校验与管理员实体:登录态是怎么保住的

登录逻辑是系统的门面,也是最容易被挑出毛病的地方。一个合格的学生管理系统至少要做到两点:密码不能明文存储,登录状态要能被统一校验。

密码存储的正确做法是加盐哈希,不要用 MD5,不要用 SHA1,更不要明文存数据库。JDK 自带的MessageDigest可以快速实现 SHA-256 加盐哈希:

package com.example.student.util; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class PasswordUtil { public static String hash(String rawPassword, String salt) { String salted = salt + rawPassword; try { MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[] digest = md.digest(salted.getBytes(StandardCharsets.UTF_8)); StringBuilder sb = new StringBuilder(); for (byte b : digest) { sb.append(String.format("%02x", b)); } return sb.toString(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("SHA-256 算法不可用", e); } } }

这里salt可以是用户的用户名,也可以是一个随机生成的固定盐值。把盐拼接在密码前面再哈希,能有效抵御彩虹表攻击。String.format("%02x", b)是把每个字节转成两位十六进制字符串,这样才能得到一个可读的 64 位哈希串。

登录 Servlet 拿到表单提交的用户名和密码后,调用 DAO 层按用户名查用户,取到数据库中存的那个盐值,把用户输入的密码哈希一遍,比对结果是否一致。

package com.example.student.servlet; import com.example.student.dao.AdminDao; import com.example.student.entity.Admin; import com.example.student.util.PasswordUtil; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; @WebServlet("/login") public class LoginServlet extends HttpServlet { private final AdminDao adminDao = new AdminDao(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username = req.getParameter("username"); String password = req.getParameter("password"); Admin admin = adminDao.findByUsername(username); if (admin != null && PasswordUtil.hash(password, admin.getSalt()).equals(admin.getPassword())) { HttpSession session = req.getSession(); session.setAttribute("loginUser", admin); session.setMaxInactiveInterval(30 * 60); resp.sendRedirect(req.getContextPath() + "/student/list"); } else { req.setAttribute("errorMsg", "用户名或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); } } }

关键点在session.setMaxInactiveInterval(30 * 60),这行把会话超时设置为 30 分钟。不设置的话,Tomcat 默认是 30 分钟,但显式写出来能让答辩评委看到你考虑了会话安全。resp.sendRedirect和forward的区别也值得说一句:登录成功后用重定向,避免浏览器刷新时重复提交表单;登录失败用转发,因为要带着错误提示回到登录页。每次新增请求时,用request.getParameter从表单取参数,参数名要和 JSP 里<input>标签的name属性完全一致,这是新手最常见的低级错误之一。

登录之后还需要一个过滤器来拦截未登录的请求,这部分在第 6 章的进阶改造里展开讲,这里先知道登录态存在session里就够了。

3.4 学生列表的 Servlet + JSP 组合:请求怎么转发的

学生列表是整个系统最核心的页面,它包含了查询、遍历、展示和操作入口。Servlet 从数据库查出数据,放到request域中,转发给 JSP 渲染。

package com.example.student.servlet; import com.example.student.dao.StudentDao; import com.example.student.entity.Student; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; @WebServlet("/student/list") public class StudentListServlet extends HttpServlet { private final StudentDao studentDao = new StudentDao(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String keyword = req.getParameter("keyword"); List<Student> list; if (keyword != null && !keyword.trim().isEmpty()) { list = studentDao.searchByName(keyword.trim()); } else { list = studentDao.findAll(); } req.setAttribute("studentList", list); req.setAttribute("keyword", keyword); req.getRequestDispatcher("/list.jsp").forward(req, resp); } }

这里有两个层面要理解。第一,@WebServlet("/student/list")是 Servlet 3.0 之后引入的注解方式,省掉了在web.xml里写<servlet>和<servlet-mapping>两段配置的繁琐,项目还原到 JDK 8 + Tomcat 9 就能直接用。如果你在web.xml里也配了/student/list,会导致启动时报映射冲突,二者只能选其一,这是第 4 章要讲的坑。

第二,查询参数keyword从请求 URL 的问号后面解析,比如/student/list?keyword=张,req.getParameter("keyword")就能拿到张。if分支判断关键词是否为空,空则查全表,非空则走模糊查询,这样既保证了列表页首次加载能显示全部数据,也支持用户按姓名检索。

对应的 DAO 方法如下:

public List<Student> searchByName(String keyword) { String sql = "SELECT * FROM student WHERE name LIKE ?"; List<Student> list = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + keyword + "%"); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Student s = new Student(); s.setId(rs.getInt("id")); s.setStudentNo(rs.getString("student_no")); s.setName(rs.getString("name")); s.setGender(rs.getString("gender")); s.setAge(rs.getInt("age")); s.setClassName(rs.getString("class_name")); s.setPhone(rs.getString("phone")); list.add(s); } } } catch (SQLException e) { e.printStackTrace(); } return list; }

PreparedStatement用?占位符预编译 SQL,再通过setString绑定参数,天然防 SQL 注入。千万不要用字符串拼接的方式写 SQL,比如"SELECT * FROM student WHERE name LIKE '%" + keyword + "%'",一旦用户在搜索框输入%' OR 1=1 --,你的全表数据就暴露了。try-with-resources写法能让Connection、PreparedStatement、ResultSet自动关闭,代码比手动finally里写close干净得多。

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> <form action="${pageContext.request.contextPath}/student/list" method="get"> <input type="text" name="keyword" value="${keyword}" placeholder="按姓名搜索"> <button type="submit">搜索</button> </form> <table border="1" cellpadding="8"> <tr> <th>学号</th><th>姓名</th><th>性别</th><th>年龄</th> <th>班级</th><th>手机号</th><th>操作</th> </tr> <c:forEach items="${studentList}" var="s"> <tr> <td>${s.studentNo}</td> <td>${s.name}</td> <td>${s.gender}</td> <td>${s.age}</td> <td>${s.className}</td> <td>${s.phone}</td> <td> <a href="${pageContext.request.contextPath}/student/edit?id=${s.id}">编辑</a> <a href="${pageContext.request.contextPath}/student/delete?id=${s.id}" onclick="return confirm('确定删除该学生吗?')">删除</a> </td> </tr> </c:forEach> </table> </body> </html>

${s.studentNo}是 EL 表达式,等价于调用s.getStudentNo()。<c:forEach>是 JSTL 核心标签库的循环,需要引入jstl依赖。这里必须说一个容易困惑的点:JSTL 标签库的 URI 在不同版本有差异,老项目常用http://java.sun.com/jsp/jstl/core,新版推荐jakarta.tags.core,但在 Tomcat 9 上按前者引入没问题。

${pageContext.request.contextPath}会动态输出项目的上下文路径,比如/student-manager。写死/student/list在本地跑没问题,一旦部署时改了应用名,所有路径全 404。这个细节是很多项目从 IDEA 内嵌 Tomcat 搬到外部 Tomcat 部署时挂掉的根源。

4. 五个让实训项目翻车的经典坑:现象、原因与解决

4.1 Servlet 启动报 ClassNotFoundException: javax.servlet

现象:Tomcat 启动时控制台直接抛java.lang.ClassNotFoundException: javax.servlet.ServletException,甚至项目都部署不上去。

原因:你用的是 Tomcat 10 或更高版本,Servlet API 已经从javax.servlet迁移到jakarta.servlet,而项目代码里用的还是javax.servlet包。也可能是 Maven 依赖引入了 Tomcat 自带的 servlet-api,打包时产生了冲突。

解决:把 Tomcat 降到 9.x,这是最省事的路。如果你必须用 Tomcat 10,那代码里所有import javax.servlet.*全局替换成import jakarta.servlet.*,同时 JSTL 依赖换成org.glassfish.web:jakarta.servlet.jsp.jstl坐标。我的血泪经验是:课设项目不要追求 Tomcat 版本新,能跑通才是硬道理。

4.2 表单提交中文乱码:页面显示一堆问号

现象:登录页输入“张三”,提交后 Servlet 里拿到的req.getParameter("name")变成一串??或者乱码字符。

原因:POST 请求的编码没有统一。JSP 页面声明的pageEncoding="UTF-8"只影响页面本身的字节解码,浏览器提交表单时默认按页面编码发送,但 Servlet 容器解码请求参数时用的是ISO-8859-1,两端编码不一致导致乱码。GET 请求的乱码原因不同,Tomcat 9 默认 URI 编码是 UTF-8 所以 GET 通常没事,但 POST 必翻车。

解决:写一个过滤器统一设置请求和响应的编码,在第 6 章会给出完整代码。如果不想写过滤器,在每个 Servlet 的doPost开头手动加req.setCharacterEncoding("UTF-8"),这是最低成本的补救。注意这一行必须在第一次读取参数之前调用,否则已经解码的参数不会重新编码。

4.3 修改数据后列表不刷新,永远显示旧值

现象:点编辑改完学生姓名,跳回列表页发现还是旧名字,但浏览器刷新一下又好了。

原因:列表页用了浏览器缓存,或者 Servlet 转发时 JSP 被浏览器缓存了。更常见的情况是你在 Servlet 里用resp.sendRedirect(req.getContextPath() + "/student/list"),但新旧地址一样,浏览器可能直接返回缓存的页面。

解决:列表页的<head>里加三个 meta 标签禁用缓存:Cache-Control: no-cache、Pragma: no-cache、Expires: 0。或者给重定向 URL 加一个时间戳参数,/student/list?t=<%=System.currentTimeMillis()%>,强制浏览器走新的请求。这个坑在 IE 和一些旧的内部系统浏览器上尤其明显,Chrome 反而不太容易触发。

4.4 路径全是 404:Servlet 注解和 web.xml 里的映射打架

现象:项目能启动,但访问/student/list报 404,访问/list又报 404,IE 浏览器的报错页面还提示“无法找到该页”。

原因:你在 Servlet 类上用@WebServlet("/student/list"),但项目保留了web.xml且里面也配置了<servlet-name>和<servlet-mapping>,或者是配置了<welcome-file>指向一个不存在的首页文件。Servlet 3.0 规范里,@WebServlet和web.xml的映射重复时,以web.xml为准,如果你的web.xml里映射的路径和注解路径不同,注解那个就被忽略了。

解决:二选一。用注解就在web.xml里只保留欢迎页配置,把所有<servlet>相关配置删干净。用web.xml就把类上的@WebServlet注解去掉。判断项目到底有没有旧配置,看src/main/webapp/WEB-INF/web.xml是否存在且不是空壳文件。还有一个隐蔽问题是:Maven 项目的web.xml版本头声明的 schema 必须是 3.0 及以上才支持注解扫描,如果头的版本是 2.3,Tomcat 会按老规范忽略注解。

4.5 部署时 MySQL 驱动 404:war 包里少了 jar 包

现象:本地 IDEA 里跑得好好的,打成 war 包扔到远程 Tomcat 的webapps目录,一访问所有涉及数据库的接口直接报ClassNotFoundException: com.mysql.cj.jdbc.Driver。

原因:Maven 依赖的scope写错了。如果 MySQL 驱动的scope写成provided,打包时不会包含进WEB-INF/lib,本地运行因为 IDEA 编译时把它放进了运行 classpath 所以没事,但 war 包里没有它。

解决:MySQL 驱动的依赖scope写默认的compile,不能用provided。provided只适用于 servlet-api、jsp-api 这类 Tomcat 自带的库。打好的 war 包可以用压缩软件打开,检查WEB-INF/lib下有没有mysql-connector-java-版本号.jar,没有就是依赖没带进去。

5. 打包部署与并发边界:从 IDEA 到生产环境 Tomcat

5.1 传统 JSP 项目打包 war:三分钟跑完的部署流程

整个系统在本地跑通之后,要面对的现实问题是:怎么让老师或验收方在没有 IDEA 的一台机器上运行你的项目。答案就是打 war 包,扔到 Tomcat 的webapps目录。

在 Maven 项目里,IDEA 右侧 Maven 面板双击package,或者直接执行命令:

mvn clean package

clean先删掉旧的target目录,package执行编译、测试、打包。测试如果不想跑,加-DskipTests跳过。打完包后,target目录下会生成一个.war文件,文件名默认是你在pom.xml里定义的项目名。

部署方法有两种。第一种是把 war 文件复制到 Tomcat 的webapps目录下,启动 Tomcat 时它会自动解压成同名目录,这种叫自动部署。第二种是把server.xml里<Host>的autoDeploy属性保持默认true,然后把 war 放进去,Tomcat 热部署会即时识别。我建议停掉 Tomcat 再放 war,启动它,完事。

cp student-manager.war /opt/tomcat9/webapps/ cd /opt/tomcat9/bin ./startup.sh

启动后访问http://服务器IP:8080/student-manager/,注意访问路径必须带 war 包的名字。如果你打出来的 war 叫student-manager.war,那么所有人都要带/student-manager前缀访问,这就是pageContext.request.contextPath在 JSP 里帮你动态拼路径的原因。

一个常见的部署翻车点是:反复部署同名 war,Tomcat 会保留老的解压目录,新 war 解压可能覆盖不干净。稳妥做法是先删掉webapps下同名目录和同名 war,再放新的进去。另外 Tomcat 的work目录会缓存编译过的 JSP,如果 JSP 页面改了但访问还是旧页面,删掉work/Catalina/localhost/应用名再重启。

5.2 Nginx 到底支不支持 JSP:反向代理的一种常见搭法

搜索框里天天有人问 “nginx 支持 jsp 吗”,直接给结论:Nginx 本身完全不解析 JSP,它拿到.jsp结尾的请求只会当静态文件处理,返回 404 或者直接下载文件。JSP 必须由 Tomcat 这类 Servlet 容器编译成 class 并执行。

那 Nginx 在这个系统里扮演什么角色?两个:一是把静态资源(CSS、JS、图片)直接拦下来返回,减轻 Tomcat 的压力;二是作为反向代理,把动态请求转发给后面的 Tomcat。一个常见的 Nginx 配置片段长这样:

server { listen 80; server_name student.example.com; location /student-manager/ { proxy_pass http://127.0.0.1:8080/student-manager/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location ~* \.(css|js|png|jpg|jpeg|gif|ico)$ { root /opt/tomcat9/webapps/student-manager/; expires 7d; } }

第一个location把所有请求都代理到本机 8080 的 Tomcat,proxy_pass末尾带路径时要注意不能写错,否则会 404。第二个location用正则匹配静态资源,Nginx 直接从磁盘返回文件,Tomcat 不再参与。这套配置能让并发能力提升一大截,但请注意:静态资源路径里的student-manager和 Tomcat 实际部署路径必须保持一致,否则 Nginx 去磁盘找文件时找不到。

如果你是单人课设,不需要 Nginx 也没问题,Tomcat 单独扛得住几十个人的演示流量。Nginx 是简历加分项,不是系统刚需。

5.3 上线前的压力预期:这套东西的并发边界在哪

JSP + Servlet + 裸 JDBC 这套组合能扛多少并发?说实话,纯理论数字意义不大,但大致的边界你要心里有数。单台 Tomcat 9 默认配置下,HTTP 线程池默认是 200 个线程,每个线程处理一个请求。如果每个请求的数据库操作耗时 50ms,理论吞吐量约 4000 QPS。但实际远达不到这个数,因为DriverManager.getConnection每次都要创建物理连接,握手和认证开销很大,JSP 首次访问还要编译,还有System.out.println如果不小心留下很多,都占 I/O。

所以如果你的系统要面对的是 200 人同时在线的演示场景,Tomcat 默认配置完全够用;如果目标是扛住 5000 并发,那这不是 JSP 该承担的任务,Spring Boot + 连接池 + Redis 缓存才是方向。这个边界在答辩时要能讲清楚,老师会欣赏这种诚实的认知。

6. 把课设做成能答辩的进阶技巧:三个低成本改造

6.1 用 Filter 统一处理编码和登录校验,去掉重复代码

现在的系统里每个 Servlet 的doPost都有req.setCharacterEncoding("UTF-8"),每个需要登录的页面都要自己判断session里有没有用户。用 Filter 可以把这两件事一次收口。

package com.example.student.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; @WebFilter("/*") public class AuthFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; req.setCharacterEncoding("UTF-8"); String uri = req.getRequestURI(); if (uri.endsWith("/login") || uri.endsWith("login.jsp") || uri.contains("/static/")) { chain.doFilter(request, response); return; } HttpSession session = req.getSession(false); if (session == null || session.getAttribute("loginUser") == null) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } chain.doFilter(request, response); } }

@WebFilter("/*")拦截所有请求,chain.doFilter放行。登录页和静态资源路径在白名单里直接放行,其他请求一律检查 session。req.getSession(false)比req.getSession()少创建一个副作用:如果当前没有 session,前者返回 null 而不是新造一个,能避免未登录用户也拿到一个脆弱的无效会话。这个 Filter 加上之后,删掉每个 Servlet 里的编码设置和登录检查代码,系统瞬间清爽。这也是很多人在简历上写的“基于 Filter 实现统一鉴权和编码处理”。

6.2 连接池替换:把 DriverManager 换成 HikariCP

裸 JDBC 每次getConnection都做三次握手,高并发下性能很差。替换成连接池是性价比最高的优化,HikariCP 是目前 Java 生态里性能最好的连接池,配置只需要改 DBUtil 一个类。

package com.example.student.util; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; public class DBUtil { private static final HikariDataSource dataSource; static { HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/student_db?useSSL=false&serverTimezone=Asia/Shanghai"); config.setUsername("root"); config.setPassword("你的数据库密码"); config.setMaximumPoolSize(20); config.setMinimumIdle(5); config.setConnectionTimeout(30000); config.setPoolName("StudentManagerPool"); dataSource = new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }

参数的核心是maximumPoolSize和minimumIdle。对于课设系统,20 个最大连接绰绰有余,MySQL 默认最大连接数是 151,maximumPoolSize如果设到 100 会超出数据库允许的连接数,启动时直接报Too many connections。connectionTimeout设置获取连接的等待时间,30 秒没拿到就直接抛异常,模块响应快速失败而不是无限挂起。

换连接池后代码调用方完全不用改,DBUtil.getConnection()返回的还是Connection,但内部拿到的已经是池化连接。关闭连接也别忘了,连接池复用的是物理连接,你调close()只是把连接归还给池子,不是真的断开。

6.3 给列表加一个导出学生信息的 Excel 下载

答辩时老师大概率会问:“你们这系统有没有导出功能?”当场掏出一个 Excel 下载导出,比解释十行框架代码都管用。

最简单的实现是用 Apache POI 的SXSSFWorkbook流式写 Excel:

package com.example.student.servlet; import com.example.student.dao.StudentDao; import com.example.student.entity.Student; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.xssf.streaming.SXSSFWorkbook; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.OutputStream; import java.util.List; @WebServlet("/student/export") public class StudentExportServlet extends HttpServlet { private final StudentDao studentDao = new StudentDao(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { List<Student> list = studentDao.findAll(); resp.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); resp.setHeader("Content-Disposition", "attachment; filename=students.xlsx"); try (SXSSFWorkbook workbook = new SXSSFWorkbook(); OutputStream out = resp.getOutputStream()) { Sheet sheet = workbook.createSheet("学生信息"); Row header = sheet.createRow(0); String[] titles = {"学号", "姓名", "性别", "年龄", "班级", "手机号"}; for (int i = 0; i < titles.length; i++) { header.createCell(i).setCellValue(titles[i]); } int rowIdx = 1; for (Student s : list) { Row row = sheet.createRow(rowIdx++); row.createCell(0).setCellValue(s.getStudentNo()); row.createCell(1).setCellValue(s.getName()); row.createCell(2).setCellValue(s.getGender()); row.createCell(3).setCellValue(s.getAge()); row.createCell(4).setCellValue(s.getClassName()); row.createCell(5).setCellValue(s.getPhone()); } } } }

resp.setHeader("Content-Disposition", "attachment; filename=students.xlsx")是触发浏览器下载的关键,attachment表示强制下载而不是内联打开。文件名是 ASCII 没问题,如果是中文文件名要 URL 编码,不然浏览器会显示乱码文件名。SXSSFWorkbook是 POI 的流式版本,数据量大时内存占用比XSSFWorkbook低很多,课设几百条数据感受不到差别,但答辩时可以提一句“用了流式写,避免大批量导出时 OOM”。这个导出功能加上去,系统的功能完整性直接上一个台阶。

回看整个这套系统的演进路径,其实和很多商业项目的成长轨迹是一样的:先确认视图和技术栈的边界,再搭骨架,然后填业务,被坑反复捶打,最后提炼通用能力。我记得自己当年做第一个 JSP 项目时,在中文乱码上耗了一整天,最后发现就是少了一句setCharacterEncoding,那种感觉只有经历过的人才懂。现在我把这些坑一条条写出来,就是希望你能绕开这一段曲折,直接把精力放在更有价值的事情上。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 2:05:01

VL822-QFN88四口USB HUB方案详解:PD快充供电与原理图设计要点

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 2:04:41

示波器FFT频谱分析实战指南:参数设置与假峰识别

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 2:03:55

树莓派4B控制42步进电机:从GPIO接线到A4988驱动的完整实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 2:03:44

TF-LLM:大语言模型驱动的可解释交通预测

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 2:03:26

车载测试人才缺口真相:从CAN总线到UDS诊断的实战技能图谱

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华