ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java酒店预订系统源码拆解:JDBC事务与MVC实战

Java酒店预订系统源码拆解:JDBC事务与MVC实战 简介一份基于 Java 的酒店预订系统实战项目面向希望掌握 Web 开发全流程的初学者与进阶开发者展示从用户查询、预订酒店到订单处理的核心业务闭环。项目覆盖 MVC 分层设计、Servlet/JSP 前后端交互、JDBC 数据库操作以及 Spring、MyBatis 等主流框架的整合方式同时涉及用户认证、权限控制、异常处理和单元测试等工程化实践便于理解企业级 Web 应用的常见组织方式。压缩包共 41 个文件以 34 个 java 源码文件为主辅以 properties 配置、xml 工程配置和 md 说明文档整体仅 83KB目录结构精简可快速定位业务模块、配置项与说明文档。已有 74 人学习下载适合作为课程设计、毕业设计或面试前的练手素材。通过阅读源码可直观理解酒店信息、房间状态、订单等表的设计思路掌握控制层、业务层与数据层之间如何协作完成一次真实预订流程项目中包含的数据库交互和权限控制代码尤其适合对照实战场景提升 Java Web 开发与排错能力。1. Java酒店预订系统为什么我建议你从这套“老技术”源码拆起如果你点进这篇文章大概率是搜“java酒店预订系统源码”想找个能跑、能学、能交作业的项目。先说结论这份java酒店预订系统.zip既不是花哨的微服务架构也不是炫技的前后端分离工程它就是一套用 Servlet JSP JDBC 写出来的经典 Java Web 单体应用。放在今天看技术栈确实“老”但正是这份“老”让它成了学 Java Web 的黄金解剖样本——没有框架帮你藏起细节从 HTTP 请求到 SQL 执行每一行代码都在明面上。这套系统适合三类人刚学完 Java SE、想搞懂 Web 项目到底怎么串起来的初学者准备面试、需要拿一个完整业务逻辑去讲 MVC 和数据库设计的求职者以及想快速改造做课程设计或毕业设计的学生。它能解决的问题很具体酒店信息的增删改查、房间状态管理、用户注册登录、在线下单预订以及订单状态的前后端联动。你下载后能跑起来改得动说得清这就够了。2. 项目跑起来从源码包到 Tomcat 部署全流程拿到 zip 包先别急着写代码。老手拿到任何源码的第一件事是把它跑起来让代码和自己建立“信任感”。我在第一次拆这套系统时从解压到浏览器看到登录页只用了不到十分钟中间踩了几个经典的坑下面的流程照做基本不会翻车。2.1 解压与目录结构先搞清楚包里有什么解压后你会看到典型的 Eclipse/IDEA 项目结构。记住这不是 Maven 工程别直接用mvn clean package去构建老老实实按 Web 项目来导入。java-hotel-system/ ├── src/ │ ├── com.hotel.entity # 实体类User, Hotel, Room, Order │ ├── com.hotel.dao # 数据访问对象含 JDBC 操作 │ ├── com.hotel.service # 业务层预订逻辑、登录校验 │ ├── com.hotel.servlet # Controller接收前端请求 │ └── com.hotel.util # 工具类DBUtil, DateUtil ├── web/ │ ├── WEB-INF/ │ │ ├── web.xml # Servlet 映射与过滤器配置 │ │ └── lib/ # 依赖 jar 包mysql-connector-java 等 │ ├── css/ js/ # 前端静态资源 │ ├── login.jsp index.jsp # 视图页面 │ └── ... └── sql/ └── hotel.sql # 建库建表脚本有同学会问为什么实体类是com.hotel.entity而不是com.hotel.model这纯粹是早期 Java 项目的习惯实体对象代表数据库表的一行记录叫 entity 还是 model 都可以重点在于职责划分清楚。另外注意lib/目录里面是手动导入的 jar 包源码包不依赖 Maven 拉取这意味着你只需要配好 JDK 和 Tomcat 就能启动入门的门槛其实更低。2.2 数据库初始化先建库再谈其他这套系统用的是 MySQL源码的sql/hotel.sql已经建好了表结构和初始数据。我一般习惯用命令行执行比 Navicat 更能看到报错细节mysql -u root -p -e CREATE DATABASE IF NOT EXISTS hotel_db DEFAULT CHARSET utf8mb4; mysql -u root -p hotel_db sql/hotel.sql第一条命令创建hotel_db数据库指定utf8mb4字符集是为了让中文房间描述不乱码。第二条命令把建表和初始数据脚本跑进去。如果你的环境没有免密登录把-p后面加上密码比如-p123456。执行完用SHOW TABLES;检查一下是否生成了预期中的表。这里有个容易忽视的点脚本里如果有DROP TABLE IF EXISTS语句执行前想清楚它会把已有的旧表直接干掉。我在做二次开发时曾不小心把线上环境的同名库给清了从那以后我每次执行 SQL 脚本前都会用grep -E DROP|DELETE看一眼。代码后逻辑说明这两条命令完成的是“基础设施准备”。第一条命令中的DEFAULT CHARSET utf8mb4很关键MySQL 默认的latin1存储中文会乱码后面你再怎么改 Java 代码都无济于事。第二条命令将包含多条 SQL 语句的脚本文件导入指定数据库。参数-e表示执行命令后退出适合写脚本自动化部署。2.3 配置 JDBC 连接参数数据源四件套数据库建好了接下来让 Java 代码知道怎么连。打开com.hotel.util.DBUtil你会看到类似下面的代码private static final String DRIVER com.mysql.jdbc.Driver; private static final String URL jdbc:mysql://localhost:3306/hotel_db?useSSLfalsecharacterEncodingutf8; private static final String USERNAME root; private static final String PASSWORD 123456; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { e.printStackTrace(); } }注意useSSLfalse这个参数。MySQL 8.x 默认开启 SSL但我们的本地开发环境根本不需要加密不关掉就会连带着警告刷屏换成高版本驱动如mysql-connector-java-8.0.x、即使DRIVER的类名也同步换成com.mysql.cj.jdbc.Driver。代码后的参数说明URL里的3306是 MySQL 默认端口hotel_db是我们在第一步创建的库名。characterEncodingutf8确保 Java 写入数据库的中文不会被转成乱码。Class.forName(DRIVER)是在加载驱动类这段代码是 JDBC 4.0 之前的老写法新版驱动支持 SPI 自动加载写不写都行但源码既然这么写就保留原汁原味。2.4 部署到 Tomcat用 IntelliJ IDEA 还是 Eclipse源码里带了.project和.classpath文件说明原生是在 Eclipse 里开发的。但这年头新手用 IDEA 更多两种方式我都试过。如果是 IDEAFile - New - Project from Existing Sources选中源码目录IDE 会自动识别 Web 模块配好 Tomcat Server 后直接Run。注意 IDEA 默认不编译WEB-INF/lib下的 jar 包依赖如果启动报ClassNotFoundException: com.mysql.jdbc.Driver去Project Structure - Artifacts里把lib目录加入打包范围。如果是 EclipseFile - Import - General - Existing Projects into Workspace然后右键项目Run As - Run on Server。Eclipse 对传统 javaweb 项目的兼容性好得多几乎不用额外配置。部署过程中最常见的报错是 404 或者请求路径对不上。这是 web.xml 里 Servlet 映射路径写的是/login但你访问的是http://localhost:8080/hotel/login导致。项目名context path默认就是部署包名如果你改了 war 包的名字访问路径也要跟着改这个坑我们放到第五章统一排查。3. 拆解一次预订流程MVC 三层到底在处理什么跑起来后很多人会急于改代码但我觉得先把系统的“一次典型请求”完整串一遍更重要。以“用户查询酒店 - 查看房间 - 提交订单”这个链路为例看看数据是怎么从前端到数据库再回到前端的结果。3.1 请求入口web.xml 里的映射规则与 Servlet 路由这套系统的入口是web.xml。我们看其中一段配置servlet servlet-nameHotelServlet/servlet-name servlet-classcom.hotel.servlet.HotelServlet/servlet-class /servlet servlet-mapping servlet-nameHotelServlet/servlet-name url-pattern/hotel/url-pattern /servlet-mappingurl-pattern用/hotel而不是*.do说明这个系统用的是“路径映射”而非“后缀映射”。浏览器里输入http://localhost:8080/hotel_project/hotel?actionqueryListTomcat 会把请求交给HotelServlet。问号后面的actionqueryList是前端传过来的指令参数Servlet 层根据这个参数决定调用哪个业务方法——这就是最原始的“前端控制器”思想很多框架的路由本质是对它的一层封装。如果你以后看 Spring MVC会发现 DispatcherServlet 干的事和这里的HotelServlet如出一辙只是把 switch-case 变成了更灵活的注解映射。所以别小看这套老代码理解了它学框架会顺畅很多。3.2 三层的边界Servlet 只做转发Service 只做业务严格按 MVC 分层这套系统有一个很值得学习的规矩Servlet 里不写 SQLDAO 里不写业务判断。截取HotelServlet的一段关键代码String action request.getParameter(action); if (queryList.equals(action)) { ListHotel hotelList hotelService.queryAllHotels(); request.setAttribute(hotelList, hotelList); request.getRequestDispatcher(/hotelList.jsp).forward(request, response); } else if (bookRoom.equals(action)) { int hotelId Integer.parseInt(request.getParameter(hotelId)); int roomTypeId Integer.parseInt(request.getParameter(roomTypeId)); int userId Integer.parseInt(request.getSession().getAttribute(userId).toString()); boolean success orderService.createOrder(userId, hotelId, roomTypeId); if (success) { response.sendRedirect(orderSuccess.jsp); } else { request.setAttribute(errorMsg, 该房间已满请更换房型); request.getRequestDispatcher(/bookFail.jsp).forward(request, response); } }这段代码里有三个值得复用的细节。第一Servlet 的职责是取参数、调服务、做跳转绝不触碰Connection或PreparedStatement。第二sendRedirect和forward的区别重定向改 URL、request 里的属性会丢转发不改 URL、属性能带到 JSP。上面成功用重定向、失败用转发是为了防止刷新页面时订单重复提交——这个坑后面还会展开。第三从 session 里拿userId时要判空否则用户没登录直接发起bookRoom这里Integer.parseInt(null)会直接抛NumberFormatException。再往下走业务层OrderService.createOrder()里会先查房间剩余数量再扣库存、插入订单。这套代码里可能是同步的synchronized或者在 SQL 里加where remain 0做防超卖具体逻辑后面第四章讲订单表结构时说。3.3 前端 JSP 里的 Java 片段老项目的黑匣子所在JSP 页面里到处都是% %和% %片段这是早期 Java Web 的典型特征。比如酒店列表页% ListHotel list (ListHotel) request.getAttribute(hotelList); if (list ! null) { for (Hotel h : list) { % div classhotel-card h3% h.getName() %/h3 p星级% h.getStarLevel() % 星/p a hrefhotel?actiondetailhotelId% h.getId() %查看房型/a /div % } } %这种写法有个大问题JSP 变成了视图和业务的混合物而request.getAttribute拿到的对象如果类型不对强转时直接抛ClassCastException。我见到的很多初学者在后端改了属性名没同步前端页面上就是 500 或者空白排查半天才发现是这里写错了。但反过来看这种“小脚本”也有好处——你不需要理解自定义标签或 EL 表达式就能看懂页面逻辑。老系统的黑匣子往往不黑只是没人静下心来逐行读。前端交互层面这套系统用了 jQuery页面底部会有$.getJSON或$.ajax的异步请求下次我们讲“房型动态加载”时再细看。4. 数据库设计与订单状态流转业务系统的骨架这一章是绝大多数人忽略但面试官最爱问的地方。酒店预订系统的核心不在前端页面而在数据库表怎么设计、订单状态怎么流转。4.1 核心表结构与关系设计sql/hotel.sql里一般包含这五张表表名主要字段说明t_userid, username, password, phone注册用户表t_hotelid, name, star_level, city, address酒店基本信息表t_room_typeid, hotel_id, type_name, price, total_count房型表外键关联酒店t_room_statusid, room_type_id, date, remain_count每日房态表典型库存表t_orderid, user_id, hotel_id, room_type_id, check_in_date, check_out_date, status订单表注意t_room_status这张表它是这套系统的精髓。酒店订房和其他电商不一样你订的是“某个日期的某间房”不是“某个酒店随便住”。所以库存一定要按“日期 房型”维度来记录remain_count表示该房型在特定日期的剩余可订数。t_order.status通常用整数或字符串标识状态机常见取值0 已取消、1 待入住、2 已入住、3 已完成。有的实现里还有 4 已过期用来处理“订了没住”的情况。代码后的说明在这个表结构中t_room_status的联合唯一索引可以建在(room_type_id, date)上防止同一天同一房型被插入多行初始数据。剩下数量扣到 0 时就不能再预定了这就是“防超卖”的数据库兜底方案。4.2 预订订单事务与并发的那点事OrderService里的createOrder方法大概率是这个项目里最复杂的业务代码了。伪代码理一下Transactional(readOnly false, rollbackFor Exception.class) public boolean createOrder(int userId, int hotelId, int roomTypeId, String date) { // 1. 用户是否登录调用方保证 // 2. 查房型当日剩余: SELECT remain_count FROM t_room_status WHERE room_type_id? AND date? // 3. 剩余为 0 直接返回失败 // 4. 扣减库存: UPDATE t_room_status SET remain_count remain_count - 1 WHERE room_type_id? AND date? AND remain_count 0 // 5. 受影响的记录行数为 1 说明扣减成功否则说明被别人抢了回滚 // 6. 插入订单记录状态置为 1待入住 // 7. 返回 true }核心在第 4 步的 SQL。它不是先查后改而是把“查”和“改”合并成一条带条件的UPDATEUPDATE t_room_status SET remain_count remain_count - 1 WHERE room_type_id ? AND date ? AND remain_count 0;注意看条件里带了remain_count 0。这是一个非常实用的技巧即使两个并发请求同时读到剩余数为 1这条 UPDATE 在数据库层的行锁保证下只有一个能执行成功另一个的影响行数为 0从而避免超卖。如果你的项目里没有加这个条件那它大概率是先用 SELECT 查再 UPDATE并发下库存必错。我见过很多学生把这套单纯当“增删改查”但实际上它的订单片段才是整个系统的利润所在。面试时如果能主动说出“原子扣库存”这个细节比背十篇八股文都管用。4.3 状态流转与用户操作映射酒店预订的流程一般这样用户下单选“待入住”到店后管理员后台点击“办理入住”变“已入住”退房后变“已完成”。用户可以在“待入住”状态下点击“取消”订单变“已取消”。这套系统的状态流转不一定做成独立的表或配置类判断逻辑经常散落在 Servlet 和 Service 里。常见的粗心问题是取消订单时只改了订单状态没有把t_room_status.remain_count加回去导致库存越攒越少最终无房可订。如果你在自己的项目里遇到“为什么写着写着没房了”十有八九是少了这一步——这是一个被我翻了车的同类项目反复验证过的结论。实操时取消订单和将对应日期库存变更放在同一个事务里只改状态是错的。状态设计上我习惯在代码里把状态存为static final int常量并集中管理而不是在 JSP 里散落一堆裸数字这样后续改成枚举时不会遗漏。5. 常见问题排查部署、数据库、编码与并发避坑记录拆这套项目时我踩过不少不该踩的坑也帮别人排查过大量同类问题。这一章把你大概率会遇到的问题提前摆出来遇到时照着索引去查少走弯路。5.1 启动 404先查 context path 再查 web.xml现象Tomcat 成功启动但访问http://localhost:8080/login.jsp报 404而访问http://localhost:8080/hotel_project/login.jsp却正常。原因Tomcat 部署时的应用名context path默认是 war 包名与源码里的项目名不一致。访问根路径时没有映射到你的应用。解决直接在访问地址里加上项目路径或者把部署包重命名为ROOT.war这样能直接通过根域名访问。在 IDEA 里可以在Run Configuration - Deployment里修改Application context更推荐改成/。5.2 页面中文变问号三层编码缺一不可现象页面显示“”数据库里存的是问号或者反过来页面正常但数据库字段乱码。原因编码链有三环——浏览器发请求的编码、Tomcat 内部处理的编码、MySQL 存储的编码。任何一环断了都会出乱码。老项目没有 Spring 的CharacterEncodingFilter需要自己注册或者每行代码手动 set。解决最省事的办法是在 web.xml 里配置编码过滤器或者确认每个 Servlet 里都调用了request.setCharacterEncoding(UTF-8)。同时确认 MySQL 连接 URL 带characterEncodingutf8。三处全对齐才能根治缺一处都是玄学。5.3 报错ClassNotFoundException: com.mysql.jdbc.Driver现象启动项目后访问任何和数据库有关的页面直接 500控制台打印找不到驱动类。这是一个超级常见但解决起来却很简单的问题。原因jar 包没有编译到WEB-INF/classes下。在 IDEA 中放在WEB-INF/lib下的 jar 包需要手动把它标记为 Library并且在 Artifacts 输出的WEB-INF/lib中包含它。Eclipse 里则是 Build Path 里没有添加这个外部 jar。解决IDEA 用户到Project Structure - Libraries添加再到Artifacts - Output Layout确认 jar 被拉到WEB-INF/lib下。Eclipse 用户右键项目Build Path - Configure Build Path - Add External JARs。如果还不行直接把 jar 复制到 Tomcat 的 lib 目录下——这个办法最暴力也最有效。5.4 刷新页面订单重复提交PRG 模式的作用现象用户提交订单后浏览器地址栏还是/hotel?actionbookRoom按 F5 刷新订单插入了两条。原因表单提交是 POST 方式提交后当前页面仍是 POST 的响应页刷新时浏览器会重复发送上次的请求导致订单重复创建。这是经典问题不是这套系统独有。解决提交成功后的处理绝不能用forward跳转用sendRedirect让浏览器跳转到一个新的 GET 请求地址比如订单成功页。这个过程叫 PRGPost/Redirect/Get模式。前面我们在 3.2 里提到成功用重定向就是为这个。5.5 并发下库存变负检查 SQL 是否带条件现象多个用户同时订最后 1 间房库里出现remain_count -1的脏数据。原因代码里先 SELECT 剩余数判断大于 0再 UPDATE 减一。在高并发下两个请求同时通过判断随后各自执行更新库存被扣到负。解决如果你发现源码中 UPDATE 语句不带WHERE remain_count 0建议把它补上。并且UPDATE之后判断rows 0就回滚。这是成本最低、效果最好的防超卖方案改一行 SQL 就能解决。如果你的业务量大了再用 Redis 分布式锁或乐观锁但学生项目和课程设计完全不需要。以上五条是我和这套源码以及类似项目打交道的血泪经验前三条属于环境问题网上也有各种答案但不如这一份来得齐全后两条是业务逻辑问题面试时能说出这两点会让人觉得你真的跑通了项目而不是只看了文档。6. 进阶方向从 JDBC 到 Spring 与 MyBatis 的迁移路径看到这里如果你已经能熟练说出项目里每个 Servlet 对应哪个 URL、每个 DAO 方法对应哪条 SQL那么这套系统的使命已经完成了一半。剩下的一半就是把它往现代框架演进去理解和体验“框架到底帮我们做了什么”。6.1 先看一份映射需求把 JDBC 代码替换为 MyBatis以OrderDAO为例原来我们用PreparedStatement拼INSERT INTO t_order VALUES(...)MyBatis 的做法是把 SQL 独立到 XML 或注解中。insert idinsertOrder parameterTypecom.hotel.entity.Order INSERT INTO t_order(user_id, hotel_id, room_type_id, check_in_date, check_out_date, status) VALUES (#{userId}, #{hotelId}, #{roomTypeId}, #{checkInDate}, #{checkOutDate}, #{status}) /insert代码后的说明#{userId}是预编译参数等价于原来PreparedStatement的?占位符但更语义化。parameterType传实体类MyBatis 用反射自动映射到表字段。你直观体会一下原来 DAO 里十几行样板代码现在只剩五六行这就是框架带来的效率提升。如果你有兴趣可以在这套系统上做一个最小迁移把t_user的insert和selectByUsername改成 MyBatis 版其余先不动。一个表一个表地迁移比重新写一套 Spring Boot Vue 更有对比冲击力也能让你切实理解 ORM 和 JDBC 的边界在哪里。Hibernate 的迁移同理重点是要理解它为什么有“一级缓存”和“懒加载”以及它对事务边界的管理策略。6.2 事务从手动提交到 Spring 接管改一处稳全局JDBC 时代的手动事务长这样conn.setAutoCommit(false); try { // 扣库存 插入订单 conn.commit(); } catch (Exception e) { conn.rollback(); }如果用 Spring只需要在OrderService上声明Service public class OrderServiceImpl implements OrderService { Transactional(rollbackFor Exception.class) public boolean createOrder(OrderDTO dto) { // 扣库存 roomTypeDao.deductStock(dto.getRoomTypeId(), dto.getDate()); // 插入订单 orderDao.insert(dto); return true; } }代码后的说明Transactional让 Spring 的 AOP 拦截方法调用自动开启和提交事务异常时自动回滚。你不用再手动 setAutoCommitSpring 管理连接的方式也更优雅。原来Class.forName(driver)和getConnection全会被 DataSource 取代你写在DBUtil里的那些代码就只作为理解底层原理的教材了。6.3 把前端 JSP 替换为前后端分离接口时的注意点很多刚学完这套系统的同学会继续学 Vue然后想把它改造成前后端分离这一步是把后端 Servlet 改造成简单的 JSON 接口。一个注意点原来的 Servlet 返回RequestDispatcher.forward(...)到 JSP 页面现在需要改写response.setContentType(application/json;charsetUTF-8)并直接输出 JSON 字符串。常见做法是不再用 JSP而是用 FastJSON 或 Jackson 将hotelList序列化后写进PrintWriter。这个改造过程会暴露一个问题老代码里的 Servlet 职责过重。很多方法里既做参数接收又做 BUG 提示当时的价值观是“能跑就行”但你拆分接口时不能照抄先按“参数校验 - 调用 Service - 返回统一 Result”的思路重写一遍接口逻辑效率反而更高。通过这次改造你会顺便弄懂 CORS 跨域是个什么东西也是在为 Spring Boot 的学习扫平障碍。6.4 一个收尾的小习惯自己造订单号整套系统最终能跑通后我强烈建议你做一个很小的功能增强把订单编号从自增 id 改成自定义生成的字符串编号比如HJ202406151430001这种格式。去写一个OrderNoGenerator工具类研究一下怎么保证并发下的唯一性。这个功能非常小但它是从“练习项目”迈向“真实系统”的重要一步。从那以后我每次做完一套学习项目都强制自己挑一个“看起来很简单但实际有坑”的模块去增强走完一遍才敢说自己彻底读懂了这套代码。希望这篇拆解笔记能帮你把java酒店预订系统.zip里的每一行代码都吃透也祝你顺利找到下一个更适合自己的实战项目。本文还有配套的精品资源点击获取
返回列表