简介:这是一份基于 JSP+Servlet+JDBC 的学生请假信息管理系统课程设计源码,面向 Java Web 方向的大学生,适合作为课程设计或毕设参考,也可用于学习传统 MVC 开发模式。系统按学生、辅导员、宿管员三类角色划分功能:学生可填写请假单、查询个人请假审核状态与晚归记录;辅导员可审核请假单、导出学生晚归记录,并维护学生、班级、课程等基础信息;宿管员可进行基础信息管理,整体业务流程贴近真实校园管理场景。压缩包共 117 个文件,其中 78 个 JSP 页面构成前端界面,Java 源码与 class 文件对应后端 servlet 与工具类,另有 SQL 数据库脚本、CSS/JS 样式脚本、JAR 依赖库和 Eclipse 项目配置文件,资源总大小仅 2.25MB,结构紧凑便于导入与二次修改。项目采用经典蓝色 UI,前端 JSP 配合 JDBC 访问数据库,登录信息明确(学生/辅导员/宿管员账号分别为 1、2、3,密码同为对应数字),导入数据库即可运行。已有 2404 人浏览学习,对需要快速搭建完整课设或理解 servlet+jsp 分层项目骨架的同学具有直接参考价值。
1. 学生请假信息管理系统课程设计:为什么是 JSP+Servlet+JDBC 这套老三样
每年课程设计提交前一两个月,被问得最多的 JavaWeb 项目,学生请假信息管理系统一定排在前三。原因不难猜:它的功能边界清晰——学生提交请假、辅导员审批、双方都能查状态;技术栈又是教科书里最标准的 JSP+Servlet+JDBC,一个学期讲的内容全都能装进去。选这个标题的人,多半不是想做出什么惊人系统,而是想要一个自己能讲清楚、能过答辩、还能在这段代码里学到点真东西的项目。但我要先说一个反直觉的结论:这个课设的翻车点从来不在业务逻辑,而在版本搭配、中文编码和请求转发这三件小事上。把这三件事提前处理好,剩下的只是按部就班写代码。
2. 打一个能跑通的最小骨架:版本搭配、目录结构、web.xml 与第一个 JSP
2.1 为什么课设选了它:Servlet 是入口,JDBC 是通道,JSP 是页面
很多同学上来就问能不能直接用 Spring Boot 写。我的建议是课设阶段别急着上框架。Spring Boot 确实省事,但你交上去的是一份课程设计,答辩老师大概率会追问“请求是怎么从页面到数据库的”“Tomcat 是怎么认识你的 Servlet 的”。这时候 Spring Boot 的内嵌 Tomcat 和自动配置反而成了黑匣子,你很难一句话讲清楚请求链路。
JSP+Servlet+JDBC 的经典三层里,每一层都有明确的活:JSP 负责把数据渲染成页面,Servlet 负责接 HTTP 请求、调业务、做跳转,JDBC 负责和 MySQL 打交道。教师端审批、学生端请假、状态查询,全部落在这条链路上。这套代码写完之后,你对“一次请求到底经历了什么”会有完整的概念,这是框架帮你代劳时感受不到的。
所以这个技术组合不是因为它新,而是因为它“透明度高”。任何一个环节出错,你都能从 Tomcat 日志和代码行里定位到具体位置。对你写简历、面试讲项目也是同样逻辑——原生 Servlet 的东西讲明白了,框架对你的加分是几何级增长的。
2.2 版本搭配别信玄学:我常用的 JDK、Tomcat、MySQL 组合
这个课设最容易在第一步就劝退人:环境版本之间互相不兼容。我现在常用的搭配如下表,按优先级排列:
| 场景 | JDK | Tomcat | MySQL | 说明 |
|---|---|---|---|---|
| 最稳妥(首选) | JDK 8 | Tomcat 9.0.x | MySQL 8.0 | 教科书兼容性最好,网上资料最全 |
| 机器装了新版 JDK | JDK 11 | Tomcat 9.0.x | MySQL 8.0 | Tomcat 9 支持到 JDK 11 没问题 |
| 只想轻量跑 | JDK 8 | Tomcat 8.5 | MySQL 5.7 | 老环境,驱动用 5.x 也可以 |
| 不推荐 | JDK 17+ | Tomcat 8.5 | MySQL 8.0 | JSP 编译经常报版本错误 |
这里有一个常见误区:版本越新越好。JDK 17 配新 Tomcat 本来可行,但很多课程设计给的是老机器的实验室环境,Tomcat 8.5 配 JDK 17 会出现 JSP 编译失败;而 Tomcat 10 之后包名从 javax.servlet 变成了 jakarta.servlet,你从网上复制的代码可能直接报“包不存在”。所以我的习惯是“先问清实验室版本再动工”,如果自己搭环境,无脑选第一行。
2.3 最小工程长这样:目录结构、web.xml 与一个能跑的 JSP
先看一个能跑通的最小工程长什么样,这里以 IDEA 的 Web 工程为例:
leave-system/ ├── src/ │ └── com/example/leave/ │ ├── servlet/ # 控制器:接收请求、调 DAO、跳转 │ ├── dao/ # 数据访问层:所有 JDBC 代码 │ ├── entity/ # 实体类:Student、LeaveInfo │ └── filter/ # 拦截器:登录校验、编码处理 ├── web/ │ ├── WEB-INF/ │ │ ├── lib/ # mysql-connector-java.jar 必须放这里 │ │ └── web.xml # Servlet 3.0/4.0 配置入口 │ ├── login.jsp # 入口页面 │ ├── add.jsp # 请假申请页 │ └── list.jsp # 请假列表页 └── .gitignore注意WEB-INF/lib这个目录,JDBC 驱动 jar 只能放这里,不能随手丢到web/根目录下,否则 Tomcat 启动后找不到驱动类。如果你用 IDEA 的 Artifact 打包,要确认lib目录被包含进了WEB-INF。这一步错了,后面所有 JDBC 代码都报ClassNotFoundException。
然后是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>student-leave-system</display-name> <!-- 欢迎页:访问根路径时默认进入登录页 --> <welcome-file-list> <welcome-file>login.jsp</welcome-file> </welcome-file-list> <!-- 如果你的 Servlet 用注解 @WebServlet,这里可以留空; 如果兼容老写法,就在 <servlet> 里逐个声明 --> </web-app>这个文件的关键是把web-app的 version 属性和你用的 Tomcat 对上。Tomcat 9 对应 Servlet 4.0,写version="4.0"能让 JSP 支持 EL 表达式和 JSTL 的默认行为。跑通验证也很简单:启动 Tomcat,浏览器访问http://localhost:8080/leave-system/,能看到login.jsp的静态内容说明骨架没问题。先别急着写业务,这一步稳了再往下走。
3. 数据库与 JDBC 层:请假表设计、连接参数和 DAO 的一手写法
3.1 三张表把请假的账记清楚:学生表、请假表与字段设计
学生请假信息管理系统的命根子在数据库。很多课设翻车不是因为代码,而是因为字段设计太随意:请假状态用字符串存、时间用VARCHAR存、审批人信息不落表。到了答辩环节,老师只要问一句“你如何统计某学生本学期请假次数”“如何查某个辅导员审批过哪些单”,你就要为当时的偷懒买单。
我一般把数据拆成三张表:学生账号表、请假申请表,再加一张可选的审批记录表(课设简版可以合并进请假表)。字段设计如下:
CREATE DATABASE IF NOT EXISTS leave_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE leave_db; -- 学生/教师账号表,role 字段区分身份 CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID', name VARCHAR(20) NOT NULL COMMENT '姓名', password VARCHAR(64) NOT NULL COMMENT '密码', role TINYINT NOT NULL DEFAULT 1 COMMENT '1学生 2辅导员', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB COMMENT '账号表'; -- 请假申请表 CREATE TABLE apply_leave ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '请假单ID', student_id INT NOT NULL COMMENT '申请人ID,对应student.id', leave_type TINYINT NOT NULL DEFAULT 0 COMMENT '0事假 1病假 2其他', start_time DATETIME NOT NULL COMMENT '请假开始时间', end_time DATETIME NOT NULL COMMENT '请假结束时间', reason VARCHAR(200) NOT NULL COMMENT '请假事由', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待审批 1已通过 2已驳回 3已撤销', apply_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '提交时间', audit_time DATETIME DEFAULT NULL COMMENT '审批时间,未审批为NULL', audit_by INT DEFAULT NULL COMMENT '审批人ID,未审批为NULL', KEY idx_student (student_id), KEY idx_status (status), CONSTRAINT fk_leave_student FOREIGN KEY (student_id) REFERENCES student(id) ) ENGINE=InnoDB COMMENT '请假申请表';这里有几个字段设计是答辩加分项。一个是status用TINYINT而不是VARCHAR,后端用常量对应 0/1/2/3,数据库层还能加约束,避免出现status='已通过'和status='审批通过'这种不一致。另一个是audit_time和audit_by允许 NULL,没有审批前就是 NULL,这比默认值填 0 干净得多。时间字段统一用DATETIME,字符串时间排序、比较都是坑。
3.2 JDBC 连接 MySQL:驱动、URL 参数与 DBUtil 的写法
JDBC 连接 MySQL 是整条链路上最容易出幺蛾子的地方。MySQL 5.7 和 8.0 的驱动类名不一样,URL 参数也有讲究。我常用的连接工具类如下,这段代码你可以直接抄:
package com.example.leave.dao; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class DBUtil { // 注意:characterEncoding、serverTimezone、allowPublicKeyRetrieval 三个参数缺一不可 private static final String URL = "jdbc:mysql://localhost:3306/leave_db" + "?useSSL=false" + "&allowPublicKeyRetrieval=true" + "&characterEncoding=utf8" + "&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PASSWORD = "你自己的密码"; static { try { // MySQL 8.x 驱动类名是 com.mysql.cj.jdbc.Driver Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError("MySQL驱动未找到,请检查WEB-INF/lib"); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } // 统一的关闭方法:ResultSet -> Statement -> Connection,反序关闭 public static void close(ResultSet rs, Statement stmt, Connection conn) { if (rs != null) { try { rs.close(); } catch (SQLException ignored) {} } if (stmt != null) { try { stmt.close(); } catch (SQLException ignored) {} } if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } } }几个参数逐个说清楚:characterEncoding=utf8保证中文字段从数据库出来不是乱码;serverTimezone=Asia/Shanghai是 MySQL 8 强制要求的时区参数,不加会报 “The server time zone value” 的错误;allowPublicKeyRetrieval=true配合useSSL=false解决 MySQL 8 默认认证插件导致的 “Public Key Retrieval is not allowed” 报错,这个坑在第五章细讲。
如果你用的是 MySQL 5.7,驱动类名改回com.mysql.jdbc.Driver,URL 里可以去掉serverTimezone。判断依据很简单:你下载的mysql-connector-java是 8.x 还是 5.x,8.x 必须用带cj的类名。
3.3 DAO 层:PreparedStatement、try-with-resources 与 ResultSet 映射
数据库连接有了,接下来是 DAO。很多同学喜欢在 JSP 或者 Servlet 里直接写 JDBC,代码混在一起,出了问题没法查。我的习惯是每个实体类配一个 DAO,比如LeaveDao.java负责所有和请假表相关的 SQL。看一个查询学生请假列表的方法:
package com.example.leave.dao; import com.example.leave.entity.LeaveInfo; import java.sql.*; import java.util.ArrayList; import java.util.List; public class LeaveDao { // 查询某个学生的全部请假记录,按提交时间倒序 public List<LeaveInfo> listByStudent(int studentId) { String sql = "SELECT * FROM apply_leave WHERE student_id = ? ORDER BY apply_time DESC"; List<LeaveInfo> result = new ArrayList<>(); // try-with-resources 自动关闭 Connection/Statement/ResultSet,省去 finally try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, studentId); // 占位符从 1 开始,禁止用字符串拼接 try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { LeaveInfo leave = new LeaveInfo(); leave.setId(rs.getInt("id")); leave.setStudentId(rs.getInt("student_id")); leave.setLeaveType(rs.getInt("leave_type")); leave.setStartTime(rs.getTimestamp("start_time")); leave.setEndTime(rs.getTimestamp("end_time")); leave.setReason(rs.getString("reason")); leave.setStatus(rs.getInt("status")); leave.setApplyTime(rs.getTimestamp("apply_time")); // audit_time 允许为 NULL,取值前先判空 Timestamp auditTime = rs.getTimestamp("audit_time"); if (auditTime != null) { leave.setAuditTime(auditTime); } result.add(leave); } } } catch (SQLException e) { // 课设阶段先打印,生产环境应使用日志框架 e.printStackTrace(); } return result; } }这段代码有三个值得坚持的习惯。第一,永远用PreparedStatement而不是Statement拼接字符串,?占位符能防 SQL 注入,也是答辩时老师爱问的点。第二,ResultSet的取值方法要和数据库字段类型匹配,DATETIME用getTimestamp,TINYINT用getInt,类型错位会报SQLException。第三,涉及可空字段时一定判空,直接rs.getTimestamp("audit_time")返回 null 没问题,但把它塞进实体类的Timestamp属性要注意后续页面的空指针。
写到这里可以验证一下链路:写一个简单的 main 方法调listByStudent(1),能查出数据说明 JDBC 通了。先不要往下写 Servlet,不然出问题时分不清是数据库问题还是接口问题。
4. Servlet 与请假状态流转:doPost 里的参数解析、审批与登录控制
4.1 请求是怎么从 JSP 走到数据库的:doGet/doPost 与 forward/redirect
JSP 页面里的表单提交后,请求流转链路是这样的:JSP 表单的 action 指向一个 Servlet 地址,Tomcat 根据@WebServlet注解找到对应类,调用doGet或doPost方法;Servlet 从request里取参数,调 DAO 访问数据库,最后通过转发forward或重定向sendRedirect把结果送回页面。
这里最容易糊涂的是 forward 和 redirect 的选择。我的原则是:查询列表用 forward,数据变更用 redirect。原因很简单——用户提交一张请假单,如果直接 forward 回列表页,地址栏还停在提交地址,用户一按 F5,表单又提交了一遍,数据库里多出一条重复数据。这是经典 POST/Redirect/GET 问题,后面的避坑章还会提。
看一个新增请假的 Servlet 核心方法:
package com.example.leave.servlet; import com.example.leave.dao.LeaveDao; import com.example.leave.entity.Student; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; @WebServlet("/leave/add") public class LeaveAddServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { // 1. 从 Session 取当前登录用户,没有则回登录页 HttpSession session = req.getSession(false); Student student = (Student) session.getAttribute("loginUser"); if (student == null) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } // 2. 参数解析:request 里取出来的都是字符串,按需转换 String leaveType = req.getParameter("leaveType"); // 0事假 1病假 String startTime = req.getParameter("startTime"); // yyyy-MM-dd HH:mm:ss String endTime = req.getParameter("endTime"); String reason = req.getParameter("reason"); // 3. 基础校验:时间倒挂直接退回填写页,error=time 让页面提示 if (startTime == null || endTime == null || startTime.compareTo(endTime) >= 0) { resp.sendRedirect(req.getContextPath() + "/add.jsp?error=time"); return; } // 4. 调用 DAO 插入数据,拿到自增主键 LeaveDao dao = new LeaveDao(); int newId = dao.insert( student.getId(), Integer.parseInt(leaveType), startTime, endTime, reason); // 5. 写完后重定向到列表页,防止刷新重复提交 resp.sendRedirect(req.getContextPath() + "/leave/list"); } }注意两个细节。req.getSession(false)里的false表示“拿不到 Session 就返回 null”,而不是创建一个新 Session,用在登录校验上语义更准确。req.getContextPath()是获取应用上下文路径,重定向 URL 必须带上它,否则部署到http://localhost:8080/leave-system/这种子路径时,重定向会跳错地方。
4.2 请假状态机:待审批、通过、驳回、撤销的四态流转与并发安全
请假系统里最关键的业务逻辑是状态流转。我的状态设计是四个值:
| 状态值 | 含义 | 学生端可见操作 | 辅导员端可见操作 |
|---|---|---|---|
| 0 | 待审批 | 撤销 | 通过 / 驳回 |
| 1 | 已通过 | 无 | 无 |
| 2 | 已驳回 | 无 | 无 |
| 3 | 已撤销 | 无 | 无 |
这个状态机有个常见的翻车点:撤销操作只允许在待审批状态下进行。很多实现只在 Servlet 里用 if 判断,比如if (status == 0) { cancel },这在单用户测试时没问题,但如果学生和辅导员同时操作同一条记录,Servlet 里读到的状态可能是旧的,导致数据被覆盖。稳妥的做法是把状态约束写进 SQL 条件里:
public int cancelIfPending(int leaveId, int studentId) { String sql = "UPDATE apply_leave SET status = 3 " + "WHERE id = ? AND student_id = ? AND status = 0"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, leaveId); ps.setInt(2, studentId); return ps.executeUpdate(); // 返回 0 说明状态已被他人变更 } catch (SQLException e) { e.printStackTrace(); return 0; } }executeUpdate返回 0 时,说明没有任何行被更新——要么数据不存在,要么状态已经不是 0。这种“条件更新”是保证并发安全最简单的方式,也是答辩时可以拿出来讲的点。审批操作同理,改成WHERE id = ? AND status = 0,辅导员就不用担心批了一张已被学生撤销的单子。
4.3 登录态与权限控制:一个 @WebFilter 拦截所有 /leave/* 请求
如果每个 Servlet 开头都写一遍登录校验,代码会非常啰嗦,也容易漏。我用一个 Filter 统一拦截leave/*路径:
package com.example.leave.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.*; import java.io.IOException; @WebFilter("/leave/*") 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; // 没有 Session 或者 Session 里没有登录用户,一律回登录页 HttpSession session = request.getSession(false); if (session == null || session.getAttribute("loginUser") == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } // 已登录则放行,继续执行后面的 Servlet chain.doFilter(request, response); } }这个 Filter 能挡住所有未登录访问,但有个边界要注意:login.jsp、登录 Servlet 本身不能放在/leave/*下面,否则会出现死循环——登录页也要被登录检查拦截,用户永远进不去。我的做法是登录页直接放根目录,登录接口用@WebServlet("/login"),不归这个 Filter 管。辅导员身份的操作同理,可以在 Filter 里再判断loginUser.getRole() == 2,或者单独给/leave/audit/*加一个角色校验 Filter。
到这里,请假提交、列表查询、审批、撤销这条主链路已经通了。JSP 页面就是把这些接口的数据渲染出来,比如学生个人信息展示页面既然能查到该生全部请假记录,就可以顺带把待审批和已通过的记录分开列表展示,状态值用 EL 表达式映射成中文,这部分篇幅不多,但页面细节决定了整个项目像不像“系统”,别只堆一排数字。
5. 避坑与常见问题排查:课程设计里最容易翻车的 5 个环节
5.1 中文乱码的三处源头:页面、请求响应与数据库连接
现象:JSP 页面显示中文正常,但提交到数据库后变成???,或者页面直接显示一堆乱码。
原因:中文乱码本质是三层编码不一致——JSP 文件本身的编码、HTTP 请求响应的编码、数据库连接的编码,任何一层用的是默认 ISO-8859-1,就会乱。
解决:三层统一成 UTF-8。JSP 文件顶部写<%@ page contentType="text/html;charset=UTF-8" %>;Servlet 里在读取任何参数之前调用req.setCharacterEncoding("UTF-8"),返回页面时写resp.setContentType("text/html;charset=UTF-8");JDBC URL 里带characterEncoding=utf8。建表时统一用utf8mb4,这三处搞定,全链路不存在乱码。
5.2 连 MySQL 8 报 Public Key Retrieval is not allowed
现象:第一次执行 JDBC 查询时抛异常,Caused by: java.sql.SQLException: Public Key Retrieval is not allowed。
原因:MySQL 8 默认的认证插件是caching_sha2_password,非 SSL 连接下客户端要获取服务器公钥,而驱动默认不允许自动获取。这个报错只出现在 MySQL 8 加新版驱动组合下。
解决:JDBC URL 追加两个参数:allowPublicKeyRetrieval=true&useSSL=false,也就是我在 DBUtil 里写的那套。如果你不想改代码,也可以在 MySQL 里把用户认证插件改回旧版:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码'; FLUSH PRIVILEGES;改数据库用户的方式对老代码兼容性更好,但课设阶段我更推荐改 URL,不动数据库配置更省事。
5.3 JSP 编译报 500:Tomcat 与 JDK 版本不匹配的翻车现场
现象:Tomcat 启动成功,访问 JSP 页面却报 500,堆栈里有Unable to compile class for JSP或者UnsupportedClassVersionError。
原因:JSP 在运行时会被翻译成 Java 类再编译,Tomcat 自带编译器和 JDK 版本不匹配时就会翻车。常见组合是 JDK 17 配 Tomcat 8.5,或者是 Tomcat 10 配网上复制的javax.servlet代码——Tomcat 10 已经改用jakarta.servlet包名,老代码全部编译不过。
解决:直接换版本组合,最省心的是回到 JDK 8 加 Tomcat 9。如果机器上 JDK 版本改不了,就在 IDEA 里给项目单独配一个 SDK,Project Structure 里把 Project SDK 和 Module SDK 都指到 JDK 8,不一定非要改系统环境变量。
5.4 刷新页面重复提交:用 PRG 而不是在 JSP 里刷一次
现象:提交请假单后,按 F5 刷新,页面弹提示“重新提交表单”,数据库里出现多条相同的请假记录。
原因:表单是 POST 请求,Servlet 处理完如果用的是forward转发到列表页,浏览器地址栏里还是 POST 地址,刷新自然把 POST 又执行了一遍。网上有人建议让 JSP 页面加载完后刷新一次来清掉请求,那是治标不治本——请求已经执行了。
解决:数据变更的操作,Servlet 里一律用sendRedirect,跳转后浏览器发起的是全新 GET 请求,F5 刷新只是重新查一次列表,不会新增数据。这是 PRG(Post-Redirect-Get)模式的简化版,也是“写操作重定向,读操作转发”这条规则的实际意义。
5.5 找不到驱动类:jar 放错位置与 8.x 驱动类名变更
现象:Tomcat 启动时或第一次访问数据库时报ClassNotFoundException: com.mysql.jdbc.Driver。
原因:两种情况。一是mysql-connector-java.jar没放进WEB-INF/lib,而是放在了 IDE 的某个外部目录,运行时 Tomcat 根本看不见;二是代码里写的是旧驱动类名com.mysql.jdbc.Driver,但 jar 是 8.x,8.x 的类名已经变成com.mysql.cj.jdbc.Driver。
解决:先确认 jar 位置——IDEA 里把它复制到web/WEB-INF/lib,且确认 Artifact 输出里有这个 jar。再看代码里的Class.forName字符串,MySQL 8 用com.mysql.cj.jdbc.Driver,5.x 用com.mysql.jdbc.Driver。如果两个都确认了还报错,删除 Tomcat 的work目录再重启,清一下缓存。
6. 给课设加分:一个统一过滤器、一条演示路径和一晚答辩预案
先说一个能立刻提升代码质量的改动:把 4.3 的登录 Filter 扩展成“编码 + 登录”二合一,在chain.doFilter之前加两行request.setCharacterEncoding("UTF-8")和response.setContentType("text/html;charset=UTF-8"),这样所有 Servlet 里不再需要重复设置编码,代码瞬间瘦身。这个 Filter 在答辩时特别好讲——从“一个请求进来先过 Filter 再进 Servlet”切入,能自然带出过滤器链、拦截范围、白名单设计三个考点。
第二个加分点是把连接池换进去。课设阶段用DriverManager没问题,但如果你想展现「我知道生产环境不能这么写」,可以加一个 HikariCP 的配置文件,把dataSource.getConnection()换成从连接池取连接,只改DBUtil一个类,其他 DAO 全都不用动。答辩被问“你觉得这个系统哪里可以优化”,这就是现成答案。
第三个是演示路径。我自己带课设时,要求每个项目准备固定的演示顺序:先用学生账号登录,展示请假列表页的权限隔离;提交一条请假申请,当场在数据库里SELECT出这条记录证明数据落库;然后切换辅导员账号,审批通过这条申请;最后切回学生账号刷新列表页,状态从“待审批”变成“已通过”。这套流程走一遍,Servlet、JDBC、状态机、Session 全部覆盖,比临时乱点有说服力。数据库里记得提前造好数据:两个学生账号、一个辅导员账号、几条不同状态的请假记录,别等到演示现场才插入。
最后一个教训是很多人会犯的:遇到SQLException直接空捕获,或者把e.printStackTrace()注释掉假装没发生。课设代码里打印可能有失优雅,但生产环境排查问题靠的就是异常堆栈。我会在项目里保留最简方案:控制台打印,只要它在演示时不炸,答辩时老师问你哪里最容易出错,你能打开控制台指着报错讲出原因,这本身就是加分。这个习惯我保留到现在,也希望帮到你。
本文还有配套的精品资源,点击获取