☰
Java+JSP+MySQL学生宿舍管理系统:从环境搭建到部署避坑全解析
2026/9/25 7:33:50 网站建设 项目流程

简介:Java Web开发中,JSP/Servlet与MySQL的组合是理解服务端动态网页技术的基础。其核心原理在于:浏览器请求经Tomcat容器路由至Servlet,业务逻辑通过JDBC访问MySQL,最终由JSP渲染响应页面。这种分层架构虽然“传统”,却清晰展示了请求分发、数据持久化、事务边界等关键机制,能帮助开发者快速理解Spring Boot的自动配置与MVC模式。基于此技术栈构建的学生宿舍管理系统,涵盖楼栋、床位、入住退宿、访客登记等典型管理场景,正是Java课程设计、毕业设计选题及内部管理系统的高频需求。本文从环境选型、数据库设计、连接池配置到并发分配事务控制,系统梳理了开发全流程与避坑要点,是快速上手的实用参考。

1. Java+JSP+Mysql实现Web学生宿舍管理系统:一个看着过时却不过时的技术组合

第一次听到“Java+JSP+Mysql实现Web学生宿舍管理系统”这个组合,很多人的第一反应是“都什么年代了还用JSP”。但你如果正在准备java课程设计案例源码、做基于jsp的毕设选题,或者在简历上还写着“熟悉Java Web开发”,那这套技术栈恰恰是绕不开的一课。JSP页面里嵌Java代码、Servlet接请求、Mysql存数据,这是2008年左右互联网公司的主流玩法,也是今天理解Spring Boot“自动配置到底帮我们做了什么”的最短路径。这套系统能解决的实际问题是:宿舍楼栋管理、学生入住退宿、床位分配、访客登记这类日常事务,数据量不大,业务逻辑直白,但足够把Java Web开发的知识串成一条线。适合谁读?Java基础刚学完、想做一个能跑通前后端交互项目的学生,以及想快速搭内部管理系统的后端开发。下面按我自己搭建这套系统的顺序来讲,从环境选型一直讲到部署避坑。

2. Java + JSP + Mysql技术选型:为什么这套组合仍是Java Web入门最快的路

2.1 三个组件的分工与版本选型

先把这个标题里的三个技术名词拆开,搞清它们各自干什么活。Java负责写业务逻辑,比如计算宿舍剩余床位、判断学生是否重复入住;JSP(Java Server Pages)负责生成动态网页,本质上是一个“在HTML里嵌入Java代码”的模板引擎,它会被Tomcat翻译成一个Servlet再编译执行;Mysql负责持久化,把学生信息、宿舍分配记录这些数据落盘,Web应用通过JDBC驱动访问它。三者合起来就是一条完整的请求链:浏览器发出HTTP请求,Tomcat容器找到对应的Servlet,Servlet调用Java业务代码,业务代码通过JDBC读写Mysql,最后把结果转发给JSP页面渲染成HTML返回浏览器。

版本选型是第一个容易翻车的地方。我一般推荐这套组合:JDK 8 + Tomcat 8.5 + Mysql 5.7或8.0 + IDEA。JDK 8不是因为它新,而是因为Tomcat 8.5和大量传统JSP项目都以JDK 8为基准环境,你按8的语法写,放到11上大概率也能跑,反过来就不一定。Tomcat版本是个大坑:Tomcat 10把Servlet API的包名从javax.servlet改成了jakarta.servlet,传统的JSP项目直接部署上去会报ClassNotFoundException,所以做JSP项目老老实实用Tomcat 8.5或9.0。Mysql版本上,5.7和8.0都行,但连接驱动类和连接URL参数有差异,后面避坑章节会细说。

组件推荐版本理由
JDK1.8Tomcat 8.5兼容性最好,市场存量项目最多
Tomcat8.5 / 9.0Servlet API包名仍是javax,传统JSP直接跑
Mysql5.7 或 8.05.7最稳,8.0需要额外处理认证和时区参数
IDEA2023.xNew Project向导内置Java Enterprise模板
Mysql驱动mysql-connector-java 5.1.49 或 8.0.33与Mysql版本对应,8.x驱动类名已变更

2.2 用IntelliJ IDEA新建一个JSP Web项目的完整配置

