本文还有配套的精品资源点击获取简介一套开箱即用的轻量级酒店管理Demo基于Java Web标准技术栈构建全程适配Eclipse开发环境。前端提供多个登录入口普通用户可通过userlogin1.html或userlogin2.html注册并直接入住空闲房间管理员使用adminlogin.html登录后能新增、删除房间信息并为已入住客人办理退房。所有页面统一由login.html跳转引导逻辑清晰。后端采用经典Servlet结构无框架依赖便于理解请求响应流程数据库脚本init_db.sql可一键初始化推荐配合Navicat进行可视化管理。项目目录结构规范包含src/com下的业务类、WebContent中的HTML静态页、WEB-INF/web.xml配置文件及编译输出路径build/classes完整覆盖Java Web应用典型组成。适合刚接触Servlet、JSP和基础数据库交互的学习者快速掌握用户登录、房间CRUD、入住/退房状态变更等核心业务场景。1. 项目概述为什么这个“小系统”值得你花两小时跑通一遍我带过不少刚学完Java基础、正卡在“学了Servlet但不知道怎么串起来”的学生也帮同事调试过几十个课堂级Web项目。说实话大多数Demo要么太简陋——一个login.jsp加个UserServlet就号称“完成登录”连密码明文传输都不处理要么太臃肿——硬塞进Spring Boot、MyBatis、Redis新手光配环境就得折腾一整天根本没机会看清HTTP请求从浏览器发出到数据库落库的完整链条。而这个酒店管理小系统恰恰卡在一个极难复制的黄金平衡点上它不炫技但每一步都踩在Java Web最核心的关节上它没用任何框架却把用户身份隔离、状态驱动业务入住/退房、房态实时反馈、前后端职责边界这些真实场景里的关键逻辑全揉进了十几个HTML文件和不到20个Java类里。关键词里反复出现的酒店管理、Java Web、Servlet、房态管理、Eclipse不是随便堆砌的标签——它们共同指向一个明确的学习靶心理解状态如何在Web应用中被定义、传递、校验和持久化。比如“自助入住”四个字背后是userlogin2.html提交表单 → RegisterServlet接收参数 → 检查房间是否available → 更新room表status字段为’occupied’ → 同时插入guest记录 → 最后跳转到成功页。这整个流程里没有JSON没有AJAX甚至没有JSP全靠纯Servlet的request.setAttribute RequestDispatcher.forward完成数据透传反而让初学者一眼看穿MVC里“C”到底干了什么。再比如“房态调控”管理员删房间不是简单DELETE FROM room而是先查该房间是否occupied是则拒绝操作并返回提示——这个if判断就是真实业务里“状态机”的雏形。我在Eclipse里第一次跑通这个项目时特意把Tomcat日志级别调成DEBUG盯着控制台里每一行“RegisterServlet: room 101 status changed from available to occupied”滚动出来那种对数据流动的掌控感是看十遍理论都换不来的。它适合谁如果你正在Eclipse里新建Dynamic Web Project还犹豫该选哪个Target Runtime如果你写完第一个HttpServlet却不知道web.xml里 和WebServlet注解的区别如果你能手写SQL建表但搞不清PreparedStatement里?占位符怎么和setString()对应——那这个项目就是为你量身定做的沙盒。它不要求你懂Maven依赖管理pom.xml只是备选原生WebContent结构完全可运行也不需要你配置Tomcat虚拟目录直接右键Run As → Run on Server就行。所有路径、包名、SQL脚本都严格遵循Java Web规范连.gitignore里排除build/和.settings/这种细节都帮你写好了。这不是一个要你“改代码才能跑”的半成品而是一个拧开就能出水的龙头——你拧动的每一圈对应的都是Servlet生命周期里的init()、service()、destroy()都是HTTP协议里的GET/POST都是数据库事务里的ACID。接下来我会带你一层层剥开它的结构不只告诉你“怎么做”更告诉你“为什么必须这么做”以及我在调试时踩过的那些坑——比如为什么adminlogin.html里form action写的是”/AdminLoginServlet”而不是”AdminLoginServlet”为什么init_db.sql里room表的status字段非要用VARCHAR(20)而不是ENUM这些看似琐碎的决定背后全是Java Web开发里血淋淋的经验。2. 整体架构与设计思路拆解没有框架的“裸奔”反而最见真章2.1 为什么坚持“零框架”Servlet原生才是最好的教具看到项目描述里强调“无框架依赖”可能有人会疑惑现在谁还手写ServletSpring Boot一行代码启动服务不好吗这个问题我问过自己不下十次。直到去年帮一个嵌入式团队做Java Web培训他们产线设备只能跑JDK 1.8内存限制死在64MB连Tomcat都要精简掉JSP模块。那时我才真正体会到框架是锦上添花的绸缎而Servlet API是支撑整座房子的地基钢筋。这个酒店系统刻意剥离所有框架正是为了让你看清三个不可替代的底层契约第一URL到Java类的映射契约。在web.xml里你一定会看到类似这样的配置servlet servlet-nameRegisterServlet/servlet-name servlet-classcom.servlet.RegisterServlet/servlet-class /servlet servlet-mapping servlet-nameRegisterServlet/servlet-name url-pattern/RegisterServlet/url-pattern /servlet-mapping这个配置的本质是告诉Tomcat“当用户访问http://localhost:8080/HotelSystem/RegisterServlet时请把请求交给com.servlet.RegisterServlet这个类处理”。而WebServlet(“/RegisterServlet”)注解不过是把这个XML配置搬进了Java类里。很多初学者以为注解是“新东西”其实它只是语法糖底层依然是容器读取类上的元数据来完成映射。我在调试时故意把web.xml里的 删掉保留WebServlet结果访问/login.html点击注册按钮直接404——这个错误瞬间让我记住了映射关系是容器启动时就解析好的不是运行时动态生成的。第二请求-响应对象的生命周期契约。HttpServletRequest和HttpServletResponse这两个对象不是你new出来的而是Tomcat在每次HTTP请求到达时由容器自动创建并注入到service()方法里的。这意味着你在doPost()里对request.setAttribute(“msg”, “success”)设置的属性只在本次请求的转发链路里有效一旦重定向response.sendRedirect这些属性就烟消云散。项目里userlogin2.html注册成功后跳转到welcome.jsp用的就是RequestDispatcher.forward()所以能拿到Servlet里设置的欢迎语而管理员删除房间失败后跳回admin.jsp则用response.sendRedirect()避免用户刷新页面重复提交。这种区别只有亲手写过原生Servlet才会刻骨铭心。第三数据库连接的资源契约。项目没用连接池而是每次操作都new Connection用完立刻close()。这看起来很“土”却是教学最佳方案。因为初学者最容易犯的错就是忘记close()导致数据库连接数爆满。我在Navicat里开着“当前连接数”监控面板一边运行项目一边观察数字跳动点击一次注册连接数1操作结束-1。这种直观反馈比背一百遍“Connection必须手动关闭”都管用。等你真正理解了连接的开销和泄漏风险再去学Druid或HikariCP才知道那些配置项如maxActive、minIdle究竟在解决什么问题。2.2 房态管理用最朴素的状态字段驱动整个业务流“房态管理”这个词听起来高大上但在这个系统里它就浓缩在room表的一个status字段里。打开init_db.sql你会看到CREATE TABLE room ( id INT PRIMARY KEY AUTO_INCREMENT, room_number VARCHAR(10) NOT NULL, floor INT NOT NULL, price DECIMAL(8,2) NOT NULL, status VARCHAR(20) DEFAULT available -- 关键只有available和occupied两种值 );注意这里没用ENUM类型也没用tinyint(1)存0/1而是用VARCHAR(20)存字符串。为什么因为教学场景下可读性优先于存储效率。当你在Navicat里查看room表数据时一眼就能看出id101的房间是’available’还是’occupied’不用查数据字典翻译0空闲、1已住。更重要的是这种设计让业务逻辑异常清晰所有关于“能不能入住”、“能不能退房”的判断都变成一句简单的SQL WHERE status ‘available’ 或 status ‘occupied’。这个状态字段像一根主线串起了所有核心操作-自助入住RegisterServlet查询room表WHERE status ‘available’ LIMIT 1找到第一个空闲房间然后UPDATE SET status ‘occupied’-管理员新增房间AddRoomServlet插入新记录时status默认为’available’-管理员删除房间DelRoomServlet先SELECT COUNT() FROM guest WHERE room_id ?如果大于0说明有客人住着直接拒绝删除-办理退房*CheckOutServlet UPDATE room SET status ‘available’ WHERE id ?同时DELETE FROM guest WHERE room_id ?。你会发现所有操作都围绕status字段的变更展开而没有任何一个地方需要去计算“当前有多少空房”——那个数字是实时查询出来的不是维护在某个变量里的。这就是状态驱动设计State-Driven Design的精髓不维护冗余状态只通过原子化的状态变更来保证数据一致性。我在教学生时会让他们把room表status字段改成’booked’预订中、’cleaning’打扫中等更多状态然后思考RegisterServlet的逻辑该怎么改——这个练习能让他们立刻理解状态机扩展的代价。2.3 前端入口的精心设计login.html为何是“统一入口”而非多余存在项目提到“所有页面统一由login.html跳转引导”初学者常觉得这是多此一举既然有userlogin1.html和userlogin2.html为啥不直接访问它们这里藏着一个重要的工程实践——入口路由集中化。login.html的代码极其简单!-- login.html -- !DOCTYPE html html headtitle酒店系统入口/title/head body h2请选择身份登录/h2 a hrefuserlogin1.html我是新客人快速注册/abrbr a hrefuserlogin2.html我是老客人凭身份证号登录/abrbr a hrefadminlogin.html我是管理员/a /body /html它的价值在于三点第一解耦前端页面与后端逻辑。userlogin1.html里form action写的是”/RegisterServlet”这个路径是硬编码的如果哪天Servlet类名变了你得改所有HTML。而login.html作为唯一入口所有跳转链接都在这里维护修改成本降到最低。第二提供身份认知缓冲。真实系统里用户不会一上来就面对两个注册页。login.html强制用户先思考“我是谁”再选择路径这模拟了真实产品的用户体验。第三为未来扩展留白。比如后续想加“忘记密码”功能只需在login.html里加一行不用动任何其他页面。我在实际部署时曾把login.html设为Tomcat的welcome-file-list第一个文件这样用户访问http://localhost:8080/HotelSystem/直接看到身份选择页专业感立现。而userlogin1.html和userlogin2.html之所以并存是因为它们代表两种典型用户旅程userlogin1.html是“零信息用户”只要填姓名、电话、身份证号就能分配空房userlogin2.html则是“有历史记录用户”输入身份证号后Servlet会查guest表如果存在且未退房直接显示入住信息——这种差异化的前端设计比写一堆if-else判断更清晰。3. 核心细节解析与实操要点从目录结构到每一行关键代码3.1 目录结构即规范为什么src/com/和WebContent/不能颠倒Eclipse里新建Dynamic Web Project时目录结构是固定的但很多新手会疑惑为什么Java类必须放在src/com/下HTML必须放在WebContent/里这背后是Java Web容器的资源加载约定。我们来看项目资源包里的目录树. ├── .gitignore ├── userlogin1.html # WebContent根目录下 ├── userlogin2.html # 同上 ├── adminlogin.html # 同上 ├── login.html # 同上 ├── init_db.sql # 数据库脚本通常放项目根目录 ├── pom.xml # Maven配置可选 ├── src/ │ └── com/ │ └── servlet/ # Servlet类如RegisterServlet.java │ └── dao/ # 数据访问类如RoomDAO.java │ └── bean/ # 实体类如Room.java、Guest.java ├── WebContent/ │ ├── userlogin1.html # 注意这里又有一个同名文件 │ ├── WEB-INF/ │ │ ├── web.xml # 核心配置文件 │ │ └── lib/ # 第三方jar包本项目为空 ├── build/ │ └── classes/ # Eclipse编译输出目录存放.class文件关键点来了WebContent是Web应用的“发布根目录”。当你把项目部署到Tomcat时Tomcat只会把WebContent下的内容包括其子目录当作可被HTTP访问的资源。所以userlogin1.html放在WebContent/下用户才能通过http://localhost:8080/HotelSystem/userlogin1.html访问到。而src/com/下的Java源码是给Eclipse编译用的编译后的.class文件会自动输出到build/classes/com/下再由Tomcat的ClassLoader加载。如果你把RegisterServlet.java直接拖到WebContent/里它永远不会被编译访问/RegisterServlet必然404。我在指导学生时会让他们做个小实验把userlogin1.html剪切到src/目录下然后刷新浏览器——页面直接404。再把它粘贴回WebContent/立刻恢复。这个实验比讲十分钟类路径Classpath都管用。另外WEB-INF是个特殊目录它下面的文件无法被浏览器直接访问。所以web.xml放在WEB-INF/里是安全的而如果你把数据库密码写在某个.properties文件里也必须放WEB-INF/下否则黑客访问http://localhost:8080/HotelSystem/db.properties就能直接下载。3.2 关键Servlet代码剖析RegisterServlet里的三次状态校验RegisterServlet是整个自助入住流程的核心它的doPost()方法看似简单实则暗藏三重校验。我们逐行拆解基于常见实现逻辑protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 第一次校验前端传参完整性 String name request.getParameter(name); String idCard request.getParameter(idCard); String phone request.getParameter(phone); if (name null || name.trim().isEmpty() || idCard null || idCard.trim().isEmpty() || phone null || phone.trim().isEmpty()) { request.setAttribute(msg, 请填写完整信息); request.getRequestDispatcher(userlogin1.html).forward(request, response); return; // 必须return否则继续执行 } // 2. 第二次校验身份证号唯一性防止重复注册 GuestDAO guestDAO new GuestDAO(); if (guestDAO.findByIDCard(idCard) ! null) { request.setAttribute(msg, 该身份证号已注册); request.getRequestDispatcher(userlogin1.html).forward(request, response); return; } // 3. 第三次校验房态可用性核心业务逻辑 RoomDAO roomDAO new RoomDAO(); Room availableRoom roomDAO.findAvailableRoom(); // SQL: SELECT * FROM room WHERE statusavailable LIMIT 1 if (availableRoom null) { request.setAttribute(msg, 抱歉暂无空房); request.getRequestDispatcher(userlogin1.html).forward(request, response); return; } // 执行入住先更新房间状态再插入客人记录 try { Connection conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 更新房间状态 String updateSql UPDATE room SET status ? WHERE id ?; PreparedStatement pstmt1 conn.prepareStatement(updateSql); pstmt1.setString(1, occupied); pstmt1.setInt(2, availableRoom.getId()); pstmt1.executeUpdate(); // 插入客人记录 String insertSql INSERT INTO guest (name, id_card, phone, room_id) VALUES (?, ?, ?, ?); PreparedStatement pstmt2 conn.prepareStatement(insertSql); pstmt2.setString(1, name); pstmt2.setString(2, idCard); pstmt2.setString(3, phone); pstmt2.setInt(4, availableRoom.getId()); pstmt2.executeUpdate(); conn.commit(); // 提交事务 request.setAttribute(msg, 入住成功房间号 availableRoom.getRoomNumber()); request.getRequestDispatcher(welcome.jsp).forward(request, response); } catch (SQLException e) { e.printStackTrace(); try { conn.rollback(); // 出错回滚 } catch (SQLException ex) { ex.printStackTrace(); } request.setAttribute(msg, 系统繁忙请稍后再试); request.getRequestDispatcher(userlogin1.html).forward(request, response); } }这段代码里有三个极易被忽略的细节第一所有校验失败后都必须return。很多新手写完request.getRequestDispatcher(...).forward()就以为结束了其实后面代码还会继续执行可能导致空指针异常或重复插入。第二身份证号唯一性校验放在房态校验之前。这是性能优化查guest表比查room表快得多guest表数据量小且id_card字段通常建了索引先拦住明显非法请求避免浪费数据库连接。第三事务的粒度控制。这里把UPDATE room和INSERT guest放在同一个事务里确保“房间状态变更”和“客人记录插入”要么全成功要么全失败。如果只对UPDATE加事务而INSERT单独执行就可能出现房间被标记为occupied但客人没录入的脏数据——这种问题在高并发下会指数级放大。3.3 数据库脚本init_db.sql的隐藏设计为什么room表主键用INT而非UUIDinit_db.sql不仅是建表语句更是业务逻辑的蓝图。我们重点看room表的创建部分CREATE TABLE room ( id INT PRIMARY KEY AUTO_INCREMENT, room_number VARCHAR(10) NOT NULL UNIQUE, floor INT NOT NULL, price DECIMAL(8,2) NOT NULL, status VARCHAR(20) DEFAULT available ); CREATE TABLE guest ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, id_card VARCHAR(18) NOT NULL UNIQUE, phone VARCHAR(15) NOT NULL, room_id INT NOT NULL, check_in_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (room_id) REFERENCES room(id) );这里有两个关键设计决策第一room.id用INT AUTO_INCREMENT而非UUID。初学者常觉得UUID“更高级”但在这个场景下INT有压倒性优势- 存储空间INT占4字节UUID占16字节同样10万条数据room表能节省1.2MB空间- 查询性能B树索引中INT比较是CPU直接指令UUID比较需逐字符扫描百万级数据下差距可达毫秒级- 可读性管理员在Navicat里看到room.id101立刻知道这是第101个房间UUID是一长串32位十六进制毫无业务意义。第二guest表的room_id是外键且NOT NULL。这强制保证了“每个客人必须关联一个有效房间”杜绝了孤儿记录。我在调试时曾手动在Navicat里删掉一条room记录结果guest表里对应room_id的记录还在导致CheckOutServlet执行UPDATE room SET status’available’时找不到目标——这个错误让我意识到外键约束不是可选项而是数据一致性的最后防线。所以init_db.sql里明确写了FOREIGN KEY而不是靠Java代码去校验。4. 实操过程与核心环节实现从Eclipse配置到Navicat可视化管理4.1 Eclipse环境搭建四步法避开90%的新手报错在Eclipse里跑通这个项目最关键的不是写代码而是环境配置。根据我帮上百人远程调试的经验以下四步必须严格按顺序执行跳过任何一步都会导致404或ClassNotFoundException第一步创建Dynamic Web Project并指定Target Runtime右键Project Explorer → New → Dynamic Web Project → 输入项目名如HotelSystem→ 在“Target runtime”下拉框中必须选择已安装的Tomcat版本如Apache Tomcat v9.0。如果下拉框为空说明你还没配置TomcatWindow → Preferences → Server → Runtime Environments → Add → 选择Tomcat版本 → 指向你的Tomcat安装目录如D:\apache-tomcat-9.0.83。这一步漏掉项目连基本结构都建不全。第二步导入源码到正确位置把下载的资源包解压将src/目录下的com/文件夹含servlet/、dao/、bean/整体复制到Eclipse项目里的src/目录下将WebContent/下的所有HTML文件userlogin1.html等和WEB-INF/文件夹整体复制到Eclipse项目里的WebContent/目录下。特别注意不要把整个资源包文件夹拖进Eclipse否则src和WebContent会错位。我见过太多人把userlogin1.html放在项目根目录结果Tomcat启动后访问http://localhost:8080/HotelSystem/userlogin1.html始终404——因为Tomcat只认WebContent/下的文件。第三步配置web.xml或启用WebServlet注解检查src/com/servlet/RegisterServlet.java如果类上有WebServlet(“/RegisterServlet”)则无需改web.xml如果没有则必须在WebContent/WEB-INF/web.xml里添加对应的 和 配置。两者不能共存否则Tomcat启动时报错。我在Eclipse的Console窗口里第一眼就看是否有“SEVERE: Error starting static Resources”这类错误有就一定是web.xml配置错了。第四步配置数据库连接DBUtil.java打开src/com/dao/DBUtil.java找到getConnection()方法修改数据库连接参数private static final String URL jdbc:mysql://localhost:3306/hotel_db?useSSLfalseserverTimezoneUTC; private static final String USER root; // 改成你的MySQL用户名 private static final String PASSWORD 123456; // 改成你的MySQL密码注意URL里的hotel_db是数据库名必须先在MySQL里创建好CREATE DATABASE hotel_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;。如果MySQL端口不是3306或者用了非root用户这里必须同步修改。我在Navicat里新建连接时会把Host、Port、Username、Password抄到DBUtil.java里一字不差。完成这四步后右键项目 → Run As → Run on Server选择你的Tomcat等待Console里出现“Server startup in [xxx] ms”然后浏览器访问http://localhost:8080/HotelSystem/login.html。如果看到身份选择页恭喜环境配置成功4.2 Navicat可视化管理实战三招搞定数据库初始化与调试Navicat不是摆设而是你调试数据库逻辑的“透视镜”。以下是我在实际操作中总结的三个必用技巧技巧一用init_db.sql一键初始化但必须手动执行两次双击init_db.sql → 点击左上角“运行”按钮绿色三角。Navicat会执行所有CREATE TABLE语句。但注意第一次执行后room表里是空的没有测试数据。这时你需要手动插入几条测试数据否则userlogin1.html注册时永远提示“暂无空房”。在Navicat的room表上右键 → “打开表” → 点击下方“”号新增行填入| id | room_number | floor | price | status ||----|-------------|-------|--------|------------|| 1 | 101 | 1 | 288.00 | available || 2 | 102 | 1 | 328.00 | available || 3 | 201 | 2 | 388.00 | available |这样就有3个空房可供测试。我习惯把room_number设为101、102、201因为楼层信息一目了然调试时不会混淆。技巧二实时监控guest和room表联动打开两个Navicat标签页一个查guest表一个查room表。在浏览器里完成一次自助入住userlogin1.html填信息提交然后立刻切换到Navicat点击guest表的“刷新”按钮蓝色循环箭头你会看到新插入的客人记录再切换到room表刷新发现id1的room.status变成了’occupied’。这种实时联动比看日志直观十倍。如果发现guest表有记录但room表status没变说明RegisterServlet里的UPDATE语句执行失败立刻去看Console里的SQLException堆栈。技巧三用“查询”功能模拟业务逻辑比如你想验证管理员删除房间的逻辑是否严谨可以在Navicat的“查询”标签页里手动执行-- 先查room 101是否被占用 SELECT COUNT(*) FROM guest WHERE room_id (SELECT id FROM room WHERE room_number 101); -- 如果结果是1说明有人住着此时执行删除会失败 DELETE FROM room WHERE room_number 101;执行第二条DELETE时Navicat会报错“Cannot delete or update a parent row: a foreign key constraint fails”。这个错误就是DelRoomServlet里“先查后删”逻辑的源头——它逼着你必须在Java代码里先SELECT COUNT(*)再决定是否DELETE。这种用数据库工具反推代码逻辑的方法能让你深刻理解外键约束的价值。5. 常见问题与排查技巧实录那些让我熬夜到凌晨的坑5.1 经典404问题排查速查表404是Java Web新手的头号敌人但90%的404都能通过这张表快速定位现象可能原因排查步骤解决方案访问/login.html显示404login.html不在WebContent/根目录在Eclipse里展开项目确认login.html直接在WebContent/下而非WebContent/WEB-INF/或src/里剪切login.html到WebContent/根目录点击userlogin1.html的“注册”按钮后404form action路径错误查看userlogin1.html源码确认中的路径是否与web.xml里 的 完全一致注意开头的/如果web.xml里是 /RegisterServlet 则action必须是”/RegisterServlet”不能是”RegisterServlet”或”./RegisterServlet”访问/RegisterServlet显示404但web.xml配置正确Servlet类未编译或classpath错误在Eclipse的Project Explorer里展开build/classes/com/servlet/确认RegisterServlet.class是否存在右键项目 → Refresh然后右键项目 → Build Project确保编译成功所有Servlet都404但HTML页面正常Tomcat未正确关联项目查看Eclipse底部Servers视图双击你的Tomcat服务器 → 在“Modules”选项卡里确认HotelSystem项目已勾选且Path为”/HotelSystem”如果未勾选点击“Add External Web Module” → 选择你的项目 → Path填”/HotelSystem”我在带学生时会让大家遇到404先别急着改代码而是打开Eclipse的“Servers”视图双击Tomcat在弹出的配置窗口里点开“Modules”这里能看到所有已部署模块的路径映射。绝大多数404问题根源都在这里没配对。5.2 数据库连接失败的五大死因java.sql.SQLException: Access denied for user rootlocalhost这类错误表面是密码错实则有更深的坑死因一MySQL 8.0默认认证插件变更MySQL 8.0开始默认认证插件从mysql_native_password改为caching_sha2_password而老版JDBC驱动不支持。解决方案在MySQL命令行里执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;死因二JDBC驱动版本不匹配项目用的是mysql-connector-java-5.1.47.jar常见于老教程但你的MySQL是8.x。必须升级驱动下载mysql-connector-java-8.0.33.jar放到WebContent/WEB-INF/lib/下并在Eclipse里右键项目 → Properties → Java Build Path → Libraries → Add JARs → 选择新驱动。死因三数据库名拼写错误DBUtil.java里的URL是jdbc:mysql://localhost:3306/hotel_db但你在MySQL里创建的是hoteldb少了个下划线。解决方案在Navicat里右键连接 → “编辑连接”核对Database字段是否与代码一致。死因四MySQL服务未启动最傻但也最常见的原因。Windows下按CtrlShiftEsc打开任务管理器 → 服务 → 找到MySQL80 → 右键启动。Mac下在终端执行brew services start mysql。死因五防火墙拦截3306端口公司电脑常有安全软件禁用MySQL端口。临时解决方案在MySQL配置文件my.iniWindows或my.cnfMac/Linux里把port3306改成port3307然后在DBUtil.java里同步修改URL为jdbc:mysql://localhost:3307/hotel_db。5.3 业务逻辑陷阱那些文档里不会写的“经验之谈”除了技术报错业务逻辑的坑更隐蔽也更致命陷阱一“退房”不等于“清空房间”CheckOutServlet执行退房时只UPDATE room SET status’available’但没清空guest表里对应的记录。这会导致guest表数据无限增长。正确做法是退房后guest记录应保留作为历史凭证但增加is_checked_out字段标识状态。我在实际项目里会把guest表结构改成ALTER TABLE guest ADD COLUMN is_checked_out TINYINT(1) DEFAULT 0; UPDATE guest SET is_checked_out 1 WHERE room_id ? AND is_checked_out 0;这样既能查历史入住记录又能区分当前在住客人。陷阱二身份证号校验过于宽松项目里只做了非空校验但真实场景中18位身份证号有严格的校验码算法。我在RegisterServlet里加了一段校验public static boolean isValidIDCard(String idCard) { if (idCard null || idCard.length() ! 18) return false; String regex ^\\d{17}[\\dXx]$; if (!idCard.matches(regex)) return false; // 这里可以加更严格的校验码计算... return true; }虽然教学项目可以省略但这个意识必须建立前端校验只是体验优化后端校验才是安全底线。陷阱三时间戳时区混乱MySQL的CURRENT_TIMESTAMP默认用系统时区而Java的Timestamp可能用JVM时区。如果服务器在北京但MySQL配置了UTC时区guest表里的check_in_time就会比实际晚8小时。解决方案在DBUtil.java的URL里强制指定时区private static final String URL jdbc:mysql://localhost:3306/hotel_db?useSSLfalseserverTimezoneAsia/Shanghai;最后分享一个小技巧我在Eclipse里给每个Servlet的doPost()方法开头都加一行日志System.out.println( RegisterServlet doPost called with params: name name , idCard idCard , phone phone);这样每次操作Console里都有清晰的输入快照。当业务出错时对照日志和数据库状态问题定位速度能提升50%。这个习惯是我从第一个Java Web项目就开始坚持的至今未改。本文还有配套的精品资源点击获取简介一套开箱即用的轻量级酒店管理Demo基于Java Web标准技术栈构建全程适配Eclipse开发环境。前端提供多个登录入口普通用户可通过userlogin1.html或userlogin2.html注册并直接入住空闲房间管理员使用adminlogin.html登录后能新增、删除房间信息并为已入住客人办理退房。所有页面统一由login.html跳转引导逻辑清晰。后端采用经典Servlet结构无框架依赖便于理解请求响应流程数据库脚本init_db.sql可一键初始化推荐配合Navicat进行可视化管理。项目目录结构规范包含src/com下的业务类、WebContent中的HTML静态页、WEB-INF/web.xml配置文件及编译输出路径build/classes完整覆盖Java Web应用典型组成。适合刚接触Servlet、JSP和基础数据库交互的学习者快速掌握用户登录、房间CRUD、入住/退房状态变更等核心业务场景。本文还有配套的精品资源点击获取
Java Web酒店管理小系统:Eclipse开发,支持用户自助入住与管理员房态调控
本文还有配套的精品资源点击获取简介一套开箱即用的轻量级酒店管理Demo基于Java Web标准技术栈构建全程适配Eclipse开发环境。前端提供多个登录入口普通用户可通过userlogin1.html或userlogin2.html注册并直接入住空闲房间管理员使用adminlogin.html登录后能新增、删除房间信息并为已入住客人办理退房。所有页面统一由login.html跳转引导逻辑清晰。后端采用经典Servlet结构无框架依赖便于理解请求响应流程数据库脚本init_db.sql可一键初始化推荐配合Navicat进行可视化管理。项目目录结构规范包含src/com下的业务类、WebContent中的HTML静态页、WEB-INF/web.xml配置文件及编译输出路径build/classes完整覆盖Java Web应用典型组成。适合刚接触Servlet、JSP和基础数据库交互的学习者快速掌握用户登录、房间CRUD、入住/退房状态变更等核心业务场景。1. 项目概述为什么这个“小系统”值得你花两小时跑通一遍我带过不少刚学完Java基础、正卡在“学了Servlet但不知道怎么串起来”的学生也帮同事调试过几十个课堂级Web项目。说实话大多数Demo要么太简陋——一个login.jsp加个UserServlet就号称“完成登录”连密码明文传输都不处理要么太臃肿——硬塞进Spring Boot、MyBatis、Redis新手光配环境就得折腾一整天根本没机会看清HTTP请求从浏览器发出到数据库落库的完整链条。而这个酒店管理小系统恰恰卡在一个极难复制的黄金平衡点上它不炫技但每一步都踩在Java Web最核心的关节上它没用任何框架却把用户身份隔离、状态驱动业务入住/退房、房态实时反馈、前后端职责边界这些真实场景里的关键逻辑全揉进了十几个HTML文件和不到20个Java类里。关键词里反复出现的酒店管理、Java Web、Servlet、房态管理、Eclipse不是随便堆砌的标签——它们共同指向一个明确的学习靶心理解状态如何在Web应用中被定义、传递、校验和持久化。比如“自助入住”四个字背后是userlogin2.html提交表单 → RegisterServlet接收参数 → 检查房间是否available → 更新room表status字段为’occupied’ → 同时插入guest记录 → 最后跳转到成功页。这整个流程里没有JSON没有AJAX甚至没有JSP全靠纯Servlet的request.setAttribute RequestDispatcher.forward完成数据透传反而让初学者一眼看穿MVC里“C”到底干了什么。再比如“房态调控”管理员删房间不是简单DELETE FROM room而是先查该房间是否occupied是则拒绝操作并返回提示——这个if判断就是真实业务里“状态机”的雏形。我在Eclipse里第一次跑通这个项目时特意把Tomcat日志级别调成DEBUG盯着控制台里每一行“RegisterServlet: room 101 status changed from available to occupied”滚动出来那种对数据流动的掌控感是看十遍理论都换不来的。它适合谁如果你正在Eclipse里新建Dynamic Web Project还犹豫该选哪个Target Runtime如果你写完第一个HttpServlet却不知道web.xml里 和WebServlet注解的区别如果你能手写SQL建表但搞不清PreparedStatement里?占位符怎么和setString()对应——那这个项目就是为你量身定做的沙盒。它不要求你懂Maven依赖管理pom.xml只是备选原生WebContent结构完全可运行也不需要你配置Tomcat虚拟目录直接右键Run As → Run on Server就行。所有路径、包名、SQL脚本都严格遵循Java Web规范连.gitignore里排除build/和.settings/这种细节都帮你写好了。这不是一个要你“改代码才能跑”的半成品而是一个拧开就能出水的龙头——你拧动的每一圈对应的都是Servlet生命周期里的init()、service()、destroy()都是HTTP协议里的GET/POST都是数据库事务里的ACID。接下来我会带你一层层剥开它的结构不只告诉你“怎么做”更告诉你“为什么必须这么做”以及我在调试时踩过的那些坑——比如为什么adminlogin.html里form action写的是”/AdminLoginServlet”而不是”AdminLoginServlet”为什么init_db.sql里room表的status字段非要用VARCHAR(20)而不是ENUM这些看似琐碎的决定背后全是Java Web开发里血淋淋的经验。2. 整体架构与设计思路拆解没有框架的“裸奔”反而最见真章2.1 为什么坚持“零框架”Servlet原生才是最好的教具看到项目描述里强调“无框架依赖”可能有人会疑惑现在谁还手写ServletSpring Boot一行代码启动服务不好吗这个问题我问过自己不下十次。直到去年帮一个嵌入式团队做Java Web培训他们产线设备只能跑JDK 1.8内存限制死在64MB连Tomcat都要精简掉JSP模块。那时我才真正体会到框架是锦上添花的绸缎而Servlet API是支撑整座房子的地基钢筋。这个酒店系统刻意剥离所有框架正是为了让你看清三个不可替代的底层契约第一URL到Java类的映射契约。在web.xml里你一定会看到类似这样的配置servlet servlet-nameRegisterServlet/servlet-name servlet-classcom.servlet.RegisterServlet/servlet-class /servlet servlet-mapping servlet-nameRegisterServlet/servlet-name url-pattern/RegisterServlet/url-pattern /servlet-mapping这个配置的本质是告诉Tomcat“当用户访问http://localhost:8080/HotelSystem/RegisterServlet时请把请求交给com.servlet.RegisterServlet这个类处理”。而WebServlet(“/RegisterServlet”)注解不过是把这个XML配置搬进了Java类里。很多初学者以为注解是“新东西”其实它只是语法糖底层依然是容器读取类上的元数据来完成映射。我在调试时故意把web.xml里的 删掉保留WebServlet结果访问/login.html点击注册按钮直接404——这个错误瞬间让我记住了映射关系是容器启动时就解析好的不是运行时动态生成的。第二请求-响应对象的生命周期契约。HttpServletRequest和HttpServletResponse这两个对象不是你new出来的而是Tomcat在每次HTTP请求到达时由容器自动创建并注入到service()方法里的。这意味着你在doPost()里对request.setAttribute(“msg”, “success”)设置的属性只在本次请求的转发链路里有效一旦重定向response.sendRedirect这些属性就烟消云散。项目里userlogin2.html注册成功后跳转到welcome.jsp用的就是RequestDispatcher.forward()所以能拿到Servlet里设置的欢迎语而管理员删除房间失败后跳回admin.jsp则用response.sendRedirect()避免用户刷新页面重复提交。这种区别只有亲手写过原生Servlet才会刻骨铭心。第三数据库连接的资源契约。项目没用连接池而是每次操作都new Connection用完立刻close()。这看起来很“土”却是教学最佳方案。因为初学者最容易犯的错就是忘记close()导致数据库连接数爆满。我在Navicat里开着“当前连接数”监控面板一边运行项目一边观察数字跳动点击一次注册连接数1操作结束-1。这种直观反馈比背一百遍“Connection必须手动关闭”都管用。等你真正理解了连接的开销和泄漏风险再去学Druid或HikariCP才知道那些配置项如maxActive、minIdle究竟在解决什么问题。2.2 房态管理用最朴素的状态字段驱动整个业务流“房态管理”这个词听起来高大上但在这个系统里它就浓缩在room表的一个status字段里。打开init_db.sql你会看到CREATE TABLE room ( id INT PRIMARY KEY AUTO_INCREMENT, room_number VARCHAR(10) NOT NULL, floor INT NOT NULL, price DECIMAL(8,2) NOT NULL, status VARCHAR(20) DEFAULT available -- 关键只有available和occupied两种值 );注意这里没用ENUM类型也没用tinyint(1)存0/1而是用VARCHAR(20)存字符串。为什么因为教学场景下可读性优先于存储效率。当你在Navicat里查看room表数据时一眼就能看出id101的房间是’available’还是’occupied’不用查数据字典翻译0空闲、1已住。更重要的是这种设计让业务逻辑异常清晰所有关于“能不能入住”、“能不能退房”的判断都变成一句简单的SQL WHERE status ‘available’ 或 status ‘occupied’。这个状态字段像一根主线串起了所有核心操作-自助入住RegisterServlet查询room表WHERE status ‘available’ LIMIT 1找到第一个空闲房间然后UPDATE SET status ‘occupied’-管理员新增房间AddRoomServlet插入新记录时status默认为’available’-管理员删除房间DelRoomServlet先SELECT COUNT() FROM guest WHERE room_id ?如果大于0说明有客人住着直接拒绝删除-办理退房*CheckOutServlet UPDATE room SET status ‘available’ WHERE id ?同时DELETE FROM guest WHERE room_id ?。你会发现所有操作都围绕status字段的变更展开而没有任何一个地方需要去计算“当前有多少空房”——那个数字是实时查询出来的不是维护在某个变量里的。这就是状态驱动设计State-Driven Design的精髓不维护冗余状态只通过原子化的状态变更来保证数据一致性。我在教学生时会让他们把room表status字段改成’booked’预订中、’cleaning’打扫中等更多状态然后思考RegisterServlet的逻辑该怎么改——这个练习能让他们立刻理解状态机扩展的代价。2.3 前端入口的精心设计login.html为何是“统一入口”而非多余存在项目提到“所有页面统一由login.html跳转引导”初学者常觉得这是多此一举既然有userlogin1.html和userlogin2.html为啥不直接访问它们这里藏着一个重要的工程实践——入口路由集中化。login.html的代码极其简单!-- login.html -- !DOCTYPE html html headtitle酒店系统入口/title/head body h2请选择身份登录/h2 a hrefuserlogin1.html我是新客人快速注册/abrbr a hrefuserlogin2.html我是老客人凭身份证号登录/abrbr a hrefadminlogin.html我是管理员/a /body /html它的价值在于三点第一解耦前端页面与后端逻辑。userlogin1.html里form action写的是”/RegisterServlet”这个路径是硬编码的如果哪天Servlet类名变了你得改所有HTML。而login.html作为唯一入口所有跳转链接都在这里维护修改成本降到最低。第二提供身份认知缓冲。真实系统里用户不会一上来就面对两个注册页。login.html强制用户先思考“我是谁”再选择路径这模拟了真实产品的用户体验。第三为未来扩展留白。比如后续想加“忘记密码”功能只需在login.html里加一行不用动任何其他页面。我在实际部署时曾把login.html设为Tomcat的welcome-file-list第一个文件这样用户访问http://localhost:8080/HotelSystem/直接看到身份选择页专业感立现。而userlogin1.html和userlogin2.html之所以并存是因为它们代表两种典型用户旅程userlogin1.html是“零信息用户”只要填姓名、电话、身份证号就能分配空房userlogin2.html则是“有历史记录用户”输入身份证号后Servlet会查guest表如果存在且未退房直接显示入住信息——这种差异化的前端设计比写一堆if-else判断更清晰。3. 核心细节解析与实操要点从目录结构到每一行关键代码3.1 目录结构即规范为什么src/com/和WebContent/不能颠倒Eclipse里新建Dynamic Web Project时目录结构是固定的但很多新手会疑惑为什么Java类必须放在src/com/下HTML必须放在WebContent/里这背后是Java Web容器的资源加载约定。我们来看项目资源包里的目录树. ├── .gitignore ├── userlogin1.html # WebContent根目录下 ├── userlogin2.html # 同上 ├── adminlogin.html # 同上 ├── login.html # 同上 ├── init_db.sql # 数据库脚本通常放项目根目录 ├── pom.xml # Maven配置可选 ├── src/ │ └── com/ │ └── servlet/ # Servlet类如RegisterServlet.java │ └── dao/ # 数据访问类如RoomDAO.java │ └── bean/ # 实体类如Room.java、Guest.java ├── WebContent/ │ ├── userlogin1.html # 注意这里又有一个同名文件 │ ├── WEB-INF/ │ │ ├── web.xml # 核心配置文件 │ │ └── lib/ # 第三方jar包本项目为空 ├── build/ │ └── classes/ # Eclipse编译输出目录存放.class文件关键点来了WebContent是Web应用的“发布根目录”。当你把项目部署到Tomcat时Tomcat只会把WebContent下的内容包括其子目录当作可被HTTP访问的资源。所以userlogin1.html放在WebContent/下用户才能通过http://localhost:8080/HotelSystem/userlogin1.html访问到。而src/com/下的Java源码是给Eclipse编译用的编译后的.class文件会自动输出到build/classes/com/下再由Tomcat的ClassLoader加载。如果你把RegisterServlet.java直接拖到WebContent/里它永远不会被编译访问/RegisterServlet必然404。我在指导学生时会让他们做个小实验把userlogin1.html剪切到src/目录下然后刷新浏览器——页面直接404。再把它粘贴回WebContent/立刻恢复。这个实验比讲十分钟类路径Classpath都管用。另外WEB-INF是个特殊目录它下面的文件无法被浏览器直接访问。所以web.xml放在WEB-INF/里是安全的而如果你把数据库密码写在某个.properties文件里也必须放WEB-INF/下否则黑客访问http://localhost:8080/HotelSystem/db.properties就能直接下载。3.2 关键Servlet代码剖析RegisterServlet里的三次状态校验RegisterServlet是整个自助入住流程的核心它的doPost()方法看似简单实则暗藏三重校验。我们逐行拆解基于常见实现逻辑protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 第一次校验前端传参完整性 String name request.getParameter(name); String idCard request.getParameter(idCard); String phone request.getParameter(phone); if (name null || name.trim().isEmpty() || idCard null || idCard.trim().isEmpty() || phone null || phone.trim().isEmpty()) { request.setAttribute(msg, 请填写完整信息); request.getRequestDispatcher(userlogin1.html).forward(request, response); return; // 必须return否则继续执行 } // 2. 第二次校验身份证号唯一性防止重复注册 GuestDAO guestDAO new GuestDAO(); if (guestDAO.findByIDCard(idCard) ! null) { request.setAttribute(msg, 该身份证号已注册); request.getRequestDispatcher(userlogin1.html).forward(request, response); return; } // 3. 第三次校验房态可用性核心业务逻辑 RoomDAO roomDAO new RoomDAO(); Room availableRoom roomDAO.findAvailableRoom(); // SQL: SELECT * FROM room WHERE statusavailable LIMIT 1 if (availableRoom null) { request.setAttribute(msg, 抱歉暂无空房); request.getRequestDispatcher(userlogin1.html).forward(request, response); return; } // 执行入住先更新房间状态再插入客人记录 try { Connection conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 更新房间状态 String updateSql UPDATE room SET status ? WHERE id ?; PreparedStatement pstmt1 conn.prepareStatement(updateSql); pstmt1.setString(1, occupied); pstmt1.setInt(2, availableRoom.getId()); pstmt1.executeUpdate(); // 插入客人记录 String insertSql INSERT INTO guest (name, id_card, phone, room_id) VALUES (?, ?, ?, ?); PreparedStatement pstmt2 conn.prepareStatement(insertSql); pstmt2.setString(1, name); pstmt2.setString(2, idCard); pstmt2.setString(3, phone); pstmt2.setInt(4, availableRoom.getId()); pstmt2.executeUpdate(); conn.commit(); // 提交事务 request.setAttribute(msg, 入住成功房间号 availableRoom.getRoomNumber()); request.getRequestDispatcher(welcome.jsp).forward(request, response); } catch (SQLException e) { e.printStackTrace(); try { conn.rollback(); // 出错回滚 } catch (SQLException ex) { ex.printStackTrace(); } request.setAttribute(msg, 系统繁忙请稍后再试); request.getRequestDispatcher(userlogin1.html).forward(request, response); } }这段代码里有三个极易被忽略的细节第一所有校验失败后都必须return。很多新手写完request.getRequestDispatcher(...).forward()就以为结束了其实后面代码还会继续执行可能导致空指针异常或重复插入。第二身份证号唯一性校验放在房态校验之前。这是性能优化查guest表比查room表快得多guest表数据量小且id_card字段通常建了索引先拦住明显非法请求避免浪费数据库连接。第三事务的粒度控制。这里把UPDATE room和INSERT guest放在同一个事务里确保“房间状态变更”和“客人记录插入”要么全成功要么全失败。如果只对UPDATE加事务而INSERT单独执行就可能出现房间被标记为occupied但客人没录入的脏数据——这种问题在高并发下会指数级放大。3.3 数据库脚本init_db.sql的隐藏设计为什么room表主键用INT而非UUIDinit_db.sql不仅是建表语句更是业务逻辑的蓝图。我们重点看room表的创建部分CREATE TABLE room ( id INT PRIMARY KEY AUTO_INCREMENT, room_number VARCHAR(10) NOT NULL UNIQUE, floor INT NOT NULL, price DECIMAL(8,2) NOT NULL, status VARCHAR(20) DEFAULT available ); CREATE TABLE guest ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, id_card VARCHAR(18) NOT NULL UNIQUE, phone VARCHAR(15) NOT NULL, room_id INT NOT NULL, check_in_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (room_id) REFERENCES room(id) );这里有两个关键设计决策第一room.id用INT AUTO_INCREMENT而非UUID。初学者常觉得UUID“更高级”但在这个场景下INT有压倒性优势- 存储空间INT占4字节UUID占16字节同样10万条数据room表能节省1.2MB空间- 查询性能B树索引中INT比较是CPU直接指令UUID比较需逐字符扫描百万级数据下差距可达毫秒级- 可读性管理员在Navicat里看到room.id101立刻知道这是第101个房间UUID是一长串32位十六进制毫无业务意义。第二guest表的room_id是外键且NOT NULL。这强制保证了“每个客人必须关联一个有效房间”杜绝了孤儿记录。我在调试时曾手动在Navicat里删掉一条room记录结果guest表里对应room_id的记录还在导致CheckOutServlet执行UPDATE room SET status’available’时找不到目标——这个错误让我意识到外键约束不是可选项而是数据一致性的最后防线。所以init_db.sql里明确写了FOREIGN KEY而不是靠Java代码去校验。4. 实操过程与核心环节实现从Eclipse配置到Navicat可视化管理4.1 Eclipse环境搭建四步法避开90%的新手报错在Eclipse里跑通这个项目最关键的不是写代码而是环境配置。根据我帮上百人远程调试的经验以下四步必须严格按顺序执行跳过任何一步都会导致404或ClassNotFoundException第一步创建Dynamic Web Project并指定Target Runtime右键Project Explorer → New → Dynamic Web Project → 输入项目名如HotelSystem→ 在“Target runtime”下拉框中必须选择已安装的Tomcat版本如Apache Tomcat v9.0。如果下拉框为空说明你还没配置TomcatWindow → Preferences → Server → Runtime Environments → Add → 选择Tomcat版本 → 指向你的Tomcat安装目录如D:\apache-tomcat-9.0.83。这一步漏掉项目连基本结构都建不全。第二步导入源码到正确位置把下载的资源包解压将src/目录下的com/文件夹含servlet/、dao/、bean/整体复制到Eclipse项目里的src/目录下将WebContent/下的所有HTML文件userlogin1.html等和WEB-INF/文件夹整体复制到Eclipse项目里的WebContent/目录下。特别注意不要把整个资源包文件夹拖进Eclipse否则src和WebContent会错位。我见过太多人把userlogin1.html放在项目根目录结果Tomcat启动后访问http://localhost:8080/HotelSystem/userlogin1.html始终404——因为Tomcat只认WebContent/下的文件。第三步配置web.xml或启用WebServlet注解检查src/com/servlet/RegisterServlet.java如果类上有WebServlet(“/RegisterServlet”)则无需改web.xml如果没有则必须在WebContent/WEB-INF/web.xml里添加对应的 和 配置。两者不能共存否则Tomcat启动时报错。我在Eclipse的Console窗口里第一眼就看是否有“SEVERE: Error starting static Resources”这类错误有就一定是web.xml配置错了。第四步配置数据库连接DBUtil.java打开src/com/dao/DBUtil.java找到getConnection()方法修改数据库连接参数private static final String URL jdbc:mysql://localhost:3306/hotel_db?useSSLfalseserverTimezoneUTC; private static final String USER root; // 改成你的MySQL用户名 private static final String PASSWORD 123456; // 改成你的MySQL密码注意URL里的hotel_db是数据库名必须先在MySQL里创建好CREATE DATABASE hotel_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;。如果MySQL端口不是3306或者用了非root用户这里必须同步修改。我在Navicat里新建连接时会把Host、Port、Username、Password抄到DBUtil.java里一字不差。完成这四步后右键项目 → Run As → Run on Server选择你的Tomcat等待Console里出现“Server startup in [xxx] ms”然后浏览器访问http://localhost:8080/HotelSystem/login.html。如果看到身份选择页恭喜环境配置成功4.2 Navicat可视化管理实战三招搞定数据库初始化与调试Navicat不是摆设而是你调试数据库逻辑的“透视镜”。以下是我在实际操作中总结的三个必用技巧技巧一用init_db.sql一键初始化但必须手动执行两次双击init_db.sql → 点击左上角“运行”按钮绿色三角。Navicat会执行所有CREATE TABLE语句。但注意第一次执行后room表里是空的没有测试数据。这时你需要手动插入几条测试数据否则userlogin1.html注册时永远提示“暂无空房”。在Navicat的room表上右键 → “打开表” → 点击下方“”号新增行填入| id | room_number | floor | price | status ||----|-------------|-------|--------|------------|| 1 | 101 | 1 | 288.00 | available || 2 | 102 | 1 | 328.00 | available || 3 | 201 | 2 | 388.00 | available |这样就有3个空房可供测试。我习惯把room_number设为101、102、201因为楼层信息一目了然调试时不会混淆。技巧二实时监控guest和room表联动打开两个Navicat标签页一个查guest表一个查room表。在浏览器里完成一次自助入住userlogin1.html填信息提交然后立刻切换到Navicat点击guest表的“刷新”按钮蓝色循环箭头你会看到新插入的客人记录再切换到room表刷新发现id1的room.status变成了’occupied’。这种实时联动比看日志直观十倍。如果发现guest表有记录但room表status没变说明RegisterServlet里的UPDATE语句执行失败立刻去看Console里的SQLException堆栈。技巧三用“查询”功能模拟业务逻辑比如你想验证管理员删除房间的逻辑是否严谨可以在Navicat的“查询”标签页里手动执行-- 先查room 101是否被占用 SELECT COUNT(*) FROM guest WHERE room_id (SELECT id FROM room WHERE room_number 101); -- 如果结果是1说明有人住着此时执行删除会失败 DELETE FROM room WHERE room_number 101;执行第二条DELETE时Navicat会报错“Cannot delete or update a parent row: a foreign key constraint fails”。这个错误就是DelRoomServlet里“先查后删”逻辑的源头——它逼着你必须在Java代码里先SELECT COUNT(*)再决定是否DELETE。这种用数据库工具反推代码逻辑的方法能让你深刻理解外键约束的价值。5. 常见问题与排查技巧实录那些让我熬夜到凌晨的坑5.1 经典404问题排查速查表404是Java Web新手的头号敌人但90%的404都能通过这张表快速定位现象可能原因排查步骤解决方案访问/login.html显示404login.html不在WebContent/根目录在Eclipse里展开项目确认login.html直接在WebContent/下而非WebContent/WEB-INF/或src/里剪切login.html到WebContent/根目录点击userlogin1.html的“注册”按钮后404form action路径错误查看userlogin1.html源码确认中的路径是否与web.xml里 的 完全一致注意开头的/如果web.xml里是 /RegisterServlet 则action必须是”/RegisterServlet”不能是”RegisterServlet”或”./RegisterServlet”访问/RegisterServlet显示404但web.xml配置正确Servlet类未编译或classpath错误在Eclipse的Project Explorer里展开build/classes/com/servlet/确认RegisterServlet.class是否存在右键项目 → Refresh然后右键项目 → Build Project确保编译成功所有Servlet都404但HTML页面正常Tomcat未正确关联项目查看Eclipse底部Servers视图双击你的Tomcat服务器 → 在“Modules”选项卡里确认HotelSystem项目已勾选且Path为”/HotelSystem”如果未勾选点击“Add External Web Module” → 选择你的项目 → Path填”/HotelSystem”我在带学生时会让大家遇到404先别急着改代码而是打开Eclipse的“Servers”视图双击Tomcat在弹出的配置窗口里点开“Modules”这里能看到所有已部署模块的路径映射。绝大多数404问题根源都在这里没配对。5.2 数据库连接失败的五大死因java.sql.SQLException: Access denied for user rootlocalhost这类错误表面是密码错实则有更深的坑死因一MySQL 8.0默认认证插件变更MySQL 8.0开始默认认证插件从mysql_native_password改为caching_sha2_password而老版JDBC驱动不支持。解决方案在MySQL命令行里执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;死因二JDBC驱动版本不匹配项目用的是mysql-connector-java-5.1.47.jar常见于老教程但你的MySQL是8.x。必须升级驱动下载mysql-connector-java-8.0.33.jar放到WebContent/WEB-INF/lib/下并在Eclipse里右键项目 → Properties → Java Build Path → Libraries → Add JARs → 选择新驱动。死因三数据库名拼写错误DBUtil.java里的URL是jdbc:mysql://localhost:3306/hotel_db但你在MySQL里创建的是hoteldb少了个下划线。解决方案在Navicat里右键连接 → “编辑连接”核对Database字段是否与代码一致。死因四MySQL服务未启动最傻但也最常见的原因。Windows下按CtrlShiftEsc打开任务管理器 → 服务 → 找到MySQL80 → 右键启动。Mac下在终端执行brew services start mysql。死因五防火墙拦截3306端口公司电脑常有安全软件禁用MySQL端口。临时解决方案在MySQL配置文件my.iniWindows或my.cnfMac/Linux里把port3306改成port3307然后在DBUtil.java里同步修改URL为jdbc:mysql://localhost:3307/hotel_db。5.3 业务逻辑陷阱那些文档里不会写的“经验之谈”除了技术报错业务逻辑的坑更隐蔽也更致命陷阱一“退房”不等于“清空房间”CheckOutServlet执行退房时只UPDATE room SET status’available’但没清空guest表里对应的记录。这会导致guest表数据无限增长。正确做法是退房后guest记录应保留作为历史凭证但增加is_checked_out字段标识状态。我在实际项目里会把guest表结构改成ALTER TABLE guest ADD COLUMN is_checked_out TINYINT(1) DEFAULT 0; UPDATE guest SET is_checked_out 1 WHERE room_id ? AND is_checked_out 0;这样既能查历史入住记录又能区分当前在住客人。陷阱二身份证号校验过于宽松项目里只做了非空校验但真实场景中18位身份证号有严格的校验码算法。我在RegisterServlet里加了一段校验public static boolean isValidIDCard(String idCard) { if (idCard null || idCard.length() ! 18) return false; String regex ^\\d{17}[\\dXx]$; if (!idCard.matches(regex)) return false; // 这里可以加更严格的校验码计算... return true; }虽然教学项目可以省略但这个意识必须建立前端校验只是体验优化后端校验才是安全底线。陷阱三时间戳时区混乱MySQL的CURRENT_TIMESTAMP默认用系统时区而Java的Timestamp可能用JVM时区。如果服务器在北京但MySQL配置了UTC时区guest表里的check_in_time就会比实际晚8小时。解决方案在DBUtil.java的URL里强制指定时区private static final String URL jdbc:mysql://localhost:3306/hotel_db?useSSLfalseserverTimezoneAsia/Shanghai;最后分享一个小技巧我在Eclipse里给每个Servlet的doPost()方法开头都加一行日志System.out.println( RegisterServlet doPost called with params: name name , idCard idCard , phone phone);这样每次操作Console里都有清晰的输入快照。当业务出错时对照日志和数据库状态问题定位速度能提升50%。这个习惯是我从第一个Java Web项目就开始坚持的至今未改。本文还有配套的精品资源点击获取简介一套开箱即用的轻量级酒店管理Demo基于Java Web标准技术栈构建全程适配Eclipse开发环境。前端提供多个登录入口普通用户可通过userlogin1.html或userlogin2.html注册并直接入住空闲房间管理员使用adminlogin.html登录后能新增、删除房间信息并为已入住客人办理退房。所有页面统一由login.html跳转引导逻辑清晰。后端采用经典Servlet结构无框架依赖便于理解请求响应流程数据库脚本init_db.sql可一键初始化推荐配合Navicat进行可视化管理。项目目录结构规范包含src/com下的业务类、WebContent中的HTML静态页、WEB-INF/web.xml配置文件及编译输出路径build/classes完整覆盖Java Web应用典型组成。适合刚接触Servlet、JSP和基础数据库交互的学习者快速掌握用户登录、房间CRUD、入住/退房状态变更等核心业务场景。本文还有配套的精品资源点击获取