简介:基于 MySQL、IDEA、Tomcat、JSP 与 Servlet 开发的宾馆管理系统,面向 JavaWeb 期末作业与课程设计场景,覆盖客房管理、预订入住、账单结算等模块,功能完整,代码附注释,适合有 Java 基础的学生直接部署和二次扩展。资源包共 1370 个文件、约 22.34MB,包含 JSP 页面、Java/Servlet 类、CSS/JS 前端脚本、SQL 数据库脚本、XML 配置及 MD 说明文档,目录按前后端与文档分层,便于定位修改。目前已有 375 人学习下载,常被用于期末大作业或课程设计参考。配套文档说明部署与使用要点,新手简单配置即可运行;项目清晰展现 JSP 与 Servlet 的协作流程,可继续扩展订房、退房、统计等功能。
1. 为什么期末作业选它:一套能讲清楚「请求从哪来到哪去」的宾馆管理系统
期末前两周,老师丢下一句「基于JavaWeb做一套管理系统,前后端交互必须完整」,不少同学的第一反应是打开百度找现成源码。搜到「javaweb期末作业基于MySQL+IDEA+Tomcat+JSP+Servlet宾馆管理系统源码+文档说明」这类标题时,先别急着下载——这套技术栈恰好覆盖了JavaWeb课程里最核心的一条链路:浏览器发请求,Tomcat接住,Servlet处理逻辑,JSP渲染页面,MySQL存数据。它不是什么炫酷的新框架,但它能让你在答辩时把每个环节讲得明明白白。这篇笔记就是围绕这个标题,把「拿到源码之后怎么做、做完怎么讲、跑不通怎么查」一次讲透。适合两类人:一是正在赶课程设计、需要独立完成并讲清原理的同学;二是工作后想用最朴素的Servlet+JSP快速搭内部小工具的开发者。别嫌它老,这套组合的调试过程本身就是最好的复习资料。
2. 先把架构和数据表定下来:从E-R图到MySQL建库脚本
2.1 为什么这套组合是期末现场的「安全牌」
常见做法是直接选Servlet+JSP而不是SpringBoot。原因很现实:SpringBoot帮你把Tomcat内嵌了、把依赖管理了、把配置自动了,答辩时老师一问「你的请求是怎么被处理的」,你很难从框架的封装里翻出底层链路。而Servlet+JSP这套组合,每一个环节都暴露在你面前——web.xml里配了什么映射,Filter拦了哪些路径,Session存了什么键,JDBC连接是谁打开的。这些恰恰是期末考核最容易追问的点。
再说技术选型:MySQL负责持久化,IDEA负责写代码和调试,Tomcat负责跑Servlet和JSP,JSP负责展示,Servlet负责控制。这五个词串起来正好是一条完整的请求处理链。有人会问:那Nginx呢?Nginx本身不解析JSP,它通常只做静态资源转发或反向代理,真正执行JSP/Servlet的还是背后的Tomcat。课程设计阶段直接把Tomcat当作Web服务器就够了,不用把Nginx拉进来增加变量。
2.2 宾馆管理系统的模块划分:别一上来就追求「大而全」
拿到项目标题后,第一件事不是写代码,而是把业务边界划清楚。一个能自圆其说的宾馆管理系统,最少需要四件事:管理员能登录;能维护客房信息;能办理预订和入住;能退房结算。在此基础上可以加客户管理、经营统计,但别贪多——期末作业的核心是可运行、可演示、可讲清楚,不是功能数量。
我一般会按这样的模块拆:管理员管理(登录、退出、密码修改)、客房管理(房间类型、房号、状态)、客户管理(登记姓名、证件号、联系方式)、预订管理(预订时间、入住时间、退房时间、状态)、入住退房(实际入住登记、退房结算)。对应到数据库里,就是五张核心表。模块画好了,数据库表结构自然就出来了。
2.3 建库建表脚本:五张表的核心字段与设计意图
这是整套系统的基础,表字段设计得合理,后面写Servlet和JSP会省一半事。下面是适合课程设计体量的建表脚本:
CREATE DATABASE IF NOT EXISTS hotel_db DEFAULT CHARACTER SET utf8mb4; USE hotel_db; -- 管理员表 CREATE TABLE t_admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, real_name VARCHAR(30), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 客房类型表 CREATE TABLE t_room_type ( id INT PRIMARY KEY AUTO_INCREMENT, type_name VARCHAR(20) NOT NULL, price DECIMAL(10,2) NOT NULL, -- 每晚价格 bed_count INT NOT NULL DEFAULT 1, -- 床位数量 remark VARCHAR(200) ); -- 客房表 CREATE TABLE t_room ( id INT PRIMARY KEY AUTO_INCREMENT, room_no VARCHAR(10) NOT NULL UNIQUE, -- 房间号,如 301 type_id INT NOT NULL, status TINYINT NOT NULL DEFAULT 0, -- 0空闲 1已预订 2已入住 3维修 floor_num TINYINT DEFAULT 1, FOREIGN KEY (type_id) REFERENCES t_room_type(id) ); -- 客户表 CREATE TABLE t_customer ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(30) NOT NULL, id_card VARCHAR(18) NOT NULL, -- 证件号码 phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 订单表(一个订单对应一次预订或入住) CREATE TABLE t_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, -- 订单编号 customer_id INT NOT NULL, room_id INT NOT NULL, order_type TINYINT NOT NULL DEFAULT 1, -- 1预订 2入住 status TINYINT NOT NULL DEFAULT 0, -- 0进行中 1已完成 2已取消 check_in_date DATE, check_out_date DATE, total_amount DECIMAL(10,2) DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (customer_id) REFERENCES t_customer(id), FOREIGN KEY (room_id) REFERENCES t_room(id) );这里有几个设计意图需要说明。第一,房间状态用 TINYINT 数字而不是字符串——「0空闲 1已预订 2已入住 3维修」这一套约定写在代码注释里,比在数据库里存中文更规范,也更容易做条件查询。第二,订单表里同时承载预订和入住两种类型,通过 order_type 区分,这样退房结算时可以直接复用这条记录,不需要额外的入住表。第三,价格放在房间类型表而不是房间表里,因为同类型的房间价格相同,改价格只改一处。
2.4 JDBC连接参数:为什么你的连接总是报错
表建好了,接下来要接数据库。课程设计里最常见的翻车点就是JDBC连接串写错。下面是一个标准的JDBC工具类模板:
package com.hotel.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/hotel_db" + "?useSSL=false&serverTimezone=Asia/Shanghai" + "&characterEncoding=utf8&allowPublicKeyRetrieval=true"; 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 RuntimeException("MySQL驱动加载失败", e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }连接串里这几个参数是踩过坑才加上的。useSSL=false 是为了避免MySQL 8.x默认开启SSL后控制台刷出一大串警告;serverTimezone=Asia/Shanghai 解决日期时间差八小时的经典问题;characterEncoding=utf8 保证中文写入不乱码;allowPublicKeyRetrieval=true 是因为MySQL 8.x使用 caching_sha2_password 认证时,首次连接需要通过公钥交换,不加这个参数会报 Public Key Retrieval is not allowed。驱动类名用 com.mysql.cj.jdbc.Driver 对应MySQL 5.1.8之后的版本,旧版的 com.mysql.jdbc.Driver 在MySQL 8.x下也能用但会有提示。连接参数落实到项目里,就是把这段代码放进 com.hotel.util 包下,之后所有Servlet通过 DBUtil.getConnection() 拿连接,用完记得在 finally 里关掉。
3. 让项目在IDEA里跑起来:Tomcat配置、依赖与最小启动
3.1 拿到源码包后先看清目录结构,别急着点运行
网上下载的「源码+文档说明」压缩包,解压后通常包含这么几块:源码目录(src/main/java、web或src/main/webapp)、数据库脚本(.sql文件)、文档说明(需求文档、设计文档、运行说明)。先把目录结构摸清楚,对照标题里的五个关键词逐个确认:MySQL部分有没有给你建表脚本;IDEA部分有没有附带 .idea 配置;Tomcat部分有没有说明版本要求;JSP/Servlet部分代码是否完整。没有 .idea 目录很正常,到你自己电脑上重新配置就行。
一个标准Maven结构的JavaWeb项目,关键目录是这样组织的:
hotel-system ├── pom.xml // Maven依赖声明 ├── src │ ├── main │ │ ├── java │ │ │ └── com.hotel │ │ │ ├── servlet // 所有Servlet类 │ │ │ ├── util // DBUtil等工具类 │ │ │ └── model // 实体类 │ │ └── webapp │ │ ├── index.jsp // 登录页/首页 │ │ ├── WEB-INF │ │ │ ├── web.xml // Servlet映射配置 │ │ │ └── lib // 依赖jar包(非Maven项目) │ │ ├── css │ │ └── js └── sql └── hotel_db.sql // 建库建表脚本不是Maven项目的话,src目录会叫 src/com/hotel 这种结构,依赖的jar包放在 WEB-INF/lib 下,MySQL驱动jar是必须检查的一项。打开IDEA后选择 File -> Open,选中项目根目录,如果是Maven项目IDEA会自动识别 pom.xml 并下载依赖,这个过程需要联网,耐心等右下角进度条结束。
3.2 IDEA里配置Tomcat:核心是把Artifact部署对
这是整个IDEA运行JavaWeb项目配置里最容易出问题的环节。步骤按下面顺序做不容易漏:
第一步,安装Tomcat。到Tomcat官网下载对应版本,Windows选zip包,解压到纯英文路径,比如 D:\tomcat。注意不要带空格和中文。配置环境变量 CATALINA_HOME 指向解压目录。
第二步,IDEA里配置Tomcat Server。打开 Run -> Edit Configurations,点左上角 +,选 Tomcat Server -> Local。在 Application server 区域点 Configure,选中Tomcat解压目录,IDEA会自动识别版本号。此时先别急着点OK,还要配置 Deployment 标签页。
第三步,配置Deployment。这是新手最容易忽略的步骤。点 Deployment 页签,点 +,选 Artifact,弹窗里会列出项目打包后的名字,一般是 hotel-system:war exploded,选它。下方的 Application context 就是访问路径,改成 /hotel 或者干脆 /,取决于你题目里要求的访问路径。这一步没配好,启动Tomcat时会提示 Error during artifact deployment,或者页面404。
第四步,回到 Server 页签,把 On frame deactivation 设为 Update classes and resources,这样你改完JSP或静态资源后,切换到浏览器就能生效,不用反复重启Tomcat。这个设置在调试阶段能省大量时间。
配置完成后,项目里需要有一个 Lombok 或其他依赖的话,检查 pom.xml 里是否声明。下面是一个最小Maven依赖清单:
<dependencies> <!-- Servlet API --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <!-- JSP API --> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>javax.servlet.jsp-api</artifactId> <version>2.3.3</version> <scope>provided</scope> </dependency> <!-- JSTL 标签库 --> <dependency> <groupId>jstl</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.30</version> </dependency> </dependencies>Servlet和JSP的scope设为provided是因为Tomcat里自带这两个API,不打进war包;JSTL和MySQL驱动需要随项目打包。版本号要在 pom.xml 的 properties 里统一管理。Tomcat和JDK的搭配有个经验值:JDK 8 搭配 Tomcat 8.5 或 9.x 最稳妥;JDK 11 以上就选 Tomcat 9.x。如果你在linux上部署,记得给Tomcat的 bin 目录下的 startup.sh 加执行权限,想开机自启就在 /etc/rc.d/rc.local 里追加一行启动命令——这些都是后话,期末阶段先在IDEA里跑通最重要。
3.3 从启动到看到页面:最小化验证清单
配置好之后点绿色三角启动Tomcat。Tomcat窗口下方会滚动日志,看到一行类似Deployment of web application directory [...] has finished的日志,说明部署成功。然后再手动做三轮验证,每轮都要有稳定预期:
第一轮,确认Tomcat本身活着。浏览器访问http://localhost:8080/,看到Tomcat默认首页说明Servlet容器正常。如果这一步就失败,问题出在Tomcat安装或端口配置,和项目无关。端口被占用就改 conf/server.xml 里的<Connector port="8080",改成8081或8082都行,改完重启Tomcat。
第二轮,确认项目上下文能访问。访问http://localhost:8080/项目名/,正常会跳转到登录页 index.jsp。如果看到404,先检查IDEA里Deployment的 Application context 和实际路径是否一致,再检查 web.xml 中 welcome-file 是否配置了 index.jsp。
第三轮,确认数据库链路通。在登录页输入用户名密码点登录——前提是你已经把第二章的建表脚本在MySQL里执行过,并在 DBUtil 里改成了自己的数据库账号密码。如果登录页面跳转但提示数据库错误,回去看JDBC连接串和驱动jar。
# 或者用命令行快速验证MySQL是否正常 mysql -uroot -p123456 -e "use hotel_db; select count(*) from t_admin;"这条命令能直接确认账号密码、数据库名、表是否都在。很多时候Tomcat和IDEA配置没问题,就是卡在MySQL这一步。
4. 把核心流程写出来:Servlet+JSP的登录、房间与预订实现
4.1 登录逻辑:Session的存取时机决定你答辩时怎么讲
登录功能是整个系统里最经典的Servlet+JSP交互示例。前端表单提交用户名密码,Servlet接收参数、查数据库、校验、写Session、跳转页面。下面是一个完整的LoginServlet写法:
package com.hotel.servlet; import com.hotel.util.DBUtil; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; import java.sql.*; @WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("utf-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); // 用PreparedStatement防止SQL注入,不是拼接字符串 String sql = "SELECT id, real_name FROM t_admin WHERE username=? AND password=?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { HttpSession session = request.getSession(); session.setAttribute("userId", rs.getInt("id")); session.setAttribute("realName", rs.getString("real_name")); response.sendRedirect("index.jsp"); // 重定向防止表单重复提交 } else { request.setAttribute("errorMsg", "用户名或密码错误"); request.getRequestDispatcher("login.jsp").forward(request, response); } } } catch (SQLException e) { e.printStackTrace(); request.setAttribute("errorMsg", "系统异常,请联系管理员"); request.getRequestDispatcher("login.jsp").forward(request, response); } } }这段代码里有三个面试官爱问的点。第一,为什么用 try-with-resources?因为Connection、PreparedStatement、ResultSet都实现了AutoCloseable,try块结束后自动关闭,不用手动写finally里那一堆null判空。第二,为什么登录成功用 sendRedirect 而不是 forward?因为转发地址栏不变,刷新页面会再次提交表单造成重复登录;重定向是两次请求,刷新只刷新目标页面。第三,为什么密码校验在SQL里做而不是先查出记录再在Java里比?因为让数据库做这件事更高效,但前提是SQL里不能拼接字符串——永远用问号占位符,这是血泪经验。
4.2 客房列表与条件查询:JSP里怎么展示数据最规范
登录后进主页,通常要按状态筛选房间。Servlet从数据库查出房间列表,放进request域,转发给JSP,JSP用JSTL标签遍历。看下面这条链路:
// RoomListServlet.java 的关键逻辑 @WebServlet("/room/list") public class RoomListServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String status = request.getParameter("status"); // 前端下拉框传来的筛选条件 // 动态拼接SQL时,只在无用户输入干扰的地方使用字符串拼接 String sql = "SELECT r.id, r.room_no, r.status, t.type_name, t.price " + "FROM t_room r JOIN t_room_type t ON r.type_id=t.id "; if (status != null && !status.isEmpty()) { sql += "WHERE r.status=? "; } sql += "ORDER BY r.floor_num, r.room_no"; List<Room> roomList = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { if (status != null && !status.isEmpty()) { ps.setInt(1, Integer.parseInt(status)); } try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Room room = new Room(); room.setId(rs.getInt("id")); room.setRoomNo(rs.getString("room_no")); room.setStatus(rs.getInt("status")); room.setTypeName(rs.getString("type_name")); room.setPrice(rs.getBigDecimal("price")); roomList.add(room); } } } catch (SQLException e) { e.printStackTrace(); } request.setAttribute("roomList", roomList); request.getRequestDispatcher("/room_list.jsp").forward(request, response); } }<%-- room_list.jsp 中遍历展示的片段 --%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <table border="1" cellpadding="8"> <tr> <th>房间号</th><th>类型</th><th>价格</th><th>状态</th><th>操作</th> </tr> <c:forEach items="${roomList}" var="room"> <tr> <td>${room.roomNo}</td> <td>${room.typeName}</td> <td>${room.price} 元/晚</td> <td> <c:choose> <c:when test="${room.status == 0}">空闲</c:when> <c:when test="${room.status == 1}">已预订</c:when> <c:when test="${room.status == 2}">已入住</c:when> <c:otherwise>维修</c:otherwise> </c:choose> </td> <td> <a href="room/book?roomId=${room.id}">预订</a> <a href="room/checkin?roomId=${room.id}">入住</a> </td> </tr> </c:forEach> </table>这套写法的好处是:Servlet里看不到HTML,JSP里看不到Java的脚本片段(没有那些<% %>到处飞的写法),数据和展示彻底分离。JSP页面里的EL表达式${room.roomNo}对应Room实体类的getRoomNo()方法,JSTL标签帮你管好循环和分支。如果期末答辩老师要求你现场给页面加一列「入住天数」,你只需要在Room实体类加一个 transient 字段或者直接在SQL里用 DATEDIFF 算出天数,再在JSP里加一个<td>标签即可。
4.3 预订房间的事务处理:两张表的状态联动别做丢
预订动作涉及两张表的更新:t_room 表把房间status从0改成1,t_order 表插入一条预订记录。这两个操作要么同时成功,要么同时失败,否则会出现在订单里查得到预订但房间状态没变的情况。用Connection的事务控制:
@WebServlet("/room/book") public class BookRoomServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("utf-8"); int roomId = Integer.parseInt(request.getParameter("roomId")); String customerName = request.getParameter("customerName"); String idCard = request.getParameter("idCard"); String phone = request.getParameter("phone"); Date checkInDate = Date.valueOf(request.getParameter("checkInDate")); Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交,开启事务 // 第一步:写入客户信息(这里简化为直接插入,也可先按证件号查重) String sqlCustomer = "INSERT INTO t_customer(name, id_card, phone) VALUES(?,?,?)"; PreparedStatement psCustomer = conn.prepareStatement(sqlCustomer, Statement.RETURN_GENERATED_KEYS); psCustomer.setString(1, customerName); psCustomer.setString(2, idCard); psCustomer.setString(3, phone); psCustomer.executeUpdate(); ResultSet keys = psCustomer.getGeneratedKeys(); int customerId = keys.next()? keys.getInt(1) : 0; // 第二步:生成订单号并插入订单 String orderNo = "BK" + System.currentTimeMillis(); String sqlOrder = "INSERT INTO t_order(order_no, customer_id, room_id, " + "order_type, status, check_in_date) VALUES(?,?,?,1,0,?)"; PreparedStatement psOrder = conn.prepareStatement(sqlOrder); psOrder.setString(1, orderNo); psOrder.setInt(2, customerId); psOrder.setInt(3, roomId); psOrder.setDate(4, checkInDate); psOrder.executeUpdate(); // 第三步:更新房间状态为已预订 String sqlRoom = "UPDATE t_room SET status=1 WHERE id=? AND status=0"; PreparedStatement psRoom = conn.prepareStatement(sqlRoom); psRoom.setInt(1, roomId); int rows = psRoom.executeUpdate(); if (rows == 0) { throw new SQLException("房间状态已变化,请刷新后重试"); } conn.commit(); // 三张表操作全部成功才提交 response.sendRedirect("room/list?status=1"); } catch (Exception e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); request.setAttribute("errorMsg", "预订失败:" + e.getMessage()); request.getRequestDispatcher("/book_room.jsp").forward(request, response); } finally { if (conn != null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } } }这段代码的示范价值在于事务边界的把握。conn.setAutoCommit(false)之后,后续所有SQL都在同一个事务里,任意一步抛异常就回滚,不会出现脏数据。UPDATE语句里特意加了AND status=0,这是乐观锁的思路——如果两个管理员同时对同一个房间操作,后执行的那个人受影响行数为0,会走进异常分支提示刷新重试,而不是静默覆盖。答辩时把这个细节讲出来,老师会知道你理解并发问题。
4.4 编码与登录拦截:用Filter解决两个隐蔽问题
中文乱码和未登录访问后台是JavaWeb作业里被问烂的两个点。用Filter一次性解决:
package com.hotel.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; @WebFilter(urlPatterns = "/*") public class EncodingFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; request.setCharacterEncoding("utf-8"); // 处理POST请求体中的中文 response.setCharacterEncoding("utf-8"); // 处理响应中的中文 chain.doFilter(request, response); } } @WebFilter(urlPatterns = {"/room/*", "/order/*"}) public class LoginFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; HttpSession session = request.getSession(false); if (session == null || session.getAttribute("userId") == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(request, response); } }编码Filter配/*所有请求都过;登录拦截Filter只拦需要登录后才能访问的URL前缀。这里有个细节:request.getSession(false)加了false参数,表示取不到Session就返回null而不是新建——如果在这里用了getSession(),未登录用户访问后台页面时也会被创建一个空Session,拦截逻辑就失效了。另外,GET请求的参数编码问题在Tomcat 8.5之后已经默认处理了UTF-8,你主要关注POST即可。
5. 跑不通的常见坑:Tomcat、MySQL与编码排查记录
5.1 Tomcat启动闪退:窗口一闪就没了,日志在哪儿看
现象:双击IDEA里的运行按钮,控制台没有输出,或者Tomcat启动后立刻退出,页面完全访问不了。
原因:Tomcat启动依赖JAVA_HOME环境变量,IDEA里虽然配置了JDK,但Tomcat本身是通过启动脚本找JAVA_HOME的。环境变量没配或者指向了不存在路径,启动脚本直接退出。另一个常见原因是8080端口被占用,Oray、微信开发者工具、其他Java进程都可能占住这个端口。
解决:Windows右键此电脑 -> 属性 -> 高级系统设置 -> 环境变量,新建 JAVA_HOME 指向JDK安装目录(比如 D:\Java\jdk1.8.0_202),在Path里加%JAVA_HOME%\bin。改完重启IDEA再试。端口冲突就打开 Tomcat 的 conf/server.xml,改<Connector port="8080"为其他端口。排查日志别只看控制台——到 Tomcat 的 logs 目录看 catalina.out 或 localhost.yyyy-MM-dd.log,里面有完整堆栈。
5.2 数据库连接失败:Can't connect 与 Access denied 要分开看
现象:Tomcat正常启动,页面也能打开,登录点完按钮报错——要么是Communications link failure或者Can't connect to MySQL server through socket '/tmp/mysql.sock',要么是Access denied for user 'root'@'localhost'。
原因:前者是MySQL服务没启动,或者连接串里 host/port 错了;后者是账号密码错误或该账号没有远程访问权限。注意那个/tmp/mysql.sock的报错常见于Linux环境,是MySQL客户端尝试通过Unix套接字连接路径不对,通常要改成-h 127.0.0.1强制走TCP。
解决:Windows下先在服务管理器确认MySQL服务是「正在运行」状态;Linux下执行systemctl status mysqld或systemctl status mysql查看。MySQL没启动就启动它,并确认配置了开机自启。账号问题就在MySQL命令行里执行ALTER USER 'root'@'localhost' IDENTIFIED BY '你的密码';。如果是MySQL 8.x,还要注意密码加密插件——DBUtil连接串里的 allowPublicKeyRetrieval=true 就是为它准备的。最后再确认代码里的URL和账号密码跟本机实际一致,别把网上抄来的localhost用户名都照搬。
5.3 中文全变问号:三个编码位置必须同时一致
现象:页面上显示的中文正常,但往数据库里插入的中文存进去全是??,或者反过来页面显示乱码。
原因:编码链路断在某一环。浏览器用UTF-8提交表单,Servlet里没有 setCharacterEncoding,JSP页面本身是GBK,数据库表又是latin1——任意一环不一致,中文必乱。
解决:统一成UTF-8。第一,MySQL建库时指定DEFAULT CHARACTER SET utf8mb4,已经建好的库用ALTER DATABASE hotel_db CHARACTER SET utf8mb4;;第二,每个JSP页面顶部写<%@ page contentType="text/html;charset=UTF-8" language="java" %>;第三,在Filter里对所有请求和响应设置UTF-8;第四,连接串加characterEncoding=utf8。四个位置都检查一遍之后乱码还没解决,大概率是MySQL连接的字符集被SQL语句覆盖了,在连接串末尾加useUnicode=true把参数再强调一次。
5.4 404与「源服务器未能找到目标资源」:Deployment和web.xml双重排查
现象:浏览器访问http://localhost:8080/hotel/login报404,Tomcat日志里没有明显异常。
原因:这个报错消息是Tomcat返回的标准404响应文案,本质是请求的URL没有匹配到任何Servlet。两种常见情况:IDEA的Deployment没把项目部署到正确上下文;或者Servlet的注解映射路径和前端表单的action路径不一致。
解决:先在IDEA的 Run 配置里看 Deployment 标签,确认 Artifact 已添加、Application context 写的是/hotel。再检查前端JSP里的提交地址——如果表单写在 login.jsp 里,<form action="login" method="post">,而LoginServlet注解是@WebServlet("/login"),那么必须保证action和注解路径完全一致。基于注解的Servlet不需要在web.xml里重复声明,混用两种方式容易出问题。项目根路径用<%=request.getContextPath()%>拼接,避免上下文变化后链接全失效。
5.5 ClassNotFoundException:驱动jar没进Artifact
现象:Tomcat启动时日志报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver。
原因:若你的项目不是Maven结构,MySQL驱动jar必须手动放到 WEB-INF/lib 目录下,IDEA不一定帮你自动把外部jar加入部署包。若是Maven项目,则检查依赖是否成功下载,有时IDEA的本地仓库里没有这个依赖版本。
解决:非Maven项目就把 mysql-connector-java-8.0.30.jar 复制到src/main/webapp/WEB-INF/lib/(非Maven是web/WEB-INF/lib/),然后在IDEA里右键jar -> Add as Library。Maven项目先执行mvn clean再重新加载,或者手动从本地仓库删掉对应版本目录让IDEA重新下载。检查一下Artifact的Output Layout里是否包含了lib下的jar。
6. 验收前的自测清单:把项目从「能跑」打磨到「能讲」
临近提交,我给你一份自测路径,照着走完一遍,答辩时基本不会卡壳。核心思路是:不要只测「能点通」,而是顺着一条完整业务链路把它走穿。
先理一条主流程:登录 → 查看空闲房间 → 预订一间房 → 在订单列表看到该订单且房间状态变为已预订 → 执行入住 → 退房结算。每一步都要确认数据库对应表的数据变化符合预期。我给你一张速查表:
| 自测项 | 操作路径 | 预期结果 |
|---|---|---|
| 登录校验 | 输入错误密码 | 提示错误信息,不跳转 |
| 登录成功 | 输入正确账号 | 跳转首页,Session生效 |
| 房间状态 | 列表页筛选空闲房间 | 只显示status=0的房间 |
| 预订联动 | 预订一间空闲房 | t_room.status变1,t_order新增记录 |
| 重复预订 | 对同一房间再点预订 | 提示失败且不产生脏数据 |
| 退房结算 | 对入住订单执行退房 | 计算金额正确,房间状态回0 |
| 未登录访问后台 | 直接访问 /room/list | 自动跳回登录页 |
第二件事,把「讲」准备好。老师让你演示时,不要只点按钮,边点边说:这个请求从前端表单发出,Tomcat接收到之后根据 web.xml 或注解找到了对应的Servlet,Servlet里调用了DBUtil拿连接执行SQL,数据拿到之后放在request域里,转发给JSP渲染成表格——这条链路说清楚,比你演示十个功能都管用。我在帮人看期末项目时最怕的不是功能少,而是项目明明跑得通,学生却说不出一句请求是怎么流转的。
第三件可以做的加分之举:给房间列表的价格字段后面标注「含税」或者「不含税」,在数据库里加一个字段,页面里加一个判断,这演示的是你对业务细节有感知。另一个好做又提分的方向是把密码存储改成哈希后再入库,不用加盐只要说明为什么不能明文存——这能展示你的工程安全意识,比多写一张表更打动人。
这套MySQL+IDEA+Tomcat+JSP+Servlet的方案,是我见过期末项目里性价比最高的组合。它没有SpringBoot那种一行依赖搞定的爽快感,但正是这种处处需要亲手搭的环境,让你在调试过程中把JavaWeb的地基夯实了。我也是从「启动Tomcat看一堆红色报错不知道从哪里下手」一路过来的,那时候连IDEA控制台里的日志都不会看。现在你至少知道:报错先看Tomcat日志,连接问题先验证MySQL链路,404先检查映射和部署上下文。按这套思路做完,这个项目就真正是你的了。希望帮到你。
本文还有配套的精品资源,点击获取