本文还有配套的精品资源,点击获取
简介:一套开箱即用的轻量级酒店管理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原生才是最好的教具
看到项目描述里强调“无框架依赖”,可能有人会疑惑:现在谁还手写Servlet?Spring Boot一行代码启动服务不好吗?这个问题我问过自己不下十次。直到去年帮一个嵌入式团队做Java Web培训,他们产线设备只能跑JDK 1.8,内存限制死在64MB,连Tomcat都要精简掉JSP模块。那时我才真正体会到:框架是锦上添花的绸缎,而Servlet API是支撑整座房子的地基钢筋。这个酒店系统刻意剥离所有框架,正是为了让你看清三个不可替代的底层契约:
第一,URL到Java类的映射契约。在web.xml里,你一定会看到类似这样的配置:
<servlet> <servlet-name>RegisterServlet</servlet-name> <servlet-class>com.servlet.RegisterServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>RegisterServlet</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表数据时,一眼就能看出id=101的房间是’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> <head><title>酒店系统入口</title></head> <body> <h2>请选择身份登录</h2> <a href="userlogin1.html">我是新客人(快速注册)</a><br><br> <a href="userlogin2.html">我是老客人(凭身份证号登录)</a><br><br> <a href="adminlogin.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 status='available' 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而非UUID
init_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.id=101,立刻知道这是第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)。如果下拉框为空,说明你还没配置Tomcat:Window → 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?useSSL=false&serverTimezone=UTC"; 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表,刷新,发现id=1的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显示404 | login.html不在WebContent/根目录 | 在Eclipse里展开项目,确认login.html直接在WebContent/下,而非WebContent/WEB-INF/或src/里 | 剪切login.html到WebContent/根目录 |
| 点击userlogin1.html的“注册”按钮后404 | form action路径错误 | 查看userlogin1.html源码,确认 | 如果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 'root'@'localhost'这类错误,表面是密码错,实则有更深的坑:
死因一:MySQL 8.0+默认认证插件变更
MySQL 8.0开始,默认认证插件从mysql_native_password改为caching_sha2_password,而老版JDBC驱动不支持。解决方案:在MySQL命令行里执行:
ALTER USER 'root'@'localhost' 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下按Ctrl+Shift+Esc打开任务管理器 → 服务 → 找到MySQL80 → 右键启动。Mac下在终端执行brew services start mysql。
死因五:防火墙拦截3306端口
公司电脑常有安全软件禁用MySQL端口。临时解决方案:在MySQL配置文件my.ini(Windows)或my.cnf(Mac/Linux)里,把port=3306改成port=3307,然后在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?useSSL=false&serverTimezone=Asia/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、入住/退房状态变更等核心业务场景。
本文还有配套的精品资源,点击获取