打开IDEA,新建项目时选择“Java Enterprise”,左侧选“Web Application”,注意不要选Spring Initializr,那样会引入Spring Boot,偏离标题讲的传统JSP路线。Server选Tomcat 8.5,并指定本地Tomcat安装目录。勾选“Create web.xml”,因为这个文件是传统JSP项目的描述符,很多旧教程和代码都依赖它。项目生成后的结构长这样:

student-dormitory/ ├── pom.xml # Maven坐标与依赖(如果用Maven) ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/dormitory/ │ │ │ ├── servlet/ # Servlet控制层 │ │ │ ├── dao/ # 数据访问层 │ │ │ ├── entity/ # 实体类 │ │ │ └── util/ # JDBC工具类 │ │ ├── resources/ │ │ │ └── db.properties # 数据库连接配置 │ │ └── webapp/ │ │ ├── WEB-INF/ │ │ │ └── web.xml # Web部署描述符 │ │ ├── jsp/ # JSP页面目录 │ │ └── index.jsp # 入口页面 │ └── test/ └── target/ # 编译输出与war包

建立项目后第一步不是写代码,而是先把Tomcat跑起来验证环境。在IDEA里配置好Tomcat后,先启动一次空项目,看到INFO: Server startup in [xxx] milliseconds再往下走。这一步能把“环境问题”和“代码问题”切分开,不然后面报错你根本分不清是Tomcat没配置好还是代码写错了。我见过太多同学把时间耗在环境上,最后发现只是Tomcat的端口被占了。

# 也可以手动启动Tomcat验证环境,避免IDEA配置干扰 cd /path/to/apache-tomcat-8.5.xx/bin ./startup.sh # Linux/Mac catalina.bat run # Windows,run模式前台启动,日志直接打在控制台 # 启动成功后浏览器访问 # http://localhost:8080/ 看到Tomcat默认首页即环境OK

这里有两个参数值得留意。一是端口,默认8080,被占用时改conf/server.xml里的<Connector port="8080",改成8081或8082都行,但改了之后访问路径要跟着变。二是catalina.bat run和startup.sh的区别:run是前台模式,日志实时打印,调试阶段用它;startup是后台模式,日志写到logs/catalina.out,适合部署后看。新手调试阶段统一用前台模式,日志里报错直接定位。

3. 数据库设计与JDBC连接池:宿舍管理系统的数据地基

3.1 六张核心表的设计:从学生、宿舍到访客登记

学生宿舍管理系统的业务实体不难梳理:管理员要登录,学生要入住,宿舍属于某栋楼,入住之后可能换寝,校外人员来访要登记。把这些实体落到Mysql里,我一般拆成六张表:管理员表admin、学生表student、楼栋表building、宿舍表dormitory、入住记录表assignment、访客登记表visitor。下面是建表SQL,字段设计上兼顾了“够用”和“不过度设计”。

CREATE DATABASE IF NOT EXISTS dormitory DEFAULT CHARACTER SET utf8mb4; USE dormitory; -- 管理员表 CREATE TABLE admin ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', username VARCHAR(32) NOT NULL UNIQUE COMMENT '登录用户名', password VARCHAR(64) NOT NULL COMMENT '登录密码,明文存储仅限学习项目', real_name VARCHAR(32) COMMENT '真实姓名', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ) ENGINE=InnoDB COMMENT='管理员表'; -- 楼栋表 CREATE TABLE building ( id INT PRIMARY KEY AUTO_INCREMENT, building_name VARCHAR(32) NOT NULL COMMENT '楼栋名称,如1号楼', manager VARCHAR(32) COMMENT '楼栋管理员姓名', floor_count INT DEFAULT 6 COMMENT '楼层数', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB COMMENT='楼栋表'; -- 宿舍表 CREATE TABLE dormitory ( id INT PRIMARY KEY AUTO_INCREMENT, building_id INT NOT NULL COMMENT '所属楼栋id', room_number VARCHAR(16) NOT NULL COMMENT '房间号,如101', bed_count INT DEFAULT 4 COMMENT '床位总数', used_bed_count INT DEFAULT 0 COMMENT '已使用床位', UNIQUE KEY uk_building_room (building_id, room_number), CONSTRAINT fk_dorm_building FOREIGN KEY (building_id) REFERENCES building(id) ) ENGINE=InnoDB COMMENT='宿舍表'; -- 学生表 CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT '学号', name VARCHAR(32) NOT NULL COMMENT '姓名', gender ENUM('男', '女') NOT NULL COMMENT '性别', phone VARCHAR(11) COMMENT '手机号', major VARCHAR(64) COMMENT '专业', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB COMMENT='学生表'; -- 入住记录表 CREATE TABLE assignment ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL COMMENT '学生id', dormitory_id INT NOT NULL COMMENT '宿舍id', bed_no INT NOT NULL COMMENT '床位号,1~bed_count', assign_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '入住时间', status ENUM('入住中', '已退宿') DEFAULT '入住中' COMMENT '状态', UNIQUE KEY uk_student_status (student_id, status), -- 注意:同一学生只能有一条入住中记录 CONSTRAINT fk_assign_student FOREIGN KEY (student_id) REFERENCES student(id), CONSTRAINT fk_assign_dorm FOREIGN KEY (dormitory_id) REFERENCES dormitory(id) ) ENGINE=InnoDB COMMENT='入住记录表'; -- 访客登记表 CREATE TABLE visitor ( id INT PRIMARY KEY AUTO_INCREMENT, visit_name VARCHAR(32) NOT NULL COMMENT '访客姓名', visit_phone VARCHAR(11) COMMENT '访客电话', target_student_no VARCHAR(20) COMMENT '被访学生学号', visit_reason VARCHAR(128) COMMENT '来访事由', visit_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '登记时间', leave_time DATETIME NULL COMMENT '离开时间' ) ENGINE=InnoDB COMMENT='访客登记表';

