Servlet+JSP实战:从零手搓酒店客房预定管理系统

📅 发布时间:2026/10/1 3:50:57
Servlet+JSP实战:从零手搓酒店客房预定管理系统
简介这是一套基于Servlet与JSP实现的酒店客房预定管理系统采用前后台分离设计面向计算机相关专业正在做毕业设计的学生以及需要Java Web项目实战练习的学习者可直接作为毕设选题使用。项目使用Tomcat作为Web服务器、MySQL作为数据库开发工具支持Eclipse或IDEA并配套Navicat进行数据管理。压缩包共170个文件约1.21MB包含41个Java源文件、41个class编译文件、34个JSP页面以及GIF、JPG图片素材、jar依赖包、XML配置和一份SQL数据库脚本覆盖从后端逻辑到前端展示的完整结构。功能上用户端支持注册登录、预约房间、客房搜索、在线留言与预定记录查询管理员端涵盖客房管理、客房分类管理、会员管理、留言管理、剩余房间查询及订房信息查询业务闭环较为完整。项目经过严格调试确保可运行已有339人学习适合作为课程设计或毕设参考帮助读者快速理解Java Web分层开发与数据库交互思路。1. 从零手搓一套酒店客房预定管理系统ServletJSP 前后台到底怎么落地很多同学做毕设或者接私活时一看到「酒店客房预定管理系统」就头大觉得业务复杂、表多、状态流转绕。其实把需求拆开看核心就三件事客人查房、下单、后台改房态。真正让人翻车的往往不是业务而是 ServletJSP 这套「老古董」在 IDEA 里怎么配、JSP 页面怎么拿到后端数据、前后台怎么用 Session 串起来。这套技术栈虽然被吐槽「传统」但它是 JavaWeb 头歌实训、课程设计、基于 JSP 的毕设选题里出现频率最高的组合面试官问起 Servlet 生命周期、JSP 九大内置对象你答得上来就是加分项。这篇笔记我按自己带学生做过的路子把环境配置、表结构、前后台交互、打包 war 部署一条线讲透新手能照着敲熟手能对照检查自己的边界处理。2. 环境与工程骨架IDEA 新建 JSP 项目到能跑通第一个 Servlet2.1 为什么选 ServletJSP 而不是直接上 SpringBoot先把这个选型问题说清楚不然很多人做到一半会怀疑自己。ServletJSP 是 JavaWeb 的原生形态请求进来由 Servlet 处理业务、JSP 负责渲染中间靠 request、session 传数据。它的好处是「没有黑匣子」——你能清楚看到一次 HTTP 请求从 Tomcat 到 Servlet 再到 JSP 的完整链路这对理解 MVC 帮助极大。酒店客房预定系统这种业务量不大的场景用原生 Servlet 完全扛得住而且部署简单打成 war 丢进 Tomcat 就能跑。常见做法是用 IDEA 建一个 Java Enterprise 或 Maven Web 项目配 Tomcat 作为运行容器。我一般会选 Maven 的 webapp 骨架因为依赖管理清晰后面打 war 也方便。注意别用太新的 JDKServlet 的javax.servlet包在 JDK 高版本加 Tomcat 10 之后会变成jakarta.servlet包名一换网上抄的代码全报红这是新手第一个大坑。2.2 用 Maven 搭骨架并配好 Tomcat先建工程。打开 IDEA新建项目选 Maven勾选 webapp 骨架或者建普通 Maven 项目后手动补src/main/webapp/WEB-INF/web.xml。pom 里引入 Servlet 和 JSP 的依赖注意 scope 用 provided因为 Tomcat 自带这些包打 war 时不该重复打进去。dependencies !-- Servlet APITomcat 已提供打包时不带入 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- JSP API同样由容器提供 -- dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency !-- JSTL 标签库JSP 页面循环遍历数据要用 -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency !-- MySQL 驱动按自己数据库版本选 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependency /dependencies依赖说明provided是关键写错成 compile 会导致 war 包里塞了 servlet-apiTomcat 启动时类冲突直接报ClassCastException。JSTL 用 1.2 版本配合% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %使用。MySQL 驱动版本要和本地数据库对齐8.x 的驱动类名是com.mysql.cj.jdbc.Driver连接串要带时区和 SSL 参数。配 Tomcat 时在 Run Configuration 里加一个 Tomcat ServerDeployment 里把当前项目的 war exploded 加进去Application context 设成/hotel。启动后访问http://localhost:8080/hotel/能看到 index.jsp 就说明骨架通了。这一步看着简单但 IDEA 版本不同菜单位置差异大找不到 Tomcat 选项通常是没装 Tomcat 插件或者项目没被识别成 Web 项目检查 Facets 里有没有 Web 描述符。2.3 第一个 Servlet验证生命周期和请求转发骨架通了先写个最小 Servlet 验证链路别急着上业务。建一个TestServlet继承HttpServlet重写doGet在里面往 request 塞个属性然后转发到 JSP。WebServlet(/test) public class TestServlet extends HttpServlet { Override public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 模拟从数据库查出的数据 req.setAttribute(msg, Servlet 转发成功); // 转发到 JSP注意路径以 / 开头相对 webapp 根 req.getRequestDispatcher(/test.jsp).forward(req, resp); } }逻辑说明WebServlet(/test)是注解式映射省去在 web.xml 里配 servlet 和 servlet-mapping。forward是服务器内部转发浏览器地址栏不变request 作用域的数据能带到 JSP。参数上req.setAttribute的 key 在 JSP 里用 EL 表达式${msg}取。这里顺带把 Servlet 生命周期过一遍容器启动或首次访问时调init每次请求调service再分发到doGet/doPost容器关闭调destroy。面试常问「Servlet 是单例吗」答案是容器里只有一个实例、多线程共享所以别在 Servlet 里定义可变的成员变量存用户数据这是血泪经验并发下必出脏数据。3. 数据库设计与前后台数据打通客房、订单、用户三张核心表3.1 表结构怎么设计才不返工酒店系统的表不用多但字段要想清楚。核心三张用户表、客房表、订单表。用户表分前后台角色用一个role字段区分0 是普通客人1 是管理员。客房表存房号、房型、价格、状态。订单表关联用户和客房记录入住离店日期和订单状态。CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(100) NOT NULL, phone VARCHAR(20), role TINYINT DEFAULT 0 -- 0 客人 1 管理员 ); CREATE TABLE t_room ( id INT PRIMARY KEY AUTO_INCREMENT, room_no VARCHAR(10) UNIQUE NOT NULL, room_type VARCHAR(30) NOT NULL, price DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 -- 0 空闲 1 已预订 2 入住中 ); CREATE TABLE t_order ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, room_id INT NOT NULL, check_in DATE NOT NULL, check_out DATE NOT NULL, status TINYINT DEFAULT 0, -- 0 待确认 1 已确认 2 已取消 create_time DATETIME DEFAULT CURRENT_TIMESTAMP );参数说明status用 TINYINT 而不是字符串查询和判断都省事前端展示时再映射成中文。price用 DECIMAL 不用 FLOAT金额计算不能有精度误差。订单表不直接存房号和用户名只存 id展示时用 JOIN 查出来避免数据冗余不一致。这里有个容易忽略的点入住离店日期要校验不能重叠同一个房间在已有订单的时间段内不能再被预订这个校验放在 Service 层做别只靠前端。3.2 用 JDBC 工具类统一管理连接别在每个 Servlet 里写一遍DriverManager.getConnection抽一个工具类出来。连接参数放配置文件改数据库不用动代码。public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/hotel?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse; private static final String USER root; private static final String PWD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(驱动加载失败, e); } } public static Connection getConn() throws SQLException { return DriverManager.getConnection(URL, USER, PWD); } // 统一关闭资源顺序ResultSet - Statement - Connection public static void close(Connection c, Statement s, ResultSet r) { try { if (r ! null) r.close(); } catch (SQLException ignored) {} try { if (s ! null) s.close(); } catch (SQLException ignored) {} try { if (c ! null) c.close(); } catch (SQLException ignored) {} } }逻辑说明静态代码块只执行一次把驱动注册好。连接串里的serverTimezone不写MySQL 8 会报时区错误这是高频翻车点。useSSLfalse是本地开发图省事生产环境要开。关闭顺序不能反先关结果集再关语句再关连接反了可能抛异常。这个工具类够用但每次请求都新建连接性能差进阶可以换 Druid 连接池毕设场景用原生 JDBC 也能过。3.3 前台客房列表Servlet 查数据、JSP 用 JSTL 渲染前台首页要展示可预订的客房。Servlet 负责查库把 List 塞进 request转发到 JSPJSP 用 JSTL 的forEach遍历。WebServlet(/roomList) public class RoomListServlet extends HttpServlet { Override public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { ListRoom rooms new ArrayList(); String sql SELECT * FROM t_room WHERE status 0 ORDER BY room_no; try (Connection c DBUtil.getConn(); PreparedStatement ps c.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { Room r new Room(); r.setId(rs.getInt(id)); r.setRoomNo(rs.getString(room_no)); r.setRoomType(rs.getString(room_type)); r.setPrice(rs.getBigDecimal(price)); rooms.add(r); } } catch (SQLException e) { e.printStackTrace(); } req.setAttribute(rooms, rooms); req.getRequestDispatcher(/front/roomList.jsp).forward(req, resp); } }对应的 JSP 片段% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table trth房号/thth房型/thth价格/thth操作/th/tr c:forEach items${rooms} varroom tr td${room.roomNo}/td td${room.roomType}/td td${room.price}/td tda hreforder?roomId${room.id}预订/a/td /tr /c:forEach /table逻辑说明Servlet 里用 try-with-resources 自动关连接比手动 close 干净。req.setAttribute(rooms, rooms)的 key 要和 JSP 里${rooms}对上对不上页面就是空白这是新手最常见的「数据没显示」原因。JSP 里${room.roomNo}走的是 getter实体类必须有getRoomNo方法字段名和属性名大小写要对。JSTL 的 taglib 声明漏了c:forEach会原样输出成文本页面看着像坏了其实是标签没解析。4. 前后台权限与订单流转Session 隔离和状态机处理4.1 用 Session 区分前后台登录态前后台要隔离客人登录后只能看前台管理员登录后进后台。做法是登录成功后往 session 塞用户对象和角色写一个过滤器拦截后台路径。WebServlet(/login) public class LoginServlet extends HttpServlet { Override public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String password req.getParameter(password); User user userService.login(username, password); if (user null) { req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } HttpSession session req.getSession(); session.setAttribute(user, user); // 按角色跳不同后台 if (user.getRole() 1) { resp.sendRedirect(req.getContextPath() /admin/index.jsp); } else { resp.sendRedirect(req.getContextPath() /roomList); } } }逻辑说明req.getSession()没有就创建有就复用。登录态存 session后续页面用${sessionScope.user.username}取。sendRedirect是重定向地址栏会变和 forward 的区别要分清登录成功用重定向防止刷新重复提交表单。req.getContextPath()拿到项目上下文路径写死/hotel换部署环境就挂这是可移植性的关键。4.2 后台订单状态流转与并发处理订单状态从待确认到已确认再到取消每次变更都要校验当前状态不能随便跳。比如已取消的订单不能再确认。这里要防并发两个管理员同时点确认可能重复扣房态。public boolean confirmOrder(int orderId) { String sql UPDATE t_order SET status 1 WHERE id ? AND status 0; try (Connection c DBUtil.getConn(); PreparedStatement ps c.prepareStatement(sql)) { ps.setInt(1, orderId); // 返回影响行数为 0 说明状态已被别人改过 return ps.executeUpdate() 1; } catch (SQLException e) { e.printStackTrace(); return false; } }参数说明SQL 里带AND status 0是乐观锁思路只有当前还是待确认才更新影响行数为 0 就说明状态已变直接返回失败提示用户刷新。这比先查再改安全避免了检查与更新之间的时间窗口。房态更新同理确认订单时把房间 status 改成已预订取消时改回空闲两个操作要么都成功要么都回滚简单场景可以用手动事务c.setAutoCommit(false)然后 commit/rollback。4.3 后台客房管理的增删改查后台要能加房、改价、删房。删房前要检查有没有关联订单有就不能删否则订单表成孤儿数据。改价只影响后续订单已生成的订单价格不变这是业务规则别写成改价后历史订单跟着变。WebServlet(/admin/room/*) public class AdminRoomServlet extends HttpServlet { Override public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action req.getPathInfo(); // 拿到 /add /update /delete if (/add.equals(action)) { String roomNo req.getParameter(roomNo); String type req.getParameter(roomType); BigDecimal price new BigDecimal(req.getParameter(price)); roomService.add(roomNo, type, price); } resp.sendRedirect(req.getContextPath() /admin/roomList); } }逻辑说明用getPathInfo()配合通配映射/admin/room/*一个 Servlet 处理多个动作减少类数量。new BigDecimal(req.getParameter(price))直接转参数为空会抛 NumberFormatException生产代码要先判空。重定向回列表页避免刷新重复提交。这里注意后台路径都放在/admin/下过滤器统一拦截这个前缀校验 session 里的 role 是不是 1不是就踢回登录页。5. 避坑与排查那些让系统跑不起来的常见问题5.1 中文乱码POST 和 GET 要分开治现象表单提交的中文存进数据库变成问号或者页面显示乱码。原因Tomcat 默认用 ISO-8859-1 解码请求体JSP 页面没声明编码。解决POST 请求在 Servlet 最前面加req.setCharacterEncoding(UTF-8)注意这行必须在getParameter之前调之后调无效。GET 请求的参数编码由 Tomcat 的server.xml里 Connector 的URIEncoding决定改成 UTF-8 并重启。JSP 页面顶部加% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %。数据库连接串也要带characterEncodingutf8三处都对齐才彻底。5.2 404 和 500路径与空指针各占一半现象访问某个 Servlet 报 404。原因WebServlet的路径和访问地址对不上或者 web.xml 里 metadata-complete 设成 true 导致注解失效。解决核对注解路径和浏览器地址注意 context path 别漏。500 报错里如果堆栈指向某行getParameter或实体 getter多半是空指针——参数没传到或者数据库查出来是 null 直接用了。养成习惯getParameter拿到的值先判空数据库字段可能为 null 的用包装类型接收。5.3 JSTL 标签不解析页面直接输出 c:forEach现象JSP 页面把c:forEach当普通文本显示出来。原因taglib 声明缺失或者 jstl 依赖没打进 war。解决页面顶部加% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %检查 pom 里 jstl 依赖的 scope 不是 provided。如果用了 Tomcat 10JSTL 的 uri 会变得换成 jakarta 版本的标签库这是升级容器时最容易踩的坑。5.4 打 war 部署到独立 Tomcat 后连不上数据库现象IDEA 里跑得好好的war 丢进 Tomcat 的 webapps 就报驱动找不到。原因MySQL 驱动 jar 没打进 war 的WEB-INF/lib。解决确认 pom 里 mysql 依赖 scope 是默认的 compile不是 provided。打 war 用mvn clean package产物在 target 目录检查 war 里有没有 mysql 的 jar。另外独立 Tomcat 的 JDK 版本要和编译时一致版本不匹配会报 UnsupportedClassVersionError。5.5 Session 失效导致后台操作跳回登录页现象后台点几下就跳回登录页。原因session 默认 30 分钟过期或者重启 Tomcat 后 session 丢失。解决开发阶段可以在 web.xml 里把 session-timeout 调大但生产别调太大占内存。重启丢 session 是正常的因为 session 默认存内存要持久化得配 Tomcat 的 session 管理器。测试时如果频繁跳登录先看是不是浏览器禁了 cookiesession 靠 JSESSIONID 这个 cookie 维持禁了就每次都是新 session。6. 进阶技巧把项目打磨到能拿得出手做到这里系统能跑了但离「能拿得出手」还差几步。分享几个我常用的收尾技巧。第一是统一响应格式别让每个 Servlet 各写各的转发抽一个 BaseServlet用反射根据方法名分发请求前台传methodlist就调list方法代码量能砍掉一半。第二是加一层简单的 Service 和 DaoServlet 只负责收参数和跳页面业务逻辑下沉这样面试时你能讲清楚分层而不是一锅粥。第三是日期处理。订单的入住离店用java.time.LocalDate比java.util.Date好用JDBC 8 驱动支持直接存取。计算住几晚用ChronoUnit.DAYS.between(checkIn, checkOut)别自己拿毫秒数除跨月跨年容易错。第四是价格计算房价乘以晚数用 BigDecimal 的multiply别用 double。验证系统是否真的没问题我一般会跑一遍完整链路注册两个账号一个客人一个管理员客人查房下单管理员后台确认房态变已预订客人取消订单房态回空闲管理员删房有订单的房删不掉。这条链路走通基本就没大问题了。再补一个边界同一房间同一时间段下两单第二单应该被拒验证前面说的日期重叠校验生效了。最后说个打包部署的细节。mvn clean package出来的 war 名字默认带版本号部署后访问路径就带版本不美观。在 pom 的 build 里配finalName指定成 hotelwar 就是 hotel.war访问路径干净。部署到独立 Tomcat 时把 war 丢进 webapps启动后 Tomcat 会自动解压改 JSP 不用重启改 Java 代码要重启这是 JSP 热更新的便利也是很多人误以为「改了没生效」的原因——其实改的是 class 不是 jsp。我自己带项目时最深的教训是别一上来就写业务先把一个 Servlet 转发到 JSP 的最小链路跑通把乱码、路径、依赖这三个基础问题解决掉后面加功能就是复制粘贴改改。很多人卡在环境上三天热情就磨没了。这套 ServletJSP 的酒店系统技术不新但把 MVC、Session、JDBC、事务这些基本功练扎实了换任何框架都是平移。希望帮到你。本文还有配套的精品资源点击获取