
简介基于Servlet、JSP与MySQL构建的JavaWeb学生宿舍管理系统是一套适合高校学生完成课程设计、期末大作业或毕业设计的完整源码项目。系统覆盖宿舍楼栋与房间管理、学生入住登记、来访记录、日常报修等常见业务模块分层结构清晰难度适中方便扩展功能或撰写论文。资源共包含202个文件以22个JSP页面、21个Java类为业务核心辅以73个JS脚本、16个CSS样式实现前端交互与界面美化另附SQL数据库脚本、JAR依赖包、Maven配置及Shell启动脚本压缩包整体7.21MB轻量易部署。源码已本地编译通过并稳定运行评审得分98分配套使用教程涵盖环境配置、数据库导入、运行演示可帮助快速上手。目前已有248人下载学习适合需要参考真实项目实现、快速启动课设或毕设的Java学习者无论是课程设计参考还是毕业设计起点这套系统都具备较高完成度。1. 从学生宿舍管理系统看 ServletJSP 的经典价值如果你搜索“学生宿舍管理系统源码”会看到大量基于 Spring Boot 前后端分离、Vue 或 React 的方案但标题里的组合——Servlet JSP MySQL——看起来像是上一个时代的东西。实际上这个组合在当前的教学环境中依然占据一席之地JavaWeb 基础课程、计算机毕业设计、以及部分老企业的内部系统仍在用它。它不需要引入 Spring 容器、不需要配置 Maven 依赖树、不需要理解自动装配只需要一个 Tomcat 和一个数据库就能把一个完整的管理系统跑起来。这篇文章围绕“基于 Servlet JSP MySQL 开发 JavaWeb 学生宿舍管理系统”来写重点回答四个问题这套技术栈的分工边界在哪里、如何把项目跑通、宿舍管理场景里的核心功能怎么落地、以及部署到生产环境前有哪些坑。内容按“架构理解 → 项目搭建 → 核心功能实现 → 部署排错”的顺序展开每一步都有可复现的命令和代码。2. Servlet、JSP 与 MySQL 的分工边界2.1 这三层天然对应 MVC 的 C、V、M一个宿舍管理系统本质上是对“学生、宿舍、入住记录、报修记录”四类实体做增删改查CRUD。把这个需求映射到技术选型上MySQL 负责存储实体数据Servlet 负责接收请求并控制业务流转JSP 负责生成响应页面。具体到一个请求的处理链路是浏览器发起GET /student/list请求。Tomcat 根据 web.xml 中的映射关系找到对应的 Servlet。Servlet 调用 DAOData Access Object层方法DAO 内部通过 JDBC 访问 MySQL。Servlet 将查询结果放入request或session作用域通过forward转发到 JSP。JSP 通过 JSTL 或 EL 表达式读取数据渲染成 HTML 并返回浏览器。这条链路里没有业务层这不规范但在课程设计和中小型内部系统中是常见做法。如果功能再多一些可以在 Servlet 和 DAO 之间加 Service 层抽离事务处理和业务校验。2.2 为什么 JSP 要放在 WEB-INF 下面而不能直接访问很多初学者会问我的 JSP 放在webapp根目录下地址栏输入路径也能访问为什么要放WEB-INFWEB-INF是 Servlet 容器Tomcat的受保护目录浏览器无法直接通过 URL 访问其中的资源只能通过服务器内部的请求转发RequestDispatcher.forward触发。把 JSP 放到WEB-INF下面的好处是防止用户绕过 Servlet 直接进入页面绕过登录过滤器的校验。使页面和 Servlet 的交互链路可控参数校验、权限检查都在 Servlet 层完成。在宿舍管理系统中我一般这样组织目录结构src/main/java/com/dorm/ controller/ # Servlet 类 StudServlet.java DormServlet.java LoginServlet.java dao/ # DAO 接口 dao/impl/ # DAO 实现 entity/ # 实体类 util/ # 数据库连接工具类 webapp/ WEB-INF/ web.xml jsp/ student/ list.jsp edit.jsp views/ login.jsp2.3 MySQL 的 JDBC 连接不是写一次就完事JDBC 是 Servlet 访问 MySQL 的唯一方式这里不引入 MyBatis、Hibernate 等框架所以连接管理代码是项目的基座。常见做法是在util包下定义DBUtil类提供静态方法获取连接。package com.dorm.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/dorm?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, java.sql.Statement stmt, java.sql.ResultSet rs) { try { if (rs ! null) rs.close(); if (stmt ! null) stmt.close(); if (conn ! null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }参数说明useUnicodetruecharacterEncodingutf8保证中文数据在写入和读取时都使用 UTF-8 编码不加这一项中文会出现乱码。useSSLfalse本地开发时关闭 SSL 验证避免连接时抛 SSLHandshakeException。serverTimezoneAsia/ShanghaiMySQL 8.x 版本必须指定时区否则会报时区错误。Class.forName(com.mysql.cj.jdbc.Driver)MySQL 5.7 对应com.mysql.jdbc.DriverMySQL 8.x 对应com.mysql.cj.jdbc.Driver版本不同驱动类名不同。这套写法在并发量高的时候会有性能隐患每次请求都建立物理连接用完即关闭。入门项目里可以接受如果觉得别扭可以引入 Apache 的BasicDataSource连接池代码改动很小。3. 从零搭建项目并跑通第一个宿舍管理页面3.1 环境准备与建库脚本开发前需要确认四样东西是否可用JDK推荐 8 或 11Tomcat 兼容性好、Tomcat推荐 9.x对应 Servlet 4.0、MySQL推荐 5.7 或 8.x两者驱动的写法略有差异、以及一个 IDEIDEA 社区版或 Eclipse。这里不展开 MySQL 的安装配置过程只提醒一点MySQL 8.x 安装时如果选择了强密码插件后续代码里连接时需要注意驱动版本不能低于 5.1.47否则认证方式不兼容。建库脚本是第一个落地的文件直接在 MySQL 命令行或 Navicat 中执行CREATE DATABASE IF NOT EXISTS dorm DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE dorm; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, name VARCHAR(20) NOT NULL COMMENT 姓名, gender VARCHAR(4) COMMENT 性别, class_name VARCHAR(50) COMMENT 班级, dorm_id INT COMMENT 宿舍ID, phone VARCHAR(11) COMMENT 手机号, reg_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 入住时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE dorm ( id INT PRIMARY KEY AUTO_INCREMENT, building VARCHAR(10) COMMENT 楼栋号, room_no VARCHAR(10) COMMENT 房间号, capacity INT DEFAULT 4 COMMENT 容量, current_count INT DEFAULT 0 COMMENT 当前人数 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字符集用utf8mb4而不是utf8因为utf8mb4兼容 emoji 和生僻字而且utf8mb4_general_ci排序规则适合中文场景。CREATE DATABASE IF NOT EXISTS的作用是幂等建库脚本重复执行不会报错。COMMENT字段不会影响运行但在多人协作和被接手时会大大降低理解成本。3.2 在 IDEA 中创建 Servlet 项目并配置依赖IDEA 社区版创建 Servlet 项目的方式有两种一是通过 Maven 骨架maven-archetype-webapp创建二是直接建 Java 项目后手动加入 Web 支持。前者更规范这里以 Maven 方式为例。在pom.xml中只加必要的依赖dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- JSP 与 JSTL用于页面中循环、格式化 -- dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency /dependenciesscopeprovided表示 Tomcat 自带的 Servlet 容器已经提供了这些类打包时不需要把 Servlet API 打进 WAR 包否则可能出现类冲突。MySQL 驱动的 scope 不设置表示默认 compile 级别会打进去。3.3 用 web.xml 还是注解映射关系可以分场景使用Servlet 3.0 之后支持WebServlet注解不需要在 web.xml 中写servlet映射。宿舍管理系统里用注解更简洁但要注意一个细节注解定义在 Servlet 类上无法动态修改 URL 模式而 web.xml 可以在不改 Java 代码的情况下调整映射路径适合运维阶段。一个典型的登录 Servlet 如下package com.dorm.controller; import com.dorm.dao.UserDao; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; WebServlet(/login) public class LoginServlet extends HttpServlet { private UserDao userDao new UserDao(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // GET 请求直接转发到登录页面 req.getRequestDispatcher(/WEB-INF/views/login.jsp).forward(req, resp); } Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String username req.getParameter(username); String password req.getParameter(password); if (userDao.validate(username, password)) { HttpSession session req.getSession(); session.setAttribute(username, username); resp.sendRedirect(req.getContextPath() /student/list); } else { req.setAttribute(errorMsg, 用户名或密码错误); req.getRequestDispatcher(/WEB-INF/views/login.jsp).forward(req, resp); } } }这里有个关键点doPost中第一行是req.setCharacterEncoding(UTF-8)它解决 POST 请求体的中文乱码问题。如果不加这一句表单提交的中文内容在 Servlet 中读出来就是乱码。而 GET 请求的乱码问题属于 Tomcat 层需要在server.xml中给 Connector 配置URIEncodingUTF-8。resp.sendRedirect(req.getContextPath() /student/list)中的req.getContextPath()返回的是项目部署路径比如/dorm。如果项目不做虚拟目录映射建议写这个前缀避免部署路径变化后硬编码路径失效。3.4 用 IDEA 配置 Tomcat 并启动如果你看到“错误找不到或无法加载主类”这类报错基本不是因为 Tomcat 本身而是 IDEA 运行时配置Run Configuration指定了错误的 Tomcat 路径或者项目没有正确打包。配置 Tomcat 的步骤是Run-Edit Configurations- 点击- 选择Tomcat Server-Local。Application server选项中点击Configure选择本地 Tomcat 的根目录比如C:\apache-tomcat-9.0.80。Deployment选项卡中点击选择Artifact然后选择dorm:war exploded格式。Application context设置为/dorm。启动前先把 Tomcat 的端口检查一下不要和本机其他进程冲突默认是 8080可以在Server选项卡中修改 HTTP port。浏览器访问http://localhost:8080/dorm/login能出现登录页就说明环境互通了。如果出现 404先看 IDEA 控制台的启动日志是否显示Deploying web application archive字样如果是Artifact is not deployed回到 Deployment 选项卡重新选择 Artifact。4. 宿舍管理的核心功能实现查询、入住、换宿4.1 分页查询是每个列表页的地基学生列表是宿舍管理系统的第一个主要页面。如果不分页一张表几十条数据还行到了几百条数据时页面渲染就会卡顿。因此这里展示一个带分页的查询实现。先定义分页工具类放在util包下package com.dorm.util; public class PageBean { private int pageNum; // 当前页码 private int pageSize; // 每页条数 private int totalCount; // 总条数 private int totalPage; // 总页数 public PageBean(int pageNum, int pageSize, int totalCount) { this.pageNum pageNum; this.pageSize pageSize; this.totalCount totalCount; this.totalPage (int) Math.ceil((double) totalCount / pageSize); } // getter / setter 略 }DAO 层关键代码public ListStudent findStudentByPage(int pageNum, int pageSize) { ListStudent list new ArrayList(); String countSql SELECT COUNT(*) FROM student; String sql SELECT * FROM student LIMIT ?, ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, (pageNum - 1) * pageSize); ps.setInt(2, pageSize); try (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.setDormId(rs.getInt(dorm_id)); list.add(s); } } } catch (SQLException e) { e.printStackTrace(); } return list; }LIMIT (pageNum - 1) * pageSize, pageSize是关键的分页公式第一页是从第 0 条开始取pageSize条数据库记录下标从 0 开始。PreparedStatement用?占位符而不是字符串拼接是为了防止 SQL 注入。在 JSP 中显示分页条时用 JSTL 判断是否显示上一页、下一页c:if test${pageBean.pageNum 1} a href${pageContext.request.contextPath}/student/list?pageNum${pageBean.pageNum - 1}上一页/a /c:if span第 ${pageBean.pageNum} / ${pageBean.totalPage} 页/span c:if test${pageBean.pageNum pageBean.totalPage} a href${pageContext.request.contextPath}/student/list?pageNum${pageBean.pageNum 1}下一页/a /c:if4.2 入住与换宿事务控制的典型场景入住操作不是简单往 student 表里插一条记录它同时要更新 dorm 表的current_count。如果只插入学生信息、不更新宿舍人数宿舍管理的“容量”和“实际人数”数据就会不一致。为了保证两个操作要么都成功、要么都失败需要在一个事务中执行public boolean checkIn(Student student, int dormId) throws SQLException { Connection conn DBUtil.getConnection(); conn.setAutoCommit(false); try { // 1. 更新宿舍人数 String udpateDorm UPDATE dorm SET current_count current_count 1 WHERE id ? AND current_count capacity; PreparedStatement ps1 conn.prepareStatement(udpateDorm); ps1.setInt(1, dormId); int affected ps1.executeUpdate(); if (affected 0) { Throw new SQLException(宿舍已满或不存在); } // 2. 插入学生记录 String insertStudent INSERT INTO student(student_no, name, gender, class_name, dorm_id, phone) VALUES(?,?,?,?,?,?); PreparedStatement ps2 conn.prepareStatement(insertStudent); // ... 为 ps2 设参数 ps2.executeUpdate(); // 3. 都成功才提交 conn.commit(); return true; } catch (SQLException e) { Throw new SQLException(事务执行失败已回滚 e.getMessage(), e); } finally { conn.setAutoCommit(true); conn.close(); } }事务中断的原因很多第一步current_count capacity条件不满足会导致更新行数为 0这时事务内部抛异常整体回滚学生表不会出现半条数据。同理换宿操作更复杂先把旧宿舍人数减 1再把新宿舍人数加 1再更新 student 表中的dorm_id。三个动作必须在一个事务里因为换宿到一半如果出现断电或异常要么变成“人不在任何宿舍”要么变成“一个占两间”。实现思路是一致的只是多出一条 UPDATE 语句。4.3 系别筛选与模糊查询的 SQL 组合学生列表页通常还需要按姓名、学号、班级进行模糊查询。SQL 层面用LIKE配合CONCATSELECT * FROM student WHERE name LIKE CONCAT(%, ?, %)注意CONCAT(%, ?, %)不能写成%?%因为占位符会被当作字符串的一部分而不会被替换。当前端表单提交了keyword参数时Servlet 里需要根据参数是否存在来拼接 SQLString keyword req.getParameter(keyword); if (keyword ! null !keyword.trim().isEmpty()) { sql WHERE name LIKE ?; params.add(% keyword.trim() %); }动态拼接 SQL 容易漏掉AND和WHERE的位置我是这么处理的把基础 SQL 写为SELECT * FROM student WHERE 11在循环拼接条件时直接写AND ...。WHERE 11在 MySQL 里性能影响极小但大大简化了代码逻辑。4.4 JSP 页面中的 JSTL 遍历与数据回显页面展示学生列表时JSP 的片断如下% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table tr th学号/th th姓名/th th性别/th th宿舍/th th操作/th /tr c:forEach items${studentList} vars tr td${s.studentNo}/td td${s.name}/td td${s.gender}/td td c:if test${s.dormId ! null} ${s.dormId}栋 /c:if c:if test${s.dormId null} 未分配 /c:if /td td a href${pageContext.request.contextPath}/student/edit?id${s.id}编辑/a a href${pageContext.request.contextPath}/student/delete?id${s.id} onclickreturn confirm(确定删除该学生吗)删除/a /td /tr /c:forEach /table${s.studentNo}对应的是 Student 类的getStudentNo()方法EL 表达式里严格遵循 JavaBean 的命名规范。如果一个属性在实体类里没有对应的 getterEL 表达式不会报错但页面上该位置就是空的查错时先看实体类 getter 是否齐全。onclickreturn confirm(...)是一个前端层面的删除确认它并不能阻止恶意请求直接访问删除 URL。真正要保证删除安全需要权限控制这属于 Servlet 层面的Filter或登录拦截器处理的范畴。5. 部署到 Tomcat 时的常见报错与解决方案5.1 数据库连接报错ClassNotFoundException 与 MalformedURLException驱动加载失败的典型报错是java.lang.ClassNotFoundException: com.mysql.jdbc.Driver。原因通常是两种MySQL jar 包没有打包进 WEB-INF/lib 目录。在 IDEA 中查看已部署的 Artifact 的WEB-INF/lib下是否有mysql-connector.jar没有就勾选Build Artifact时把依赖包含进去。驱动类名写错MySQL 8 以下用com.mysql.jdbc.Driver8 以上用com.mysql.cj.jdbc.Driver。确认版本后逐字对照。MalformedURLException通常出现在配置serverTimezone或连接 URL 写错时比如地址写成jdbc:mysql://localhost:3306/dorm少了?后面的参数。可以打印 URL 字符串检查看是否被转义或被截断。5.2 JSP 编译不通过或提示无法编译 class 文件JSP 第一次被访问时Tomcat 会把它转换成 Java 文件再编译成 class。如果 Tomcat 工作目录下没有写入权限或者 JDK 版本与 Tomcat 不匹配会在服务器日志里看到 JasperException。排查顺序是打开 Tomcat 的logs目录查找localhost.2025-xx-xx.log和catalina.out查看具体异常栈。在 IDEA 中查看 JSP 编译后的 Java 文件路径Tomcat 的工作目录是CATALINA_BASE/work/Catalina/localhost/dorm/org/apache/jsp在这个目录下能看到转换后的login_jsp.java文件可以直接查看 JSP 中的 EL 表达式被翻译成了什么代码。如果是 JDK 11 及以上版本注意 JSP 编译时对javax.servlet相关类的访问权限要求必要时在pom.xml中显式添加providedscope 的 javax.servlet-api 依赖因为某些情况下 Tomcat 本身提供的类不会自动参与 IDE 的编译 classpath。5.3 前端页面中文乱码与浏览器缓存不一致乱码问题分三层排查请求乱码、响应乱码、数据库乱码。在 Servlet 的 doGet 和 doPost 开头各加一行resp.setContentType(text/html;charsetUTF-8);JSP 页面顶部加% page languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-8 %浏览器端如果在修改 JSP 后看不到效果可以强制刷新CtrlF5因为 JSP 输出到浏览器的是 HTML浏览器可能保留了旧版本的缓存。Tomcat 检测到 JSP 文件变化后会触发 Java 文件重新编译但如果本机时间不准会导致 Tomcat 认为源文件没有变化这时候删除 Tomcat 的work目录再重启即可。5.4 宿舍管理系统常见的 404 原因Artifact 未部署或 context path 不一致在 IDEA 中点击右侧 Tomcat 启动按钮后很多人直接访问http://localhost:8080/dorm/login得到 404。原因往往是 IDEA 的Run Configuration中 Deployment 的 Application context 配置成了/dorm但浏览器访问的端口和 URL 与配置不一致。先看启动日志中的Tomcat started on port(s): 8080确认端口。然后在浏览器中访问/dorm/login时注意 IDEA 默认的 Launch 页面是/login它不会自动加 context path。手动修改 URL 为/dorm/login后大部分问题能解决。如果仍返回 404就按排除法先在地址栏输入/dorm/看是否有 Tomcat 的默认欢迎页或 404 页面如果连/dorm/都 404则部署环节有问题如果/dorm/能出来而/dorm/login404说明 web.xml 或WebServlet的 URL 映射没有生效。检查注解中的路径是否以/开头或者 web.xml 中是否有重复映射。6. 给 ServletJSP 宿舍管理系统增加记录日志的简单技巧无论项目多么小打印日志始终是排查问题的重要手段。Servlet 项目没有日志框架时最常见的是在异常处执行e.printStackTrace()但这种输出全部写到控制台和catalina.out无法区分级别也无法定位是哪一次请求产生的。如果不引入 log4j2 或 Logback只用 JDK 自带的java.util.logging也能搞定在DBUtil类里添加一个静态日志对象然后在连接成功和失败时分别输出日志import java.util.logging.Logger; import java.util.logging.Level; public class DBUtil { private static final Logger LOGGER Logger.getLogger(DBUtil.class.getName()); public static Connection getConnection() throws SQLException { Connection conn null; try { conn DriverManager.getConnection(URL, USER, PASSWORD); LOGGER.log(Level.INFO, 数据库连接成功URL: {0}, URL); } catch (SQLException e) { LOGGER.log(Level.SEVERE, 数据库连接失败检查URL、用户名或密码, e); throw e; } return conn; } }Level.INFO和Level.SEVERE是标准的日志级别生产环境可以通过调整 logging.properties 里的级别来控制输出量。相比e.printStackTrace()这种方式能准确记录到哪个过程出了问题以及当时传入的连接参数是什么。另一个技巧是给 Servlet 定义一个BaseServlet父类把日志输出和执行时间统计放到父类中自动完成子类只写业务逻辑public class BaseServlet extends HttpServlet { private static final Logger LOGGER Logger.getLogger(BaseServlet.class.getName()); Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { long startTime System.currentTimeMillis(); super.service(req, resp); long costTime System.currentTimeMillis() - startTime; LOGGER.log(Level.INFO, 请求路径: {0}处理耗时: {1}ms, new Object[]{req.getRequestURI(), costTime}); } }service方法是 Servlet 生命周期的核心方法它内部根据请求类型自动调用doGet、doPost等。重写service并加上时间统计后所有继承它的 Servlet 都会自动具备请求耗时监控能力不需要在每个 Servlet 里重复写日志代码。这在排查“宿舍管理系统操作很卡”的问题时非常好用能快速定位是数据库慢还是 Servlet 逻辑慢。如果希望通过日志把每次重要操作如删除学生、修改宿舍容量都记下来可以在 DAO 层的写操作中加一条LOGGER.log(Level.INFO, 执行 SQL: {0}参数: {1}, new Object[]{sql, params.toString()})配合数据库的 binlog 审计更稳妥。日志输出的时间、SQL 语句、执行结果能形成一条完整的操作痕迹比单纯在 Controller 层打印“操作成功”更有价值。本文还有配套的精品资源点击获取