字段类型上几个值得考量的点。学号student_no用VARCHAR(20)而不是INT,因为学号可能以0开头,整型会丢掉前导零,而且学号不需要参与数值计算。性别字段用ENUM('男', '女')看着直观,但实际业务里如果后面要扩展性别选项,ENUM改起来很痛苦,更稳妥的写法是存编码0/1并在Java层做映射。assignment表里给(student_id, status)加了唯一索引,这是宿舍分配的核心约束——防止同一个学生被分配两次还都处于“入住中”。这个索引会在第4章的业务代码里配合事务一起生效。

宿舍表dormitory里的bed_count和used_bed_count是典型的“冗余计数”字段。另一个设计是每次分配时去数assignment表里入住中的记录数来决定剩余床位,不需要冗余字段。但实际场景下,宿舍列表页要显示“2/4人”这种信息,每次都去COUNT一次,数据量上来后查询会变慢。我倾向于保留冗余字段,但必须在事务里同步更新它,否则会出现“显示有床位但实际已满”的数据不一致。

3.2 JDBC连接Mysql的两种方式与连接池关键参数

JSP项目访问Mysql,最原始的方式是DriverManager.getConnection(url, user, password),每次请求都新建一个物理连接。这种方式在教程demo里没问题,但放到宿舍管理系统这种多人同时访问的场景下,高频创建连接会让Mysql的进程数飙升,响应变慢。更推荐的常见做法是引入连接池,把连接创建好放在池子里复用。轻量级方案用Druid或HikariCP都行,传统JSP项目里Druid用得最多,配置直观而且带监控页面。

# src/main/resources/druid.properties driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/dormitory?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username=root password=你的数据库密码 initialSize=5 maxActive=20 maxWait=3000 minIdle=2
// util/JdbcUtil.java package com.dormitory.util; import com.alibaba.druid.pool.DruidDataSourceFactory; import javax.sql.DataSource; import java.io.InputStream; import java.sql.Connection; import java.sql.ResultSet; import java.sql.Statement; import java.util.Properties; public class JdbcUtil { private static DataSource dataSource; static { try { InputStream in = JdbcUtil.class.getClassLoader() .getResourceAsStream("druid.properties"); Properties props = new Properties(); props.load(in); dataSource = DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new RuntimeException("初始化Druid连接池失败", e); } } public static Connection getConnection() throws Exception { return dataSource.getConnection(); } public static void close(Connection conn, Statement stmt, ResultSet rs) { if (rs != null) { try { rs.close(); } catch (Exception ignored) {} } if (stmt != null) { try { stmt.close(); } catch (Exception ignored) {} } if (conn != null) { try { conn.close(); } catch (Exception ignored) {} } } }

