简介:JavaWeb宿舍管理系统是一套基于JavaWeb技术的宿舍管理项目源码,面向高校、培训机构及个人开发者,可高效管理宿舍分配、床位信息、住宿查询、报修工单等日常事务,同时适合课程设计、毕业设计或JavaWeb入门进阶练习。压缩包共238个文件,约4.1MB,其中java/jsp/class文件构成后端业务逻辑,js/css/html构建前端交互界面,sql脚本用于初始化数据库,配合png、jpg等图片素材和xml/properties等配置文件,结构清晰,可直接导入Eclipse并结合Tomcat部署运行。该系统覆盖Servlet、JSP、JDBC、Spring框架、MVC分层等关键知识点,并包含用户登录、角色权限管理等安全机制,源码目录完整、注释明确,便于快速理解各模块调用关系。已有1560人学习下载,尤其适合希望以真实项目提升开发能力的初学者参考借鉴。
1. 项目概述:为什么“宿舍管理系统”成了JavaWeb课设的经典之选
如果你正在搜“JavaWeb宿舍管理系统”这类项目,大概率是两种情况:一是学校课程设计给出了这个题目,二是你在找一个能完整串起JSP、Servlet、MySQL的练手项目。这个题目确实经典,因为宿舍管理系统的业务边界足够清晰——无非是宿舍楼栋、房间、学生、入住退宿这几张表的事,但又能把增删改查、登录鉴权、多条件查询、分页这些Web开发的常见操作全涵盖进去,作为个人练手或毕设,非常合适。
我最初拿到这个题目时,第一反应是“这会不会太简单了”。真做下来才发现,一个五脏俱全的管理系统并不好写。它不像商城项目那样有商品、购物车、订单这些复杂的业务联动,但正因为简单,反而对基本功要求更高:登录状态怎么保持、权限怎么区分、数据库连接怎么写才不容易泄漏、分页怎么在JSP里优雅实现,每一处都是面试官爱问、考试爱考的点。
本文会完整复盘一个基于JavaWeb的宿舍管理系统的设计与实现思路,覆盖技术选型、数据库设计、核心功能编码、前后端联调,以及我踩过的典型坑。无论你是刚学完Java基础想找个项目练手,还是正在赶课设的DDL,这篇文章都能提供一份可以直接参考的完整方案。
2. 功能规划与核心技术选型
2.1 需求拆解:一个宿舍管理系统到底要做哪些事
在动笔写代码之前,先花时间把需求拆清楚,这是整个项目最值得投入的环节。宿舍管理系统的典型用户角色有两种:管理员和宿管员(有的学校还会加一个学生角色,但核心是前两类)。
先列功能清单:
- 用户登录与注册:管理员和宿管员使用账号密码登录,密码加密存储,登录后Session保持状态。
- 宿舍楼管理:对宿舍楼栋进行增删改查,包括楼名、楼号、楼层数、每层房间数等基础信息。
- 宿舍房间管理:房间的入住人数、床位数量、当前居住学生列表,判断房间是否已满。
- 学生入住管理:录入学生信息,分配宿舍,退宿时释放床位。
- 学生信息查询:按学号、姓名、楼栋、房间号等多条件组合查询和分页展示。
- 来访登记(可选加分项):对外来人员进行登记和注销。
- 管理员面板:统计总楼栋数、总房间数、入住率、空余床位等核心指标,展示在首页看板。
这些功能看起来多,但本质上可以归纳为“两张主表加若干关联表”的CRUD:宿舍楼表、宿舍房间表、学生表、入住记录表,以及用户表。业务不难,难的是如何把CRUD写得干净、健壮、好维护。这也是这个项目真正有价值的地方——它逼着你像个正规项目一样去组织代码,而不是在一个Servlet里堆几百行。
2.2 选型思路:为什么是JSP + Servlet + MySQL,而不是Spring Boot
拿到这个题目,很多同学第一反应是“现在谁还不用Spring Boot”。我的观点是:如果你想练手Spring Boot,直接去写一个更复杂的项目更有价值。但宿舍管理系统如果用了Spring Boot + MyBatis-Plus + Thymeleaf,反而会掩盖很多底层逻辑,比如Servlet的生命周期、HttpServletRequest和HttpServletResponse的工作方式、数据库连接怎么手动管理,这些都是校招笔试和面试的基础考点。
我的技术栈选择如下:
| 组件 | 选型 | 理由 |
|---|---|---|
| 前端页面 | JSP + JSTL + EL | 课设标配,能直观地理解服务端渲染的过程 |
| 后端控制层 | Servlet 3.0+ | 最基础的JavaWeb请求处理机制,便于理解HTTP协议 |
| 业务层 | Service + DAO两层 | 比Controller直接写JDBC更好维护,也符合分层思想 |
| 数据库 | MySQL 5.7 | 最常见的教学和面试版本,8.0也兼容 |
| JDBC工具 | 手写DBUtil + 连接池(Druid) | Druid可以监控SQL执行情况,方便调试 |
| 前端样式 | Bootstrap + adminlte模板(可用静态模板) | 不需要手写CSS,界面美观且响应式 |
这个组合在2025年的今天看起来有点“复古”,但恰恰因为“复古”,你能完全掌控每个环节。当我后来去面试时,面试官对这个项目的兴趣远超那些千篇一律的Spring Boot商城,因为他可以顺着问你“Session实现原理”“SQL注入如何避免”“连接池参数怎么调”,而这些都是这个项目里真实遇到的。
2.3 开发环境准备:用VSCode还是IDEA
这里插一句工具选择。很多人搜索“vscode创建javaweb项目”,说明有一部分同学没有装IDEA,或者电脑配置有限。VSCode确实能开发JavaWeb项目,只要你装好以下插件:
- Extension Pack for Java(内含语言服务、调试器、Maven支持)
- Tomcat for Java(支持在VSCode中直接启动Tomcat)
创建项目的推荐方式是用Maven骨架:在VSCode中按Ctrl+Shift+P,输入“Java: Create Java Project”,选“Maven Archetype”,然后选maven-archetype-webapp,这样会生成标准的webapp目录结构(src/main/java、src/main/webapp、WEB-INF/web.xml)。
不过我还是建议,只要电脑能带得动,尽量用IDEA Community版(免费)或Eclipse。因为VSCode对JavaWeb的调试体验相对弱,尤其是断点调试时不太方便。我自己在VSCode里踩过半天Tomcat部署路径的坑,后面换到IDEA后顺手多了。
3. 数据库设计与核心表结构
3.1 表结构设计:先从“入住”这个业务核心出发
数据库设计是整个系统的基础,设计得好不好直接决定后面写业务逻辑的复杂度。当时我的寝室长跟我说了一句至今受用的话:“你先把数据的流转路径画出来,再开表。”
我最后设计的是5张核心表:
t_user:用户表,字段包括id、username、password(MD5加密)、role(1管理员、2宿管员)、create_time。t_building:宿舍楼表,字段包括id、building_name(楼名,如“梅苑”)、building_no(楼号)、floor_count(层数)、room_count(每层房间数)、remark。t_room:宿舍房间表,字段包括id、building_id(外键关联楼栋)、room_no(房间号,如“501”)、bed_count(床位数)、current_people(当前人数)。t_student:学生表,字段包括id、student_no(学号)、name、gender、college(学院)、class_name(班级)、phone、create_time。t_dorm_record:入住记录表,字段包括id、student_id(外键)、room_id(外键)、check_in_time、check_out_time、status(1入住中、0已退宿)。
这里有一个设计细节值得展开:为什么不直接把room_id放到学生表里面?
因为宿舍分配是会产生历史记录的。一个学生入住A栋101,后来调宿到B栋202,如果只在学生表里存一个room_id,那第一次的入住记录就丢了。用单独的入住记录表,可以让“学生”与“房间”形成多对多关系,而这正是业务现实的正确建模。这个设计在和面试官聊的时候是很好的加分项。
3.2 建表SQL及注意事项
直接贴出我建表的SQL片段,做了必要的注释:
-- 用户表 CREATE TABLE `t_user` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', `password` VARCHAR(64) NOT NULL COMMENT '密码,存MD5值', `role` TINYINT DEFAULT 2 COMMENT '1=管理员,2=宿管员', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 宿舍楼表 CREATE TABLE `t_building` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `building_name` VARCHAR(50) NOT NULL, `building_no` VARCHAR(20) NOT NULL, `floor_count` INT DEFAULT 6, `remark` VARCHAR(200) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 房间表 CREATE TABLE `t_room` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `building_id` INT NOT NULL, `room_no` VARCHAR(10) NOT NULL, `bed_count` INT DEFAULT 4, `current_people` INT DEFAULT 0, CONSTRAINT fk_room_building FOREIGN KEY (building_id) REFERENCES t_building(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 学生表 CREATE TABLE `t_student` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `student_no` VARCHAR(20) NOT NULL UNIQUE COMMENT '学号', `name` VARCHAR(50) NOT NULL, `gender` TINYINT DEFAULT 1 COMMENT '1=男,2=女', `college` VARCHAR(100), `class_name` VARCHAR(100), `phone` VARCHAR(20), `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 入住记录表 CREATE TABLE `t_dorm_record` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `student_id` INT NOT NULL, `room_id` INT NOT NULL, `check_in_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `check_out_time` DATETIME, `status` TINYINT DEFAULT 1 COMMENT '1=入住中,0=已退宿', CONSTRAINT fk_record_student FOREIGN KEY (student_id) REFERENCES t_student(id), CONSTRAINT fk_record_room FOREIGN KEY (room_id) REFERENCES t_room(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;几个关键决策:
- 字符集统一用
utf8mb4,因为utf8在MySQL里存储emoji和特殊汉字时可能报错。我在第一版项目里用了utf8,结果录入学生姓名时遇到生僻字直接插入失败,后来通宵把所有表改成utf8mb4才解决。 - 外键约束一定要加。虽然很多生产项目为了性能会去掉外键,但课设项目加上外键,既能保证数据一致性,也能在答辩时展示你对数据库规范化的理解。
- 唯一索引建在
t_student.student_no上,防止同一学号重复录入。
3.3 补充基础数据与密文密码
初始化管理员账号时,不要直接在SQL里写明文密码。我用一个工具类生成MD5:
public static String md5(String source) { try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] bytes = md.digest(source.getBytes(StandardCharsets.UTF_8)); StringBuilder sb = new StringBuilder(); for (byte b : bytes) { String hex = Integer.toHexString(b & 0xff); if (hex.length() == 1) sb.append('0'); sb.append(hex); } return sb.toString(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } }登录对比时,对用户输入的密码做同样的MD5后与数据库比对。虽然MD5不算安全的加密算法(彩虹表可破解),但作为课设完全够用,如果要提升安全性可以加盐。
4. 后端核心代码实战:从工具类到业务闭环
4.1 数据库连接管理:手写连接池还是用Druid
数据库连接这块有几个选择:DriverManager原始连接、C3P0、DBCP、Druid。我的建议是直接用Druid,理由很实际:配置简单、自带监控页面、性能稳定。
在src/main/resources下放druid.properties:
driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/dorm_system?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username=root password=你自己的密码 initialSize=5 maxActive=20 maxWait=3000再用一个DBUtil封装:
public class DBUtil { private static DataSource dataSource; static { try (InputStream is = DBUtil.class.getClassLoader().getResourceAsStream("druid.properties")) { Properties props = new Properties(); props.load(is); dataSource = DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }然后是BaseDAO,封装JdbcTemplate风格的常用方法,写一个queryForList和一个update就够用了。在这个项目里我没有引入第三方ORM,而是用Apache Commons DButils,它是一个非常轻量的封装,避免写大量重复的try-catch-finally。如果你不想引入额外依赖,手写PreparedStatement也不难。
4.2 基于三层架构的功能分层实现
代码组织我采用标准三层架构:
servlet包:接收请求,解析参数,调用service,跳转或重定向。service包:业务逻辑,比如“入住”这个方法里需要同时更新房间人数和插入入住记录。dao包:只做SQL交互。
这个分层的价值在“入住登记”这个方法上体现得最清楚:
public boolean checkIn(Student student, int roomId) throws Exception { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 插入学生 studentDao.insert(conn, student); // 2. 更新房间人数+1 int updated = roomDao.increasePeople(conn, roomId); if (updated != 1) throw new RuntimeException("房间更新失败"); // 3. 插入入住记录 recordDao.insert(conn, student.getId(), roomId); conn.commit(); // 全部成功才提交 return true; } catch (Exception e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); } }这个过程是面试时的高频提问点:为什么要用事务?因为插入学生和更新房间是两个独立的SQL操作,任何一个失败都会导致数据不一致(比如学生录入了但没住进房间)。用事务把它们捆绑成一个“原子操作”,要么全部成功,要么全部回滚。
4.3 登录鉴权:Session和过滤器是标配
登录逻辑不算复杂:从请求中取username和password,调用userService校验,成功后把用户对象放进Session,重定向到首页。关键是退出登录时,一定要调用session.invalidate()或者session.removeAttribute("user")。
但“登录后能访问哪些页面”这个问题就需要过滤器来解决。一个傻瓜式的写法是在每个Servlet里判断Session是否有用户,但这样既重复又容易漏。标准做法是写一个AuthFilter:
@WebFilter("/*") public class AuthFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String uri = request.getRequestURI(); // 放行登录相关的页面和静态资源 if (uri.endsWith("/login.jsp") || uri.contains("/user/login") || uri.endsWith(".css") || uri.endsWith(".js") || uri.endsWith(".png")) { chain.doFilter(req, resp); return; } // 未登录用户跳回登录页 HttpSession session = request.getSession(false); if (session == null || session.getAttribute("user") == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(req, resp); } }这里有一个非常值得注意的点:getSession()和getSession(false)的区别。前者如果当前请求没有Session会创建一个新的,后者不会。在过滤器里用getSession(false)是标准的习惯,否则每次未登录请求都会被创建出无用的Session,浪费服务器内存。
如果系统支持管理员和宿管员两种角色,可以在过滤器里增加权限判断——比如“添加楼栋”的操作只允许管理员执行。但课设阶段做到角色区分即可,更细粒度可以用一个注解+拦截器实现,属于加分项,不强制。
4.4 核心业务实现:学生宿舍分配与信息展示
宿舍分配的逻辑是:用户选择一栋楼 → 展示该楼所有房间的空余情况 → 选中某个有空位的房间 → 录入学生信息并提交。这里最容易出错的点是并发场景下,两个管理员同时给同一个房间分配学生,会导致房间人数超员。
解决思路是执行更新语句时带上条件,用乐观锁的思路:
UPDATE t_room SET current_people = current_people + 1 WHERE id = ? AND current_people < bed_count这条SQL保证了“当前人数小于床位数时才能加1”。如果返回值是0,说明房间已满或不存在,业务层直接提示“房间已满”。这种写法比先查再更新更合理,因为两条SQL之间存在时间窗口,并发下会出现都查到了人数是3(床位数4)、都去分配的情况。
学生信息展示页面我会设计成表格 + 多条件筛选 + 分页:
- 查询条件:学号(精确模糊都支持)、姓名、楼栋下拉框、房间号。
- 分页参数:
pageNum、pageSize,SQL用LIMIT ?, ?。 - 页面上显示“共xx条记录,第x/xx页”。
分页的SQL只需注意偏移量计算:(pageNum - 1) * pageSize。JSP页面上做一个简单的分页导航条即可,不必引入PageHelper这类框架,手动实现反而能加深理解。
4.5 JSP页面的核心写法与细节
JSP里的核心逻辑一定要用JSTL标签,不要用<% %>脚本段,否则页面一团糟。在web.xml或项目依赖中引入JSTL后,在JSP顶部写:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>遍历房间列表展示时,一个典型片段:
<c:forEach items="${roomList}" var="room"> <tr> <td>${room.roomNo}</td> <td>${room.bedCount}</td> <td>${room.currentPeople}</td> <td> <c:choose> <c:when test="${room.currentPeople >= room.bedCount}"> <span class="tag tag-full">已满</span> </c:when> <c:otherwise> <span class="tag tag-available">有空位</span> </c:otherwise> </c:choose> </td> </tr> </c:forEach>注意,EL表达式中访问的是JavaBean的属性名,也就是getRoomNo()方法对应的roomNo,首字母大小写一定要对,否则渲染出来是空的。
5. 常见问题与排查技巧实录
5.1 中文乱码问题
乱码是JavaWeb新手最大的敌人。我总结出三个层面:
- JSP页面乱码:在JSP第一行写
<%@ page contentType="text/html;charset=UTF-8" language="java" %>。 - 请求参数乱码:POST请求在Servlet里调用
request.setCharacterEncoding("UTF-8")必须在读取任何参数之前;GET请求需要在Tomcat的server.xml里配置URIEncoding,或者在代码里手动new String(param.getBytes("ISO-8859-1"), "UTF-8")。 - 数据库乱码:连接URL加
characterEncoding=utf8,并且确认表结构是utf8mb4。
排查乱码有一个笨但有效的方法:在前端页面填写“测试中文”,在Servlet里打印System.out.println(param),比较控制台编码和页面显示,就能判断是哪一层出了问题。
5.2 Tomcat启动失败或者访问404
- 404的第一反应先看项目部署名:访问路径是
http://localhost:8080/项目名/...,而不是http://localhost:8080/...。 - 如果确认路径没问题但依然404,检查Servlet注解或web.xml中的映射URL前缀是否正确。
- 如果启动时端口被占用,用
netstat -ano | findstr 8080(Windows)或lsof -i:8080(macOS/Linux)找到进程ID,杀掉即可。
5.3 数据库连接失败的常见原因
- 驱动类名版本问题:MySQL 5.x用
com.mysql.jdbc.Driver,MySQL 8.x用com.mysql.cj.jdbc.Driver。 - 时区报错:连接URL里必须带
serverTimezone=Asia/Shanghai。 - 密码错误:检查druid.properties是否被正确加载到classpath中,有时候改了配置没重启Tomcat,使用的是旧配置。
5.4 JS和CSS加载不出来
这个问题比较隐蔽。如果你的页面使用了过滤器且放行的URL写得过于严格(比如只放行了*.css),但页面里引用CSS的路径是相对路径,导致过滤器没有拦截匹配到,就会表现为“页面没有样式”。
排查方法:打开浏览器开发者工具(F12),看Network里CSS的请求状态是404还是403,如果是404说明路径拼接有问题,需要前端加${pageContext.request.contextPath}作为上下文路径前缀。
5.5 登录后刷新页面提示重复提交或Session失效
表单重复提交是一个高频问题。解决思路有两种:
- 使用PRG模式(Post/Redirect/Get):用户在登录成功后,Servlet用
response.sendRedirect()而不是request.getRequestDispatcher().forward()跳转到首页,这样浏览器地址栏变成GET请求,刷新不会重复提交表单。 - 表单加隐藏token:登录时生成一个随机token存在Session中,表单提交时带上token,Servlet校验后删除。这种方法能防止表单重复提交,也能抵御CSRF攻击,面试时可以提一提。
6. 写在最后:踩过这些坑,你的JavaWeb才算入门
做这个项目最大的收获,不是学会了JSP标签怎么用、SQL怎么写,而是真正理解了“一个请求从浏览器到服务器再到数据库,最后回到浏览器”的完整链路。这种全链路感知,是看再多的教学视频也替代不了的。
最后分享一个小技巧:项目做完后,手动删除Tomcat的work目录下的缓存文件,再重新启动项目。很多时候你改了Java代码但页面还显示旧内容,就是因为Tomcat的JSP缓存没有更新。这个坑我遇到不下三次,每次都以为是代码没保存。
另外一个建议是给项目加上一个“操作日志”功能,记录谁在什么时间执行了什么操作。这个功能不难,但能显著提升项目的完整度和答辩分数。如果在做课设,这个加分会很实在。
如果你正在做JavaWeb的第一个完整项目,我建议不要急着追求项目数量,这个宿舍管理系统够你折腾半个月了。把它吃透,把每个“为什么”都问一遍,你的JavaWeb基本功就能超过大多数人。
本文还有配套的精品资源,点击获取