
简介这是一份面向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 version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 display-nameStudentMS/display-name !-- 欢迎页访问根路径时默认打开 -- welcome-file-list welcome-filelogin.jsp/welcome-file /welcome-file-list !-- 全局字符编码过滤器解决中文乱码 -- filter filter-nameEncodingFilter/filter-name filter-classcom.student.util.EncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameEncodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping !-- 会话超时单位分钟 -- session-config session-timeout30/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?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/ShanghaiuseSSLfalse; 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(); } } }参数说明useUnicodetruecharacterEncodingUTF-8保证中文存取不乱码serverTimezoneAsia/Shanghai是 MySQL 8.0 必须加的否则报时区错误useSSLfalse去掉 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 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字符集用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 ListStudent findByPage(int pageNum, int pageSize) { ListStudent 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) {} } ListStudent 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?actionlist); } private void delete(HttpServletRequest req, HttpServletResponse resp) throws IOException { int id Integer.parseInt(req.getParameter(id)); dao.delete(id); resp.sendRedirect(req.getContextPath() /student?actionlist); } private void edit(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 实际项目应查单条这里简化演示 resp.sendRedirect(req.getContextPath() /student?actionlist); } 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?actionlist); } }关键点doPost里第一行必须setCharacterEncoding(UTF-8)否则表单提交的中文全是乱码。doGet的中文参数问题在 Tomcat 8 以后默认 URI 编码是 UTF-8一般不用额外处理但如果你的 Tomcat 配置改过就要在server.xml的 Connector 上加URIEncodingUTF-8。增删改完成后用sendRedirect重定向到列表页避免刷新重复提交。3.4 JSP 页面用 JSTL 渲染列表和分页条JSP 里尽量少写 Java 代码用 JSTL 和 EL 表达式。列表页核心结构% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html headtitle学生信息列表/title/head body h2学生信息管理系统/h2 a hrefadd.jsp新增学生/a table border1 cellpadding8 tr th学号/thth姓名/thth性别/th th年龄/thth专业/thth电话/thth操作/th /tr c:forEach items${students} vars 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 hrefstudent?actiondeleteid${s.id} onclickreturn confirm(确定删除)删除/a /td /tr /c:forEach /table !-- 分页条 -- div 第 ${pageNum} / ${totalPages} 页 c:if test${pageNum 1} a hrefstudent?actionlistpageNum${pageNum - 1}上一页/a /c:if c:if test${pageNum totalPages} a hrefstudent?actionlistpageNum${pageNum 1}下一页/a /c:if /div /body /htmlc:forEach遍历 Servlet 放进 request 的students列表EL 表达式${s.name}直接取属性。分页条用c:if判断边界第一页不显示上一页最后一页不显示下一页。删除链接加onclick确认框防止误删。4. 中文乱码、404 和 500JSP Servlet 项目最常见的五类翻车现场4.1 现象表单提交后数据库里中文变问号原因POST 请求默认用 ISO-8859-1 解码Servlet 没设编码或者数据库连接串没加characterEncoding。解决三处同时设 UTF-8——JSP 页面pageEncodingUTF-8、Servletreq.setCharacterEncoding(UTF-8)、JDBC URL 加useUnicodetruecharacterEncodingUTF-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 port8080 ...处改成 8081 或其他空闲端口。改完记得重启 Tomcat并且清理 IDE 的缓存重新部署。5. 让系统更实用会话拦截、批量删除和 JSP 图片定位的小技巧5.1 用 Filter 做登录拦截防止未登录直接访问系统做完 CRUD 后下一步就是加登录。登录本身不难难的是防止别人绕过登录页直接访问student?actionlist。常见做法是写一个登录过滤器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 数组搞定列表页每行加一个 checkboxname 统一为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就能实现不需要 JavaScriptimg srcseatmap.png usemap#seatMap border0 map nameseatMap area shaperect coords34,44,270,350 hrefstudent?actiondetailseatA1 altA1座位 area shaperect coords290,172,333,250 hrefstudent?actiondetailseatA2 altA2座位 /mapcoords是矩形左上角和右下角的像素坐标shape可以是rect、circle、poly。坐标值需要你用画图工具量出来或者用在线 image map 生成器。这个技巧在实验室管理、图书馆座位预约这类场景里很实用。5.4 验证系统是否真的跑通三个必测场景写完代码别急着交至少测这三个场景第一新增一个带中文姓名和生僻字专业的学生看数据库里存的是不是正常汉字第二连续翻到最后一页再翻回来看数据有没有重复或丢失第三不登录直接访问student?actionlist看是否被踢回登录页。这三个场景过了系统基本就稳了。我自己的习惯是每次改完 DAO 层先把count()和findByPage()单独跑一遍确认分页数据对得上再联调页面。JSP Servlet 这套东西虽然老但把链路走通一遍后面学任何框架都是在补细节而不是从零猜。希望帮到你。本文还有配套的精品资源点击获取