druid.properties里这几个参数是必调的。initialSize是启动时就创建的连接数,设成5够用;maxActive是池子里最多同时活跃的连接数,宿舍管理系统这种规模的Web应用,20已经完全够用,设太大反而浪费Mysql的内存;maxWait是拿不到连接时的最长等待时间,单位毫秒,设3000表示等3秒还拿不到就抛异常,避免请求无限挂起。连接URL里的serverTimezone=Asia/Shanghai是给Mysql 8.0用的,不设会报时区错误;allowPublicKeyRetrieval=true是应对Mysql 8的caching_sha2_password认证插件的,后面避坑章节会专门说。

这里要理解一个关键点:JdbcUtil里的conn.close()并不是真的断开数据库连接,而是把连接归还给连接池。很多新手以为close就是关闭,导致写代码时不敢close。在连接池模式下,close的含义是“归还”,所以DAO层用完连接必须调用close,否则连接会被一直占用,最终把maxActive耗光,表现就是页面卡死然后报Cannot get connection。

4. 从登录到宿舍分配:一条完整的JSP + Servlet + Mysql请求链

4.1 登录模块的MVC拆解:JSP页面、Servlet控制层、DAO数据层

搭好了环境和数据表,接下来走通第一条完整链路:登录。传统JSP项目最常见的分层是JSP做视图、Servlet做控制器、DAO做数据访问,再加上一个实体类在层间传递数据。对于JSP入门来说,网上有些案例把JDBC代码直接写在JSP页面里,一两页能跑通,但业务一复杂就变成一坨没人敢动的代码。我们按MVC路子来,每层各干各的活,后面加功能时好扩展。

先看登录页面的JSP,核心就是一个表单,提交到LoginServlet。注意表单提交方式和字段名要和Servlet里getParameter的参数名对得上。

<%-- jsp/login.jsp --%> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>学生宿舍管理系统 - 登录</title> </head> <body> <h2>学生宿舍管理系统</h2> <form action="${pageContext.request.contextPath}/login" method="post"> <label>用户名:<input type="text" name="username"/></label><br/> <label>密 码:<input type="password" name="password"/></label><br/> <button type="submit">登 录</button> </form> </body> </html>

${pageContext.request.contextPath}是EL表达式,用来拼项目上下文路径。比如项目部署名是dormitory,那这段会被解析成/dormitory/login。这里建议用这个写法而不是写死/login,因为项目部署名改掉时表单提交路径也得跟着改,写死就踩坑。method="post"是为了避免密码出现在URL上,也能避开URL长度限制。

接下来是Servlet。这里用注解方式配置映射,不需要在web.xml里逐个声明,代码量更少。登录完成后把用户信息存进Session,后续页面靠Session来判断是否已登录。

// servlet/LoginServlet.java package com.dormitory.servlet; import com.dormitory.dao.AdminDao; import com.dormitory.entity.Admin; 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 final AdminDao adminDao = new AdminDao(); @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 设置请求编码,解决中文乱码 request.setCharacterEncoding("UTF-8"); // 2. 获取表单参数 String username = request.getParameter("username"); String password = request.getParameter("password"); // 3. 基础非空校验 if (username == null || username.trim().isEmpty() || password == null || password.trim().isEmpty()) { request.setAttribute("errorMsg", "用户名和密码不能为空"); request.getRequestDispatcher("/jsp/login.jsp").forward(request, response); return; } // 4. 调用DAO验证用户名密码 Admin admin = adminDao.findByUsernameAndPassword(username, password); if (admin != null) { HttpSession session = request.getSession(); session.setAttribute("loginAdmin", admin); // 重定向到主页,避免刷新时重复提交表单 response.sendRedirect(request.getContextPath() + "/jsp/index.jsp"); } else { request.setAttribute("errorMsg", "用户名或密码错误"); request.getRequestDispatcher("/jsp/login.jsp").forward(request, response); } } }

这段代码里有三个细节值得展开。第一个是request.setCharacterEncoding("UTF-8")必须放在读取参数之前,放在后面读取到的参数还是乱码。第二个是request.getRequestDispatcher(...).forward(...)和response.sendRedirect(...)的区别:forward是服务器内部转发,浏览器URL不变,适合失败时回登录页并携带错误提示;sendRedirect是302重定向,浏览器重新发起一次请求,适合登录成功切到主页,防止用户按F5刷新时表单被重复提交。第三个是Session里存了loginAdmin对象,之后每个需要鉴权的页面都检查这个属性是否存在,不存在就跳回登录页。

然后是DAO层。用户名和密码的校验在DAO里完成,返回的Admin对象为null表示查不到。

