尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

JavaWeb宿舍管理系统课设实战:从环境配置到答辩加分项

JavaWeb宿舍管理系统课设实战:从环境配置到答辩加分项 简介面向JavaWeb初学者和课程设计人群这套基于JSPServlet实现的宿舍管理系统覆盖用户管理、宿舍分配、资源预订等常见业务模块既适合毕业设计、课程设计参考也能帮助理解MVC分层、请求转发与数据库交互流程。压缩包共281个文件大小约4.68MB以jsp页面、java源码、class编译产物、html静态页、js脚本及jar依赖为主另含sql脚本、xml配置和说明文档目录结构清晰方便按前端页面、后端代码、依赖库分类学习。系统采用DAO、Service、Servlet分层实现业务类负责数据读写Servlet接收请求并完成转发可直观体会JavaWeb项目的典型组织方式在此基础上还能延伸学习JDBC数据库操作、Session会话管理、异常处理与项目部署调试等知识点。已有64人学习浏览适合需要完整可运行案例进行对照练习、梳理原生Servlet写法的读者。1. 宿舍管理系统这个 javaweb 大作业值不值得自己动手跑一遍期末周的下载文件夹里总有几个标注着“javaweb大作业通过jspservlet实现宿舍管理系统.zip”的课设包。这个压缩包解决的是 JavaWeb 课程里最常见的一个交付需求用 JSP 做页面、Servlet 处理请求、JDBC 操作 MySQL把学生入住、调换宿舍、报修和来访登记串成一个能跑通的后台管理系统。它适合正在赶 JavaWeb 大作业的人也适合想在一周内完整走一遍前后端交互链路的初学者。先说结论这个方向值得做代码量适中、逻辑层次清晰但版本搭配和数据库初始化是绝大多数人翻车的地方。下面我按自己带课设的顺序从解压、建库、部署、改功能到答辩前复验把这个项目完整拆给你看。2. 环境准备JDK、Tomcat、MySQL 和 IDEA 的搭配方案拿到 zip 后第一件事不是解压而是先判断这个项目的技术版本。JavaWeb 课设项目对环境极其敏感JDK、Tomcat、MySQL 三个版本只要错一个后面全是编译错误和运行时异常。我见过有人用 JDK 17 去跑一个老课设反射调用直接报模块访问错误查了两天最后发现是版本问题非常浪费时间。2.1 JDK 8 Tomcat 9 MySQL 5.7最稳妥的课设组合打开项目源码先看 Servlet 类的 import 语句。如果导入的是 javax.servlet.http.HttpServlet那这个项目是按 Java EE 8 规范写的应该用 Tomcat 9 或更低版本如果导入的是 jakarta.servlet.http.HttpServlet就必须用 Tomcat 10因为 Servlet 5.0 开始包名从 javax 换成了 jakarta。市面上流传的课设 zip 九成都是前者所以 Tomcat 9 几乎是一个通用答案。组合适用项目特征备注JDK 8 Tomcat 9 MySQL 5.7import javax.servlet.*老驱动最稳课设首选JDK 8 Tomcat 9 MySQL 8.0import javax.servlet.*新驱动需要换驱动类名并加时区参数JDK 17 Tomcat 10import jakarta.servlet.*新规范写法课设少见JDK 版本我建议固定在 8不是因为不能装新的而是课设代码里有很多不规范写法比如直接用反射操作私有字段、依赖一些老 APIJDK 11 以后模块系统对这些行为限制很严报错信息新手根本看不懂。MySQL 从 5.7 换到 8.0 只有一个坑最明显驱动类名不一样。老代码里写 com.mysql.jdbc.Driver在 MySQL 8 的驱动包下已经移除了必须改成 com.mysql.cj.jdbc.Driver连接 URL 还要加 serverTimezoneAsia/Shanghai否则建立连接时直接报时区异常。判断方法很简单打开项目里 WEB-INF/lib 目录看 jar 包名字mysql-connector-java-5.x.jar 和 8.x.jar 的处理方式完全不同。2.2 依赖管理保留 lib 目录还是迁移到 Maven大作业 zip 里最常见的形态是 WebContent/WEB-INF/lib 下堆着一排 jar 包这是 JavaWeb 课程的经典结构。好处是解压即用不需要联网拉取依赖坏处是 jar 版本混乱缺依赖时排查全靠眼睛看。我的建议是如果项目两三天内就要交保留 lib 方式不要为追求工程化去迁 Maven。迁移过程中会遇到依赖 scope 配置错、war 包里没打依赖、IDEA 和 Maven 两套 classpath 互相干扰的问题每一个都够你折腾半天。维度lib 目录Maven上手难度低复制即用中需要理解坐标和 scope依赖冲突排查手动看 jar命令行依赖树打包一致性容易漏 jar配置好基本不会漏课设适用度高中如果你坚持用 Maven 把课设代码搬进新工程注意三点servlet-api 和 jsp-api 的 scope 必须是 provided这两个 jar 由 Tomcat 提供打包 war 时不能带进去mysql 驱动和 Druid 连接池用 compilepom.xml 里配置 maven-war-plugin 指定 war 包名否则生成的 war 名和 Tomcat 上下文路径对不上。下面是迁移时最常用的依赖片段。dependencies !-- Servlet 和 JSP 由 Tomcat 提供打包时排除 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency !-- MySQL 驱动和连接池 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency /dependencies这段配置里最容易出错的是第一个依赖scope 写成 compile 的同学不在少数。一旦写成 compile导出 war 包时 servlet-api 会被打进去Tomcat 启动时检测到自带 jar 和项目 jar 冲突直接抛 java.lang.LinkageError这是非常典型的迁移翻车现场。provided 的作用就是告诉打包工具编译期我用你运行期容器提供打包时你别带。2.3 解压后的目录结构先认位置再改代码非 Maven 的课设项目目录通常长这样DORM/ (项目根目录) ├─ src/ │ ├─ com/xxx/entity/ 实体类 │ ├─ com/xxx/dao/ JDBC 数据访问层 │ ├─ com/xxx/servlet/ 控制器层 │ └─ com/xxx/util/ 工具类 └─ WebContent/ ├─ WEB-INF/ │ ├─ lib/ 所有依赖 jar │ ├─ web.xml 部署描述符 │ └─ classes/ 编译产物 ├─ css/ js/ images/ ├─ login.jsp └─ index.jsp用 IDEA 导入这个项目时要确认三件事src 目录被标记为 Sources Root否则 IDEA 不认类一个能跑通的 JavaWeb 项目src 下面至少要拆出 entity实体类、dao数据访问、servlet控制器、util工具四层。WebContent 目录被标记为 Web 资源目录否则 JSP 和 web.xml 不会进 artifactlib 目录保持默认即可不要额外标记。如果在 Project Structure 里看到目录图标带红色标记就是这三处没设对。环境变量方面IDEA Ultimate 关联 Tomcat 时不太依赖 JAVA_HOME但如果你准备手动把 war 包丢进 Tomcat 的 webapps 目录来演示那 JAVA_HOME 必须指向 JDK 8 安装目录CATALINA_HOME 指向 Tomcat 解压目录并且 PATH 里要包含 %JAVA_HOME%\bin。检查办法很简单命令行敲 java -version 和 catalina version两个命令都有正常输出说明环境没问题。web.xml 也要打开看一眼里面配置的 context path 和欢迎页决定了项目访问入口。3. 数据库设计六张表、初始化脚本和 JDBC 连接配置宿舍管理系统的核心业务是“人住到哪、坏了谁修、来了谁访”表结构不需要刻意复杂。很多课设项目把表拆到十几张最后数据对不上、外键乱飞答辩时被老师一问就卡壳。我这里按六张表的方案讲逻辑清晰功能覆盖也够完整。3.1 表结构admin、student、building、dormroom、repair、visitoradmin 存登录账号building 表示宿舍楼栋dormroom 表示具体房间用 beds 字段记录空床位数量student 关联到楼栋和房间repair 记录报修visitor 登记外来人员。六张表之间只有三条外键关系简单但完整。下面是建表 SQL按 MySQL 5.7 和 8.0 都兼容的写法给出注意统一使用 utf8mb4 字符集。CREATE DATABASE IF NOT EXISTS dorm DEFAULT CHARSET utf8mb4; USE dorm; CREATE TABLE admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50) ) ENGINEInnoDB DEFAULT CHARSET utf8mb4; CREATE TABLE building ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, floors INT NOT NULL DEFAULT 6 ) ENGINEInnoDB DEFAULT CHARSET utf8mb4; CREATE TABLE dormroom ( id INT PRIMARY KEY AUTO_INCREMENT, building_id INT NOT NULL, room_no VARCHAR(20) NOT NULL, capacity INT NOT NULL DEFAULT 4, beds INT NOT NULL DEFAULT 4, UNIQUE KEY uk_building_room (building_id, room_no), CONSTRAINT fk_dorm_building FOREIGN KEY (building_id) REFERENCES building(id) ) ENGINEInnoDB DEFAULT CHARSET utf8mb4; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, gender VARCHAR(4), phone VARCHAR(20), build_id INT, room_id INT, status VARCHAR(20) DEFAULT 在住, CONSTRAINT fk_student_building FOREIGN KEY (build_id) REFERENCES building(id), CONSTRAINT fk_student_room FOREIGN KEY (room_id) REFERENCES dormroom(id) ) ENGINEInnoDB DEFAULT CHARSET utf8mb4; CREATE TABLE repair ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, room_id INT NOT NULL, content VARCHAR(500) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, status VARCHAR(20) DEFAULT 待处理, CONSTRAINT fk_repair_student FOREIGN KEY (student_id) REFERENCES student(id), CONSTRAINT fk_repair_room FOREIGN KEY (room_id) REFERENCES dormroom(id) ) ENGINEInnoDB DEFAULT CHARSET utf8mb4; CREATE TABLE visitor ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, visitor_name VARCHAR(50) NOT NULL, visit_time DATETIME DEFAULT CURRENT_TIMESTAMP, leave_time DATETIME, CONSTRAINT fk_visitor_student FOREIGN KEY (student_id) REFERENCES student(id) ) ENGINEInnoDB DEFAULT CHARSET utf8mb4;字段设计里有几个细节值得注意。dormroom 表用 building_id room_no 做了联合唯一索引防止同一栋楼出现两个相同房号这是一个很实用的约束。beds 和 capacity 分开设计capacity 表示房间总容量beds 表示剩余床位分配宿舍时判断 beds 是否大于 0而不是拿 capacity 去减这样逻辑更直观。student 表的 status 字段是冗余设计用来表示“在住、已退宿、调换中”等状态课设答辩时这个字段能引出状态流转的话题让业务逻辑显得更完整。外键我建议保留因为答辩老师会检查数据库设计是否规范外键和唯一索引是加分项项目要求里没写不等于不做。3.2 初始化数据一把梭插入才能点开页面就有内容空数据库跑起来非常尴尬登录进去每个列表都是空白演示时老师点开学生管理没有数据气氛直接冷场。初始化脚本要准备至少一个管理员账号、两栋楼、若干房间和几个学生。密码这里我建议用明文不是安全意识的问题而是答辩老师要现场登录明文最方便。INSERT INTO admin(username, password, real_name) VALUES (admin, 123456, 宿管员); INSERT INTO building(name, floors) VALUES (1号楼, 6), (2号楼, 6); INSERT INTO dormroom(building_id, room_no, capacity, beds) VALUES (1, 101, 4, 4), (1, 102, 4, 4), (1, 103, 4, 4), (2, 201, 4, 4), (2, 202, 4, 4); INSERT INTO student(student_no, name, gender, phone, build_id, room_id, status) VALUES (2023001, 张三, 男, 13800138000, 1, 1, 在住), (2023002, 李四, 男, 13800138001, 1, 1, 在住), (2023003, 王五, 男, 13800138002, 1, 2, 在住); INSERT INTO repair(student_id, room_id, content, status) VALUES (1, 1, 宿舍灯管损坏, 处理中), (2, 1, 空调不制冷, 已完成); INSERT INTO visitor(student_id, visitor_name) VALUES (1, 张三父亲);初始化数据的规律是三个学生分别占用 101 房间两个床位和 102 房间一个床位这样分配宿舍功能演示时101 显示剩余 2 个床位、102 显示剩余 3 个床位数据是真实可算的。报修表放了“处理中”和“已完成”两条状态筛选功能有数据可查。这里要注意INSERT INTO repair 里的 student_id 和 room_id 对应的是表里自增后的实际 ID如果之前导入顺序有变化这些 ID 也会变所以导入后先查一下再写报修数据更稳妥。3.3 JDBC 连接配置db.properties DBUtil 通用类连接数据库这块课设项目最常见也最可靠的方案是写一个 DBUtil 类读取 db.properties 配置文件然后用 DriverManager 获取连接。下面是完整代码兼容 MySQL 5.7 和 8.0关键参数我都写在配置里了。drivercom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/dorm?useSSLfalsecharacterEncodingutf8serverTimezoneAsia/Shanghai usernameroot password123456package com.dorm.util; import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.Statement; import java.util.Properties; public class DBUtil { private static String driver; private static String url; private static String username; private static String password; static { try (InputStream in DBUtil.class.getClassLoader() .getResourceAsStream(db.properties)) { Properties props new Properties(); props.load(in); driver props.getProperty(driver); url props.getProperty(url); username props.getProperty(username); password props.getProperty(password); Class.forName(driver); } catch (Exception e) { e.printStackTrace(); } } public static Connection getConnection() throws Exception { return DriverManager.getConnection(url, username, password); } public static void close(Connection conn, Statement stmt, ResultSet rs) { if (rs ! null) try { rs.close(); } catch (Exception e) {} if (stmt ! null) try { stmt.close(); } catch (Exception e) {} if (conn ! null) try { conn.close(); } catch (Exception e) {} } }这段代码有三个关键点。第一db.properties 必须放在 src 根目录因为类加载器会从 classpath 根路径找它放在子包里就加载不到。第二Class.forName(driver) 写在 static 块里保证驱动在类加载时只注册一次每个连接无需重复注册。第三close 方法严格按 ResultSet 到 Statement 到 Connection 的顺序关闭只关 Connection 的写法在连接池场景下会耗尽连接。如果你在 IDEA 里运行时找不到 db.properties八成是资源文件没有进入 classes 输出目录检查 Project Structure 里的 Resources 标记即可。提示如果你想把连接配置换成 Druid 连接池核心逻辑不变只是把 getConnection 换成 dataSource.getConnection()。但课设项目用 DriverManager 完全够连接池不是必选项不要为了追求工程化给自己加戏。4. 核心功能实现登录拦截、CRUD 分页和宿舍分配事务功能实现的顺序很关键。我一般建议先做登录和拦截再做列表分页最后做宿舍分配事务。登录是入口列表是门面事务是深度按这个顺序写每一层都建立在之前的基础上思路不会乱。4.1 登录逻辑与 Filter 拦截不让未登录的人进任何页面登录流程本身很标准LoginServlet 接收用户名密码查 admin 表成功就写入 Session失败就回登录页带错误提示。但只做登录不写过滤器会有一个致命问题——用户直接在地址栏敲 index.jsp 就能绕过登录看到所有页面。所以 Filter 必须跟上。package com.dorm.servlet; import com.dorm.util.DBUtil; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; WebServlet(/login) public class LoginServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, java.io.IOException { req.setCharacterEncoding(utf-8); String username req.getParameter(username); String password req.getParameter(password); String sql SELECT id, real_name FROM admin WHERE username? AND password?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); ResultSet rs ps.executeQuery(); if (rs.next()) { HttpSession session req.getSession(); session.setAttribute(userId, rs.getInt(id)); session.setAttribute(realName, rs.getString(real_name)); resp.sendRedirect(req.getContextPath() /index.jsp); } else { req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(login.jsp).forward(req, resp); } } catch (Exception e) { e.printStackTrace(); } } }这段代码里最推荐的是 try-with-resources 写法Connection、PreparedStatement、ResultSet 都实现了 AutoCloseable这样写不用手动 close代码还干净。另一个重点是 preparedStatement 的参数绑定任何用户输入都必须通过 ? 占位符传值绝对不能拼接 SQL 字符串这个习惯在课设里养成以后工作面试都会受益。Filter 的写法如下拦截范围用 /*再手动放行登录页、登录请求和静态资源。package com.dorm.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; WebFilter(/*) public class LoginFilter 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.contains(login.jsp) || uri.endsWith(/login) || uri.startsWith(/css/) || uri.startsWith(/js/)) { chain.doFilter(req, resp); return; } HttpSession session request.getSession(false); if (session null || session.getAttribute(userId) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }Filter 里我习惯用 request.getSession(false) 而不是 getSession()。区别在于getSession(false) 在没有 Session 时返回 null不会强制新建一个会话如果写成 getSession()未登录用户每次请求都会拿到一个新 Session过滤器的判断就永远失效。放行静态资源也很关键不然 CSS 和 JS 被拦截登录页会变成一坨裸 HTML。如果你是 web.xml 配置风格的项目没有 WebFilter 注解记得在 web.xml 里补上对应的 filter 和 filter-mapping。4.2 学生管理列表DAO 分页 Servlet 转发 JSTL 渲染学生列表是宿舍管理系统的门面。功能上要支持分页和按姓名、学号搜索。DAO 层我推荐写一个 StudentDao提供 getPageList 和 getTotalCount 两个方法一个查当前页数据一个查总记录数然后把结果封装进 PageBean 返回给 Servlet。package com.dorm.dao; import com.dorm.entity.Student; import com.dorm.util.DBUtil; import java.sql.*; import java.util.ArrayList; import java.util.List; public class StudentDao { public ListStudent getPageList(String keyword, int start, int pageSize) { ListStudent list new ArrayList(); String sql SELECT s.*, b.name AS build_name, r.room_no FROM student s LEFT JOIN building b ON s.build_idb.id LEFT JOIN dormroom r ON s.room_idr.id WHERE s.name LIKE ? OR s.student_no LIKE ? LIMIT ?, ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % keyword %); ps.setString(2, % keyword %); ps.setInt(3, start); ps.setInt(4, pageSize); ResultSet rs ps.executeQuery(); while (rs.next()) { Student s new Student(); s.setId(rs.getInt(id)); s.setStudentNo(rs.getString(student_no)); s.setName(rs.getString(name)); s.setGender(rs.getString(gender)); s.setBuildName(rs.getString(build_name)); s.setRoomNo(rs.getString(room_no)); list.add(s); } } catch (Exception e) { e.printStackTrace(); } return list; } public int getTotalCount(String keyword) { String sql SELECT COUNT(*) FROM student WHERE name LIKE ? OR student_no LIKE ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % keyword %); ps.setString(2, % keyword %); ResultSet rs ps.executeQuery(); if (rs.next()) { return rs.getInt(1); } } catch (Exception e) { e.printStackTrace(); } return 0; } }这里用 LEFT JOIN 而不是子查询目的是在一次查询里把楼栋名和房号带出来避免在 JSP 里对每个学生再发一次数据库请求这是黑马 javaweb 课程里反复强调的 DAO 层优化思路。WHERE 里 LIKE 用 OR 连接匹配学号或姓名一个输入框覆盖两个字段。LIMIT 的两个参数第一个是偏移量第二个是每页条数偏移量算法是 (当前页 - 1) 乘以每页条数这步算错翻页就会重复或漏数据。Servlet 端处理很简单接收 page 参数、默认 1pageSize 默认 8然后把 PageBean 塞进 request 转发到 student_list.jsp。JSP 里用 JSTL 渲染数据代码比 Java 脚本段干净得多。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html head title学生管理/title /head body table border1 width100% tr th学号/thth姓名/thth性别/thth楼栋/thth房间/thth操作/th /tr c:forEach items${pageBean.list} vars tr td${s.studentNo}/td td${s.name}/td td${s.gender}/td td${s.buildName}/td td${s.roomNo}/td td a hrefstudent_edit?id${s.id}编辑/a a hrefstudent_delete?id${s.id} onclickreturn confirm(确定删除?)删除/a /td /tr /c:forEach /table div a hrefstudent_list?page${pageBean.currentPage - 1}上一页/a span${pageBean.currentPage} / ${pageBean.totalPage}/span a hrefstudent_list?page${pageBean.currentPage 1}下一页/a /div /body /html这个 JSP 有三个容易踩的点一是 taglib 前缀写 c 后必须用 ${pageBean.list} 这种 EL 表达式取数据不能再混用 % %二是删除链接里的 confirm 确认弹窗返回 false 时不会跳转这是误删数据唯一的后悔药三是上一页和下一页的链接如果当前页是第 1 页上一页会变成第 0 页正确做法是在上一页链接上做 page 1 判断否则数据库执行 LIMIT -8,8 会直接报错。4.3 宿舍分配与调换一个事务防止床位数据对不上分配宿舍是整个答辩里最容易被追问的模块。逻辑上要同时做两件事更新 student 表的 room_id 和 build_id以及 dormroom 表的 beds 字段空床位减一。如果第一条 SQL 成功、第二条失败学生住进去了但系统里床位余量没变数据就对不上了。所以这两条 SQL 必须放在同一个事务里。public boolean assignRoom(int studentId, int roomId) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); String checkSql SELECT beds FROM dormroom WHERE id? FOR UPDATE; try (PreparedStatement checkPs conn.prepareStatement(checkSql)) { checkPs.setInt(1, roomId); ResultSet rs checkPs.executeQuery(); if (rs.next() rs.getInt(beds) 0) { conn.rollback(); return false; } } String updateStudent UPDATE student SET room_id?, build_id (SELECT building_id FROM dormroom WHERE id?) WHERE id?; try (PreparedStatement ps conn.prepareStatement(updateStudent)) { ps.setInt(1, roomId); ps.setInt(2, roomId); ps.setInt(3, studentId); ps.executeUpdate(); } String updateDorm UPDATE dormroom SET bedsbeds-1 WHERE id?; try (PreparedStatement ps conn.prepareStatement(updateDorm)) { ps.setInt(1, roomId); ps.executeUpdate(); } conn.commit(); return true; } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (Exception ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (Exception e) {} } } }这段代码里有三个细节值得展开。checkSql 里的 FOR UPDATE 是行级锁作用是在事务提交前这条 dormroom 记录不允许被其他事务修改。没有这个锁并发分配时两个人同时看到最后一间房还剩一个床位就会超额入住。第二个细节是更新 student 的 build_id 用了子查询直接从 dormroom 表拿对应的楼栋 ID避免在 Java 代码里再查一次数据库。第三个细节是调换宿舍的场景要更复杂先恢复旧房间的床位再给学生关联新房间最后新房间床位减一顺序不能反任何一步失败都回滚。finally 块里把 autoCommit 恢复成 true 再关闭连接这一步很多人忽略。如果你用连接池非自动提交状态会被带回连接池下一个请求拿到这个连接时事务状态是脏的后续的所有查询都会出现诡异行为。这种问题看日志根本看不出来只能一行行盯着代码找。如果答辩老师问死锁怎么解决你回答“统一按固定顺序加锁先查后写”就足够了。5. 部署运行与避坑指南从 IDEA 启动到答辩现场排查功能写完了最紧张的时刻是第一次点击运行按钮。JavaWeb 项目的部署环节有一套固定的操作顺序顺序错了报错信息千奇百怪。这一章把 IDEA 配置 Tomcat 的完整流程、四个高频报错和答辩前的复验步骤讲透。5.1 在 IDEA 里关联 Tomcat 并启动项目的标准流程IDEA 配置 Tomcat 的路径是固定的Run 菜单Edit Configurations加号Tomcat ServerLocal。然后 Application server 选择 Tomcat 9 解压目录Deployment 标签页加 Artifact类型选 war explodedApplication context 填 /dorm。配置完成后启动浏览器访问 http://localhost:8080/dorm/login.jsp。我辅导课设的血泪经验是90% 的人卡在 Application context 这一步。IDEA 默认生成的是一个带随机字符的路径你在浏览器里按默认地址访问永远 404。IDEA 的 Tomcat 配置里还有一个 Open browser 选项默认会在启动后打开浏览器但打开的 URL 是 localhost:8080 不带 context path十有八九打不开项目首页。手动改成 http://localhost:8080/dorm/login.jsp 再勾选或者干脆不勾选。war exploded 和 war 的区别也值得说清楚。exploded 是解压目录方式改 JSP 保存后刷新立即生效适合开发调试war 是压缩包方式适合导出后扔到外置 Tomcat 的 webapps 目录做最终部署。答辩演示建议直接用 IDEA 里的 exploded不要用 war因为 war 每次改动都要重新构建打包演示现场改代码根本来不及。注意在 Deployment 标签页里如果 Artifact 是空的说明项目结构没有导出成功。检查 Project Structure 里的 Web 模块设置确保 WebContent或 webapp目录被正确标记为 Web 资源目录web.xml 路径能被识别。5.2 高发错误排查3 个现场最容易炸的坑坑一404页面找不到。现象Tomcat 启动日志正常但访问任何页面都返回 404。原因分两类一类是 URL 路径和 Servlet 映射不匹配比如 WebServlet 注解写的是 student_list但页面链接里写的却是 studentList大小写不对或者少个下划线。另一类是 war 包导出后部署到外置 Tomcat打开发现 JSP 全丢了原因是 Artifact 打包时没把 WebContent 下的页面打进去。解决先看浏览器地址栏实际请求的路径再去 Servlet 类里核对注解或 war 包结构预览路径对齐后再刷新。IDEA 里直接项目右键 Build Artifact 重新构建然后观察输出目录里有没有 JSP 文件。坑二500 ClassNotFoundException: com.mysql.jdbc.Driver。现象编译不报错Tomcat 也能启动但第一次点登录按钮时页面直接 500控制台抛 ClassNotFoundException。原因mysql 驱动 jar 没有放进 WEB-INF/lib 目录但 IDEA 的 classpath 里有它所以编译期正常、运行期炸掉。这种情况很隐蔽解决方法是 Project Structure 里的 Artifacts 标签页打开 Output Layout把 mysql 驱动 jar 加进 WEB-INF/lib 目录。这种上下文不一致的 classpath 问题看起来像玄学本质是编译期和运行期用的依赖不同。坑三中文乱码。现象登录页面中文正常但查询出来的学生姓名全是问号。原因三处编码不一致——JSP 文件头部的 pageEncoding、数据库连接 URL 的 characterEncoding、MySQL 表本身的 charset。解决统一改成 utf8mb4JDBC URL 里加 characterEncodingutf8JSP 头部写成 % page pageEncodingUTF-8 %再给项目加一个 CharacterEncodingFilter在请求入口统一处理编码。数据库连接报 Communications link failure 或 Access denied 的检查顺序是MySQL 服务有没有启动db.properties 账号密码对不对3306 端口有没有被占用。5.3 答辩前 20 分钟的完整复验清缓存、导数据、走通全流程交项目前一天按下面的顺序完整走一遍能筛掉九成现场事故。检查项操作通过标准数据库服务确认 MySQL 已启动并设为自动启动命令行能登录端口占用netstat -ano 检查 8080Tomcat 能正常启动依赖完整性检查 WEB-INF/lib 下所有 jar驱动、连接池、JSTL 都在浏览器缓存用无痕窗口打开系统每页都是最新代码功能全流程登录-查询-新增-编辑-删除-分配-导出无 500 无白屏用无痕窗口这一点很关键普通窗口的缓存会让你以为代码改了没生效实际上浏览器还在用旧资源。功能全流程一定要实际点一遍特别是分配宿舍和调换宿舍这两个操作涉及事务是答辩时最容易现场出 bug 的环节。以前有次演示时列表页 500问题就出在导出 war 时 WEB-INF/lib 丢了驱动 jar这个坑我记到现在所以导出前检查 lib 目录已经成为我固定的肌肉记忆。6. 答辩加分的两个实现Excel 导出和学生入住率图表6.1 用 POI 把学生名单导出成 Excel答辩时导出 Excel 比展示网页表格更有说服力。用 Apache POI 的 XSSFWorkbookServlet 里几十行代码就能实现。resp.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); resp.setHeader(Content-Disposition, attachment;filenamestudent_list.xlsx); XSSFWorkbook workbook new XSSFWorkbook(); XSSFSheet sheet workbook.createSheet(学生名单); String[] headers {学号, 姓名, 性别, 楼栋, 房间}; Row headerRow sheet.createRow(0); for (int i 0; i headers.length; i) { headerRow.createCell(i).setCellValue(headers[i]); } ListStudent list studentDao.getPageList(, 0, 100); for (int i 0; i list.size(); i) { Row row sheet.createRow(i 1); row.createCell(0).setCellValue(list.get(i).getStudentNo()); row.createCell(1).setCellValue(list.get(i).getName()); row.createCell(2).setCellValue(list.get(i).getGender()); row.createCell(3).setCellValue(list.get(i).getBuildName()); row.createCell(4).setCellValue(list.get(i).getRoomNo()); } workbook.write(resp.getOutputStream());Content-Disposition 里的 attachment 表示触发浏览器下载而不是在页面内打开filename 是下载文件名。注意导出前清空搜索条件否则导出的只是当前筛选后的结果。打开 Excel 提示文件损坏多半是响应流里混入了多余输出检查 Servlet 里有没有写 out.print 之类的代码。6.2 用 ECharts 画宿舍入住率一个 JSON 接口就够了首页加一个各楼栋入住率的柱状图视觉效果会完全不一样。ECharts 只需要一个 HTML 文件引入 CDN数据从 Servlet 返回 JSON。fetch(http://localhost:8080/dorm/room_stat) .then(resp resp.json()) .then(data { const chart echarts.init(document.getElementById(chart)); chart.setOption({ title: { text: 各楼栋入住率 }, xAxis: { data: data.map(d d.buildName) }, yAxis: { type: value, max: 100 }, series: [{ type: bar, data: data.map(d d.rate) }] }); });配套的 Servlet 查询 building 表和对应楼栋已入住人数计算入住率后返回 [{buildName: 1号楼, rate: 82}, {buildName: 2号楼, rate: 67}] 这种格式。注意返回值里的 rate 如果是整数除以 total 后不要直接取整乘以 100 再保留一位小数。数据转换建议用 Gson 库把 List 转成 JSON 字符串比手写字符串拼接靠谱得多手写拼接少个引号整个页面白屏这种问题排查起来非常耗时间。我自己带课设的习惯是交之前关掉所有项目重新打开 IDEA从启动 Tomcat 开始完整走一遍流程直到导出 Excel 成功为止。这次全程用无痕窗口操作防止浏览器缓存干扰判断。宿舍管理系统这个方向很经典把登录、分页、事务、导出、图表都补齐了代码结构清晰功能有深度交上去不会丢人。希望帮到你。本文还有配套的精品资源点击获取
返回列表