
简介一套基于 jspservletjdbc 的学生请假信息管理系统课程设计面向计算机相关专业大学生用于期末课设或毕业设计参考。系统按学生、辅导员、宿管员三类角色划分功能涵盖学生个人信息查看、请假单填写与审核进度查询辅导员端请假审核、晚归记录导出及学生/班级/课程信息管理宿管员端则包含基础信息管理整体业务流程完整、角色权限清晰。资源包共 117 个文件以 JSP 页面为主辅以 Java 源码、Class 编译文件、数据库 SQL、Jar 依赖库及少量 CSS/JS 资源压缩包大小约 2.25MBSQL 文件可供直接导入数据库源码结构便于二次开发与功能扩展。已有 2404 人学习下载适合需要快速上手 ServletJDBC 整合开发、理解请假审批流程或补充课程设计文档的读者使用。1. 学生请假信息管理系统为什么课程设计选 JSPServletJDBC 组合依然能打学生请假信息管理系统是 Java 课程设计里出现频率最高的题目之一。它的核心痛点很明确手工请假流程繁琐纸质单子容易丢统计起来更是折磨。这个题目很适合用来串起 JSP 做页面展示、Servlet 做请求控制、JDBC 做数据库读写这三块技能。很多人会纠结要不要直接上 Spring Boot但课程设计这个场景下ServletJSPJDBC 反而能把 Web 请求的底层逻辑看得更清楚。如果你正面临这门课设想用最短时间做出一个能跑、能演示、能答辩的项目这篇笔记会告诉你完整路径。2. 技术选型拆解Servlet 管控制、JSP 管展示JDBC 管数据为什么不直接上框架2.1 课程设计场景下传统三层比 Spring Boot 更合适很多同学看到 Spring Boot 就眼热觉得课设标题写上 Spring Boot 就上档次。但实际情况是课程设计的评分重点往往是“你是否真的理解了 Web 开发的基本流程”。JSPServletJDBC 这个组合是 Java Web 的原始形态一次请求从 JSP 页面发出经过 Servlet 的 doGet/doPost 方法再到 JDBC 代码操作数据库没有任何框架帮你蒙住细节。老师问“Servlet 的生命周期是什么”“JDBC 连接怎么关闭”你能直接答上来这才是课设答辩的底气。如果你已经会 Spring Boot再回来写这三件套反而要注意别把框架思维带进来。比如 Spring Boot 里你习惯用 Autowired 注入 Service但这里你要手动 new DAO 对象手动在 finally 里关闭 Connection。这些动作虽然繁琐但它还原了 Web 应用的原始面貌。从时间成本来看这三个组件的学习曲线比 Spring Boot 全家桶短得多对只修了 Java 和数据库课程的学生来说更友好。2.2 项目目录结构和运行环境Tomcat 9 MySQL 5.7 的最小配置先说我常用的课设环境组合JDK 1.8Tomcat 9.0MySQL 5.7 或 8.0Eclipse 或 IDEA 的 Dynamic Web Project 或者 Maven 项目。如果是纯手工课设我建议直接用 IDEA 的 Java Enterprise 项目模板。核心的目录结构是下面这样的src/ ├── com/student/leave/ │ ├── dao/ # 数据访问层 │ │ ├── BaseDao.java │ │ ├── StudentDao.java │ │ └── LeaveDao.java │ ├── model/ # 实体类 │ │ ├── Student.java │ │ └── Leave.java │ ├── servlet/ # 控制器层 │ │ ├── LoginServlet.java │ │ ├── LeaveApplyServlet.java │ │ └── LeaveApproveServlet.java │ └── util/ │ └── DBUtil.java # JDBC 连接工具类 webapp/ ├── index.jsp ├── login.jsp ├── student/ │ ├── apply_leave.jsp │ └── my_leave.jsp ├── admin/ │ └── approve_leave.jsp └── WEB-INF/ ├── web.xml └── lib/ # mysql-connector-java.jar这个目录结构对应了 MVC 思想model 是实体类dao 负责数据操作servlet 是控制器jsp 是视图。注意 WEB-INF/lib 里要放 mysql-connector-java.jar否则后面 JDBC 连接 MySQL 时会直接报 ClassNotFoundException。Tomcat 的 server.xml 里端口默认 8080如果被占用可以改成 8081这个改动在答辩前一定要确认现场换端口容易慌。2.3 一次请假申请的完整请求流转从 JSP 表单到数据库的路径理解了目录结构之后最关键的是理清请求流转路径。我用最典型的“学生提交请假申请”来走一遍完整链路学生在student/apply_leave.jsp表单里填写请假开始时间、结束时间、理由点提交按钮。表单的 action 指向 LeaveApplyServletmethod 为 POST。Tomcat 收到请求后根据 web.xml 里的 url-pattern 或者 WebServlet 注解找到对应的 Servlet 类。Servlet 的 doPost 方法调用request.getParameter(startDate)等方法取出表单参数。把参数封装成 Leave 实体对象调用 LeaveDao 的 insert 方法。LeaveDao 内部用 JDBC 的 PreparedStatement 执行 INSERT SQL。Connection 来自 DBUtil 工具类执行完成后在 finally 里关闭连接。返回成功页面通过response.sendRedirect(my_leave.jsp)跳转。这条链路每一步都有明确的职责。JSP 只负责收集和展示数据不写业务逻辑Servlet 只做参数接收和流程跳转不写 SQLDAO 只面对数据库不关心页面长什么样。这样分层的好处是答辩时你可以清楚地说出“我用了 MVC 模式”。注意很多同学图省事把 SQL 写在 Servlet 里。课设验收时老师如果让你现场改一个字段名你会发现 Servlet 和 JSP 里到处都要改。把 SQL 收拢到 DAO 层是成本最低的架构优化没有之一。3. 数据库设计及 JDBC 连接层三张表、一个工具类、一套增删改查封装3.1 核心表设计学生表、请假表、审批人表字段别贪多学生请假信息管理系统的数据模型不复杂但表结构设计得好不好直接影响后续的 SQL 写起来舒不舒服。我通常只用三张表学生表存登录和身份信息请假表存申请内容与审批状态审批人表存辅导员或管理员。CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) UNIQUE NOT NULL, name VARCHAR(50) NOT NULL, class_name VARCHAR(50), password VARCHAR(50) NOT NULL ); CREATE TABLE approver ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, role VARCHAR(20) ); CREATE TABLE leave ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, start_date DATE NOT NULL, end_date DATE NOT NULL, days INT, reason VARCHAR(500), status TINYINT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, approver_id INT, approve_comment VARCHAR(200), FOREIGN KEY (student_id) REFERENCES student(id), FOREIGN KEY (approver_id) REFERENCES approver(id) );字段设计上有几个习惯可以参考days字段不建议在 Java 代码里手动算直接插入前用 SimpleDateFormat 算好或者查询时用 SQL 的DATEDIFF(end_date, start_date) 1生成。status用 TINYINT 而不是 VARCHAR因为数字状态在 SQL 里比较和统计都更高效。create_time设置 DEFAULT CURRENT_TIMESTAMP插入时就不需要显式赋值。表名leave在 MySQL 里不是保留字但为了保险我习惯写成反引号包裹的形式避免某些版本数据库报语法错误。3.2 JDBC 连接工具类DriverManager 连接 MySQL 的完整写法JDBC 连接 MySQL 是整套系统的基础推荐写一个工具类 DBUtil把驱动加载、连接获取、资源释放都收敛到一起后面所有 DAO 都从这儿拿连接。public class DBUtil { // useUnicode 和 characterEncodingutf8 是中文不乱码的关键参数 private static final String URL jdbc:mysql://localhost:3306/leave_db ?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 123456; static { // MySQL 8.0 必须用 cj 版本的驱动类名 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, PreparedStatement ps, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (ps ! null) ps.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn ! null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }这段代码里最容易被忽略的是 URL 参数。MySQL 8.0 之后驱动类名变成了com.mysql.cj.jdbc.Driver而 MySQL 5.7 用的还是com.mysql.jdbc.Driver写错直接 ClassNotFoundException。URL 里的serverTimezoneAsia/Shanghai不加的话连接时会报时区错误useSSLfalse则是为了省去本地开发时的 SSL 证书告警。getConnection 每次新建物理连接在课设这种低并发场景下完全够用如果答辩被问到性能问题就说“生产环境会用连接池课设为了展示 JDBC 原理所以用 DriverManager”这是加分回答。3.3 BaseDao 封装把增删改查写成通用方法减少重复代码每张表都写一套完整的 CRUD 很啰嗦我习惯先封装一个 BaseDao把通用的查询、更新逻辑抽出来LeaveDao 和 StudentDao 里只需要写各自的 SQL 和参数即可。这里的关键是使用可变参数和 PreparedStatement既能防 SQL 注入又能让代码保持干净。public class BaseDao { protected Connection conn; protected PreparedStatement ps; protected ResultSet rs; // 通用更新参数按 SQL 中 ? 的顺序依次传入 protected int executeUpdate(String sql, Object... params) { int rows 0; try { conn DBUtil.getConnection(); ps conn.prepareStatement(sql); for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } rows ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.close(conn, ps, null); } return rows; } // 通用查询返回 Map 列表列名作为 key兼容任意表结构 protected ListMapString, Object executeQuery(String sql, Object... params) { ListMapString, Object list new ArrayList(); try { conn DBUtil.getConnection(); ps conn.prepareStatement(sql); for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } rs ps.executeQuery(); ResultSetMetaData md rs.getMetaData(); while (rs.next()) { MapString, Object row new HashMap(); for (int i 1; i md.getColumnCount(); i) { row.put(md.getColumnLabel(i), rs.getObject(i)); } list.add(row); } } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.close(conn, ps, rs); } return list; } }这两个方法的参数设计是核心。Object... params是可变参数调用时直接executeUpdate(sql, studentId, startDate)就行不用自己拼字符串能有效防止 SQL 注入。executeQuery返回ListMapString, Object用列名做 key这样不同表的查询结果都能用同一套代码处理不用每个 DAO 都写一遍 ResultSet 遍历。这里特意用md.getColumnLabel(i)而不是getColumnName(i)是为了让 SQL 里的字段别名也能正常映射后面 JSP 回显时省很多事。4. Servlet 控制器与 JSP 视图交互请假申请、审批流转和页面回显4.1 请假申请 ServletdoPost 里如何接收参数并组装实体BaseDao 搭好了这一章要落到的核心是 Servlet 和 JSP 的协同。先说学生提交请假这个最核心的流程。表单在apply_leave.jsp里提交后发给LeaveApplyServlet它的 doPost 方法代码如下WebServlet(/applyLeave) public class LeaveApplyServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); HttpSession session req.getSession(); Student student (Student) session.getAttribute(student); String startDate req.getParameter(startDate); String endDate req.getParameter(endDate); String reason req.getParameter(reason); Leave leave new Leave(); leave.setStudentId(student.getId()); leave.setStartDate(Date.valueOf(startDate)); leave.setEndDate(Date.valueOf(endDate)); leave.setReason(reason); LeaveDao dao new LeaveDao(); int result dao.insertLeave(leave); if (result 0) { resp.sendRedirect(student/my_leave.jsp); } else { req.setAttribute(error, 申请失败请重试); req.getRequestDispatcher(student/apply_leave.jsp).forward(req, resp); } } }这里有三个细节值得讲。第一req.setCharacterEncoding(UTF-8)必须放在所有getParameter之前否则 POST 请求里的中文参数必然乱码。第二Date.valueOf(startDate)是把页面传来的2025-03-01这类字符串转成 java.sql.Date 的标准写法表单控件用input typedate时格式正好能匹配。第三失败时用forward而不是sendRedirect这样能把error属性带到 JSP 页面里回显给用户而不是无声无息地跳走。提交成功后的sendRedirect则是必须的它能打断刷新时的重复 POST 提交问题这个后面避坑章还会展开。4.2 审批与状态流转UPDATE 语句实现待审批、通过、驳回审批功能是管理系统里最能体现设计思路的部分。辅导员打开approve_leave.jsp页面看到所有 status0 的记录每条记录后面有“通过”和“驳回”两个按钮两个按钮共用同一个 ApproveServlet用隐藏字段区分动作WebServlet(/approveLeave) public class ApproveLeaveServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String leaveId req.getParameter(leaveId); String action req.getParameter(action); String comment req.getParameter(comment); // 用数字状态机0 待审批1 通过2 驳回 int status pass.equals(action) ? 1 : 2; int approverId 1; // 实际从 session 里获取审批人 ID LeaveDao dao new LeaveDao(); boolean ok dao.updateStatus(Integer.parseInt(leaveId), status, approverId, comment); if (ok) { resp.sendRedirect(admin/approve_leave.jsp); } else { req.setAttribute(error, 审批操作失败); req.getRequestDispatcher(admin/approve_leave.jsp).forward(req, resp); } } }对应的 DAO 方法只需要一条 UPDATEpublic boolean updateStatus(int leaveId, int status, int approverId, String comment) { String sql UPDATE leave SET status?, approver_id?, approve_comment? WHERE id?; int rows executeUpdate(sql, status, approverId, comment, leaveId); return rows 0; }这个简单的状态机设计在答辩时可以讲出不少内容状态用数字而非字符串杜绝了拼写错误每个状态转换都在同一处收口后续如果加“已销假”状态只需要改这里和页面枚举。如果你想让系统更真实可以在 UPDATE 前加一层判断比如“只有 status0 的记录才能执行审批”用一个WHERE id? AND status0就能防止重复审批覆盖前面审批人的意见。4.3 JSP 页面回显与记录列表JSTLEL 表达式替代 Java 脚本段很多课设的 JSP 页面里直接写% for (...) { %能跑但可读性极差。推荐用 JSTL 的c:forEach配合 EL 表达式来展示请假列表代码干净而且答辩时更有说服力。学生个人信息展示页面和请假记录列表都用同一套思路。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html body table trth理由/thth开始日期/thth结束日期/thth天数/thth状态/th/tr c:forEach items${leaveList} varleave tr td${leave.reason}/td td${leave.startDate}/td td${leave.endDate}/td td${leave.days}/td td c:choose c:when test${leave.status 0}待审批/c:when c:when test${leave.status 1}已通过/c:when c:otherwise已驳回/c:otherwise /c:choose /td /tr /c:forEach /table /body /html对应的 Servlet 查询端要在 doGet 里把数据放到 request 作用域protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session req.getSession(); Student student (Student) session.getAttribute(student); LeaveDao dao new LeaveDao(); ListMapString, Object list dao.findByStudentId(student.getId()); req.setAttribute(leaveList, list); req.getRequestDispatcher(student/my_leave.jsp).forward(req, resp); }${leave.status}里的status能不能直接取出来取决于 executeQuery 返回的 Map 的 key 是否和数据库列名一致。我在 BaseDao 里特意用getColumnLabel就是为了这个。如果你在 DAO 里返回的是 Leave 实体对象EL 表达式同样生效靠的是 getter 方法名二者任选其一但不要混用否则 JSP 里要么字段全空要么报属性找不到。提示如果用实体类Leave 里必须有getStatus()、getStartDate()这些方法如果用 Map注意列别名大小写。EL 表达式对这两者的查找规则不一样这是新手最容易卡半小时的地方。5. 课程设计避坑指南乱码、驱动、路径、刷新重复提交、登录态五个高频翻车点做学生请假信息管理系统的课设最折磨人的往往不是业务逻辑而是环境配置和几个隐蔽的编码问题。这五个坑每一条都是历届课设被踩到烂的按“现象 → 原因 → 解决”来说清楚。5.1 POST 中文乱码JSP 页面声明和 Servlet 编码不一致现象请假理由提交后存进数据库成了问号页面上回显也是乱码。原因JSP 页面明明写了pageEncodingUTF-8但 Servlet 里没调req.setCharacterEncoding(UTF-8)Tomcat 默认按 ISO-8859-1 解码 POST 请求体。数据库连接 URL 里如果也没带characterEncodingutf8那即使 Servlet 侧对了数据库存进去还是乱。解决三处必须一致。第一Servlet 的 doPost 第一行加req.setCharacterEncoding(UTF-8)第二JDBC URL 带useUnicodetruecharacterEncodingutf8第三数据库表和字段的字符集是 utf8mb4。这三处挨个检查完乱码问题能根除不要靠new String(bytes)那种粗暴方式去硬转那只会把双重编码的字符搞得更乱。5.2 JDBC 连接 MySQL 失败驱动类名、URL 参数、jar 包版本三方都别错现象启动 Tomcat 后访问页面控制台报ClassNotFoundException: com.mysql.jdbc.Driver或者连接时抛时区相关异常。原因MySQL 5.7 和 8.0 的驱动类名不一样。8.0 之后改成了com.mysql.cj.jdbc.Driver老教材里写的com.mysql.jdbc.Driver在 8.0 驱动下加载不了。URL 里缺少serverTimezoneAsia/Shanghai时驱动会拒绝连接因为服务器时区不确定。解决MySQL 8.0 就写Class.forName(com.mysql.cj.jdbc.Driver)同时确保 WEB-INF/lib 下放的是mysql-connector-java-8.x.jar。项目里如果有旧版本的 jar 残留一定要删干净再重新导入否则两个驱动并存时还会报其他奇怪错误。驱动 jar 包版本和 MySQL 服务端版本要大致匹配这是很多人忽略的点。5.3 Tomcat 部署路径 404contextPath 和 url-pattern 对不上现象表单 action 写的/applyLeave提交后 404或者按钮跳转到一个不存在的地址。原因IDEA 部署 Web 项目时Tomcat 会给应用加一层上下文路径比如http://localhost:8080/leave_system/。表单里写绝对路径/applyLeave实际请求成了http://localhost:8080/applyLeave落到了 ROOT 应用上自然找不到你的 Servlet。解决表单的 action 写成${pageContext.request.contextPath}/applyLeave这样会自动拼上项目上下文路径。Servlet 里跳转页面时则用resp.sendRedirect(req.getContextPath() /student/my_leave.jsp)。理解了 contextPath 这个概念之后这个坑基本不会再踩而且答辩时能顺手解释“为什么部署路径变了页面还能正常跳转”。5.4 JSP 页面加载后自动刷新导致请假记录重复提交现象为了“提交成功后页面自动刷新一次以显示最新记录”在 apply_leave.jsp 里加了一行meta http-equivrefresh content0结果数据库里每提交一次就多出一条重复请假记录。原因meta refresh 在页面加载完成后会重新发出上一次的 POST 请求Servlet 收到后再次执行了 insert。浏览器端的自动刷新和手动刷新一样都会重放原请求而 Servlet 没有做幂等处理。解决提交成功后的跳转改成resp.sendRedirect(my_leave.jsp)不要留在原页面更不要在提交页放 meta refresh。重定向之后地址栏变成查询页的 GET 地址刷新只会重新查列表不会再触发 insert。记住一个原则所有插入或更新的请求处理完之后一律走重定向不要 forward 到同一个页面。5.5 登录态掉失session 与 Cookie 的隐蔽场景现象学生登录成功后进入首页点任意功能链接又被弹回 login.jsp反复登录反复失效。原因session 靠客户端的 JSESSIONID Cookie 维持。如果浏览器启用了阻止第三方 Cookie 的隐私模式或者你把session.setAttribute(student, student)写在了某个永远走不到的分支里session 里就一直取不到值。另一个隐蔽场景是开发时多个窗口开着旧页面Tomcat 重启之后 session 全部失效被当成“登录过期”。解决登录成功后立刻调用session.setAttribute(student, student)然后在需要登录态的 Servlet 里统一做拦截校验比如抽一个 BaseServlet在它的 doGet/doPost 开头写if (session.getAttribute(student) null) { resp.sendRedirect(login.jsp); return; }。Tomcat 重启后 session 丢失是正常行为不是代码 Bug答辩时被问到要能说清楚 session 的生命周期。6. 让系统更像个产品的三个小技巧导出统计、动态 SQL、连接池替换6.1 导出请假统计用 CSV 响应头替代 POI只加几行代码答辩时如果能导出当月请假报表演示效果会好很多。最省事的方案不是引 Apache POI而是用 CSV浏览器识别为 Excel 格式不依赖额外 jar 包。在 Servlet 里设置响应头然后逐行输出resp.setContentType(text/csv;charsetUTF-8); resp.setHeader(Content-Disposition, attachment;filenameleave_report.csv); PrintWriter out resp.getWriter(); out.println(学生姓名,班级,开始日期,结束日期,天数,状态);这是血泪经验CSV 用 Excel 打开时中文容易乱码解决方式是在输出第一行之前写一个 BOM 字符out.write(0xFEFF);Excel 就能正确识别 UTF-8 编码演示现场不至于尴尬。6.2 动态 SQL 拼接按班级、状态、时间范围过滤实际使用中“查看请假记录”往往需要多条件过滤比如管理员想知道“软件2班最近一周有多少人请假”。这需要动态拼接 SQL我一般用 StringBuilder 配合参数 ListStringBuilder sql new StringBuilder(SELECT * FROM leave l JOIN student s ON l.student_ids.id WHERE 11 ); ListObject params new ArrayList(); if (className ! null !className.isEmpty()) { sql.append(AND s.class_name ? ); params.add(className); } if (status 0) { sql.append(AND l.status ? ); params.add(status); }WHERE 11是动态条件拼接的经典写法每个后续条件都能直接用 AND 开头不需要判断是不是第一个。关键点在于参数顺序必须和 SQL 里问号的出现顺序完全一致这个顺序错了查询结果会非常诡异。这个技巧也可以作为“SQL 注入防御”的答辩素材。6.3 连接池替换和验收自测收尾前的最后两步想再提升一点工程素养可以把 DBUtil 的 DriverManager 换成 Druid 数据源改动量只有 getConnection 那一小段引入一个 jar、改一个静态代码块20 行以内private static DruidDataSource dataSource new DruidDataSource(); static { dataSource.setUrl(jdbc:mysql://localhost:3306/leave_db?serverTimezoneAsia/Shanghai); dataSource.setUsername(root); dataSource.setPassword(123456); dataSource.setInitialSize(2); dataSource.setMaxActive(10); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); }交代码之前我习惯做三个自测清掉 Tomcat 的 work 目录再重新部署防止旧 class 文件残留用浏览器完整走一遍“登录 → 请假 → 审批 → 查看记录”主流程把数据库服务停掉确认页面上不会把异常堆栈直接抛给用户。这些步骤看起来琐碎但每一条都在真实的答辩现场救过我。希望这份笔记能帮你在课程设计里少踩几个坑。本文还有配套的精品资源点击获取