// dao/AdminDao.java package com.dormitory.dao; import com.dormitory.entity.Admin; import com.dormitory.util.JdbcUtil; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; public class AdminDao { public Admin findByUsernameAndPassword(String username, String password) { // 使用PreparedStatement防SQL注入,不要用Statement拼字符串 String sql = "SELECT id, username, real_name FROM admin WHERE username = ? AND password = ?"; try (Connection conn = JdbcUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { Admin admin = new Admin(); admin.setId(rs.getInt("id")); admin.setUsername(rs.getString("username")); admin.setRealName(rs.getString("real_name")); return admin; } } } catch (Exception e) { e.printStackTrace(); } return null; } }

DAO层必须用PreparedStatement而不是Statement。PreparedStatement把参数通过?占位符传进去,由驱动做转义处理,能防SQL注入;Statement是直接把字符串拼进SQL,如果用户名里带' or '1'='1这类内容,拼出来就是一条永远成立的条件,登录直接被绕过。try-with-resources写法会自动关闭Connection、PreparedStatement、ResultSet,省得手动调close。

这里有个取舍:密码用明文存储,仅限学习项目够用;但如果你要拿这套东西当毕设甚至上生产,建议至少用MD5加盐或BCrypt。Mysql里存的是哈希值,密码框里输入的内容经过同样算法哈希后才去比对。这个改动不复杂——在Servlet里拿到明文后调用一个EncryptUtil.md5(password)再传给DAO就行,DAO层的SQL不需要改。

4.2 宿舍分配的业务实现:剩余床位查询与更新的事务边界

宿舍分配是这个系统里最有“含金量”的业务,因为它不是简单的新增一条记录,而是同时涉及“查剩余床位”“更新已住人数”“插入入住记录”“防止重复入住”四个动作,任何一个没做好都会出数据不一致。我还是按Servlet+Service+DAO来拆,但核心逻辑放在Service层。

先看查询剩余床位的SQL。这里的关键是used_bed_count < bed_count这个条件,以及分配记录时用SELECT ... FOR UPDATE锁行,避免两个管理员同时分配导致超卖。

// service/AssignService.java package com.dormitory.service; import com.dormitory.dao.AssignmentDao; import com.dormitory.dao.DormitoryDao; import com.dormitory.dao.StudentDao; import com.dormitory.entity.Assignment; import com.dormitory.entity.Dormitory; import com.dormitory.util.JdbcUtil; import java.sql.Connection; public class AssignService { public boolean assignDormitory(Long studentId, Long dormitoryId) { Connection conn = null; try { conn = JdbcUtil.getConnection(); // 开启手动事务,关闭自动提交 conn.setAutoCommit(false); // 1. 锁住宿舍行,防止并发分配导致床位超卖 Dormitory dorm = new DormitoryDao().findByIdForUpdate(conn, dormitoryId); if (dorm == null) { conn.rollback(); return false; } if (dorm.getUsedBedCount() >= dorm.getBedCount()) { conn.rollback(); return false; } // 2. 校验学生是否已有入住中记录 if (new AssignmentDao().existsActiveAssignment(conn, studentId)) { conn.rollback(); return false; } // 3. 计算床位号:已用床位+1,比如4人间当前住2人,新学生分到3号床 int bedNo = dorm.getUsedBedCount() + 1; // 4. 插入入住记录 Assignment assignment = new Assignment(); assignment.setStudentId(studentId); assignment.setDormitoryId(dormitoryId); assignment.setBedNo(bedNo); assignment.setStatus("入住中"); new AssignmentDao().insert(conn, assignment); // 5. 更新宿舍已住人数 new DormitoryDao().increaseUsedBedCount(conn, dormitoryId); // 全部成功才提交 conn.commit(); return true; } catch (Exception e) { e.printStackTrace(); try { if (conn != null) conn.rollback(); } catch (Exception ignored) {} return false; } finally { try { if (conn != null) conn.setAutoCommit(true); } catch (Exception ignored) {} try { if (conn != null) conn.close(); } catch (Exception ignored) {} } } }

这段代码的手动事务是重点。Mysql的InnoDB引擎默认autocommit=1,每一条SQL都自动提交,这在单条记录操作时没问题,但宿舍分配这种“多步骤写操作”一旦中间某一步失败了,前面成功的步骤也会留下半截子数据——比如插入了入住记录但宿舍已住人数没更新。所以这里conn.setAutoCommit(false)把多个操作包在同一个事务里,要么全部成功commit,要么全部回滚rollback。

