☰
JSP+Servlet学生信息管理系统实战:从CRUD到分页与乱码排查
2026/10/8 9:07:26 网站建设 项目流程

简介:这是一份面向Java Web初学者与课程设计者的学生信息管理系统完整源码,基于JSP与Servlet实现,采用MVC设计模式,涵盖用户登录、学生与班级信息的增删改查等核心功能。资源包共576个文件,约2.75MB,其中html、css、png、js等前端资源占比较大,用于构建页面结构与交互样式;20个java源文件与20个class文件对应Servlet、DAO等后端逻辑;另含9个jar依赖、4个jsp页面、1个sql初始化脚本及若干json、xml配置,完整呈现从数据库建表到请求响应的开发链路。已有996人学习下载。读者可借此理解Servlet接收HTTP请求、JDBC操作数据库、会话管理与异常处理的协作方式,并参考其目录组织与部署结构,快速搭建可运行的Java Web实践环境,适合作为课程作业或入门练手项目。

1. 从 JSP + Servlet 搭学生信息管理系统:为什么它仍是理解 Java Web 的最佳入口

很多人第一次接触 Java Web,都是从 JSP 和 Servlet 开始的。学生信息管理系统这个题目看起来朴素,但它几乎覆盖了 Web 开发的所有核心环节:请求怎么进来、参数怎么接、数据库怎么连、页面怎么渲染、会话怎么管。你把这套东西跑通一遍,后面学 Spring Boot 也好,看 MyBatis 也好,心里都有底。

我见过不少同学直接上框架,结果连doGet和doPost的区别都说不清,出了 404 或 500 只能靠猜。JSP + Servlet 的好处是链路短、黑匣子少,每一步你都能看到。这篇文章面向的是想用 JSP + Servlet 真正做出一个能跑的学生信息管理系统的开发者,从环境搭建、数据库设计、CRUD 实现到分页和排错,全部落到可复现的代码和参数上。读完你至少能拿到一个能增删改查、能分页、能处理中文乱码的完整系统骨架。

2. 环境与项目骨架:把 JSP + Servlet 的最小可运行链路搭起来

2.1 工具链选型和版本边界

做 JSP + Servlet 项目,工具链的选择直接决定你后面踩多少坑。我一般推荐下面这套组合,原因是兼容性好、资料多、出问题容易搜到答案:

组件推荐版本说明
JDK8 或 11JDK 8 最稳,Servlet 容器兼容性最好
Tomcat8.5 或 9.0不要用 Tomcat 10,包名从javax.*变成jakarta.*,老代码全挂
MySQL5.7 或 8.08.0 注意驱动类名和时区参数
IDEEclipse / IDEAIDEA 对 Web 项目支持更好,但 Eclipse 更轻
Servlet API3.1 / 4.0跟 Tomcat 版本匹配

这里最关键的一条血泪经验:Tomcat 10 是分水岭。如果你的web.xml或 Servlet 代码里写的是javax.servlet.*,放到 Tomcat 10 上直接 ClassNotFound。新手最容易在这里翻车,所以要么锁死 Tomcat 9,要么全量迁移到jakarta.servlet.*,不要混着用。

2.2 项目目录结构和 web.xml 配置

一个标准的 JSP + Servlet 项目目录长这样:

StudentMS/ ├── src/ │ └── com/student/ │ ├── dao/ # 数据库操作层 │ ├── entity/ # 实体类 │ ├── servlet/ # 控制层 │ └── util/ # 工具类(DBUtil 等) ├── WebContent/ (或 web/) │ ├── WEB-INF/ │ │ ├── web.xml # 部署描述符 │ │ └── lib/ # 第三方 jar │ ├── css/ │ ├── js/ │ └── *.jsp # 页面 └── ...

web.xml是 Servlet 的注册中心。Servlet 3.0 以后可以用@WebServlet注解替代,但理解web.xml的配置方式对排查问题很有帮助:

