☰
Java Web学生管理系统实战:Servlet+JSP+DBCP2从零搭建与排错指南
2026/9/29 2:06:55 网站建设 项目流程

简介:这是一套面向计算机专业本科生的Java Web课程设计与期末大作业实战项目,专为需完成系统开发类大作业的学生及Java Web初学者设计,解决项目选题难、功能不完整、文档缺失等常见痛点。资源包共77个文件,含20个Java源码、20个编译后Class文件、3个JSP页面、3个CSS与3个JS前端资源、9张界面截图(JPG)、1个建表SQL脚本、1份Word版课程设计报告(docx)及配套Web配置文件(XML、web.xml等),完整覆盖MVC分层结构与前后端交互逻辑,压缩包仅4.03MB,轻量易部署。已有161人学习下载,资源结构清晰,包含标准Eclipse项目配置(.project、.classpath)、数据库脚本、系统流程图与README说明,开箱即用,可直接运行调试并拓展功能,是夯实Servlet、JSP、MySQL与Tomcat整合能力的优质练手案例。

1. 这不是“抄完就能交”的Java Web学生管理系统:它是一块检验你是否真懂Servlet生命周期、JDBC连接池边界、MVC分层缝合度的试金石

如果你正被「Java Web学生管理系统」期末大作业压得喘不过气,手头那份网上搜来的「源码+数据库+报告」解压后跑不起来、登录页404、增删改查全报空指针、MySQL连不上还死活看不出错在哪——别急着换项目,这恰恰说明你踩中了90%初学者必经的「伪完成陷阱」:代码有,结构乱;功能列全,耦合缠死;数据库建了,事务没设;报告写了,日志没留。这个系统表面是CRUD练习,实则是Servlet容器启动流程、JSP/EL表达式渲染边界、DAO层SQL注入防护粒度、以及Tomcat线程池与MySQL最大连接数隐性冲突的综合考场。适合刚学完Java基础+HTML+MySQL,但还没在真实Web容器里调试过request域生命周期、没亲手配过dbcp2连接池、更没为一个delete操作加过事务回滚的学生。它不考算法,考的是你能不能把教科书里的「MVC三层」真正缝合成一条能呼吸、可调试、会报错的活链路。


2. 从零搭起可运行骨架:用最简Servlet+JSP组合绕过Spring Boot黑匣子

很多同学一上来就搜「Spring Boot学生管理系统」,结果陷入starter依赖冲突、自动配置覆盖、Thymeleaf模板路径错位的泥潭。而课程设计明确要求「Java Web」,本质是考察你对Servlet规范的理解深度。我们回归本质:用原生Servlet + JSP + JDBC,在Tomcat 9.0+上跑通最小闭环。这不是倒退,而是把黑匣子拆开看齿轮怎么咬合。

2.1 创建标准Java Web动态项目结构(非Maven)

提示:别用IDE一键生成的「Maven Web App」模板——它默认带web.xml且路径约定混乱。手动建标准结构才能看清容器加载顺序。

student-system/ ├── src/ │ └── main/ │ ├── java/ │ │ └── com/example/student/ │ │ ├── servlet/ │ │ │ ├── LoginServlet.java │ │ │ └── StudentListServlet.java │ │ ├── dao/ │ │ │ └── StudentDao.java │ │ └── model/ │ │ └── Student.java │ └── webapp/ │ ├── WEB-INF/ │ │ ├── web.xml ← 必须存在,定义Servlet映射 │ │ └── views/ │ │ ├── login.jsp │ │ └── list.jsp │ └── static/ │ └── css/style.css └── lib/ └── mysql-connector-java-8.0.33.jar ← 显式引入,避免IDE自动管理版本错乱

关键点:web.xml是Servlet 3.0前的硬性入口,必须手写。它定义了容器启动时谁先加载、URL路径如何映射到类。很多「源码跑不通」的根因就是这个文件缺失或servlet-class路径写错。

2.2 写死第一个Servlet:LoginServlet的request生命周期验证

// src/main/java/com/example/student/servlet/LoginServlet.java package com.example.student.servlet; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 1. 打印请求参数,确认表单数据已到达 System.out.println("【DEBUG】Received username: " + req.getParameter("username")); System.out.println("【DEBUG】Received password: " + req.getParameter("password")); // 2. 设置响应编码,避免中文乱码 resp.setContentType("text/html;charset=UTF-8"); // 3. 转发到成功页(非重定向!验证request域传递) req.setAttribute("message", "登录成功,欢迎回来!"); req.getRequestDispatcher("/WEB-INF/views/list.jsp").forward(req, resp); } }