SELECT ... FOR UPDATE是另一个关键。它把宿舍那一行数据加了行级排他锁,事务提交前其他事务想更新这一行会被阻塞。如果没有这把锁,并发场景下两个管理员同时分配,两个请求都读到used_bed_count=2,都判断“还有床位”,都插入入住记录,4人间就住了5个人。加了锁后,第二个请求会等第一个事务提交后才能读到新的used_bed_count=3,再判断只剩1个床位,就不会超卖。

第三个值得说的是床位号算法:这里简单粗暴地取used_bed_count + 1,比如4人间已住2人,新学生就分到3号床。这样可以省去“遍历床位找空位”的查询。缺点是有中间退宿时会出现“床上有人但床位号空着”的情况,比如1号床退宿后新学生被分到3号床。对于学习项目和大多数宿舍管理场景这够用了,如果业务要求连续分配空床位号,就需要改成SELECT所有已用床位号然后在Java里找最小空缺。

<%-- jsp/assign.jsp —— 宿舍分配页面的核心表单 --%> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head><title>分配宿舍</title></head> <body> <h3>为学生分配宿舍</h3> <form action="${pageContext.request.contextPath}/assign" method="post"> <label>学号:<input type="text" name="studentNo"/></label><br/> <label>楼栋: <select name="buildingId"> <option value="1">1号楼</option> <option value="2">2号楼</option> </select> </label><br/> <label>宿舍:<input type="text" name="dormitoryRoom" placeholder="如101"/></label><br/> <button type="submit">确认分配</button> </form> </body> </html>

JSP页面里楼层和宿舍如果用下拉框动态加载,就需要在页面加载时通过AJAX请求一个返回JSON列表的Servlet。传统JSP项目做这个也很直接:Servlet里用response.setContentType("application/json;charset=UTF-8")然后out.write(jsonString),前端用jQuery的$.getJSON消费。如果不想引入前端框架,也可以在后端用request.setAttribute("dormitoryList", list)然后JSP里<c:forEach>循环生成<option>。c:forEach需要引入JSTL标签库,在pom.xml里加jstl和standard两个依赖,这是传统JSP项目常用的做法。

5. JSP学生宿舍管理系统避坑:部署与运行中绕不开的5个坑

5.1 页面中文乱码:Tomcat 8.5下JSP页面和请求参数的编码不一致

现象:页面上显示中文正常,但往Mysql里插入的学生姓名变成??;或者页面上直接出现乱码。原因:三处编码不一致——JSP页面本身的pageEncoding、Servlet里读取请求参数时的request.setCharacterEncoding、JDBC连接URL里的characterEncoding。三处必须统一成UTF-8,少一处都会出问题。

解决:第一处,JSP文件头部加<%@ page contentType="text/html;charset=UTF-8" language="java" %>;第二处,Servlet在读取参数前加request.setCharacterEncoding("UTF-8");第三处,druid.properties的URL里加characterEncoding=utf8。如果查Mysql客户端里已经是乱码,说明之前插入时就已经坏了,删掉重插,不要指望靠改查询来恢复。

5.2 Tomcat 10把javax.servlet换成jakarta.servlet,传统JSP项目直接404

现象:项目在Tomcat 8.5上跑得好好的,换到Tomcat 10后部署war包,打开页面报404,控制台打ClassNotFoundException: javax.servlet.http.HttpServlet。原因:Tomcat 10开始Servlet API的包名从javax.*迁移到jakarta.*,传统JSP项目里的HttpServlet、HttpServletRequest都是老包名,容器找不到对应类。

解决:最省事的方法是换回Tomcat 8.5或9.0,这也是我做JSP项目坚持用8.5的原因。如果必须用Tomcat 10,就把所有import javax.servlet改成import jakarta.servlet,注意是全局替换,漏一个都不行。还有一部分坑是某些老JDBC驱动或第三方库内部引用了javax.*,这种只能换库版本。

5.3 连接Mysql 8报错:Public Key Retrieval is not allowed

现象:启动项目后第一次访问数据库时报Public Key Retrieval is not allowed,但用Navicat连同一个数据库却正常。原因:Mysql 8.0默认认证插件是caching_sha2_password,客户端第一次连接时需要用RSA公钥加密密码,驱动默认不允许从服务器获取公钥。

