恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
宿舍管理系统实战:JSP+Servlet+Struts+MySQL全流程解析
首页
资讯中心
/
宿舍管理系统实战:JSP+Servlet+Struts+MySQL全流程解析
宿舍管理系统实战:JSP+Servlet+Struts+MySQL全流程解析
发布时间:2026/10/8 8:46:33
1. 项目概述与整体思路宿舍管理系统听起来像是个学生时期课设的经典命题但真正动手把它从“能跑”做到“能用、够稳、好改”中间还是有很多门道的。这个项目用的是 JSP Servlet Struts MySQL 这条比较经典的技术路线没有上 Spring 全家桶反而更适合拿来理解 JavaWeb 最底层的请求-响应流转过程。先说清楚这个东西到底是干嘛的。宿舍管理系统解决的是后勤管理中最琐碎的问题学生入住退房、床位分配、来访登记、水电费用统计、日常报修处理、公告发布。如果用 Excel 表来管数据一多必然乱套而且多人协作时很容易出现“同一张床分给两个人”这种尴尬情况。系统要做的就是把这些流程信息化让管理员在网页上就能完成全部操作同时让学生能查看自己的住宿信息、提交报修和留言。这个项目适合谁来参考正在做课程设计或毕业设计的计算机专业学生想把 JavaWeb 知识串起来复习的初级开发者以及需要快速搭建一个内部管理系统的业务人员。不要觉得 JSP/Servlet 已经过时——恰恰因为它的核心机制足够直白你才能看清楚一次 HTTP 请求从浏览器到服务器再到数据库的完整生命周期。等你后面去学 Spring MVC你会发现很多概念都是从这套东西演进过来的理解了底层再看框架真的是一通百通。项目的整体设计思路基于经典 MVC 分层JSP 负责 View展示数据Servlet 和 Struts Action 共同承担 Controller接收请求、调用业务逻辑、转发页面JavaBean/DAO 负责 Model与 MySQL 交互。Struts 在这个系统里主要接管了部分模块的请求映射和表单处理其余路径仍用原生 Servlet这种“混合骑”模式在实际项目中其实很常见尤其在老系统升级迭代的过渡期。2. 核心技术点深度拆解2.1 为什么选择 JSP Servlet Struts 这条技术路线先回答一个很多人会困惑的问题既然有 Struts为什么还要用 Servlet直接用 Struts 全部搞定不行吗从架构演进的角度说Struts 1.x/2.x 本身是基于 Servlet 的封装它的核心 ActionServlet 本质上就是一个 Servlet。在很多实际项目里系统并不是一步到位重构的往往是核心模块用成熟的 Struts 框架而一些简单的功能点比如图片验证码、文件上传、导出 Excel直接挂原生 Servlet 更快更灵活。这个项目采用这种方案一是为了让初学者能同时对比“框架处理请求”和“原生处理请求”两种方式二是某些功能用 Servlet 写确实比硬套 Struts 要省事得多。MySQL 在这条链路中的位置是数据持久化层。为什么选 MySQL 而不是 Oracle 或 SQL Server核心原因是部署成本低、社区资料多、语法足够标准。对于宿舍管理系统这个量级的数据几千学生、几万条报修记录MySQL 的性能余量绰绰有余。另一个实际考虑是 MySQL 的可视化工具Navicat、Workbench对学生党极其友好建库建表、调试 SQL 的成本被压到很低。2.2 JavaBean、Servlet、JSP 三者如何协调工作理解这套系统的关键在于把“请求处理流程”在脑子里跑通。用户在前端页面点了“添加学生”按钮浏览器向服务器发送一个 POST 请求请求路径指向 Servlet 或 Struts 的 Action。Servlet/DispatchAction 接收到请求后第一步是解析请求参数学生姓名、学号、宿舍号等第二步是调用业务层组件Service/DAO第三步是拿到操作结果后做页面跳转。这里有一个非常容易踩的坑Servlet 和 JSP 的职责切分。我在最开始写这类系统时总喜欢在 JSP 里直接通过% %写大段 Java 代码去查数据库结果页面逻辑一团乱。正确的做法是 JSP 里只允许通过 JSTL 标签或 EL 表达式读取 request/session 中的现成数据不允许出现业务逻辑。页面加载时先由 Servlet 把数据塞进 request 域再 forward 到 JSPJSP 负责纯展示。听起来很简单但实际编码时很多人会把“在 Servlet 中拼接 HTML”和“在 JSP 中写 Java”当作捷径。前者让代码像一坨移动的字符串拼图后者让页面文件变成一团逻辑垃圾。这个项目的代码结构里两种坏味道都必须避免。2.3 Struts 的定位与实际配置要点Struts 在这个系统里的角色主要体现在两处路由映射和表单封装。以注册登录功能为例传统 Servlet 写法需要你手动调用request.getParameter(username)然后逐个取值、逐个设到对象里。而 Struts 的 ActionForm 可以自动完成参数到 JavaBean 的映射表单属性名和 JavaBean 字段名只要一致框架自动填充。这个能力在录入学生信息、批量修改床位的时候能节省大量样板代码。配置 Struts 的核心在struts-config.xml。你需要仔细维护action-mappings把每个 URL 路径映射到对应的 Action 类同时声明它用的 ActionForm、成功或失败时跳转的页面。我见到过不少新手在 Action 类里抛了异常但配置里没有写exception处理结果一报错就是一个满屏 stack trace 的白页问题排查全靠猜。需要注意一个版本问题这个项目说的是 Struts实际网上能找到的多数教学资源是 Struts 1.x 和 2.x 混杂的。1.x 靠配置文件2.x 靠注解。如果你在导入项目时遇到ClassNotFoundException: org.apache.struts.action.Action大概率是 jar 包版本和代码不匹配。我的建议是如果你从 0 手写直接用 Struts 2 起步社区更活跃坑也没那么多如果是修改老代码先辨别它的web.xml里配的是ActionServlet1.x还是StrutsPrepareAndExecuteFilter2.x。2.4 MySQL 数据库设计表结构是系统的骨架宿舍管理系统我个人倾向于先画 ER 图再动手建表。这个系统的核心实体有学生、宿舍楼、房间、床位、管理员、访客记录、报修工单、缴费记录、公告。实际操作中最需要花心思的是“宿舍与学生”的关系设计。一个学生住一个床位一个床位属于一个房间一个房间属于一栋楼。而历史入住记录又是另一张表——因为学生可能会换宿舍退宿之后不能把原来的记录抹掉要把打扫卫生、维修记录等留档。因此我建议采用三张核心表加若干外键student学生信息主表字段包括 id、学号、姓名、性别、班级、联系方式等dormitory宿舍房间表字段包括 id、楼栋、房间号、床位总数、已住人数、是否满员assign_record入住分配记录表包含学生 id、宿舍 id、入住时间、退宿时间、状态为什么不直接在student表里加一个dormitory_id字段完事因为这样只能记录“当前状态”无法追溯历史。比如到了大四需要统计“曾经住过 3 号楼的学生有哪些”如果没有中间表数据根本无法还原。这就是典型的“业务驱动表设计”——你不仅要知道系统现在要为谁服务还要预判半年后新增的需求。水电费用的表设计也要注意。不建议用“一个宿舍一行一个字段存电费、一个字段存水费”这种平铺式设计而应该用bill_record明细表每条记录包含宿舍 id、费用类型水/电、费用金额、计费周期、是否已缴。好处是实现“按月份查某个宿舍历史缴费”时一个简单的WHERE dormitory_id? AND bill_month BETWEEN ? AND ?就能完成不用来回修改表结构。2.5 数据库连接JDBC 还是连接池另一个绕不开的问题是数据库连接的建立方式。最简单的写法是Class.forName(com.mysql.cj.jdbc.Driver); Connection conn DriverManager.getConnection( jdbc:mysql://localhost:3306/dorm?useSSLfalseserverTimezoneAsia/Shanghai, root, password);能跑但性能极差。每来一个请求就建立一次物理连接高并发下 MySQL 默认的最大连接数默认 151很快被打满系统表现为“有时能开页面有时直接白屏”。正确的做法是用连接池建议用Druid或C3P0。Druid 是阿里开源的自带监控页面配置也很简单。driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/dorm?useUnicodetruecharacterEncodingUTF-8useSSLfalseserverTimezoneAsia/Shanghai usernameroot password123456 initialSize5 maxActive50 maxWait10000通过DruidDataSource获取连接后使用完调用conn.close()其实不是真的断开而是把连接还给池子下一个请求还能复用。这一步改造对系统性能的提升是立竿见影的。连接 MySQL 8.x 和 5.x 有两个关键差异必须注意。一是驱动类名5.x 用com.mysql.jdbc.Driver8.x 用com.mysql.cj.jdbc.Driver用错会直接报ClassNotFoundException。二是 8.x 在连接串里必须显式指定serverTimezone否则会抛The server time zone value ...异常。这俩坑几乎每个新手都要踩一次。3. 实操过程与核心环节实现3.1 完整的开发环境搭建清单先列一套我实测下来兼容性最好的环境组合组件版本建议说明JDK1.8 或 11老项目建议 1.8Struts 1.x 在更高版本容易出反射报错Tomcat8.5 或 9.0Servlet 3.x / 4.x 规范MySQL5.7 或 8.0课程设计绝大多数用的是这两个版本IDEIntelliJ IDEA 或 Eclipse各有各的配置套路Maven3.6用于管理 jar 依赖不少人是直接用 IDEA 打开别人的项目结果发现 JDK 版本不匹配、Tomcat 没有配置、jar 包全部报红。这让我想起网上很常见的一个提问在 IDEA 里配置 JavaWeb 项目到底该怎么设。我建议你不管拿到的是手写项目还是 Maven 项目都先点击File - Project Structure - Modules检查 Dependencies 里是否有完整的依赖列表。如果是手工拷 jar 的形式把web/WEB-INF/lib目录设置为 Libraries 即可。然后配置 TomcatRun - Edit Configurations - Add New - Tomcat Server Local在Deployment里点加号把项目的war exploded加进去Application context 建议设为/dorm。这样启动后访问地址就是http://localhost:8080/dorm。3.2 数据库建库脚本与初始化数据下面给一套可以直接复制的建库脚本核心片段。宿舍管理系统的表结构不只一张表但核心骨架这样搭CREATE DATABASE IF NOT EXISTS dorm_system DEFAULT CHARACTER SET utf8mb4; USE dorm_system; CREATE TABLE student ( id INT AUTO_INCREMENT PRIMARY KEY, student_no VARCHAR(20) UNIQUE NOT NULL, name VARCHAR(50) NOT NULL, gender CHAR(2) DEFAULT 男, class_name VARCHAR(50), phone VARCHAR(20), status TINYINT DEFAULT 1 COMMENT 1-在校 0-离校, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE dormitory ( id INT AUTO_INCREMENT PRIMARY KEY, building VARCHAR(20) NOT NULL, room_no VARCHAR(20) NOT NULL, bed_count INT DEFAULT 4, used_count INT DEFAULT 0, UNIQUE KEY uk_building_room (building, room_no) ) ENGINEInnoDB; CREATE TABLE assign_record ( id INT AUTO_INCREMENT PRIMARY KEY, student_id INT NOT NULL, dormitory_id INT NOT NULL, assign_time DATETIME DEFAULT CURRENT_TIMESTAMP, leave_time DATETIME NULL, status TINYINT DEFAULT 1 COMMENT 1-在住 0-已退, FOREIGN KEY (student_id) REFERENCES student(id), FOREIGN KEY (dormitory_id) REFERENCES dormitory(id) ) ENGINEInnoDB;注意dormitory表里用了UNIQUE KEY(building, room_no)这是为了防止同一栋楼里插入两遍201 房间。很多人一开始会忽略这个约束等到分配宿舍时发现“同一个房间出现两次”才回头补但那张表里可能已经有脏数据了处理起来非常麻烦。建表这一步约束能给全就给全。MySQL 5.7 和 8.0 的默认存储引擎都是 InnoDB所以不用显式指定但写上ENGINEInnoDB是一个好习惯方便后来者一眼看出表的事务支持。宿舍管理系统的数据量不大但涉及缴费和入住操作事务仍然重要。3.3 登录模块实现与 Session 管理登录是几乎所有管理系统都绕不开的模块也是理解 Servlet/Session 联动机制的最佳场景。先实现一个原生 Servlet 作为登录入口因为它的流程最透明。前端登录表单提交到LoginServletWebServlet(/login) public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username request.getParameter(username); String password request.getParameter(password); UserDao dao new UserDao(); User user dao.findByUsernameAndPassword(username, password); if (user ! null) { request.getSession().setAttribute(loginUser, user); response.sendRedirect(request.getContextPath() /index); } else { request.setAttribute(errorMsg, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); } } }注意登录成功后使用sendRedirect而不是forward。原因是如果继续 forward浏览器地址栏仍是/login用户刷新页面就会触发表单重复提交。用重定向可以让地址变为/index刷新时只是重新 GET index 页面不会再提交一次登录请求。这个细节在工作面试和实际项目中都很加分。Session 超时时间在web.xml里可以统一配置session-config session-timeout30/session-timeout /session-config单位是分钟。对于宿舍管理系统来说30 分钟是一个合理的值——太短学生填个报修单填到一半就被踢下线太长又容易让闲置的会话占用服务器内存。还有一个经常被忽略的问题退出登录。多数项目退出就是删 Session 里的用户对象但更严谨的方式是让Session.invalidate()把整个会话销毁并且删除用户相关的其他状态。我在一处代码评审里看到有人用session.removeAttribute(loginUser)做退出但购物车等多处状态还留在 Session 里等于退出了个寂寞。3.4 学生入住分配的核心业务实现这个业务逻辑是整个系统最复杂的一块涉及多表联查和事务控制。需求是管理员选择一个学生再选择一个房间点击分配后系统自动检查房间是否已满员、学生是否已分配过其他房间然后写入assign_record同时把dormitory.used_count加 1。核心代码片段如下public boolean assignStudent(Integer studentId, Integer dormId) { Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DBUtils.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 检查房间是否已满 String checkRoomSql SELECT bed_count, used_count FROM dormitory WHERE id? FOR UPDATE; ps conn.prepareStatement(checkRoomSql); ps.setInt(1, dormId); rs ps.executeQuery(); if (rs.next()) { int bedCount rs.getInt(bed_count); int usedCount rs.getInt(used_count); if (usedCount bedCount) { return false; // 房间已满 } } // 2. 检查学生是否已有在住记录 String checkStudentSql SELECT COUNT(*) FROM assign_record WHERE student_id? AND status1; ps conn.prepareStatement(checkStudentSql); ps.setInt(1, studentId); rs ps.executeQuery(); if (rs.next() rs.getInt(1) 0) { return false; // 学生已有住宿记录 } // 3. 插入分配记录 String insertSql INSERT INTO assign_record(student_id, dormitory_id) VALUES(?,?); ps conn.prepareStatement(insertSql); ps.setInt(1, studentId); ps.setInt(2, dormId); ps.executeUpdate(); // 4. 更新房间已住人数 String updateSql UPDATE dormitory SET used_count used_count 1 WHERE id?; ps conn.prepareStatement(updateSql); ps.setInt(1, dormId); ps.executeUpdate(); conn.commit(); return true; } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { DBUtils.close(conn, ps, rs); } }这里有几个关键点值得说道说道。第一是事务。分配宿舍涉及“插入入住记录”和“更新房间人数”两步操作任何一步失败都会导致数据不一致。所以必须用setAutoCommit(false)开启事务最后统一 commit。如果中间抛异常就 rollback。第二是SELECT ... FOR UPDATE。这是 MySQL InnoDB 提供的行级锁语法作用是锁定这条宿舍记录防止两个管理员同时给 201 房间分配学生时都查询到used_count 3都认为可以入住结果实际变成 5 个人住 4 人间。用悲观锁解决并发问题在业务简单的时代是稳妥的选择。当然也可以先在 WHERE 里加used_count bed_count做更新那叫乐观锁方案各有各的应用场景。第三return false其实不太优雅。用户看到“分配失败”四个字并不知道具体原因。更好的是定义业务异常或者返回一个结果对象包含失败原因。但作为课程设计或内部工具给个 false 然后前端提示“没有可用房间或学生已在住”也能凑合。3.5 数据列表的分页与模糊搜索宿舍管理系统的学生列表、报修列表、访客记录都会面对同一类需求数据多了以后页面加载极慢用户又需要按条件搜索。如果一次把几千条数据全塞到页面上浏览器压力大不说用户体验也差。这里必须在 SQL 层面做分页而不是查出全量后在 Java 代码里自己切分。MySQL 分页语法是LIMIT ?, ?。第一个参数是偏移量第二个是每页条数。实现分页查询的核心步骤int pageNum Integer.parseInt(request.getParameter(pageNum) null ? 1 : request.getParameter(pageNum)); int pageSize 10; int offset (pageNum - 1) * pageSize; String keyword request.getParameter(keyword); String sql SELECT * FROM student WHERE name LIKE ? OR student_no LIKE ? LIMIT ?, ?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, % keyword %); ps.setString(2, % keyword %); ps.setInt(3, offset); ps.setInt(4, pageSize);还需要一个总条数的查询String countSql SELECT COUNT(*) FROM student WHERE name LIKE ? OR student_no LIKE ?;有了总条数和每页条数就能算出总页数totalPage (totalCount pageSize - 1) / pageSize;。前端展示时可以做简单的分页导航条c:if test${currentPage 1} a hrefstudentList?pageNum${currentPage - 1}keyword${param.keyword}上一页/a /c:if c:forEach begin1 end${totalPage} vari a hrefstudentList?pageNum${i}keyword${param.keyword}${i}/a /c:forEach c:if test${currentPage totalPage} a hrefstudentList?pageNum${currentPage 1}keyword${param.keyword}下一页/a /c:if注意要把keyword参数在分页链接里带上否则你一翻页搜索条件就丢了回到全量列表。这个小细节是分页功能“看起来好用”的关键。3.6 JSP 页面中照片定位与展示的处理技巧搜索热词里有人问到“JSP 图片如何对坐标定位”这确实是个挺实用的场景。宿舍管理系统中人脸照片、宿舍楼照片的展示经常会遇到“同一张页面里图片旁边要放置学生的具体信息”这种布局需求。如果你是在 JSP 中用原生 div CSS 定位图片标准做法是给外层容器设position: relative给图片容器设position: absolute并指定 top/left 距离。比如宿舍平面图上要标记每个床位的位置可以让每个床位编号通过坐标浮在图片上。div styleposition:relative; width:800px; height:600px; img src${pageContext.request.contextPath}/images/dorm_plan.png stylewidth:800px; height:600px;/ div styleposition:absolute; left:120px; top:180px; border:2px solid red;101床/div div styleposition:absolute; left:220px; top:180px; border:2px solid blue;102床/div /div坐标怎么算最简单的办法是先在浏览器里打开图片按 F12 打开开发者工具鼠标悬停在图片的对应位置读取右下角显示的坐标值然后写进代码。如果你的需求更加动态化比如后台可以设置每个床位在平面图上的坐标那就在数据库加x_pos、y_pos两个字段页面用 EL 表达式动态输出div styleposition:absolute; left:${bed.xPos}px; top:${bed.yPos}px; ${bed.bedNo} /div这里有一个很容易踩的坑写了绝对定位但外层容器没有设position: relative结果坐标是相对于整个页面定位的图片一滚动标记位置全部错乱。先确认父容器是否设置了相对定位再调整子元素的坐标。3.7 Struts 与 Servlet 在项目中的分工策略实务里这个项目如何同时驾驭 Struts 和原生 Servlet我建议按模块划分学生管理、宿舍分配交给 Struts 处理原因是这类模块表单字段多用 Struts 的 ActionForm 自动封装参数能省大量代码。登录认证、图片验证码、Excel 导出使用原生 Servlet原因是这些功能要么逻辑单一、要么涉及二进制流输出没必要套框架。一个 Struts 2 的 Action 实现学生新增类似这样public class StudentAction extends ActionSupport { private Student student; private StudentService studentService new StudentService(); public String add() throws Exception { studentService.add(student); return SUCCESS; } }这里的Student对象由框架通过反射自动填充前端表单的input namestudent.name /、input namestudent.studentNo /会自动映射到student对象的同名属性。对比原生 Servlet 你需要在doPost里写十几行getParameter加setXxx的代码省下来的量非常可观。但要注意 Struts 的拦截器栈默认会拦截所有 Action 请求如果你的页面里出现 jQuery 异步提交到 Struts Action而返回的是一段 JSON 字符串需要配置package namedefault extendsstruts-default result-types result-type namejson classorg.apache.struts2.json.JSONResult/ /result-types /package否则 Struts 会把响应包装成内部 HTML 格式前端拿到的东西没法解析常表现为“明明后台返回了数据前端却取不到”。这类问题排查起来非常痛苦因为它不报错只是数据结构和预期不符。4. 常见问题与排查技巧实录4.1 MySQL 连接报错时区与驱动不匹配这是出现频率最高的坑尤其是把项目从 MySQL 5.7 换到 8.0 之后。典型报错有两类一类是java.sql.SQLException: The server time zone value йʱ is unrecognized...意思是服务器时区不被 JDBC 驱动识别。解决办法在连接 URL 末尾加serverTimezoneAsia/Shanghai。如果你的 MySQL 是 8.x还需要把驱动类从com.mysql.jdbc.Driver改成com.mysql.cj.jdbc.Driver。另一类是Loading class com.mysql.jdbc.Driver. This is deprecated.这只是一个警告不影响功能但说明你用的是 MySQL 8.x 的驱动连接 5.x 的数据库或者驱动版本和代码里的类路径不匹配。强迫症可以去 Maven 仓库下载mysql-connector-java-8.0.x.jar并把代码里的Class.forName改成 8.x 的类名。我在网上经常看到有人手动下载 MySQL 5.7.44 的安装包但解压后启动服务失败提示[ERROR] [MY-014060] invalid mysql server upgrade。这个问题通常是你的 data 目录里残留了旧版本的数据文件和数据文件版本不一致。最简单的解决办法是备份数据后删掉 MySQL 安装目录下的data文件夹用mysqld --initialize-insecure重新初始化一个无密码的 data 目录。这个操作对 5.7 和 8.0 都适用属于“重启解决 90% 问题”之外的强力招数。4.2 JSP 页面中文乱码的根因与处置中文乱码是 JavaWeb 的老大难问题但根因无外乎四层页面编码、请求编码、响应编码、数据库编码。四层必须统一成 UTF-8缺任意一个都可能出现乱码。页面层JSP顶部必须写% page contentTypetext/html;charsetUTF-8 languagejava %请求和响应编码层在 Servlet 的doGet/doPost开头加request.setCharacterEncoding(UTF-8); response.setCharacterEncoding(UTF-8);MySQL 数据库层建库时指定DEFAULT CHARACTER SET utf8mb4同时连接 URL 里加characterEncodingUTF-8。顺序很重要request.setCharacterEncoding必须在第一次调用getParameter()之前执行。如果你先调用了参数再设置编码后设置是无效的。基于这个原因我习惯写一个 Filter 在过滤链的最前端统一处理编码而不在每个 Servlet 里重复写。这样既避免了重复劳动又防止遗漏导致“某一个接口乱码”。WebFilter(urlPatterns /*) public class EncodingFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding(UTF-8); response.setCharacterEncoding(UTF-8); chain.doFilter(request, response); } }这个 Filter 是整个系统“不乱码”的最关键保障。加了它之后你基本不用操心每个 Servlet 的内部编码问题。4.3 Tomcat 端口占用与内存溢出排查开发过程中最常见的卡壳场景改完代码重启 Tomcat却提示Port 8080 was already in use.原因是上一个 Tomcat 进程没被正常关闭比如 IDEA 里点了 Stop但后台 java 进程还在。解决方法是命令行执行netstat -ano | findstr 8080找到LISTENING状态对应的 PID然后taskkill /F /PID PID强制结束。Windows 和 Linux 的命令不同但排查逻辑一致。另一个跟内存相关的报错是java.lang.OutOfMemoryError: PermGen space这个多发生在 Tomcat 8 以下的版本因为 JDK 8 以前方法区叫 PermGen默认大小很小。部署了 Struts 多个 jar 包后很容易耗尽。解决办法是修改catalina.bat里的JAVA_OPTSset JAVA_OPTS-Xms256m -Xmx512m -XX:MaxPermGen128mJDK 8 之后 PermGen 换成了 Metaspace默认使用系统内存一般不用再手动配。如果你在启动时看到java.lang.OutOfMemoryError: Java heap space说明-Xmx不够用调大些或者检查代码里是不是有对象一直被 Session 引用无法释放。4.4 页面跳转 404 与 405 错误对照开发调试时最容易让人困惑的是明明路径看着没问题却总是 404。我把常见情况整理成表方便排查现象可能原因解决要点访问/studentList报 404Servlet 未配置映射或注解路径写错检查WebServlet值是否与地址一致访问/studentAdd报 405表单 POST 但 Servlet 只重写了doGet改为实现doPost或让doGet转发Struts Action 返回 404struts.xml里包名或 result 路径不对检查result namesuccess页面是否存在页面能打开但 CSS/JS 全部失效JSP 里静态资源路径写死使用${pageContext.request.contextPath}拼接请求转发后地址栏不变且刷新重复提交登录成功后用了 forward改用sendRedirect静态资源路径的问题尤其隐蔽。你的页面在/dorm/login.jsp时可以正常加载 CSS但转发到/dorm/user/list.jsp时如果 CSS 路径写的是css/style.css浏览器会解析成/dorm/user/css/style.css自然就 404。统一改成link relstylesheet href${pageContext.request.contextPath}/css/style.css可以一劳永逸。4.5 数据库死锁与事务隔离级别宿舍分配模块中大量使用了SELECT ... FOR UPDATE和事务操作如果多个会话同时操作同一行记录可能出现死锁错误Deadlock found when trying to get lock; try restarting transaction出现死锁不代表代码有 bug而是并发场景下的正常现象。业务上需要注意两点一是尽量保持事务短小不要在事务里做耗时操作比如调用外部接口二是所有操作同一组表的 SQL 都要保持相同的加锁顺序。举个例子分配宿舍时永远先查dormitory再插入assign_record而不是在另一个方法里先操作assign_record再查dormitory。对于宿舍管理系统这种内部系统并发量并不大绝大多数死锁通过“捕获异常后重试一次”就能解决。但如果你是想把系统放到较高并发环境里用建议把事务隔离级别从默认的REPEATABLE_READ调整到READ_COMMITTED并用乐观锁在dormitory表加version字段代替悲观锁这样死锁概率会小很多。MySQL 锁的分类也是搜索热词中的高频话题简单梳理理解就行按粒度分有表锁MyISAM和行锁InnoDB按模式分有共享锁LOCK IN SHARE MODE和排他锁FOR UPDATE按实现分有记录锁、间隙锁、临键锁。其实咱们日常遇到的大部分死锁问题都是因为 InnoDB 的间隙锁在REPEATABLE_READ隔离级别下导致插入操作阻塞引起的。4.6 系统性能优化笔记宿舍管理系统本身压力不大但做性能优化能让你在答辩或汇报时多一个亮点。我实际应用中压过几个优化点加索引assign_record表的student_id和dormitory_id都是外键频繁出现在 WHERE 条件中必须加索引。student表的student_no是唯一键用来做学号查询天然是索引。关注高频查询条件的列索引不要加滥了每个表 3-5 个即可。SQL 预编译用PreparedStatement而不是Statement一方面防 SQL 注入另一方面 MySQL 对预编译语句有缓存机制同样结构的 SQL 第二次执行会更快。浏览器缓存对于纯静态资源CSS、JS、图片在响应头设置Cache-Control: max-age86400刷新页面时就不用重复下载。这个配置在 Filter 里做即可。分页出现在最需要的地方列表页、报修记录页必须搞。首页统计面板不需要分页因为展示的是聚合结果。有同事问我“系统数据量就几千条做这么多优化是不是闲着没事”我的观点是优化不只是为了当前系统更快而是为了让你理解什么是数据库的合理运用。你如果能在答辩时说清楚“我在哪些表加了索引、为什么这么加、事务隔离级别是怎么设置的、死锁是怎么处理的”这个项目的层次感一下就出来了。4.7 常见问题速查表最后把我在开发和维护这类系统时踩过的坑整理成一个速查表类别现象快速解决方案DB 连接Access denied for user rootlocalhost密码错误或 MySQL 8.x 默认插件为caching_sha2_password改为mysql_native_passwordDB 连接Communications link failureMySQL 服务没启动检查net start mysql或服务列表DB 连接启动报Invalid mysql server upgrade删除旧 data 目录重新初始化中文乱码页面显示???统一页面、Filter、连接 URL 三处 UTF-8JSP 报错页面上有大量 stack trace检查 EL/JSTL jar 是否引入常见于手动拷贝 jar 场景StrutsNo action mapped for namespace /struts.xml的 action name 与表单 action 不一致图片定位坐标错位父容器必须position:relative且确认外层没有 transform高并发宿舍分配偶尔丢记录事务 SELECT FOR UPDATE并保证加锁顺序5. 写在最后的使用体会宿舍管理系统这种“小而全”的项目其实最适合作为理解 JavaWeb 全流程的跳板。它的业务逻辑足够清楚不涉及复杂的支付、推荐等业务场景你可以把精力全部放在“请求怎么走、数据怎么存、页面怎么渲染”这三件事上。我个人的建议是不要满足于“能跑起来”。你可以试着给自己加几个问题——如果把系统部署到公网服务器上需要改什么如果学生端换成小程序、后台管理员端保持不变接口层面要动哪里如果要多支持一栋新宿舍楼是否需要改表结构这些问题想明白了你对 JavaWeb 的理解就超过大多数停留在“抄代码运行成功”阶段的学习者。再分享一个小技巧调试时尽量用浏览器的开发者工具查看 Network 面板里请求的状态码和响应内容。很多人遇到问题第一反应是打断点但对于 Web 项目先看 HTTP 层往往更高效。404 就先查路径映射405 就先查请求方法500 再去翻控制台日志。按照这个顺序排查很多问题五分钟内能定位。这个系统后续如果要扩展我建议优先做三件事一是把登录模块升级为角色权限控制管理员、宿管员、学生各一套菜单二是把 SQL 语句全部迁到 MyBatis 里管理三是引入日志框架记录关键操作日志以便追溯。这三点做完项目就从一个课设级别的小系统变成一个具备真实生产环境雏形的管理系统了。