逻辑说明:

  • doPost()处理表单提交,req.getParameter()是获取前端数据的第一道关卡。如果这里打印为空,说明HTML表单method="post"没写,或name属性与Java代码不一致;
  • req.setAttribute()将数据存入request作用域,forward()保持同一请求链路,确保list.jsp能通过${message}取到值;
  • resp.setContentType()必须在getWriter()之前调用,否则中文变方块——这是新手高频翻车点。

2.3 配置web.xml绑定URL与Servlet(Tomcat启动失败的首查项)

<!-- src/main/webapp/WEB-INF/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"> <servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.example.student.servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> <!-- 注意:/login 不是 /login.do --> </servlet-mapping> <!-- 添加默认欢迎页 --> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app>

参数说明:

  • <servlet-class>必须是完整包路径,IDE自动生成常漏掉com.example.student.前缀;
  • <url-pattern>的斜杠开头表示应用根路径,/login对应http://localhost:8080/student-system/login;
  • <welcome-file>指定访问http://localhost:8080/student-system/时自动加载的页面,避免404。

3. 数据库层落地:用DBCP2连接池替代Class.forName()直连,解决并发下的连接耗尽

很多「源码」直接在DAO里写DriverManager.getConnection(),看似能跑,但一并发就报Communications link failure。这不是MySQL问题,是Java端连接没回收。DBCP2是Apache维护的成熟连接池,比C3P0更轻量,比HikariCP对初学者更友好(无需复杂配置)。

3.1 在WEB-INF下建context.xml声明数据源(Tomcat专属)

<!-- src/main/webapp/META-INF/context.xml --> <?xml version="1.0" encoding="UTF-8"?> <Context> <Resource name="jdbc/studentDB" auth="Container" type="javax.sql.DataSource" factory="org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory" driverClassName="com.mysql.cj.jdbc.Driver" url="jdbc:mysql://localhost:3306/student_db?useSSL=false&amp;serverTimezone=Asia/Shanghai&amp;allowPublicKeyRetrieval=true" username="root" password="123456" maxTotal="20" maxIdle="10" minIdle="5" maxWaitMillis="10000" validationQuery="SELECT 1" testOnBorrow="true"/> </Context>

关键参数说明:

  • maxTotal="20":整个应用最多20个连接,超过则排队等待;
  • validationQuery="SELECT 1"+testOnBorrow="true":每次从池取连接前执行校验,避免拿到失效连接;
  • URL中&amp;是XML转义,不是&,写错直接导致Tomcat启动失败;
  • allowPublicKeyRetrieval=true是MySQL 8.0+必需参数,否则报Public Key Retrieval is not allowed。

3.2 在DAO中通过JNDI查找数据源(非new Connection)

// src/main/java/com/example/student/dao/StudentDao.java package com.example.student.dao; import com.example.student.model.Student; import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.sql.DataSource; import java.sql.*; import java.util.ArrayList; import java.util.List; public class StudentDao { private DataSource dataSource; public StudentDao() { try { Context initContext = new InitialContext(); Context envContext = (Context) initContext.lookup("java:/comp/env"); dataSource = (DataSource) envContext.lookup("jdbc/studentDB"); // 与context.xml中name一致 } catch (NamingException e) { throw new RuntimeException("Failed to lookup DataSource", e); } } public List<Student> findAll() throws SQLException { String sql = "SELECT id, name, gender, age, major FROM student"; try (Connection conn = dataSource.getConnection(); // 从此处开始借连接 PreparedStatement stmt = conn.prepareStatement(sql); ResultSet rs = stmt.executeQuery()) { List<Student> students = new ArrayList<>(); while (rs.next()) { Student s = new Student(); s.setId(rs.getLong("id")); s.setName(rs.getString("name")); s.setGender(rs.getString("gender")); s.setAge(rs.getInt("age")); s.setMajor(rs.getString("major")); students.add(s); } return students; } } }

逻辑说明:

  • InitialContext().lookup("java:/comp/env")是JNDI标准路径,Tomcat将context.xml中定义的数据源绑定到这里;
  • try-with-resources自动关闭Connection/PreparedStatement/ResultSet,这是连接池回收的关键——若手动close()遗漏,连接永远不归还池中;
  • findAll()方法无SQL拼接,天然防注入,但实际项目中需用?占位符处理动态条件。