解决:在JDBC连接URL上加参数allowPublicKeyRetrieval=true,同时建议把useSSL=false也带上,避免SSL握手带来的额外报错。完整URL是:

jdbc:mysql://localhost:3306/dormitory?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true

5.4 找不到Mysql驱动:ClassNotFoundException: com.mysql.jdbc.Driver

现象:启动项目报ClassNotFoundException: com.mysql.jdbc.Driver,或No suitable driver found for jdbc:mysql...。原因:要么mysql-connector的jar没放进WEB-INF/lib目录,要么Mysql 8.x的驱动类名变了——8.0以上类名是com.mysql.cj.jdbc.Driver,老代码里写的是com.mysql.jdbc.Driver。

解决:确认两件事。Maven项目先看pom.xml里有没有mysql-connector依赖,非Maven项目把jar物理拷到src/main/webapp/WEB-INF/lib/下;类名改成com.mysql.cj.jdbc.Driver。注意用Maven时别光在pom.xml里加了依赖就以为完事,IDEA要触发一次Reload All Maven Projects,否则jar没实际进入classpath。

5.5 并发分配宿舍时床位超卖:两个管理员同时分配了同一个床位

现象:4人间宿舍,两个管理员同时给两个学生分配,最后显示住了5个人,入住记录里床位号还重复了。原因:分配业务里“查剩余床位”和“更新已住人数”不是原子操作,两个请求同时读到used_bed_count=4,都判断有空床,都执行了插入。

解决:数据库层面的方案是给宿舍行加锁,即业务代码里用SELECT ... FOR UPDATE锁住宿舍记录,这个在第4章的AssignService里已经实现。如果用的是共享的同一个Tomcat实例,也可以在Java层用synchronized锁住分配方法,这在单机部署时确实能解决;但一旦部署了多个Tomcat节点,或者以后换Spring Boot做集群,synchronized就失效了。所以正确方向是学数据库锁和事务,这是从这个项目里能带走的底层能力。顺带一个排查技巧:如果怀疑超卖,直接查assignment表里同一个dormitory_id不同bed_no的数量是否超过bed_count,用GROUP BY dormitory_id HAVING COUNT(*) > MAX(bed_count)就能扫出异常数据。

6. 收尾:把系统打成WAR包部署到Tomcat,以及后续能走多远

自己电脑上跑通只是第一步,真正的Web项目要能部署到服务器上让其他人访问。传统JSP项目的部署方式是把项目打成WAR包丢进Tomcat的webapps目录,Tomcat启动时自动解压。用IDEA打包很简单:菜单Build→Build Artifacts→ 选择xxx:war exploded→Build,或者Maven项目直接执行mvn clean package,然后到target/目录下拿dormitory.war。把它复制到Tomcat的webapps/下,启动Tomcat,访问http://服务器IP:8080/dormitory/就能看到系统首页。

检查部署是否成功,看两个地方:一是Tomcat的logs/catalina.out里有没有Deployment of web application archive [dormitory.war] has finished;二是访问http://服务器IP:8080/dormitory/login.jsp能否正常打开登录页。如果报404,先看webapps下是否已经有同名目录,Tomcat不会覆盖解压出问题残留的目录,删掉旧的再重启。

这套系统做完之后,往后走有三条路。第一条是把JSP替换成前后端分离:后端用Spring Boot提供REST接口,前端用Vue或React去调。你会发现Servlet里的doPost就是Controller,request.getParameter就是@RequestParam,JdbcUtil就是JdbcTemplate,业务逻辑从JSP项目迁移过去只是换个壳。第二条是保留JSP但引入更多Java基础能力,比如用Filter做登录鉴权拦截器,用Listener在启动时初始化连接池,用自定义标签简化页面重复代码,这些在java面试题里都是高频考点。第三条是往工程化走:加Maven依赖管理、写单元测试覆盖AssignService的分配逻辑、用Git做版本管理,把项目从“能跑”推向“能维护”。

我自己的习惯是,每写一个Web系统前先把“更新和查询的事务边界”画出来,这个从JSP时代养成的习惯,到现在写Spring Boot项目都还在用。因为业务可以变、框架可以换,但数据一致性的底线,永远是写代码之前就要想清楚的事。希望帮到你,也祝这套Java+JSP+Mysql的学生宿舍管理系统不仅是答辩和课程设计里的一个分数,更是你读懂Java Web世界的第一块拼图。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询