简介这是一份完整的学生宿舍管理系统开发项目基于 Java Web 经典技术组合 Servlet、JSP 和 MySQL 实现适合正在学习 Java 服务端开发的学生也适用于课程设计、毕业设计或新手练习。系统覆盖宿舍管理日常业务包括管理员登录验证、学生档案管理、宿舍床位分配、楼栋信息维护、宿舍管理员配置等能帮助使用者理解前端页面与后端逻辑的协同工作。资源包共 295 个文件压缩包大小约 16.88 MB。包内包含 30 个 Java 源文件及对应编译后的 class 文件13 个 JSP 页面40 个 CSS 样式与 10 个 JavaScript 脚本25 个依赖 Jar 包另有 SQL 脚本和 4 个数据库文件可直接建表与初始化同时附有大量图片和图标素材便于调整界面。已有 5921 人学习下载。通过该资源可获得可运行的项目源码、数据库文件、页面样式与第三方依赖库快速完成环境搭建和功能复现也能对照源码梳理 Servlet、JSP、MySQL 三层交互流程为后续扩展宿舍报修、考勤等功能打下基础。1. 一套基于 ServletJSPMySQL 的 javaWeb 课设资源与其被 Spring Boot 的黑匣子劝退不如手写一遍 JDBC如果你刚学完 JavaWeb 就冲去写 Spring Boot大概率会被自动配置绕得晕头转向。这套学生宿舍管理系统偏偏反着来用 ServletJSPMySQL 把整个 javaWeb 链路实实在在地铺开下载资源的类命名已经把模块分好了LoginServlet 管登录、BuildingServlet 管楼栋、LiveServlet 管入住退宿BaseDao 负责所有 JDBC 操作CpachaUtil 专门生成验证码图片。它不是给生产系统用的但对两类人非常合适正在做课设、需要一份能讲清楚每一步的项目以及被 Spring Boot 惯坏了、想回头补 Servlet 根基的开发者。整个系统的数据流不复杂可每一层都能点进去看现场这是它最大的教材价值。2. 先把结构盘明白从 LoginServlet 到 BaseDao一次请求要穿过几个类拿到任何课设资源我习惯先不看页面而是把类名过一遍这套系统的 .class 名字已经把职责写在脸上了。更难得的是它的分层很老派一个 Servlet 对应一张管理表既不玩反射分发也不上 Spring MVC完全符合 JSP Model 2 的老路子。把这一层看清楚后面调参和排错才能定位到具体文件。2.1 类名即职责一个 Servlet 管一张表这就是 JSP Model 2 的老路子这套资源的 9 个类可以分成三条线控制器、工具、数据访问。控制器全部继承 HttpServlet在 web.xml 里配置 url-pattern工具类 CpachaUtil 和 BaseDao 不处理请求只给控制器和 JSP 提供公共能力。它们的对应关系很清楚类名对应业务说明LoginServlet登录 / 登出校验验证码、查用户表决定跳转首页还是回登录页AdminServlet管理员管理管理后台账号一般只有超级管理员才能调用BuildingServlet楼栋管理维护楼栋名称、地址、楼层数是宿舍的外层容器DormitoryServlet宿舍管理维护房间号、床位、已住人数、所属楼栋和性别DormitoryManagerServlet宿管管理维护宿管账号并把宿管绑定到某栋楼StudentServlet学生管理学生信息增删改查、按班级或宿舍查询LiveServlet入住 / 退宿 / 调宿核心业务会同时修改宿舍表和学生表BaseDaoJDBC 封装提供连接、更新、查询的基础方法CpachaUtil验证码生成生成随机验证码字符串并画成图片这种一个 Servlet 管一张表的写法优点很直观映射文件好写答辩时老师问“你这个楼栋删了宿舍怎么办”你能直接指到对应 doPost 里。缺点也明确公共代码重复比如每个 Servlet 都要写权限判断。所以它适合课设不适合迭代超过三个月的项目。真到要重构再考虑抽 BaseServlet 做反射分发现在先别过度设计。2.2 一次登录请求的完整链路CpachaUtil 验证码、BaseDao 查库、Session 跟踪登录是最能串起全流程的模块我建议下下来第一件事就是跑通它。整个链路分五步第一步浏览器打开 login.jsp页面里嵌一个img srccaptcha指向生成验证码的 Servlet。CpachaUtil 先生成随机四位字符串把答案放进 session再用 BufferedImage 把文字画成图片返回同时响应头里要设置Cache-Control: no-store防止浏览器缓存旧图。第二步用户提交表单LoginServlet 的 doPost 先做编码处理再去 session 里取出刚才存的验证码和表单提交的验证码做忽略大小写的比较。注意这里有个普遍问题验证码放进 request 还是 session。放进 request 范围下一跳请求就丢了正确的做法是 session 且校验通过后立刻移除避免重复使用。第三步验证码通过后LoginServlet 拿着用户名和密码去 BaseDao 查询。查询语句根据资源里的表结构走通常查管理员表和宿管表两张表按角色字段决定去 login 后的哪个首页。密码这一层课设很多是明文如果你想加分至少在代码里补一层 MD5 或者 BCrypt答辩时能说清楚就可以。第四步查到记录数等于 1 就放行在 session 里放入当前用户对象重定向到主页面查不到就转发回 login.jsp带上“用户名或密码错误”的提示。第五步主页面的侧边栏根据不同角色展示不同菜单菜单链接全部带上 Session 校验每次请求先判断用户是否登录。这层操作在资源里可能写进了每个 Servlet也可能有过滤器统一处理阅读顺序应当是登录成功 → 首页 → 菜单对应的 Servlet → 对应 JSP。2.3 BaseDao 的封装边界连接、占位符、结果集映射一手包办BaseDao 是整套资源里除了 LiveServlet 之外最值得读的类。常见做法是拆成三个核心方法getConnection 负责拿连接executeUpdate 负责增删改query 负责查询并返回 List。先看一个典型的连接和更新封装public class BaseDao { protected static String url jdbc:mysql://localhost:3306/dorm_system?useUnicodetruecharacterEncodingUTF-8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue; protected static String user root; protected static String password 123456; static { try { // MySQL 8.x 的驱动类5.x 的 com.mysql.jdbc.Driver 在这里会抛 ClassNotFoundException Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } protected Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } protected int executeUpdate(String sql, Object... params) { try (Connection conn getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException(执行更新失败 sql, e); } } }这里有两个细节值得说透。第一个是为什么用 PreparedStatement 而不是 Statement一是防 SQL 注入参数走占位符不会拼进 SQL 语句二是不用手动处理字符串引号写起来更干净。第二个细节是setObject(i 1, params[i])PreparedStatement 的下标从 1 开始和数组从 0 开始差一个这里写错最容易越界。类名换成你自己项目里的逻辑不用动。查询方法建议做成泛型加回调的方式让每个 Servlet 自己去定义结果集怎么变成实体对象protected T ListT query(String sql, ResultSetMapperT mapper, Object... params) { ListT list new ArrayList(); try (Connection conn getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } try (ResultSet rs ps.executeQuery()) { while (rs.next()) { list.add(mapper.map(rs)); } } } catch (SQLException e) { throw new RuntimeException(查询失败 sql, e); } return list; }ResultSetMapper 是函数式接口StudentServlet 里可以这么用query(sql, rs - new Student(rs.getInt(id), rs.getString(stu_no)))。这样 BaseDao 不用依赖任何实体类所有表都能复用。课设项目把这三段代码吃透JDBC 的要点就齐了。另一件事是这套 BaseDao 没有事务控制LiveServlet 里同时更新宿舍表和学生表时就可能出现中间态这个我在第三章会详细说也算是一个可优化点。3. 动手把核心流程跑通建表、写 JDBC 连接串、实现宿舍分配理论看完了现在按能复现的顺序走一遍。我在本地用 IDEA 2023 Tomcat 9 MySQL 8.0 跑通这套资源下面的脚本和代码都按这个组合给。如果你是 VSCode 配环境逻辑完全一致只是部署方式不同排错时留意一下 Tomcat 版本就行。3.1 建库建表楼栋、宿舍、学生、宿管四张表的关联设计这套资源的表结构是典型的课设风格四张业务表加一张管理员表表之间用逻辑外键关联不建物理外键。好处是删数据方便坏处是代码里必须管好关联关系。先按这个脚本建库CREATE DATABASE IF NOT EXISTS dorm_system DEFAULT CHARACTER SET utf8mb4; USE dorm_system; CREATE TABLE t_building ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 楼栋名称, address VARCHAR(100) COMMENT 楼栋位置, floors INT DEFAULT 6 COMMENT 楼层数 ) COMMENT 楼栋表; CREATE TABLE t_dormitory ( id INT PRIMARY KEY AUTO_INCREMENT, building_id INT NOT NULL COMMENT 所属楼栋 id, room_no VARCHAR(20) NOT NULL COMMENT 房间号, bed_num INT NOT NULL DEFAULT 4 COMMENT 总床位数, stay_num INT NOT NULL DEFAULT 0 COMMENT 已住人数, gender TINYINT NOT NULL DEFAULT 1 COMMENT 1 男2 女, UNIQUE KEY uk_building_room (building_id, room_no) ) COMMENT 宿舍表; CREATE TABLE t_manager ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, real_name VARCHAR(30), building_id INT COMMENT 负责的楼栋, phone VARCHAR(20) ) COMMENT 宿管表; CREATE TABLE t_student ( id INT PRIMARY KEY AUTO_INCREMENT, stu_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, name VARCHAR(30) NOT NULL, gender TINYINT NOT NULL DEFAULT 1 COMMENT 1 男2 女, phone VARCHAR(20), class_name VARCHAR(50), dormitory_id INT COMMENT 当前宿舍退宿置空, status TINYINT DEFAULT 1 COMMENT 1 在住0 已退宿 ) COMMENT 学生表; CREATE TABLE t_admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL ) COMMENT 管理员表;关键点有三个宿舍表的唯一索引uk_building_room防止同一栋楼出现两个重复房间号这是删重的基础stay_num是冗余字段它让宿舍列表查询不需要 COUNT 子查询性能更好代价是每次入住退宿都得手动维护不建物理外键的做法配合业务代码里先删子表再删主表的顺序是课设答辩时的常见话题。3.2 JDBC 连接串配置驱动类、时区、字符集一个都不能少连 MySQL 8 最常见的翻车就是驱动类配错。老驱动com.mysql.jdbc.Driver在 5.x 里能用换到 8.x 会直接 ClassNotFoundException必须改成com.mysql.cj.jdbc.Driver。连接串建议单独放一个 db.properties别写死在类里jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/dorm_system?useUnicodetruecharacterEncodingUTF-8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password123456BASE DAO 侧静态块加载方式参考这段static { try (InputStream in BaseDao.class.getClassLoader().getResourceAsStream(db.properties)) { Properties p new Properties(); p.load(in); Class.forName(p.getProperty(jdbc.driver)); url p.getProperty(jdbc.url); user p.getProperty(jdbc.username); password p.getProperty(jdbc.password); } catch (Exception e) { throw new ExceptionInInitializerError(e); } }连接串里四个参数分别负责不同的问题characterEncodingUTF-8解决中文乱码serverTimezoneAsia/Shanghai解决 MySQL 8 的时区报错useSSLfalse关掉 SSL 握手本地调试更快allowPublicKeyRetrievaltrue配合 MySQL 8 的 caching_sha2_password 认证不加它会报 Public Key Retrieval is not allowed。这些参数看似琐碎实际都是我会反复遇到的生产级别问题。3.3 LiveServlet 宿舍分配先找空床位再用条件更新防超睡LiveServlet 是整套资源的业务核心几乎所有有含金量的逻辑都在这里。分配宿舍的场景可以拆成三步先找合适宿舍再占床位最后写学生关联。关键点在第二步要用条件更新而不是先查后改。// 第一步在指定楼栋里找一个没满员、性别匹配的宿舍 String findSql SELECT id, room_no, bed_num, stay_num, gender FROM t_dormitory WHERE building_id ? AND gender ? AND stay_num bed_num ORDER BY stay_num DESC LIMIT 1; ListDormitory list baseDao.query(findSql, rs - new Dormitory( rs.getInt(id), rs.getString(room_no), rs.getInt(bed_num), rs.getInt(stay_num)), buildingId, gender); if (list.isEmpty()) { // 当前楼栋没有空闲宿舍返回提示信息 return; } Dormitory dorm list.get(0); // 第二步占用一个床位条件是宿舍还没满 String updateDorm UPDATE t_dormitory SET stay_num stay_num 1 WHERE id ? AND stay_num bed_num; int rows baseDao.executeUpdate(updateDorm, dorm.getId()); if (rows 0) { // 最后一次床位刚好被并发请求抢走重新走分配流程 return; } // 第三步更新学生的宿舍关系和在住状态 baseDao.executeUpdate(UPDATE t_student SET dormitory_id ?, status 1 WHERE id ?, dorm.getId(), studentId);这里最重要的一行是 WHERE 里的stay_num bed_num。如果写成“先查出来Java 里判断小于床位数再执行 UPDATE”两个线程同时查都会看到空床位然后一起加一最后数据就超了。把条件写进 UPDATE让数据库的行锁帮我们兜底是标准的防并发覆盖手法。课设数据量下很难实测到并发但老师一问能说出这层道理就是加分项。事务问题跟着就来了。这套代码的 executeUpdate 每次拿新连接、自动提交第二步成功第三步失败时宿舍已经占用但学生没关联上。最直接的补救是给 BaseDao 加一个带 Connection 参数的重载在 LiveServlet 里手动控制conn.setAutoCommit(false)最后统一提交或回滚。改造量不大但能让项目完整性上一个台阶。3.4 JSP 用 ELJSTL 渲染列表不要在页面里嵌 Java 脚本很多老课设的 JSP 里全是% for (...) { %能跑但后期改样式特别痛苦。这套资源既然配了 Servlet 转发页面层可以直接用 EL 表达式加 JSTL把列表渲染这段写干净% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table classtable table-bordered thead trth房间号/thth床位/thth状态/th/tr /thead tbody c:forEach items${dormList} vard tr td${d.roomNo}/td td${d.stayNum} / ${d.bedNum}/td td${d.stayNum d.bedNum ? 有空床 : 已满}/td /tr /c:forEach /tbody /table使用这个页面前DormitoryServlet 里必须有一行request.setAttribute(dormList, list)然后request.getRequestDispatcher(dormitory_list.jsp).forward(request, response)。EL 表达式取属性名和实体类 getter 有关${d.roomNo}对应的就是getRoomNo()命名别搞混。JSTL 标签库还需要 lib 目录里放着 jstl.jar 和 standard.jar这一步漏了页面会报 TLD 错误属于 Tomcat 环境的经典坑。4. 避坑记录ServletJSPMySQL 项目最常见的五个翻车现场我在给学员项目做环境整备时下面五个问题反复出现。每一条都是现网实踩不是理论推断按“现象→原因→解决”写直接对照排查。4.1 Tomcat 10 跑老项目报 ClassNotFoundException 还是 404现象项目启动正常但一访问 Servlet 路径页面报错日志里能看到java.lang.ClassNotFoundException: javax.servlet.http.HttpServlet或者浏览器直接 404。原因Tomcat 10 开始把 Java EE 的命名空间从javax.*迁到了jakarta.*老课设里的 import 语句和 web.xml 头声明全部失效。很多人的电脑装了多个 TomcatIDEA 部署时 Runtime 选错也会这样。解决最省事的是换成 Tomcat 9 或 8.5版本匹配后第一个报错就消失。如果你坚持用 Tomcat 10得把所有 import 里的 javax.servlet 改成 jakarta.servletJSP 顶部的引用也要改工作量不小。我的一般做法是项目属性里单独指定 Tomcat 9 路径避免和系统默认的 Tomcat 10 冲突。4.2 验证码图片能刷新可登录永远提示验证码错误现象验证码图片显示正常人眼确认没抄错但每次提交都说“验证码错误”。刷新图片后再试还是错。原因常见三个来源生成验证码时把字符串放进了 request登录时却去 session 里取两次拿的不是同一个对象登录成功后没有 remove 旧验证码导致同一个验证码能反复用图片响应没禁止缓存浏览器一直显示第一次加载的旧图。解决统一把验证码答案放进 session登录校验通过后立刻移除。图片输出前执行resp.setHeader(Cache-Control, no-store)。另外注意 Linux 服务器的字体问题CpachaUtil 绘制图片时如果指定的字体不存在会抛 FontFormatException部署到服务器后才会暴露本地 Windows 反而没事。4.3 中文乱码页面上全是问号问题到底出在哪一环现象学生姓名、楼栋名称写入 MySQL 后变成???或者从数据库读出来在页面上显示成乱码。原因至少三个环节可能坏JSP 页面 Content-Type 没声明 UTF-8Servlet 收到请求后没执行request.setCharacterEncoding(UTF-8)JDBC 连接串没带characterEncodingUTF-8。三者缺一就可能出现而且排查时很容易只盯一处。解决不要依赖每个 Servlet 自己写编码写一个 Filter 拦截所有请求统一设置 request 和 response 的编码。数据库、表、连接串三处字符集全部用 utf8mb4建库脚本里已经带了。连接串参数我在 3.2 节列过别漏。这三层都对齐后字符集问题基本清零。4.4 宿舍分配“超卖”两个线程抢到同一张空床现象并发提交两个学生入住同一栋楼最终矛盾出现宿舍表的已住人数超过了床位数或者两个学生关联到同一个宿舍而床位只有一个。原因代码写成了“先查宿舍判断有空床再执行 update”这是经典的读改写竞态。两个请求同时查到stay_num bed_num又都去做加一最后一次写覆盖了前一次的结果。解决按 3.3 节做法把床位判断写进 UPDATE 的 WHERE 里靠数据库行锁保证同一时刻只有一个请求能成功加一。受影响行数为 0 就说明床位没了重新走分配流程。这套逻辑在课设里不一定能遇到并发但它是“写了有加分的业务意识”。4.5 MySQL 8 连接失败驱动类、时区、公钥三个坑叠一起现象日志报java.sql.SQLException: Access denied for user rootlocalhost或者Public Key Retrieval is not allowed又或者直接是ClassNotFoundException。原因驱动版本和数据库版本不匹配。MySQL 5.x 的com.mysql.jdbc.Driver用不了 MySQL 8MySQL 8 默认的 caching_sha2_password 认证握手时需要取公钥连接串没放行就会拒绝。还有时区问题CST这种缩写多个数据库含义不一样不显式指定就报错。解决驱动类换成com.mysql.cj.jdbc.Drivermysql-connector-java 版本用 8.0.x。连接串固定带三段serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue。如果本地密码是空或有特殊符号注意 properties 文件转义这也是容易被忽略的一环。5. 把课设改写成作品验证码、连接池和部署检查三处升级资源跑起来只是起点。课设评分高低往往看你在标准功能之外做了什么。下面三个升级点是我每次拿到这类项目必做的按性价比从高到低排列。5.1 验证码升级把纯字符串改成加减法运算原版的 CpachaUtil 生成四字符随机串逻辑简单但防刷能力弱。改成算数验证码只需替换答案生成逻辑Random r new Random(); int a r.nextInt(10) 1; // 1~10 int b r.nextInt(10) 1; char op new char[]{, -, *}[r.nextInt(3)]; String expr a op b ?; int result op ? a b : op - ? a - b : a * b; session.setAttribute(captchaResult, result); // 绘制图片时画 expr 字符串登录时取 result 做比较改动集中在 CpachaUtil 和 LoginServlet 比较逻辑两处画图部分完全复用。这个升级的答辩价值在于解释清楚验证码的作用不是加密而是区分人和脚本。5.2 数据库连接升级用 Druid 替换 DriverManagerBaseDao 里每次 getConnection 都新建连接在并发场景下会拖垮数据库。接入 Druid 后连接复用、空闲回收、监控面板都有。在 web.xml 里简单配置监控过滤器就能看到 SQL 执行统计filter filter-namedruidWebStatFilter/filter-name filter-classcom.alibaba.druid.support.http.WebStatFilter/filter-class init-param param-nameexclusions/param-name param-value*.js,*.css,/druid/*/param-value /init-param /filter同时把 BaseDao 里的 getConnection 替换成从 DruidDataSource 拿连接销毁连接时调用 dataSource 而不是 DriverManager。这套改完连接池参数如 initialSize、maxActive 都可以在 db.properties 里调性能和排错能力都比裸 JDBC 强一截。5.3 部署上线前的 5 分钟检查单最后给出我个人的部署验证习惯适用于任何课设资源先在本地执行完整建库脚本确认表数量和字段注释都正常。检查 db.properties 的账号密码注意密码里的、必须转义否则解析截断。打包 war 时确认 JSP 目录下的 taglib 引用和 lib 里的 jar 一致JSTL 依赖别漏。部署到 Tomcat 后先访问验证码图片再测登录、楼栋增删、宿舍分配三个核心流程按这个顺序排查。看日志时直接查Caused by行往上翻八成是环境问题。从那以后我每次拿到课设资源都强制自己走这套流程先建库、再读 BaseDao、然后跑一次 LiveServlet 的核心逻辑三个地方通了才动其它代码。这个顺序帮我避掉过太多环境假象也把这套 ServletJSPMySQL 的架构真正刻进了直觉里。希望帮到你。本文还有配套的精品资源点击获取