
简介面向高校计算机专业学生的Java Web学生宿舍管理系统源码包兼顾学习、毕业设计与期末大作业场景。系统覆盖宿舍信息、学生信息、水电费等核心管理模块并实现登录验证、信息增删改查及条件检索等常用功能所有源码均已在本地编译运行通过评审分达到98分难度适中适合具备JavaWeb基础者参考或二次开发且经助教老师审定内容可靠。压缩包为zip格式共133个文件、约3.3MB其中包含20个Java类、20个class编译文件、18个JSP页面、52个JavaScript脚本、5个CSS样式、SQL数据库脚本以及Eclipse工程配置文件等目录结构完整可快速导入IDE运行。已有144人学习下载。通过这份项目既能理清ServletJSPDAO的分层开发流程和数据库表设计思路也能为毕业设计文档编写、答辩演示和功能扩展提供稳定可用的起点。1. 学生宿舍管理系统为什么它是JavaWeb课程设计里的常青树如果你用“javaweb学生宿舍管理系统”去搜会发现这个题目几乎出现在每一届的JavaWeb课程设计和毕业设计选题里。原因很简单它不是一个玩具项目。宿舍管理天然包含楼栋、房间、床位、学生、入住、退宿、调宿、访客、报修、水电费这些业务实体数据之间有清晰的关联关系天然适合用来展示数据库外键设计、事务处理、多表联查这些核心能力。市面上那些“高分项目”之所以被反复推荐并不是因为技术多前沿而是它把JavaWeb最常见的技术栈——Servlet/JSP或SpringSpringMVCMyBatis、MySQL、Tomcat——完整地串了一遍从建库建表到登录鉴权再到CRUD报表全部业务闭环。这篇笔记适合两类人。一类是做课程设计或毕业设计的学生需要的是一个能跑通、能讲清楚、答得上答辩问题的完整基线另一类是想练手JavaWeb项目完整案例的开发者想看看一个“看起来简单”的管理系统里哪些地方才是真正决定项目能不能用的细节。我会按我自己做这套系统的顺序来写先建库建表再做登录鉴权和权限拦截再做核心维护逻辑最后说部署和排错。整个过程用IDEA运行javaweb项目配置开始到Tomcat正式部署结束每一步都给出能直接抄的代码和参数。在动手之前先说一个容易被忽略的结论这类项目拿到高分或者能被真正用起来关键不在“功能多”而在“数据完整”。很多人的系统能登录、能增删改查但一查数据就会发现——房间床位状态是乱的学生退宿后历史记录没了宿舍楼和学生的关联字段对不上。这些问题在答辩现场一问就露馅。下面从数据库开始把这一步做扎实。2. 数据库设计先行宿舍管理的核心表与字段约束2.1 宿舍管理系统的实体关系拆解一个宿舍管理系统最核心的实体是楼栋、房间、床位、学生、入住记录。常见做法是用五张表加一张关联表来建模。楼栋和房间是父子关系房间和床位是父子关系学生和床位通过入住记录建立多对多关系——一个学生多次入住不同床位一个床位在不同时间段住过不同学生。字段设计上要注意几个关键点。楼栋表要有楼栋编号和楼栋名称房间表要有房间号、所属楼栋ID、房间类型四人间/六人间、可住人数、当前已住人数。床位表要有床位号、所属房间ID、状态空闲/占用。学生表要有学号、姓名、性别、学院、专业、联系方式。入住记录表是这张设计里最重要的表它必须包含学生ID、楼栋ID、房间ID、床位ID、入住时间、退宿时间并且用退宿时间为空来表示“当前正在入住”。这个模型的关键在于房间的已住人数不应该是手工维护的而是通过统计入住记录里退宿时间为空的记录数得出来的。我见过很多版本的项目在房间表里放一个“已住人数”字段然后在入住和退宿时手动加减这种做法在并发和异常情况下必然导致数据不一致。正确做法是房间表只存固定容量入住人数每次通过SQL实时统计。-- 房间的已住人数通过统计入住记录得到不在房间表冗余维护 SELECT r.room_id, r.capacity, COUNT(sr.student_id) AS current_count FROM room r LEFT JOIN stay_record sr ON r.room_id sr.room_id AND sr.checkout_time IS NULL WHERE r.room_id ? GROUP BY r.room_id, r.capacity这段SQL里LEFT JOIN保证了没有入住记录的空房间也会出现在结果中checkout_time IS NULL这个条件筛出当前有效的入住记录。逻辑说明房间容量是房间表的固有属性当前人数是动态状态两者通过统计关联得到。参数说明r.capacity字段类型建议用TINYINT四人间、六人间这种容量值不会超过255用INT是浪费。2.2 建表SQL与字段类型选型字段类型选型上有一个血泪经验学号不要用INT要用VARCHAR(20)。原因有两个一是学号经常以0开头INT会丢掉前导零二是学号是字符串语义不参与数值运算。同理床位号、房间号这类编号字段也都是字符串语义。时间字段统一用DATETIME不要用TIMESTAMP后者在2038年会有溢出问题而且受时区影响排查问题时会多一个干扰项。CREATE TABLE building ( building_id INT PRIMARY KEY AUTO_INCREMENT COMMENT 楼栋ID, building_no VARCHAR(10) NOT NULL UNIQUE COMMENT 楼栋编号如A栋, building_name VARCHAR(50) NOT NULL COMMENT 楼栋名称 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT楼栋表; CREATE TABLE room ( room_id INT PRIMARY KEY AUTO_INCREMENT, building_id INT NOT NULL, room_no VARCHAR(10) NOT NULL, capacity TINYINT NOT NULL DEFAULT 4, room_type VARCHAR(20) DEFAULT 四人间, UNIQUE KEY uk_building_room (building_id, room_no), CONSTRAINT fk_room_building FOREIGN KEY (building_id) REFERENCES building(building_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT房间表; CREATE TABLE bed ( bed_id INT PRIMARY KEY AUTO_INCREMENT, room_id INT NOT NULL, bed_no VARCHAR(5) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0空闲 1占用, UNIQUE KEY uk_room_bed (room_id, bed_no), CONSTRAINT fk_bed_room FOREIGN KEY (room_id) REFERENCES room(room_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT床位表; CREATE TABLE student ( student_id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, student_name VARCHAR(50) NOT NULL, gender TINYINT COMMENT 0女 1男, college VARCHAR(50) COMMENT 学院, major VARCHAR(50) COMMENT 专业, phone VARCHAR(20) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生表; CREATE TABLE stay_record ( record_id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, building_id INT NOT NULL, room_id INT NOT NULL, bed_id INT NOT NULL, checkin_time DATETIME NOT NULL, checkout_time DATETIME DEFAULT NULL, CONSTRAINT fk_record_student FOREIGN KEY (student_id) REFERENCES student(student_id), CONSTRAINT fk_record_room FOREIGN KEY (room_id) REFERENCES room(room_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT入住记录表;这段建表SQL里有几个设计决策要说明。外键约束FOREIGN KEY在这个项目里必须保留——很多人为了省事去掉外键结果业务层代码逻辑写错时脏数据直接进了库答辩现场被问数据一致性时非常被动。UNIQUE KEY用于防止楼栋内重复房间号、房间内重复床位号这是数据完整性的最后一道防线。所有表都用InnoDB因为MyISAM不支持事务而入住和退宿操作必须在一个事务里完成——同时更新床位状态和插入入住记录任何一个失败都要回滚。参数说明autoincrement起始值如果用了默认1在删除数据后再次插入会跳号这是正常现象不要试图去改AUTO_INCREMENT的值让它连续。字符集统一utf8mb4如果你用utf8存emoji昵称会报错虽然学生表里大概率没有emoji但统一字符集能少一个排查方向。排序规则用默认的utf8mb4_general_ci即可不要用utf8mb4_unicode_ci在这个项目里没有任何性能或正确性差异纯属折腾自己。2.3 初始化数据的坑楼栋、房间、床位一次性生成建完表之后要生成初始化数据。楼栋和房间可以手动插入几条但床位如果也是手动一条条插50个房间每个4个床位就是200条INSERT纯属体力活。常见做法是用一个存储过程或者一个Java程序批量生成。我更推荐写一个简单的Java初始化程序在项目启动时执行而不是用存储过程——因为存储过程对新手来说是个黑匣子出错了不好排查。// 初始化数据生成器为每个房间生成床位记录 public void initBedData() { // 假设room表里已经有数据了查出所有房间ID ListInteger roomIds roomMapper.selectAllRoomIds(); for (Integer roomId : roomIds) { Room room roomMapper.selectById(roomId); // 根据房间容量生成对应数量的床位床号从1开始编号 for (int i 1; i room.getCapacity(); i) { Bed bed new Bed(); bed.setRoomId(roomId); bed.setBedNo(String.valueOf(i)); bed.setStatus(0); // 初始状态为空闲 bedMapper.insert(bed); } } }这段代码逻辑说明先查出所有房间ID逐个获取容量按容量生成床位。参数说明床号用String.valueOf(i)生成从1开始比如1号床就是“1”四人间就有1到4号床。这里有个细节床号不要补零成“01”因为排序时“01”“02”“10”的字典序是对的但如果不补零“1”“2”“10”的字典序会乱到时候管理页面上床位排序就错了。如果你一定要补零String.format(%02d, i)可以生成两位编号。初始化数据这一步做到位后面做入住分配时会非常顺畅——因为所有房间的床位都是完整的不会出现在界面上下拉框里看不到某些床位的情况。3. 从登录到鉴权用Filter还是Interceptor怎么选3.1 登录模块的设计密码加密与Session管理这个系统的登录模块是整个项目里第一个能体现“工程意识”的地方。最忌讳的做法是密码明文存数据库登录时直接SELECT * FROM user WHERE username? AND password?。这种写法在答辩时一旦被问到安全问题基本等于送分题给对方。正确做法是密码加盐哈希哈希算法用SHA-256或BCrypt再配合一个随机盐值。// 注册时生成随机盐值并用盐值密码做SHA-256哈希 public String hashPassword(String rawPassword, String salt) { String salted salt rawPassword; // 盐值前置简单且够用 MessageDigest md MessageDigest.getInstance(SHA-256); byte[] hash md.digest(salted.getBytes(StandardCharsets.UTF_8)); StringBuilder sb new StringBuilder(); for (byte b : hash) { sb.append(String.format(%02x, b)); } return sb.toString(); }逻辑说明盐值前置拼接后做哈希这样即使两个人使用相同密码因为盐值不同最终的哈希值也不同可以防止彩虹表攻击。参数说明盐值可以用UUID.randomUUID().toString()生成存到用户表单独的salt字段里MessageDigest是JDK自带的工具类不需要引入额外依赖。如果你用的是Spring Security直接用它提供的BCryptPasswordEncoder即可但在这个项目里手写SHA-256就够了因为功能简单不需要引入一套完整的安全框架。登录成功后用户信息放进Session。这里有个关键细节不要把用户密码哈希值放进Session只放用户ID、用户名、角色这些必要信息。Session的maxInactiveInterval建议设置为30分钟——Tomcat默认就是30分钟不需要额外配置。如果是“记住我”功能需要用到CookieToken方案但一般的宿舍管理系统用不着别给自己挖坑。3.2 Filter拦截器配置哪些页面需要登录才能访问登录鉴权的核心问题是整个系统的哪些URL需要登录才能访问。常见做法是定义一个LoginFilter在web.xml或Spring配置里注册然后配置url-pattern。但这里有一个新手容易翻车的点很多人把url-pattern配成/*然后把不需要登录的资源——比如登录页面、登录请求、CSS、JS、图片——全部在Filter里放行。这个思路没错但如果在Filter里忘了放行静态资源就会出现一个诡异现象首页能打开但页面样式全部丢失因为CSS被拦截了。filter filter-nameloginFilter/filter-name filter-classcom.dorm.filter.LoginFilter/filter-class /filter filter-mapping filter-nameloginFilter/filter-name url-pattern/*/url-pattern /filter-mappingpublic void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String uri request.getRequestURI(); // 登录页面、登录请求、静态资源直接放行 if (uri.endsWith(/login.jsp) || uri.endsWith(/login) || uri.contains(/static/) || uri.endsWith(.css) || uri.endsWith(.js) || uri.endsWith(.png) || uri.endsWith(.jpg)) { chain.doFilter(req, resp); return; } // 检查Session中是否有登录用户 HttpSession session request.getSession(false); if (session ! null session.getAttribute(loginUser) ! null) { chain.doFilter(req, resp); } else { // 未登录则重定向到登录页 response.sendRedirect(request.getContextPath() /login.jsp); } }逻辑说明request.getSession(false)里的false参数是关键——如果传true未登录用户的请求会强制创建一个新Session这意味着每个未被拦截的请求都会产生一个无用Session时间长了Tomcat内存里全是垃圾Session。参数说明静态资源放行条件用uri.contains(/static/)覆盖项目里统一存放静态资源的目录.css、.js、图片后缀单独再兜底一层双保险。放行条件里还包括了/login这个POST请求路径否则用户提交登录表单时会被当成未登录拦截这是最常见的一个逻辑漏洞。3.3 角色权限管理员和宿管员的菜单级控制宿舍管理系统一般有两种角色系统管理员和宿管员。管理员可以管理楼栋、房间、学生、账号宿管员只能做日常操作——分配房间、登记入住、退宿。如果用SSM框架可以在Interceptor里判断角色也可以用更简单的方案在JSP页面里用自定义标签或c:if来控制菜单显隐。但真正的后端拦截不能只靠隐藏菜单因为懂一点HTTP的人可以直接构造URL去访问管理员接口。public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); User user (User) session.getAttribute(loginUser); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); return false; } // 超级管理员直接放行 if (admin.equals(user.getRole())) { return true; } // 普通管理员只能访问以 /dorm 开头的URL访问 /system 开头的URL则拒绝 String uri request.getRequestURI(); if (uri.startsWith(request.getContextPath() /system/) !admin.equals(user.getRole())) { response.setStatus(403); request.getRequestDispatcher(/WEB-INF/views/error/403.jsp).forward(request, response); return false; } return true; }这段代码是SpringMVC的HandlerInterceptor实现配置在spring-mvc.xml里只对DispatcherServlet映射的URL生效。逻辑说明这个方案比纯Filter更精细因为Interceptor在HandlerMapping之后执行可以拿到处理器信息可以用RequestMapping的路径来精确控制权限而不是靠字符串匹配URL。参数说明/system/这个路径前缀约定是管理员功能模块/dorm/是宿管日常操作模块这个前缀约定在Controller的RequestMapping里要严格遵守否则权限控制就是摆设。虽然SpringMVC的Interceptor功能更强大但Filter负责的登录状态检查和Interceptor负责的权限检查在实际项目中经常会同时存在。分工是Filter管“你是谁”的粗粒度拦截Interceptor管“你能干什么”的细粒度控制。如果你用ServletJSP做这个项目没有Interceptor可用那就把角色判断也加进Filter里逻辑差不多只是代码会多一点。4. 核心业务逻辑入住、退宿、调宿的事务边界4.1 入住分配事务里完成状态更新与记录插入入住操作是宿舍管理系统里最需要小心的地方因为它涉及多张表的联动修改床位状态从空闲变成占用、房间已住人数加一、新建一条入住记录。这三个操作必须在一个事务里完成否则任何一步失败都会留下脏数据——比如床位状态变成占用了但入住记录没插进去床位就平白无故消失了。Transactional(rollbackFor Exception.class) public void checkIn(CheckInRequest req) { // 1. 校验床位状态是否为空闲 Bed bed bedMapper.selectByIdForUpdate(req.getBedId()); if (bed.getStatus() 1) { throw new BusinessException(该床位已被占用); } // 2. 校验学生是否已有未退宿记录 int activeCount stayRecordMapper.countActiveByStudentId(req.getStudentId()); if (activeCount 0) { throw new BusinessException(该学生已有未退宿的入住记录); } // 3. 更新床位状态 bedMapper.updateStatus(req.getBedId(), 1); // 4. 插入入住记录 StayRecord record new StayRecord(); record.setStudentId(req.getStudentId()); record.setBuildingId(req.getBuildingId()); record.setRoomId(req.getRoomId()); record.setBedId(req.getBedId()); record.setCheckinTime(new Date()); // 退宿时间默认为NULL表示当前正在入住 stayRecordMapper.insert(record); }逻辑说明Transactional注解保证方法内所有操作在同一个事务中任何一个异常都会整体回滚。rollbackFor Exception.class这个参数非常重要——Spring默认只在抛出RuntimeException时回滚如果你抛的是自定义的BusinessException通常继承自RuntimeException那没问题但如果你抛的是受检异常必须加上这个参数否则事务不会回滚数据照样写入。参数说明selectByIdForUpdate里的FOR UPDATE是行级锁防止两个管理员同时给同一个学生分配同一个床位——虽然管理器系统并发场景不多但加上不亏代价只是多几行SQL。校验逻辑里有个关键的业务规则一个学生只能有一条未退宿的入住记录。这条规则能杜绝一个学生同时住两个床位的情况但也意味着如果要调宿必须先退宿再入住或者另写一个调宿方法。业务规则用代码写死后数据库层面不需要再加唯一索引——因为stay_record表允许同一学生有多条历史记录只有“当前未退宿的记录最多一条”这个约束而这个约束用唯一索引不好表达。4.2 退宿逻辑不要删除记录要更新状态退宿操作最常见的错误是直接DELETE掉入住记录。这样做看起来干净但历史记录就全部丢了——你是为了要一个“住在哪个房间、什么时候住过”的历史才设计的stay_record表如果退宿时删记录这张表就失去意义了。正确做法是把checkout_time字段从NULL更新为当前时间表示这次入住已经结束。这和MySQL里修改结构时先备份数据是一个道理——数据一旦删除就没有后悔药了。Transactional(rollbackFor Exception.class) public void checkOut(Integer recordId) { // 1. 查出入住记录确认它是未退宿状态 StayRecord record stayRecordMapper.selectById(recordId); if (record null) { throw new BusinessException(入住记录不存在); } if (record.getCheckoutTime() ! null) { throw new BusinessException(该记录已完成退宿请勿重复操作); } // 2. 更新退宿时间 stayRecordMapper.updateCheckoutTime(recordId, new Date()); // 3. 释放床位 bedMapper.updateStatus(record.getBedId(), 0); }逻辑说明退宿只做时间戳更新和床位状态释放不删除任何数据。这里要注意更新退宿时间和释放床位是两个独立的SQL必须放在同一个事务里——如果updateCheckoutTime成功而updateStatus失败就会出现“学生已退宿但床位还被占用”的问题。参数说明checkout_time字段在数据库里允许为NULLNULL就是“未退宿”的语义不要用某个特殊日期比如1970-01-01来替代NULL这种魔法值会给所有查询增加一层判断逻辑。4.3 查询列表的SQL优化分页的count和page要一致管理系统的首页通常是房间列表或学生列表右上角一堆筛选条件、下面一个表格、底部一个分页栏。分页查询的SQL是一个常见翻车点count语句和page语句的WHERE条件不一致。比如count查的是筛选后的总条数而page查的是从第1页开始的数据但你没加同一个筛选条件导致总页数和实际数据对不上——用户看到“共3页”点第2页却一片空白。尽量用同一条筛选逻辑生成两个SQL或在Mapper里把动态SQL抽出来复用。MyBatis的sql标签可以抽公共片段这是最省事的做法。sql idroomListWhere where if testbuildingId ! null AND r.building_id #{buildingId} /if if testroomType ! null and roomType ! AND r.room_type #{roomType} /if if teststatus ! null AND r.status #{status} /if /where /sql select idcountRoomList resultTypeint SELECT COUNT(*) FROM room r include refidroomListWhere/ /select select idselectRoomList resultTypeRoomVO SELECT r.*, b.building_name FROM room r LEFT JOIN building b ON r.building_id b.building_id include refidroomListWhere/ ORDER BY r.building_id, r.room_no LIMIT #{offset}, #{pageSize} /select逻辑说明sql标签定义公共WHERE片段count和page两个查询都引用它这样从根源上杜绝条件不一致。参数说明LIMIT #{offset}, #{pageSize}是MySQL的分页语法offset的算法是(pageNum - 1) * pageSize这个计算在Service层完成不要在SQL里算。比如第2页每页10条offset (2-1) * 10 10代表从第11条开始取。5. 避坑指南IDEA运行、数据库连接、中文乱码的5个经典问题5.1 IDEA配置Tomcat后端口被占用现象点Run按钮启动Tomcat控制台报Port 8080 was already in use或者在浏览器访问项目路径时看到的是另一个应用的页面。如果你的电脑里同时装了Oracle、Nacos或者其他Java应用8080被占用的概率不低。原因8080是Tomcat的默认端口但其他开发工具也可能用这个端口。IDEA里创建Tomcat Server配置时会默认使用Tomcat安装目录下的server.xml里的配置但如果你手动改过端口又在IDEA里配了JMX端口两个Tomcat可能都在监听8080。解决先找到占用端口的进程lsof -i:8080Mac/Linux或netstat -ano | findstr 8080Windows看到PID后用tasklist或ps -ef定位是什么程序。如果是残留的Java进程直接kill掉如果是被你的其他项目占用就在conf/server.xml里改HTTP端口把8080改成8090。改完之后在IDEA的Server | HTTP port里也要同步改这两个地方的端口必须一致否则IDEA会带着不同配置启动Tomcat启动后访问的端口和预期不一致你会误以为项目部署失败了。5.2 JDBC连接MySQL报中文乱码现象页面上显示的学生姓名、楼栋名称全是???或者从数据库里查出来的是乱码控制台日志也打印中文乱码但数据库和表的字符集都已经是utf8mb4了。原因三层编码设置不匹配。第一层是数据库连接URL——如果你的连接串是jdbc:mysql://localhost:3306/dorm?characterEncodingutf8那没问题但如果漏了characterEncodingutf8驱动会使用MySQL服务器的默认字符集很可能不是UTF-8。第二层是Tomcat的URI编码——GET请求的URL参数默认用ISO-8859-1解码浏览器发送UTF-8编码的中文参数Tomcat用错编码解码自然乱码这个需要在server.xml的Connector节点上配置URIEncodingUTF-8。第三层是页面响应编码——JSP页面顶部没有加% page contentTypetext/html;charsetUTF-8 %响应头里没有charsetUTF-8浏览器就会用默认编码解析。解决连接URL加参数是第一步URIEncoding加到Connector节点是第二步JSP页面的page指令是第三步。排查时按这个顺序检查三处一般能解决。特别注意characterEncoding的值是单数UTF-8不是UTF8两个写法MySQL驱动都接受但统一用带连字符的写法减少记忆负担。5.3 MyBatis返回的Map类型字段为null时丢键现象查询结果用MapString, Object接收时某个字段为NULLMap里直接没有这个key。前端JSP用${map.fieldName}取值取不到变成空白和预期不符。原因MyBatis默认的callSettersOnNulls配置是false意味着当查询结果为NULL时ResultSet里的列不会调用Map的put方法所以Map里缺失这个键。解决在mybatis-config.xml里加一行setting namecallSettersOnNulls valuetrue/。这样NULL字段也会以null值放进Map。这个配置在调试时非常有帮助——否则你很难区分“这个字段是NULL”和“这个字段在Map里不存在”排查问题时会多绕很多弯路。5.4 修改了代码但浏览器还是旧页面现象在IDEA里改了JSP或Java代码重启了Tomcat但是浏览器访问的还是旧内容按CtrlF5强制刷新也没用。原因IDEA热部署的配置不对或者浏览器缓存了旧页面。IDEA默认的On Update Action是Restart Server如果没有设置成Update classes and resources修改Java代码后不会自动编译并替换class文件而JSP文件不改的话页面内容也不会变。另外浏览器对静态资源的缓存策略也会导致旧内容滞留。解决在IDEA的Run/Debug Configurations | Server | On Update Action里选择Update classes and resources这样每次修改代码后按CtrlF10可以热更新。如果是JSP修改后不生效检查Tomcat的web.xml里jsp-config的checkInterval是不是默认的0——为0时不检查JSP变更需要手动设置成checkInterval1/checkInterval秒或在conf/web.xml里改。另外开发阶段可以给JSP页面加个版本号参数script src/static/js/app.js?v20240101强制浏览器重新加载。5.5 数据库连接池参数设置不当导致Connection耗尽现象系统运行一段时间后所有页面都卡住控制台报Connection is not available, request timed out重启Tomcat后恢复正常但过一会儿又重新卡住。原因连接池的最大连接数太小或者某个请求占用了连接但没有释放——比如Connection没有放在finally块或try-with-resources里关闭连接泄漏了。Tomcat的JDBC连接池默认maxActive是20如果系统有几十个并发的慢查询20个连接很快就用完了。解决这个问题的根治办法不是调大maxActive而是找到泄漏连接的代码。排查思路在连接池配置里加上testOnBorrowtrue和validationQuerySELECT 1这样每次借出连接时会校验连接是否有效但这个是亡羊补牢——真正有效的做法是检查所有DAO层代码确保Connection、PreparedStatement、ResultSet都用try-with-resources关闭。如果没找到泄漏点可以临时把maxActive调到50看看是否缓解但这只是治标连接泄漏不解决50个连接也会耗尽。6. 从开发到上线Tomcat部署、验证清单和后续演进6.1 手动部署到Tomcat的三种方式对比在IDEA里直接Run能跑起来不代表项目就算“部署完了”。真正上线时你面对的是没有IDEA的服务器环境。能证明自己真的理解部署流程的方法是把WAR包丢到Tomcat的webapps目录下启动Tomcat访问成功。这三种方式各有适用场景——IDEA内嵌部署只适合开发调试manager界面热部署适合偶尔更新手动放WAR包到webapps最传统也最可控。# 先用Maven打包生成WAR文件 mvn clean package -DskipTests # 把打好的WAR包复制到Tomcat的webapps目录 cp target/dormitory.war /opt/tomcat/apache-tomcat-9.0.85/webapps/ # 启动Tomcat前台启动便于看日志 /opt/tomcat/apache-tomcat-9.0.85/bin/startup.sh # 查看启动日志确认没有异常 tail -f /opt/tomcat/apache-tomcat-9.0.85/logs/catalina.out逻辑说明-DskipTests跳过测试代码编译但编译测试代码本身不会跳过——如果你写了一些失败的单测导致打包失败这个参数只跳过执行。WAR包放到webapps后Tomcat启动时会自动解压并部署。参数说明catalina.out是Tomcat的标准输出日志启动时如果有Exception或Error都在这个文件里排查启动失败的第一动作就是tail这个文件而不是去翻IDEA的控制台。部署到服务器上时我一般还会做两件事。一是把conf/server.xml里的Host节点配置一个appBase指向专门的部署目录而不是默认的webapps这样系统升级时只需要替换WAR包不会把Tomcat自带的默认应用也暴露出去。二是改掉Tomcat默认的8005端口——这是Tomcat的关闭端口任何本机进程都可以通过向这个端口发送SHUTDOWN命令来关闭Tomcat不换端口等于给系统留了一个后门。这个动作虽小但体现的是上线意识答辩时提到这个细节会很加分。6.2 验收验证清单从用户视角检查每一个核心流程系统做完之后要按用户的操作路径把核心流程全部走一遍这是赶在答辩前最后一刻才发现“原来这里根本没通”的最后防线。我建议准备一份验证清单每完成一项打个勾不要凭感觉说“应该没问题”。第一个必测项是“登录-退出-登录”的闭环——登录成功后刷新页面是否保持登录状态退出后再访问受保护的URL是否被正确重定向到登录页。第二个是“学生入住全流程”——录入学生信息、分配房间、确认入住、查看房间已住人数是否从0变1、查看该学生的入住记录是否生成。第三个是“退宿全流程”——退宿后床位状态是否变为空闲、房间已住人数是否减1、该生的入住记录是否保留了退宿时间。第四个是“重复操作防护”——同一个学生能不能重复入住、同一个床位能不能被分配两次、退宿过的床位能不能再次分配。这里有一个常用技巧用Postman或浏览器的开发者工具直接构造请求去测试接口。如果你只在页面上点点点可能会漏掉一些前端已经隐藏掉的操作方式——比如直接用POST请求调用退宿接口但传的recordId不存在或者重复调用两次退宿接口看看后端会不会报错还是会把床位状态弄乱。这些异常路径在答辩现场被问到的概率很高别觉得“正常流程没问题就行了”。6.3 这个方向值不值得做投入产出比与后续演进说了这么多最后聊聊你可能会纠结的问题花时间做一个JavaWeb学生宿舍管理系统值得吗我的看法是如果你只是一个学期要交课程设计的学生或者正在准备校招需要补充项目经验这个方向性价比很高。它的技术栈基础——Servlet/JSP或SSM、MySQL、Tomcat——是所有JavaWeb开发的公共底座你在这个项目里踩过的坑在下一个项目里大概率还会遇到提前踩一遍等于给自己攒经验。但如果你的目标是简历上写一个“高含金量”的项目我会提醒你一句宿舍管理系统属于烂大街选题面试官看到这个项目名称的瞬间可能会下意识降低期待值。这时候你逆风翻盘的方式只有一个——能在介绍项目时说出数据库事务边界在哪、连接池参数怎么调、权限拦截怎么做、部署时改过哪些默认配置一切能体现“工程细节”的东西。绣花一样的CRUD代码远不如对生产环境问题的理解有价值后者才是面试官真正想听的。如果做完这个系统还想往上走一步我建议你朝两个方向演进。一个是功能深化把手工分配床位升级成自动分配算法把水电费从手动录入改成按房间用量自动计算把统计报表从“查出来再算”改成定时任务聚合数据。另一个方向是技术演进把JSP换成Vue前后端分离把SSM换成Spring Boot加MyBatis-Plus这样你等于从JavaWeb直接跨进了现在更主流的开发范式。系统本身的教学价值就在这个位子上一根线串起Servlet到Spring Boot的演进逻辑不会让重构显得生硬。我做了这么多年项目最深的感受是不要轻视任何一个看起来简单的管理系统。它朴素的外表下藏着的都是真问题——并发控制、事务边界、权限模型、部署排错。能把一个“简单项目”的每一个细节都讲清楚比只会吹嘘“我们用了微服务架构”但一问细节就卡壳的人强太多。希望这个方案的每一步能帮到你动手跑通的那一刻你会发现它远没有传说中那么可怕。本文还有配套的精品资源点击获取