<?xml version="1.0" encoding="UTF-8"?> <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"> <display-name>StudentMS</display-name> <!-- 欢迎页,访问根路径时默认打开 --> <welcome-file-list> <welcome-file>login.jsp</welcome-file> </welcome-file-list> <!-- 全局字符编码过滤器,解决中文乱码 --> <filter> <filter-name>EncodingFilter</filter-name> <filter-class>com.student.util.EncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>EncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- 会话超时,单位分钟 --> <session-config> <session-timeout>30</session-timeout> </session-config> </web-app>

这段配置做了三件事:指定默认首页、注册全局编码过滤器、设置会话超时。url-pattern写/*表示拦截所有请求,包括静态资源,所以过滤器内部要判断是否需要设置编码,避免影响图片和 CSS 的加载。

2.3 数据库连接工具类:一次写对,后面少调半天

数据库连接是所有 CRUD 的基础。我一般把 JDBC 连接封装成一个DBUtil,用静态块加载驱动,用DriverManager获取连接。生产环境当然要用连接池,但学习和中小型项目这样写足够清晰:

package com.student.util; import java.sql.*; public class DBUtil { // MySQL 8.0 驱动类名是 com.mysql.cj.jdbc.Driver // MySQL 5.7 用 com.mysql.jdbc.Driver private static final String DRIVER = "com.mysql.cj.jdbc.Driver"; private static final String URL = "jdbc:mysql://localhost:3306/student_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false"; private static final String USER = "root"; private static final String PASSWORD = "your_password"; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new RuntimeException("数据库驱动加载失败", e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } // 统一关闭资源,顺序:ResultSet -> Statement -> Connection public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs != null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (stmt != null) stmt.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }

参数说明:useUnicode=true&characterEncoding=UTF-8保证中文存取不乱码;serverTimezone=Asia/Shanghai是 MySQL 8.0 必须加的,否则报时区错误;useSSL=false去掉 SSL 警告。这三个参数是新手最常漏的,漏一个就可能连不上或存进去变问号。

3. 学生信息管理系统的 CRUD 落地:Servlet 接参、DAO 操作、JSP 渲染

3.1 实体类和数据库表设计

先把表建好,字段和实体类一一对应。学生表不需要太复杂,够用就行:

CREATE DATABASE student_db DEFAULT CHARACTER SET utf8mb4; USE student_db; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, sno VARCHAR(20) NOT NULL UNIQUE COMMENT '学号', name VARCHAR(50) NOT NULL COMMENT '姓名', gender VARCHAR(4) DEFAULT '男' COMMENT '性别', age INT COMMENT '年龄', major VARCHAR(50) COMMENT '专业', phone VARCHAR(20) COMMENT '联系电话', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

字符集用utf8mb4而不是utf8,因为utf8在 MySQL 里是阉割版,存不了 emoji 和部分生僻字。学号加UNIQUE约束,防止重复录入。

对应的实体类:

package com.student.entity; public class Student { private int id; private String sno; private String name; private String gender; private int age; private String major; private String phone; // 无参构造 + getter/setter 省略,IDE 一键生成 public Student() {} public Student(String sno, String name, String gender, int age, String major, String phone) { this.sno = sno; this.name = name; this.gender = gender; this.age = age; this.major = major; this.phone = phone; } // getter/setter ... }

实体类保持纯粹,不掺业务逻辑。这样 DAO 层拿到的就是干净的数据载体,后面换 ORM 框架也能直接复用。

3.2 DAO 层:增删改查的 SQL 和参数绑定

DAO 层负责所有数据库操作。用PreparedStatement而不是Statement,这是防 SQL 注入的基本功:

package com.student.dao; import com.student.entity.Student; import com.student.util.DBUtil; import java.sql.*; import java.util.*; public class StudentDao { // 新增学生 public boolean add(Student s) { String sql = "INSERT INTO student(sno, name, gender, age, major, phone) VALUES(?,?,?,?,?,?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, s.getSno()); ps.setString(2, s.getName()); ps.setString(3, s.getGender()); ps.setInt(4, s.getAge()); ps.setString(5, s.getMajor()); ps.setString(6, s.getPhone()); return ps.executeUpdate() > 0; } catch (SQLException e) { e.printStackTrace(); return false; } } // 分页查询:pageNum 从 1 开始,pageSize 每页条数 public List<Student> findByPage(int pageNum, int pageSize) { List<Student> list = new ArrayList<>(); String sql = "SELECT * FROM student ORDER BY id DESC LIMIT ?, ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, (pageNum - 1) * pageSize); ps.setInt(2, pageSize); ResultSet rs = ps.executeQuery(); while (rs.next()) { Student s = new Student(); s.setId(rs.getInt("id")); s.setSno(rs.getString("sno")); s.setName(rs.getString("name")); s.setGender(rs.getString("gender")); s.setAge(rs.getInt("age")); s.setMajor(rs.getString("major")); s.setPhone(rs.getString("phone")); list.add(s); } } catch (SQLException e) { e.printStackTrace(); } return list; } // 统计总数,分页要用 public int count() { String sql = "SELECT COUNT(*) FROM student"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { if (rs.next()) return rs.getInt(1); } catch (SQLException e) { e.printStackTrace(); } return 0; } // 按 id 删除 public boolean delete(int id) { String sql = "DELETE FROM student WHERE id=?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, id); return ps.executeUpdate() > 0; } catch (SQLException e) { e.printStackTrace(); return false; } } // 更新 public boolean update(Student s) { String sql = "UPDATE student SET sno=?, name=?, gender=?, age=?, major=?, phone=? WHERE id=?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, s.getSno()); ps.setString(2, s.getName()); ps.setString(3, s.getGender()); ps.setInt(4, s.getAge()); ps.setString(5, s.getMajor()); ps.setString(6, s.getPhone()); ps.setInt(7, s.getId()); return ps.executeUpdate() > 0; } catch (SQLException e) { e.printStackTrace(); return false; } } }

分页 SQL 用LIMIT ?, ?,第一个参数是偏移量(pageNum-1)*pageSize,第二个是每页条数。这里用 try-with-resources 自动关闭连接,比手动 close 更安全。注意count()方法单独抽出来,因为分页需要知道总条数才能算总页数。

3.3 Servlet 控制层:请求分发和参数接收

Servlet 是整个系统的调度中心。一个 Servlet 可以处理多个操作,通过action参数区分:

package com.student.servlet; import com.student.dao.StudentDao; import com.student.entity.Student; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; import java.util.List; @WebServlet("/student") public class StudentServlet extends HttpServlet { private StudentDao dao = new StudentDao(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action = req.getParameter("action"); if (action == null) action = "list"; switch (action) { case "list": list(req, resp); break; case "delete": delete(req, resp); break; case "edit": edit(req, resp); break; default: list(req, resp); } } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); // POST 中文参数必须设 String action = req.getParameter("action"); if ("add".equals(action)) { add(req, resp); } else if ("update".equals(action)) { update(req, resp); } } private void list(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int pageNum = 1; int pageSize = 10; String p = req.getParameter("pageNum"); if (p != null && !p.isEmpty()) { try { pageNum = Integer.parseInt(p); } catch (NumberFormatException ignored) {} } List<Student> list = dao.findByPage(pageNum, pageSize); int total = dao.count(); int totalPages = (total + pageSize - 1) / pageSize; req.setAttribute("students", list); req.setAttribute("pageNum", pageNum); req.setAttribute("totalPages", totalPages); req.getRequestDispatcher("/student_list.jsp").forward(req, resp); } private void add(HttpServletRequest req, HttpServletResponse resp) throws IOException { Student s = new Student( req.getParameter("sno"), req.getParameter("name"), req.getParameter("gender"), Integer.parseInt(req.getParameter("age")), req.getParameter("major"), req.getParameter("phone") ); dao.add(s); resp.sendRedirect(req.getContextPath() + "/student?action=list"); } private void delete(HttpServletRequest req, HttpServletResponse resp) throws IOException { int id = Integer.parseInt(req.getParameter("id")); dao.delete(id); resp.sendRedirect(req.getContextPath() + "/student?action=list"); } private void edit(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 实际项目应查单条,这里简化演示 resp.sendRedirect(req.getContextPath() + "/student?action=list"); } private void update(HttpServletRequest req, HttpServletResponse resp) throws IOException { Student s = new Student(); s.setId(Integer.parseInt(req.getParameter("id"))); s.setSno(req.getParameter("sno")); s.setName(req.getParameter("name")); s.setGender(req.getParameter("gender")); s.setAge(Integer.parseInt(req.getParameter("age"))); s.setMajor(req.getParameter("major")); s.setPhone(req.getParameter("phone")); dao.update(s); resp.sendRedirect(req.getContextPath() + "/student?action=list"); } }

关键点:doPost里第一行必须setCharacterEncoding("UTF-8"),否则表单提交的中文全是乱码。doGet的中文参数问题在 Tomcat 8 以后默认 URI 编码是 UTF-8,一般不用额外处理,但如果你的 Tomcat 配置改过,就要在server.xml的 Connector 上加URIEncoding="UTF-8"。增删改完成后用sendRedirect重定向到列表页,避免刷新重复提交。

3.4 JSP 页面:用 JSTL 渲染列表和分页条

JSP 里尽量少写 Java 代码,用 JSTL 和 EL 表达式。列表页核心结构:

<%@ 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> <a href="add.jsp">新增学生</a> <table border="1" cellpadding="8"> <tr> <th>学号</th><th>姓名</th><th>性别</th> <th>年龄</th><th>专业</th><th>电话</th><th>操作</th> </tr> <c:forEach items="${students}" var="s"> <tr> <td>${s.sno}</td> <td>${s.name}</td> <td>${s.gender}</td> <td>${s.age}</td> <td>${s.major}</td> <td>${s.phone}</td> <td> <a href="student?action=delete&id=${s.id}" onclick="return confirm('确定删除?')">删除</a> </td> </tr> </c:forEach> </table> <!-- 分页条 --> <div> 第 ${pageNum} / ${totalPages} 页 <c:if test="${pageNum > 1}"> <a href="student?action=list&pageNum=${pageNum - 1}">上一页</a> </c:if> <c:if test="${pageNum < totalPages}"> <a href="student?action=list&pageNum=${pageNum + 1}">下一页</a> </c:if> </div> </body> </html>

<c:forEach>遍历 Servlet 放进 request 的students列表,EL 表达式${s.name}直接取属性。分页条用<c:if>判断边界,第一页不显示上一页,最后一页不显示下一页。删除链接加onclick确认框,防止误删。

4. 中文乱码、404 和 500:JSP + Servlet 项目最常见的五类翻车现场

4.1 现象:表单提交后数据库里中文变问号

原因:POST 请求默认用 ISO-8859-1 解码,Servlet 没设编码,或者数据库连接串没加characterEncoding。解决:三处同时设 UTF-8——JSP 页面pageEncoding="UTF-8"、Servletreq.setCharacterEncoding("UTF-8")、JDBC URL 加useUnicode=true&characterEncoding=UTF-8。三处缺一不可,只改一处照样乱码。

4.2 现象:访问 Servlet 报 404

原因通常有三种:@WebServlet的路径和访问 URL 不一致;web.xml里 metadata-complete 设为 true 导致注解被忽略;项目没有部署到 Tomcat 的 webapps 下。解决:先看 Tomcat 控制台启动日志有没有报错,再确认访问路径是否带上了项目上下文(如/StudentMS/student),最后检查web.xml的 version 是否支持注解。

4.3 现象:报 500,日志显示 ClassNotFoundException: com.mysql.cj.jdbc.Driver

原因:MySQL 驱动 jar 没放进WEB-INF/lib目录。解决:把mysql-connector-java-8.x.jar复制到WEB-INF/lib下,而不是只加到 IDE 的 build path。IDE 的 build path 只影响编译,运行时 Tomcat 只认WEB-INF/lib里的 jar。这是新手最常踩的坑之一。

4.4 现象:分页查询第二页数据重复或为空

原因:LIMIT偏移量算错,或者pageNum没有做边界校验。解决:偏移量公式固定为(pageNum-1)*pageSize,并且在 Servlet 里对pageNum做范围限制,小于 1 设为 1,大于总页数设为总页数。另外注意ORDER BY id DESC必须加,否则 MySQL 返回顺序不保证稳定,分页会出现数据跳动。

4.5 现象:修改 Tomcat 配置后项目启动变慢或端口冲突

原因:8080 端口被占用,或者server.xml改坏了。解决:用netstat -ano | findstr 8080找到占用进程,要么杀掉要么改 Tomcat 端口。改端口在conf/server.xml的<Connector port="8080" ...>处,改成 8081 或其他空闲端口。改完记得重启 Tomcat,并且清理 IDE 的缓存重新部署。

5. 让系统更实用:会话拦截、批量删除和 JSP 图片定位的小技巧

5.1 用 Filter 做登录拦截,防止未登录直接访问

系统做完 CRUD 后,下一步就是加登录。登录本身不难,难的是防止别人绕过登录页直接访问student?action=list。常见做法是写一个登录过滤器:

@WebFilter("/student") public class LoginFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; HttpSession session = request.getSession(); if (session.getAttribute("user") == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(req, resp); } }

@WebFilter("/student")只拦截学生相关请求,登录页和静态资源不受影响。判断 session 里有没有user,没有就踢回登录页。注意sendRedirect后要return,否则代码继续往下走会报错。

5.2 批量删除:一个 checkbox 数组搞定

列表页每行加一个 checkbox,name 统一为ids,提交到 Servlet 后用getParameterValues拿数组:

String[] ids = req.getParameterValues("ids"); if (ids != null && ids.length > 0) { for (String id : ids) { dao.delete(Integer.parseInt(id)); } }

批量删除的 SQL 也可以拼成DELETE FROM student WHERE id IN (?,?,?),但动态占位符数量要跟数组长度一致,用循环拼?即可。数据量不大时逐条删更简单,不容易出错。

5.3 JSP 图片坐标定位:用 image map 做机房座位图

有同学问过 JSP 页面里图片怎么对坐标定位,比如做一个机房座位图,点某个座位显示学生信息。这用 HTML 的<map>和<area>就能实现,不需要 JavaScript:

<img src="seatmap.png" usemap="#seatMap" border="0"> <map name="seatMap"> <area shape="rect" coords="34,44,270,350" href="student?action=detail&seat=A1" alt="A1座位"> <area shape="rect" coords="290,172,333,250" href="student?action=detail&seat=A2" alt="A2座位"> </map>

coords是矩形左上角和右下角的像素坐标,shape可以是rect、circle、poly。坐标值需要你用画图工具量出来,或者用在线 image map 生成器。这个技巧在实验室管理、图书馆座位预约这类场景里很实用。

5.4 验证系统是否真的跑通:三个必测场景

写完代码别急着交,至少测这三个场景:第一,新增一个带中文姓名和生僻字专业的学生,看数据库里存的是不是正常汉字;第二,连续翻到最后一页再翻回来,看数据有没有重复或丢失;第三,不登录直接访问student?action=list,看是否被踢回登录页。这三个场景过了,系统基本就稳了。

我自己的习惯是每次改完 DAO 层,先把count()和findByPage()单独跑一遍,确认分页数据对得上再联调页面。JSP + Servlet 这套东西虽然老,但把链路走通一遍,后面学任何框架都是在补细节,而不是从零猜。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询