简介采用Java语言并整合Servlet、JDBC、JSP与MySQL这些Java Web常用技术构建出一套可直接运行的学生信息管理系统源码包。它面向Java Web初学者、课程设计或毕业设计人员能够系统演练从浏览器页面发起请求到后台Servlet处理再到数据库存取数据的完整调用链路。整个资源压缩后约4.48MB共78个文件其中包含12个Java业务类、20个JSP页面、24个class字节码文件以及用于初始化数据库的SQL建表脚本可覆盖环境配置、编译部署和功能调试等环节。截至目前已有6944人学习下载。借助源码可以清晰梳理请求在页面、控制器与数据层之间的流转过程理解MVC分层结构的实际落地方式并掌握学生信息增删改查、动态页面渲染、数据库连接等关键编码细节。随包SQL文件能快速创建学生信息表便于直接运行验证目录按源码、页面与输出文件区分结构清楚既适合对照学习也方便抽出某个模块快速改造成其他管理类系统。这套项目演示了前端JSP与后端Servlet的协作模式能有效缩短课程设计或毕业设计的开发周期。1. 学生信息管理系统最“传统”的 JavaWeb 组合却是最不该跳过的项目样板Servlet JSP JDBC MySQL 这套组合在 Spring 系框架满天飞的今天常被人一眼扫过就打入“过时技术”的冷宫。但真到了 Java 面试问“一次 HTTP 请求从浏览器打到数据库再返回页面中间经过哪些对象、谁在什么时候被创建”能立刻讲清楚的人其实不多。这份资源是一套完整的 Web 学生信息管理系统Servlet 做控制器JSP 做动态页面JDBC 负责和 MySQL 通信前端页面提交表单、后端查询数据库、再把结果回显到表格里正好把 Java Web 开发的主干流程全部串了起来。适合两类人一是要做课程设计、需要一个能跑通的完整项目的学生二是准备 Java 面试、发现自己对 Servlet 生命周期和 JDBC 连接管理模模糊糊的从业者。项目代码量不大但足以拆穿不少“黑匣子”。2. 先看清数据流Servlet 生命周期与一次请求的完整路径2.1 从 JSP 提交到 Servlet 分发请求到底走了几站很多新手第一次打开这套系统的源码习惯直接找StudentInfoServlet.java从头读到尾结果越看越乱。我一般建议反过来先看web目录下的 JSP 页面再看 Servlet最后看 DAO因为请求方向就是从页面发起、经过 Servlet、落入数据库的。在list_student.jsp里通常会看到类似这样的代码form actionStudentInfoServlet?actionadd methodpost 学号input typetext namestudentNo 姓名input typetext namename 班级input typetext nameclassName input typesubmit value添加 /form这段代码说明两件事第一表单提交到名为StudentInfoServlet的 ServletHTTP 方法为 POST第二URL 上带了actionadd参数告诉 Servlet“本次请求要执行新增操作”。这是老牌 JavaWeb 项目最常见的分发方式——用一个 Servlet 接收所有请求靠action参数区分业务。提示action参数不是 Servlet 规范强制要求的而是早期项目里约定俗成的写法。理解了这一点后面看switch或if-else分发的代码就不会懵。2.2 Servlet 生命周期谁创建了它谁销毁了它Servlet 生命周期是 Java 面试题里的常客这套系统正好能验证它的三个阶段。看StudentInfoServlet的源码你会发现它继承自HttpServlet重写了doGet和doPost。生命周期中的关键节点如下public class StudentInfoServlet extends HttpServlet { // 1. init: 首次请求到达时执行一次用来初始化连接池或读取配置 Override public void init() throws ServletException { System.out.println(StudentInfoServlet init...); } // 2. service: 每次请求都会经过这里框架自动根据 HTTP 方法分发给 doGet/doPost Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String action request.getParameter(action); // 分发给不同的业务方法 if (list.equals(action)) { // 查询全部学生 } else if (delete.equals(action)) { // 按学号删除 } } Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 统一交给 doGet 处理避免前后端方法不一致导致 405 doGet(request, response); } }这段代码的核心逻辑是init方法在整个 Servlet 生命周期中只会执行一次适合放数据库连接池初始化之类的重量级操作doGet和doPost处理具体业务destroy在应用卸载时执行释放资源。Servlet 是单实例多线程的——一个 Servlet 只有一个实例但多个请求会同时进入doGet所以里面如果用了共享变量就得考虑线程安全。提示doPost里调用doGet是常见写法能让 GET 和 POST 共用同一套分发逻辑避免重复代码。2.3 JDBC 不是被淘汰了而是被封装了很多人在网上看到“JDBC 过时了”的说法其实 Spring JDBC、MyBatis 底层都是 JDBC。这套系统里能清楚地看到 JDBC 最原始的样子加载驱动、获取连接、创建 Statement、执行 SQL、处理结果集、关闭资源。在DBUtil.java中你会看到类似这样的工具代码public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/student_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingUTF-8; private static final String USER root; private static final String PASSWORD root; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { // 反向关闭:rs - stmt - conn if (rs ! null) { try { rs.close(); } catch (SQLException e) { } } if (stmt ! null) { try { stmt.close(); } catch (SQLException e) { } } if (conn ! null) { try { conn.close(); } catch (SQLException e) { } } } }这段代码要留意三个参数useSSLfalse是为了避免 MySQL 8.0 默认的 SSL 握手警告serverTimezoneAsia/Shanghai是解决驱动读取时区失败的关键characterEncodingUTF-8保证中文正常读写。Class.forName这行在 MySQL 8.0 驱动下可以省略但保留它能兼容老版本驱动属于稳定性优先的做法。3. 把环境先跑起来JDK、Tomcat、MySQL 的版本匹配与初始参数3.1 版本匹配矩阵这里不讲玄学只讲可用组合这套系统虽然是老技术栈但对版本适配仍然敏感。最常见的翻车现场是安装了 MySQL 8.0却用了 MySQL 5.x 时代的com.mysql.jdbc.Driver结果启动直接报ClassNotFoundException。JDK 8 或 JDK 11对应 Tomcat 8.5 或 Tomcat 9兼容性最好MySQL 5.7驱动类用com.mysql.jdbc.DriverJDBC URL 可不带serverTimezoneMySQL 8.0驱动类必须是com.mysql.cj.jdbc.DriverURL 必须带serverTimezoneJSP 页面JSP 2.3 / Servlet 3.1 规范Tomcat 9 默认支持不需要额外引入依赖从实际经验看最省事的一套组合是JDK 8 Tomcat 8.5 MySQL 5.7 mysql-connector-java-5.1.49.jar。如果非要上 MySQL 8.0就用mysql-connector-java-8.0.33.jar并确保驱动类名和时区参数都改了。3.2 建库建表把字段约束和字符集一次设对项目sql目录下通常放着建库建表脚本。直接导入是不够的你要确认三件事数据库字符集是utf8mb4而不是utf8排序规则是utf8mb4_general_ci学生表的学号字段有唯一约束。CREATE DATABASE IF NOT EXISTS student_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE student_db; CREATE TABLE student ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT 自增主键, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号唯一, name VARCHAR(50) NOT NULL COMMENT 姓名, gender VARCHAR(10) DEFAULT 男 COMMENT 性别, age INT DEFAULT 0 COMMENT 年龄, class_name VARCHAR(50) DEFAULT COMMENT 班级, phone VARCHAR(20) DEFAULT COMMENT 联系电话, email VARCHAR(100) DEFAULT COMMENT 邮箱, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生信息表;建表的几个细节值得说明student_no设置UNIQUE是为了防止重复学号这比在 Java 代码里判断更可靠create_time用DEFAULT CURRENT_TIMESTAMP插入时不用显式赋值ENGINEInnoDB支持事务后续做批量删除或级联操作时有用。age设置DEFAULT 0是对应实际搜索里常被问到的“MySQL 设置默认值为 0”的场景很多新手忽略了这个字段导致前端传入空值时数据库报错。注意如果数据库已经建好了但发现字符集不对不要手动去改表直接执行ALTER DATABASE student_db CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;再对每张表执行ALTER TABLE student CONVERT TO CHARACTER SET utf8mb4;。3.3 部署到 Tomcat手把手把项目放进 webapps这套系统以普通 Web 项目结构存在src放 Java 源码web放 JSP 和静态资源out是编译产物。在 IDEA 里配置 Tomcat 时要确认 Artifact 类型是Web Application: Exploded也就是解压部署模式这样修改 JSP 后不用重启 Tomcat。# 如果不用 IDEA也可以手动编译部署 mkdir -p /path/to/tomcat/webapps/student_system cp -r web/* /path/to/tomcat/webapps/student_system/ cp -r out/production/classes/* /path/to/tomcat/webapps/student_system/WEB-INF/classes/ cp lib/mysql-connector-java-8.0.33.jar /path/to/tomcat/webapps/student_system/WEB-INF/lib/部署完成后Tomcat 会自动解压student_system目录。访问路径是http://localhost:8080/student_system/list_student.jsp。注意 JSP 文件如果直接放在web根目录URL 里必须带文件名如果想隐藏 JSP 物理路径得在web.xml里配置welcome-file-list或使用 Servlet 转发。3.4 连接数据库失败时先看这五个日志位置数据库连接不上是这套系统出现频率最高的问题。根据实际经验依次检查五个位置第一处是 Tomcat 控制台异常栈看是ClassNotFoundException驱动没放还是CommunicationsException连接被拒第二处是DBUtil.java里的 URL看主机名写没写错、端口是不是 3306第三处是 MySQL 服务状态Linux 下报ERROR 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock时说明 MySQL 服务没起来第四处是驱动版本MySQL 8.0 必须配mysql-connector-java-8.x和com.mysql.cj.jdbc.Driver第五处是防火墙或远程访问权限本地跑通常没问题跨主机访问需要检查 MySQL 的user表权限。4. 核心业务功能逐层拆解从 DAO 数据操作到 JSP 动态回显4.1 实体类和 DAO 层先写 Java 对象再写 SQL 映射项目中src目录下的model包通常放着Student.java实体类字段和数据库表一一对应。DAO 类则负责把 SQL 封装成 Java 方法。这么分层不是因为架子大而是为了让 Servlet 代码保持干净——Servlet 只做参数接收和页面跳转具体 SQL 全在 DAO 里。public class StudentDao { // 查询所有学生返回 ListStudent public ListStudent findAll() { ListStudent list new ArrayList(); String sql SELECT id, student_no, name, gender, age, class_name, phone, email FROM student ORDER BY id DESC; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); 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)); s.setEmail(rs.getString(email)); list.add(s); } } catch (SQLException e) { e.printStackTrace(); } return list; } }这段代码用了 JDK 7 的 try-with-resources 语法比手动在finally里关连接更安全确保Connection、PreparedStatement、ResultSet都会自动关闭。注意PreparedStatement的两个优势一是预编译 SQL 更高效二是参数用?占位能有效防止 SQL 注入这是 JDBC 增删改查中必须养成的习惯。4.2 新增和更新PreparedStatement 的传参细节新增学生是整个系统里最值得逐行读的代码因为它同时涉及表单数据获取、参数绑定、影响行数判断和页面跳转四件事。public boolean addStudent(Student s) { String sql INSERT INTO student (student_no, name, gender, age, class_name, phone, email) VALUES (?, ?, ?, ?, ?, ?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, s.getStudentNo()); ps.setString(2, s.getName()); ps.setString(3, s.getGender()); ps.setInt(4, s.getAge()); ps.setString(5, s.getClassName()); ps.setString(6, s.getPhone()); ps.setString(7, s.getEmail()); return ps.executeUpdate() 0; } catch (SQLException e) { e.printStackTrace(); return false; } }参数绑定的顺序必须和 SQL 中问号顺序一致这是新手最容易翻车的地方。setInt和setString的类型也要对得上数据库字段是INT就传int是VARCHAR就传String。executeUpdate()返回受影响行数大于 0 表示插入成功。更新操作UPDATE的代码结构完全相同只是 SQL 换成UPDATE student SET name ?, ... WHERE id ?要注意WHERE条件别漏。4.3 删除操作先按主键删还是先按学号删删除学生信息有两条路按自增主键id删或按唯一学号student_no删。我建议按主键删因为主键是数据库内部逻辑标识不受业务规则影响而学号可能会被人为修改尽管设置了唯一索引仍存在先改学号再删除的边界场景。public boolean deleteStudent(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; } }Servlet 里接收删除请求时会从 URL 参数取出idint id Integer.parseInt(request.getParameter(id))。这里有个隐藏的坑——如果前端传了一个非数字字符串parseInt会抛NumberFormatException导致整个请求 500。常见做法是先做一次正则校验或者在 catch 里跳转到错误页面而不是让异常直接打到 Tomcat 控制台。4.4 Servlet 接收参数与页面跳转forward 和 redirect 不能混用Servlet 处理完业务后要决定把请求交给哪个 JSP。这里有两种跳转方式区别很微妙但特别重要。// 方式一请求转发 forwardURL 不变request 里的属性可以带过去 request.setAttribute(studentList, list); request.getRequestDispatcher(list_student.jsp).forward(request, response); // 方式二重定向 redirectURL 会变request 属性丢失 response.sendRedirect(list_student.jsp);新增或修改成功后我推荐用重定向否则用户按 F5 刷新页面会再次提交表单产生重复数据。查询列表则用请求转发这样studentList属性才能被 JSP 读取到。这个区别就是常说的“转发是服务器内部跳转重定向是浏览器重新发起请求”在面试里也常被拿来追问。4.5 JSP 端如何渲染数据JSTL 与 EL 表达式JSP 页面拿到 Servlet 传来的ListStudent后用 EL 表达式和 JSTL 标签库进行遍历展示。这是 JSP 比较现代的一种写法——页面上尽量不写 Java 脚本片段% %否则页面会变得又乱又难维护。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html head title学生列表/title /head body table border1 tr th学号/thth姓名/thth性别/thth年龄/thth班级/thth操作/th /tr c:forEach items${studentList} varstu tr td${stu.studentNo}/td td${stu.name}/td td${stu.gender}/td td${stu.age}/td td${stu.className}/td td a hrefStudentInfoServlet?actioneditid${stu.id}修改/a a hrefStudentInfoServlet?actiondeleteid${stu.id} onclickreturn confirm(确定删除吗)删除/a /td /tr /c:forEach /table /body /html${stu.studentNo}会自动调用Student类的getStudentNo()方法所以实体类里的 getter/setter 必须写全。${studentList}引用的是 Servlet 里setAttribute(studentList, list)的那个键名。用c:forEach遍历时varstu是每次循环的临时变量名后面通过${stu.xxx}取字段。5. 避坑指南能跑通但不够稳的六个细节比业务逻辑更值得检查5.1 中文乱码一个 JDBC URL 参数引发的问题现象数据库表里中文显示正常但页面读出来全是???或乱码。 原因数据库连接 URL 里没有指定characterEncodingUTF-8。MySQL 驱动默认使用的字符集不是 UTF-8导致中文在传输过程中被转码丢失。 解决在 JDBC URL 末尾追加characterEncodingUTF-8同时确认 JSP 页面头部有contentTypetext/html;charsetUTF-8以及 Servlet 里执行了request.setCharacterEncoding(UTF-8)。这三者缺一不可少了任何一个都可能出现乱码。5.2 数据库连接失败MySQL 8.0 的时区报错现象Tomcat 启动正常访问页面时抛出The server time zone value Öйú±ê׼ʱ¼ä is unrecognized页面 500。 原因MySQL 8.0 的驱动强制要求指定时区而我当时用的是老版驱动的连接 URL没带serverTimezone参数。 解决在 URL 中加入serverTimezoneAsia/Shanghai。如果是建在一个需要跨时区部署的场景下也可以直接改 MySQL 全局变量SET GLOBAL time_zone 8:00;。5.3 驱动类找不到驱动包没进 WEB-INF/lib现象部署到 Tomcat 后运行时报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver。 原因在 IDEA 里跑的时候IDEA 自动把lib目录下的 jar 加进了 classpath但部署到独立 Tomcat 时jar 需要显式放在WEB-INF/lib目录下才会被 Web 应用加载。我刚转用本地 Tomcat 部署时也踩过以为“在 IDEA 里能跑就等于打包能跑”其实不是。 解决把mysql-connector-java-8.0.33.jar复制到WEB-INF/lib中并确认构建产物里包含了这个目录。5.4 端口被占用Tomcat 启动报Port 8080 was already in use现象启动 Tomcat 时控制台报端口占用提示The Tomcat connector configured to listen on port 8080 failed to start有时候还会伴随Address already in use。 原因上一个 Tomcat 实例没关干净或本地其他程序占了 8080。 解决执行netstat -ano | findstr 8080Windows或lsof -i:8080macOS/Linux找出占用进程 ID杀掉后重启 Tomcat。也可以直接改conf/server.xml里的端口号顺手把三个端口8005、8080、8009一起改掉更省事。5.5 表单重复提交刷新页面数据库里多了一条记录现象新增一条学生信息后按 F5 刷新列表页数据库里多了同一条记录。 原因新增成功后用的是请求转发forward浏览器地址栏停留的还是提交地址刷新等于重新执行了INSERT。 解决新增、修改、删除操作完成后一律使用重定向response.sendRedirect(StudentInfoServlet?actionlist)让浏览器跳转到列表页彻底脱离表单提交的上下文。5.6 使用旧版 URL 手动测试加了 Servlet 映射却仍然 404现象Tomcat 能访问 JSP 文件但访问http://localhost:8080/student_system/StudentInfoServlet时返回 404。 原因项目中StudentInfoServlet上的 URL 映射是/StudentInfoServlet不是/student_system/StudentInfoServlet。404 往往是把 Web 应用上下文路径/student_system漏掉了。 解决访问地址应为http://localhost:8080/student_system/StudentInfoServlet?actionlist不要凭记忆拼 URL。此外如果用的是注解WebServlet(/StudentInfoServlet)要注意 Tomcat 7 以上才支持注解老项目里写在web.xml的话得确认servlet-name和servlet-mapping两个标签都配齐了。6. 用一套固定验收流程确认系统能上线从列表到增删改的每一个页面都过一遍项目写完并不等于能交付在把student_system.war发给别人之前我会按照下面这套顺序完整过一遍每一步都盯住 URL、页面数据、数据库状态三个维度。这套流程同样适用于你自己复现时判断“系统是不是真的没问题”。第一步验证列表查询。浏览器打开http://localhost:8080/student_system/StudentInfoServlet?actionlist确认能显示所有学生且姓名、班级等中文字段无乱码。接着打开 MySQL 命令行执行SELECT COUNT(*) FROM student;比对页面显示的条数和数据库条数是否一致。第二步验证新增操作。在新增页面填写完整信息提交后先看列表页是否出现新数据再执行SELECT * FROM student ORDER BY id DESC LIMIT 1;确认学号、姓名、班级字段与页面提交值完全一致。故意再提交一次完全相同的学号页面应当给出提示而不能抛出底层 SQL 异常。第三步验证修改操作。在列表页点“修改”确认表单已回填原数据。修改姓名或班级后提交列表页应立即显示新值数据表里同一行记录的name和class_name也应同步变化。这里我最常发现的问题是把UPDATE语句里的WHERE id ?写漏导致所有学生被改成同一条数据。第四步验证删除操作。点击删除弹出确认框后确认列表页该行消失。最关键的动作是立刻刷新页面确认这条记录不会“复活”——如果删完刷新又出现了说明删除走的是假删除逻辑或者 SQL 里WHERE条件没生效。第五步验证异常路径。故意访问一个不存在的请求地址比如StudentInfoServlet?actionxxx系统应能给出响应而不是直接 500。故意在浏览器直接访问delete接口但传一个非法 id系统应能捕获异常并跳转到错误提示页而不是输出一长串堆栈信息。第六步验证重启后的状态。关闭 Tomcat重启后再访问列表页确认数据没有丢失。这一步看起来多余但能发现数据库连接在程序重启后能否正常重建也能确认没有把数据存到内存或临时文件里。这套系统里最值得反复体会的其实是 Servlet 生命周期和 JDBC 连接管理这两条暗线。Servlet 的init和destroy是什么时机触发、连接关闭不干净会导致什么后果这些经验不是看概念能得到的。我印象最深的是自己第一次用这套技术栈做管理系统时因为图省事没写DBUtil.close()导致 Tomcat 跑了半天后数据库连接耗尽页面全部卡死。从那以后我每次写完一个 JDBC 操作都会强制检查一遍连接关闭并且在验收流程里故意加了一条“连续点二十次查询再看数据库连接数”的测试。这个习惯后来也帮我避开了不少别的坑。希望这篇拆解能帮你把项目的结构看清、环境跑通、逻辑理顺。资源本身在网上就能找到下载后按第三章的版本匹配对照配置通常半小时内就能跑起来。本文还有配套的精品资源点击获取