Java酒店预订系统:从Servlet/JSP到JDBC的全栈实战解析
简介一份基于Java的酒店预订系统实战源码包定位于Java Web学习与课程设计参考适合希望理解完整业务链路的初学者和希望梳理服务端技术栈的开发者。整个zip包共41个文件34个Java源文件构成核心逻辑properties、xml等配置负责环境与参数设定md文档辅助阅读压缩包仅83KB轻量紧凑、便于逐模块拆解。示例从酒店与房间信息展示、用户登录到提交预订订单串联起MVC分层、Servlet/JSP交互、JDBC或MyBatis数据访问等技术点并可能涉及Spring依赖管理、事务控制、权限校验和异常处理等工程化设计数据库表也覆盖用户、房间、订单等核心实体。目前已有74人学习/下载对想快速浏览一份可运行项目、巩固Java Web开发认识的同学来说是一个信息密度较高的实用范例。项目仅供学习研究请勿用于商业用途。1. Java 酒店预订系统第一眼就该读完的 Web 全栈实战样本很多人在简历上写“熟悉 Java Web 开发”但真被问到“一个预订请求从点击按钮到数据库返回中间经过哪些类”就卡住了。这个 java酒店预订系统恰好就是用来治这个问题的。它不搞微服务也不谈高并发而是一个老老实实的课程设计级实战项目用户注册登录、酒店检索、房间预订、订单管理一条业务线从头走到尾。Servlet/JSP 负责前后端交互JDBC 或 MyBatis 落数据库最终部署在 Tomcat 上就能跑。适合两类人一类是准备课程设计或毕业设计的在校生想找一个结构完整、答辩时能讲清楚的项目另一类是学过 Java 语法但没完整做过 Web 项目的人想用最小成本把 MVC 分层、数据库设计、部署排错串一遍。把这份源码拆明白比硬背十道 Java 八股文都实在。2. 先拆技术栈MVC 分层、Servlet/JSP 与 JDBC 在项目里怎么协作拿到源码包第一步不是急着点运行而是先看包结构。一个合格的课程设计项目包名基本能反映分层思想。常见的结构是controller、service、dao、entity、util五层对应 MVC 里的 Controller、Model以及数据访问层。理解这套结构你就知道一个浏览器请求进来之后是怎么一步步变成 SQL、再变成页面数据的。2.1 一个预订请求的流转路径从 Servlet 到 JSP 再到 DAO我在拆这类项目时习惯先找一个最简单的功能点顺着读下去比如“按城市搜索酒店”。这个功能通常会经过一个类似下面这样的 ServletWebServlet(/hotel/search) public class HotelSearchServlet extends HttpServlet { private HotelService hotelService new HotelService(); Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 收参数前端搜索表单提交的城市与入住日期 request.setCharacterEncoding(UTF-8); String city request.getParameter(city); String checkIn request.getParameter(checkIn); // 2. 调 Service 层真正的业务判断不写在 Servlet 里 ListHotel hotels hotelService.searchByCity(city, checkIn); // 3. 把结果放进 request转发给 JSP 渲染 request.setAttribute(hotels, hotels); request.getRequestDispatcher(/WEB-INF/hotel_list.jsp) .forward(request, response); } }这段代码是 Controller 层的标准骨架。注意它只做三件事收参数、调 Service、转发视图。业务校验“城市为空怎么提示”“日期格式不对怎么处理”都不应该堆在 Servlet 里。WebServlet注解里的 URL 路径要和前端表单的action/hotel/search严格一致大小写和斜杠都不能错这是新手最容易翻车的地方之一。再往下看 Service 层。它通常长这样先校验参数再调 DAO 查询数据库最后把结果封装返回。事务边界一般也在这里比如“创建订单”要先锁房间、再插订单、再改房间状态三步要么全成功要么全回滚。如果你在 Service 里看到connection.setAutoCommit(false)和connection.commit()说明作者对事务有基本概念如果直接在每个 DAO 方法里各自操作那大概率没有事务控制下单时可能出现房间扣了但订单没生成的尴尬状态。Servlet 和 JSP 的分工也要注意Servlet 干的是逻辑活JSP 只负责把数据渲染成 HTML。好项目的 JSP 里几乎看不到 Java 代码片段只用 JSTL 标签和 EL 表达式烂项目的 JSP 顶部全是% ... %甚至把 SQL 都写在页面里。后者虽然能跑但维护起来是场灾难。2.2 JDBC 连接与预编译查询连接串里最容易栽跟头的是时区数据访问层是这类项目的重头戏。如果项目用的是原生 JDBC大概率会有一个DBUtil或JdbcUtil之类的工具类静态块加载驱动对外提供getConnection()。我在源码里见过最多的写法是这种public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/hotel_db?useSSLfalsecharacterEncodingUTF-8serverTimezoneAsia/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) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }这个工具类看起来简单但每个参数都有讲究。useSSLfalse是关掉 SSL 警告MySQL 8 默认开 SSL本地开发不关会刷一堆红色告警characterEncodingUTF-8保证中文不乱码serverTimezoneAsia/Shanghai是 MySQL 8 的强制要求不配时区直接报The server time zone value的异常。驱动类名com.mysql.cj.jdbc.Driver是 MySQL 8 的如果是 5.7要写成com.mysql.jdbc.Driver写错就是ClassNotFoundException。DAO 层建议用PreparedStatement而不是Statement因为预编译有两个实际好处一是防止 SQL 注入用户输入的城市名带单引号也不会把 SQL 搞崩二是同样的 SQL 执行多遍时数据库端有缓存性能更好。拿到源码后你可以搜索一下项目里有没有Statement.createStatement()加字符串拼接的写法如果有这是给代码挑刺的好素材也是面试时能讲的点。2.3 查“可用房间数”这段 SQL 写得好不好直接暴露水平酒店预订系统里最核心的查询不是“查所有酒店”而是“查询某个时间段内哪些房间可订”。这个查询的 SQL 写法直接决定了系统在房量一大之后会不会卡死。常见做法是用“不存在重叠订单”来判断房间可用SELECT r.id, r.room_no, r.price, rt.type_name FROM room r JOIN room_type rt ON r.type_id rt.id WHERE r.hotel_id 101 AND r.status 1 AND r.id NOT IN ( SELECT o.room_id FROM orders o WHERE o.status IN (1, 2) AND o.check_in_date #{checkOut} AND o.check_out_date #{checkIn} );这段 SQL 的关键是日期交叉判断。判断两个区间是否有重叠用的不是“包含”而是订单的开始日期 要预订的结束日期 AND 订单的结束日期 要预订的开始日期。比如订单占的是 6 月 1 日到 6 月 3 日你要订 6 月 2 日到 6 月 4 日那么check_in_date(6月1日) checkOut(6月4日)为真check_out_date(6月3日) checkIn(6月2日)也为真重叠成立这间房被排除。这套逻辑在面试八股文里也常被拿来当场景题考理解了就不会忘。参数说明r.status 1表示房间状态为可售o.status IN (1, 2)表示待支付和已支付的订单都算占用已取消和已完成的订单不计入#{checkIn}和#{checkOut}是外部传入的日期参数如果项目用 MyBatis 就是#{}用原生 JDBC 就是?。另外要注意子查询里的o.room_id最好有索引否则订单表一大这个NOT IN会变成全表扫描。源码里如果没建索引你可以手动补一条ALTER TABLE orders ADD INDEX idx_room_date (room_id, check_in_date, check_out_date);作为优化点。3. 把六张核心表建出来酒店、房型、房间、用户、订单与状态管理看一个 Java Web 课程设计值不值得下载先看它的数据库脚本。真正用心的项目会附一个完整的hotel_db.sql里面是建库、建表、插入测试数据的全套语句。拿到这份脚本别急着执行先读结构。一个标准的酒店预订系统通常由五到六张表组成表与表之间的关系就是整个业务模型的骨架。3.1 表结构设计主键、外键与冗余字段怎么摆我在前面列过核心表这里直接展开看 DDL。不同的项目字段名会有差异但结构逻辑是通用的CREATE TABLE hotel ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, city VARCHAR(50) NOT NULL, address VARCHAR(200), stars TINYINT DEFAULT 3, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE room_type ( id INT PRIMARY KEY AUTO_INCREMENT, hotel_id INT NOT NULL, type_name VARCHAR(50) NOT NULL, -- 大床房 / 双床房 / 套房 price DECIMAL(10,2) NOT NULL, FOREIGN KEY (hotel_id) REFERENCES hotel(id) ); CREATE TABLE room ( id INT PRIMARY KEY AUTO_INCREMENT, hotel_id INT NOT NULL, type_id INT NOT NULL, room_no VARCHAR(20) NOT NULL, status TINYINT DEFAULT 1, -- 0维修 1可售 2已锁定 FOREIGN KEY (hotel_id) REFERENCES hotel(id), FOREIGN KEY (type_id) REFERENCES room_type(id) ); CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(64) NOT NULL, phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) UNIQUE NOT NULL, user_id INT NOT NULL, room_id INT NOT NULL, check_in_date DATE NOT NULL, check_out_date DATE NOT NULL, total_price DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 1, -- 1待支付 2已支付 3已取消 4已完成 create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user(id), FOREIGN KEY (room_id) REFERENCES room(id) );这张表的设计有个细节值得注意orders表里没有冗余酒店名和房型名查询订单详情时要 JOIN 三张表才能拿到完整信息。主流做法其实应该冗余一份快照把下单时的酒店名、房型名、单价直接存在订单里否则酒店改价或改名后历史订单展示的数据就会对不上。不过课程设计项目这样写也说得过去因为数据量小性能压力可以忽略。如果你要优化可以在orders表加hotel_name、room_type_name两个冗余字段这个改动是很好的答辩加分项。room表单独抽出来而不是直接把房间挂在room_type下这个建模是合理的。因为“大床房”是一个类型同一家酒店可能有 10 间大床房每间房的房间号和状态需要独立管理。如果把房间号直接写在room_type表里那 10 间房就得插 10 条类型记录价格还得重复维护非常别扭。3.2 订单状态机与房间状态为什么不能混为一谈很多新手会犯一个错误用订单状态去反推房间状态甚至两张表共用一套状态码。这里必须拆开。订单状态描述的是“这笔交易进行到哪一步”房间状态描述的是“这间房此刻能不能被预订”。两套状态需要独立维护但在业务事件里联动。表状态字段取值含义ordersstatus1待支付ordersstatus2已支付ordersstatus3已取消ordersstatus4已完成roomstatus0维修中roomstatus1可售roomstatus2已锁定典型的联动场景是用户下单但未支付订单变待支付(1)此时房间应该被标记为已锁定(2)防止别人同时订同一间用户支付成功后订单变已支付(2)房间理论上被占用但很多项目此时不改变房间状态因为“占用”是通过订单表的日期区间判断出来的用户取消订单订单变已取消(3)房间状态要立刻改回可售(1)。如果代码里忘记把房间状态改回来就会出现“数据库里没订单但房间永远是锁定状态”的灵异现象。我在源码里排查时一般会重点搜索UPDATE room SET status的所有调用点梳理每个调用点的触发条件。如果发现“取消订单”功能没有对应更新房间状态这就是一个确定的 bug。反过来如果项目把 room.status 直接改成已预订还要区分是哪个日期段被订了那这个设计就太僵硬了同一间房不同日期的可用性没法表达。记住一个原则房间的“某段时间不可用”由订单表通过日期区间表达room.status 只表达“当前物理状态”。4. 本地跑起来Tomcat 部署、数据库初始化与三处环境配置源码下载下来最让人上头的瞬间就是点 Run 之后报一堆错。其实这类 Java Web 项目的运行环境非常固定JDK、数据库、Tomcat 三件套配好成功率至少在八成以上。剩下两成的坑集中在版本匹配和路径配置上。这一章把完整流程走一遍。4.1 环境准备与版本匹配JDK 8 配合 Tomcat 8/9 最稳课程设计级项目写成于不同年份依赖的环境差别很大。我建议先确认三件事JDK 版本、MySQL 版本、Tomcat 版本。最稳妥的组合是 JDK 8 Tomcat 8 或 9 MySQL 5.7。JDK 8 的兼容性最好老项目基本都能跑MySQL 8 也可以但驱动类和 URL 参数要按前面说的调整。JDK 环境变量配置是出现频率最高的求助问题之一这里给最小可用配置。Windows 下在系统环境变量里新建JAVA_HOME指向 JDK 安装目录比如C:\Program Files\Java\jdk1.8.0_202然后把%JAVA_HOME%\bin追加到Path。macOS 或 Linux 下在~/.bash_profile或~/.zshrc里加export JAVA_HOME$(/usr/libexec/java_home -v 1.8) export PATH$JAVA_HOME/bin:$PATH配置完在终端执行java -version能输出版本号就说明生效。注意这里有坑如果系统里装了多个 JDKjava -version显示的未必是JAVA_HOME指向的版本Windows 下因为Path里先出现了其他 JDK 路径会造成版本错乱。我一般会用where java或which java看实际解析到哪个路径再调整环境变量顺序。Tomcat 不用安装解压即用。Windows 下运行bin/startup.batmacOS/Linux 下运行bin/startup.sh。但我不推荐双击启动因为窗口一闪而过根本看不到错误。更推荐用命令行前台模式启动cd /path/to/apache-tomcat-9.0.x bin/catalina.sh runrun模式会把日志直接打到控制台报错信息当前就能看到。startup.sh是后台启动日志写到logs/catalina.out适合确认没问题之后再用。4.2 数据库初始化与连接配置启动失败十有八九出在这两步运行项目前先把数据库准备到位。源码里一般有hotel_db.sql或者类似init.sql的脚本。先创建数据库再导入mysql -u root -p -e CREATE DATABASE IF NOT EXISTS hotel_db DEFAULT CHARSET utf8mb4; mysql -u root -p hotel_db hotel_db.sql导入完成后进数据库看一眼表是否齐全SHOW TABLES;。如果脚本里有USE hotel_db;那第一条建库命令可以省略。这里有个细节导入命令的重定向在 Windows CMD 和 PowerShell 里都支持如果提示找不到文件检查是不是路径没切对或者 SQL 文件编码不对导致中文乱码。然后要改数据库连接配置。这类项目通常把连接信息放在src/db.properties、jdbc.properties或WEB-INF/classes下的配置文件里参数大致如下参数名示例值说明jdbc.drivercom.mysql.cj.jdbc.DriverMySQL 8 驱动类5.7 用 com.mysql.jdbc.Driverjdbc.urljdbc:mysql://localhost:3306/hotel_db?useSSLfalsecharacterEncodingUTF-8serverTimezoneAsia/Shanghai注意库名、时区、编码jdbc.usernameroot你的数据库用户名jdbc.password123456你的数据库密码按实际改改完配置强烈建议先写个最简单的测试类验证连接而不是直接启动 Tomcat。测试类里就三行Class.forName(com.mysql.cj.jdbc.Driver); Connection conn DriverManager.getConnection(url, username, password); System.out.println(conn ! null);如果这里能通过数据库连接基本没问题如果报错错误信息会精确告诉你是驱动缺失、密码错误还是时区问题比在 Tomcat 日志里大海捞针快得多。4.3 部署到 Tomcatwar 包还是 IDE 直接发布部署方式有两种。一是把项目打成 war 包扔进 Tomcat 的webapps目录启动后自动解压发布。这种方式适合你已经有一个可运行的项目。命令mvn clean package -DskipTests cp target/hotel-system.war /path/to/apache-tomcat-9.0.x/webapps/二是在 IDEA 或 Eclipse 里配置 Tomcat Server点击运行。这种方式适合需要断点调试的场景。注意 IDE 部署时有个路径问题IDEA 默认会把应用部署到 Tomcat 的webapps目录之外而且访问路径通常不带项目名而是/。如果你在浏览器里访问http://localhost:8080/hotel/打不开先确认访问路径是带项目名还是不带。Tomcat 的server.xml里配置的端口默认 8080如果启动日志里看到Port 8080 was already in use说明端口被占了解决方法是关掉占用进程或改端口。启动成功的标志是日志里出现Server startup in [xxx] milliseconds。之后先访问登录页或首页如果页面能出来再走一遍“注册 - 登录 - 搜酒店 - 下单”的完整流程。任何一个环节报 500立刻去看logs/localhost.logTomcat 把应用内未捕获的异常都写在这份日志里带完整堆栈定位比控制台输出准得多。5. 避坑与常见问题翻车率最高的五个点及排查路径这部分直接上血泪经验。课程设计项目跑不起来的故障九成是环境问题代码问题反而是少数。下面五条是我拆过的 Java Web 项目里出现频率最高的坑每条按现象、原因、解决三步讲清楚。5.1 页面中文全部是问号改了三处编码还是不对现象浏览器里打开页面所有汉字显示成????或乱码数据库里存的中文也是问号。原因编码不统一。项目里涉及编码的位置至少有三个JSP 文件的pageEncoding、Servlet 接收请求时的request.setCharacterEncoding、数据库 JDBC URL 里的characterEncoding。只改其中一两个链条上总有一个环节在用默认编码比如 Tomcat 8 前的默认 ISO-8859-1。另外MySQL 表本身的字符集如果是 latin1存中文必然出问题。解决先统一到 UTF-8。确保每个 JSP 头部是pageEncodingUTF-8所有 Servlet 在读取参数前调用request.setCharacterEncoding(UTF-8)JDBC URL 带characterEncodingUTF-8建库脚本里DEFAULT CHARSET utf8mb4。如果表已经建好了就执行ALTER TABLE 表名 CONVERT TO CHARACTER SET utf8mb4;修复。5.2 ClassNotFoundException: com.mysql.jdbc.Driver现象Tomcat 启动正常但一访问连接数据库的页面就报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver。原因MySQL 驱动 jar 包没有放进正确的位置。Java Web 项目的驱动包必须放在WEB-INF/lib目录下Tomcat 启动时才会加载。放在桌面、放在 IDEA 的 External Libraries 里运行时都找不到。另外 MySQL 8 和 5.7 的驱动类名不同把 8 的 jar 包配成 5.7 的类名一样报这个错。解决确认你用的驱动版本。MySQL 5.7 用mysql-connector-java-5.x.jar类名com.mysql.jdbc.DriverMySQL 8 用mysql-connector-java-8.x.jar类名com.mysql.cj.jdbc.Driver。把 jar 文件复制到WEB-INF/lib/下重启 Tomcat。5.3 Access denied for user rootlocalhost 或时区异常现象启动后数据库操作一直失败错误信息要么是权限拒绝要么是The server time zone value йʱ is unrecognized。最后那个乱码般的错误信息就是中文字符集和时区没配对。原因权限拒绝除了密码错误还有一种可能是 MySQL 8 默认用了caching_sha2_password认证插件老版本驱动不支持。时区异常则是 JDBC URL 里缺serverTimezone参数MySQL 8 对此是强制要求MySQL 5.7 没有这个限制。解决连接串里加serverTimezoneAsia/ShanghaiuseSSLfalse。认证插件问题有两个方案换 8.x 驱动或者执行 SQLALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码;。本地开发我更推荐前者不用动数据库配置。5.4 Tomcat 启动失败端口 8080 被占用现象启动 Tomcat控制台刷了一堆日志后失败明确提示Port 8080 was already in use。原因之前残留的 Tomcat 实例没关干净或者别的程序占了 8080IDEA 内置的某个服务、之前跑过的 Spring Boot 项目常见于此。解决先找到占用进程。Windows 下执行netstat -ano | findstr 8080拿到 PID 后taskkill /PID pid /FmacOS/Linux 下执行lsof -i:8080然后kill -9 pid。想省事的话直接改 Tomcat 端口编辑conf/server.xml里的Connector port8080改成 8081 或 8089 都行同时项目里的访问地址也要跟着改。5.5 连续点两次“提交订单”生成了两笔重复订单现象网速慢的时候用户连点提交按钮数据库里出现两条一模一样的订单只是订单号不同。原因前端没有做按钮禁用后端没有做重复提交校验。这是 Web 项目里最经典的并发幂等坑也是面试很容易被追问的点。课程设计源码里大概率没有处理但你可以自己补上。解决前端最简单的办法是点击后立即将按钮置灰document.getElementById(submitBtn).disabled true;后端更可靠的方案是给订单号加唯一索引用户点击生成订单号后插入时靠数据库约束兜底重复插入直接抛异常。还可以用 Session 存一个下单令牌提交时对比并清除。我的建议是至少做到“前端禁用按钮 订单号唯一索引”成本最低效果最稳。6. 进阶用法把 JDBC 换成 MyBatis 之前先做三个验证点很多人在拿到这个项目之后第一个想法是“我要把它改成 Spring Boot MyBatis 版本”。这个方向是对的但直接动手改很容易把项目改废。我的习惯是在动刀重构之前先按三个验证点把老代码跑透确认自己真正理解了原系统的行为再谈改造。第一个验证点DAO 层接口是否稳定。如果你准备把 JDBC 换成 MyBatis先数一下项目里所有 DAO 方法把方法名、参数、返回类型整理出来。MyBatis Mapper 的接口签名和原来保持一致Service 层就不用动。我一般会把每个 DAO 方法对应的 SQL 都抄一遍标注清楚有没有动态条件。这个动作做完别说改造光是对着 SQL 讲业务逻辑都能讲半小时。第二个验证点事务边界在哪儿。原项目里如果事务都写在 Service 层改造 MyBatis 时就配一个DataSourceTransactionManager把Transactional加在 Service 方法上。如果原项目压根没有事务控制改造时要自己把“下单”这种多步操作包进事务。这个点不提前想好改用 MyBatis 之后代码看着清爽了bug 反而更难抓。第三个验证点连接管理方式。原项目如果用DriverManager.getConnection()每次新建连接改成 MyBatis 之后建议换连接池HikariCP 或 Druid 都行。连接池初始化参数里maximum-pool-size对于课程设计项目不需要太大10 到 20 就够核心是connection-timeout不要设成 0否则数据库抖动时请求会无限等下去。做完这三个验证点再动手改。从那以后我每次拿到一份新的项目源码不管多急都会强制自己先走一遍“建库、配连接、看日志、跑通核心流程”四步然后用一小时把 DAO 方法清单和事务边界整理出来再谈任何技术升级。这个习惯帮我躲过了不少“改完跑不起来”的尴尬局面。这套项目逻辑不复杂但产业链完整非常适合当练手样本。希望帮到你。本文还有配套的精品资源点击获取