4. MVC分层缝合:用RequestDispatcher串联Servlet-DAO-JSP,拒绝Service层空壳化

很多「源码」的Service类只有public void addStudent(Student s)方法,里面直接new DAO调用,既没事务控制也没异常包装。真正的MVC缝合,是让Servlet只管流程调度,DAO专注数据操作,JSP纯粹展示,中间用清晰的异常流和数据流穿起来。

4.1 Servlet中捕获DAO异常并转向错误页

// src/main/java/com/example/student/servlet/StudentListServlet.java package com.example.student.servlet; import com.example.student.dao.StudentDao; import com.example.student.model.Student; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.sql.SQLException; import java.util.List; public class StudentListServlet extends HttpServlet { private StudentDao studentDao = new StudentDao(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try { List<Student> students = studentDao.findAll(); req.setAttribute("students", students); // 传给JSP req.getRequestDispatcher("/WEB-INF/views/list.jsp").forward(req, resp); } catch (SQLException e) { // 1. 记录详细错误(生产环境应打到log4j) e.printStackTrace(); // 2. 设置错误信息,转发到统一错误页 req.setAttribute("errorMessage", "查询学生列表失败:" + e.getMessage()); req.getRequestDispatcher("/WEB-INF/views/error.jsp").forward(req, resp); } } }

为什么不用throws SQLException?因为Servlet规范要求doGet/doPost只能抛ServletException或IOException。强制catch+包装,是把底层数据库异常转化为用户可读提示的第一步。

4.2 JSP中用JSTL+EL安全展示数据(拒绝<% %>脚本片段)

<!-- src/main/webapp/WEB-INF/views/list.jsp --> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!DOCTYPE html> <html> <head> <title>学生列表</title> <link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/style.css"> </head> <body> <h2>学生信息列表</h2> <c:if test="${not empty errorMessage}"> <div class="alert alert-danger">${errorMessage}</div> </c:if> <c:if test="${not empty students}"> <table border="1"> <tr> <th>ID</th> <th>姓名</th> <th>性别</th> <th>年龄</th> <th>专业</th> <th>操作</th> </tr> <c:forEach items="${students}" var="s"> <tr> <td>${s.id}</td> <td>${s.name}</td> <td>${s.gender}</td> <td>${s.age}</td> <td>${s.major}</td> <td> <a href="${pageContext.request.contextPath}/edit?id=${s.id}">编辑</a> | <a href="${pageContext.request.contextPath}/delete?id=${s.id}" onclick="return confirm('确定删除${s.name}?')">删除</a> </td> </tr> </c:forEach> </table> </c:if> <c:if test="${empty students}"> <p>暂无学生数据</p> </c:if> <a href="${pageContext.request.contextPath}/add.jsp">添加新学生</a> </body> </html>

关键细节:

  • ${pageContext.request.contextPath}动态获取应用上下文路径,避免硬编码/student-system/;
  • <c:forEach>替代<% for() {} %>,JSTL标签天然防XSS,${s.name}自动HTML转义;
  • onclick="return confirm(...)"是前端轻量级二次确认,比后端拦截更及时。

5. 避坑指南:那些让90%人卡住3小时以上的具体问题与血泪解法

现象 → 原因 → 解决,每条都来自真实调试记录,不是教科书理论。

5.1 现象:Tomcat启动成功,但访问http://localhost:8080/student-system/报404,控制台无任何错误日志

→ 原因:web.xml中<welcome-file>指向的index.jsp不存在,或index.jsp放在webapp/下却未在WEB-INF/外;
→ 解决:确认src/main/webapp/index.jsp存在(不在WEB-INF内),且内容为<% response.sendRedirect("login.jsp"); %>跳转到登录页;检查IDE部署设置中「Deployment Assembly」是否包含src/main/webapp目录。

5.2 现象:登录后跳转到list.jsp,页面显示${students}原始EL表达式,而非学生表格

→ 原因:JSP未启用EL表达式,常见于Tomcat高版本默认禁用;
→ 解决:在list.jsp顶部添加<%@ page isELIgnored="false" %>,或在web.xml中全局开启(Servlet 3.0+):

<jsp-config> <jsp-property-group> <url-pattern>*.jsp</url-pattern> <el-ignore>false</el-ignore> </jsp-property-group> </jsp-config>

5.3 现象:MySQL插入中文显示为??,但Navicat里看是正常的

→ 原因:JDBC URL缺少字符集参数,或Tomcat服务器启动参数未设-Dfile.encoding=UTF-8;
→ 解决:JDBC URL必须含characterEncoding=UTF-8(已写在context.xml示例中);在IDEA中右键Tomcat →「Edit Configurations」→「VM options」添加-Dfile.encoding=UTF-8。

5.4 现象:StudentDao.findAll()执行时报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver

→ 原因:MySQL驱动jar包未放入WEB-INF/lib/,或Tomcat的lib/目录下放了旧版驱动(如mysql-connector-java-5.1.x);
→ 解决:只保留一份驱动——删除Tomcatlib/下所有mysql-connector*.jar,将mysql-connector-java-8.0.33.jar复制到项目src/main/webapp/WEB-INF/lib/;检查jar包内部是否有com/mysql/cj/jdbc/Driver.class(用7-Zip打开jar验证)。

5.5 现象:删除学生后刷新list.jsp,被删学生仍显示,重启Tomcat才消失

→ 原因:浏览器缓存了GET请求结果,或DAO层未真正执行DELETE SQL;
→ 解决:在StudentListServlet.doGet()开头添加强制不缓存头:

resp.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); resp.setHeader("Pragma", "no-cache"); resp.setDateHeader("Expires", 0);

同时检查DELETE SQL是否执行:在DAO的delete方法中加System.out.println("Executing DELETE SQL...");,确认控制台有输出。


6. 报告与答辩提纲:用三个可验证动作证明你不是Ctrl+C/V,而是真跑通了

课程设计报告不是代码粘贴大赛。答辩老师最想听的,是你能否说清「哪里可能出错、你怎么发现、怎么修复」。以下三个动作,每个都能现场演示,且直击评分核心点。

6.1 演示「连接池压力测试」:用ab命令制造并发,证明DBCP2生效

安装Apache Bench(Linux/macOS自带,Windows需下载):

# 模拟10个用户,各发起50次请求 ab -n 500 -c 10 http://localhost:8080/student-system/list

观察Tomcat控制台:

  • 若看到[INFO] org.apache.tomcat.dbcp.dbcp2.BasicDataSource - ... create connection重复出现但总数≤20,说明连接池生效;
  • 若出现大量java.sql.SQLNonTransientConnectionException: Could not create connection to database server,说明maxTotal设太小或MySQLmax_connections不足(需在MySQL中执行SET GLOBAL max_connections=200;)。

6.2 演示「SQL注入防御」:在登录框输入' OR '1'='1,验证是否仍能绕过密码

修改LoginServlet.doPost(),故意用拼接SQL(仅用于演示):

// 危险写法(演示用,勿在正式代码中出现) String sql = "SELECT * FROM user WHERE username='" + username + "' AND password='" + password + "'";

此时输入admin' --即可登录;
再切回预编译PreparedStatement写法,同样输入,返回「用户名或密码错误」——这就是你报告里要写的「安全加固措施」。

6.3 演示「事务一致性」:模拟转账场景,人为制造异常验证回滚

新增TransferServlet,在student表中加balance字段:

ALTER TABLE student ADD COLUMN balance DECIMAL(10,2) DEFAULT 0.00; UPDATE student SET balance=1000.00 WHERE id=1; -- 张三 UPDATE student SET balance=500.00 WHERE id=2; -- 李四

Servlet中:

conn.setAutoCommit(false); // 关闭自动提交 try { // 张三减100 stmt1.executeUpdate("UPDATE student SET balance=balance-100 WHERE id=1"); // 故意制造空指针(模拟业务异常) String s = null; s.length(); // 抛出NullPointerException // 李四加100(此行不会执行) stmt2.executeUpdate("UPDATE student SET balance=balance+100 WHERE id=2"); conn.commit(); } catch (Exception e) { conn.rollback(); // 必须显式回滚 throw e; }

演示:执行转账后张三余额不变,证明事务回滚生效。这是你区别于「CRUD搬运工」的关键证据。

最后说句实在话:我带过17届Java课设,见过太多同学花3天调通登录,却在web.xml路径上卡8小时。这份笔记里每一个命令、每一行配置、每一个System.out.println,都是当年我蹲在机房里一行行试出来的。它不保证你拿满分,但能让你在答辩时被问到「连接池怎么配的」、「EL表达式为啥不解析」时,不用低头翻手机找答案。希望帮到你。

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

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

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

立即咨询