首页 / 资讯中心 / 文章详情

JavaWeb房屋租赁管理系统:Servlet+JSP+MySQL从环境搭建到答辩避坑全指南

JavaWeb房屋租赁管理系统:Servlet+JSP+MySQL从环境搭建到答辩避坑全指南 ★ FEATURED ARTICLE
简介面向物业、中介公司及个人房东的房源管理场景这份JavaWeb项目以Servlet、JSP、MySQL为核心提供房屋信息、租客信息与租赁合同的一体化管理既可直接用于实际业务也适合javaweb初学者研究完整请求-响应流程。资源包共1796个文件约10.06MB包含119个Java源文件、102个JSP页面、146个CSS样式、195张PNG图片、7个JAR依赖库以及SQL脚本与Eclipse工程配置文件层级清晰便于按模块查阅。系统经过多次测试覆盖增删改查、前后端数据交互、会话管理、Filter与Listener等关键知识点。已有3127人学习下载。通过运行和拆解这套源码可以快速理解Servlet如何接收请求并处理业务逻辑、JSP如何完成动态界面展示以及MySQL建表与SQL操作在实际项目中的落地方式对提升JavaWeb开发与工程组织能力很有帮助。1. 课程设计年年撞车的 JavaWeb 房屋租赁管理系统到底值不值得认真做每年课程设计和毕业设计扎堆的时候「JavaWeb 房屋租赁管理系统」都是出现频率最高的题目之一而且题目往往直接限定技术栈servlet jsp mysql。很多同学第一反应是这套技术太老不如直接上 Spring Boot 来得痛快。但实际批改和答辩环节里老师看重的是你对 HTTP 请求处理、会话管理、JDBC 操作和分层设计这些基本功的理解而这些恰恰是 servletjspmysql 这条路能讲得最透的地方。本文从环境搭建、数据库设计、核心代码落地到踩坑排查把这条链路完整走一遍让你课设答辩能讲清楚原理代码能跑得起来后续想往 Spring Boot 迁移也知道每一步在干什么。2. 为什么课程设计首选 servlet jsp mysql 这套经典组合2.1 它不是过时技术而是把 MVC 原理摊开给你看的教学案例Spring Boot 固然开发效率高但它的自动配置把太多细节封装成了黑匣子一个 RequestMapping 注解背后发生了什么、请求参数如何绑定、过滤器链怎么生效新手很难看清。servlet jsp mysql 这条路要求你亲手写 HttpServlet、手动从 HttpServletRequest 里取参数、自己控制转发和重定向反而把 JavaWeb 最底层的运转逻辑暴露得干干净净。房屋租赁管理系统恰好是这种技术栈的理想载体它有用户登录和权限区分、房源信息的增删改查、租约下单这种涉及多表更新的事务场景还有搜索和分页这类高频操作。把这些功能用 servlet 逐一实现你就能完整经历一个 JavaWeb 项目的所有典型环节。答辩时老师问到「请求进来之后是怎么走到你的业务代码的」「数据库连接是怎么管理的」你都能指着自己写的代码讲清楚而不是甩锅给框架。这套组合的另一个优势是部署简单。一个 Tomcat、一个 MySQL、一个打了 war 包的工程就能跑起来不需要 Redis、不需要 MQ对课程设计来说复杂度刚刚好。如果选 Spring Boot 加一堆中间件光环境问题就能消耗大量时间反而偏离了课程设计考察的核心目标。2.2 JSP Model 2 分层Servlet 控制、JSP 展示、DAO 存取数据常见做法是把项目按 JSP Model 2 的思想分层这也是 servletjspmysql 项目最标准的组织方式。所谓 Model 2本质是 MVC 在 JavaWeb 里的落地JSP 只负责渲染页面Servlet 负责接收请求、调度业务逻辑、控制页面跳转JavaBean 和 DAO 负责封装数据和访问数据库。我一般会把工程目录分为这样几块你也可以直接照这个结构搭src/main/java ├── com.rental.web # Servlet 层也叫控制层 │ ├── UserServlet.java │ ├── HouseServlet.java │ └── OrderServlet.java ├── com.rental.service # 业务层处理登录校验、下单事务等 ├── com.rental.dao # 数据访问层只写 JDBC 和 SQL ├── com.rental.entity # 实体类对应数据库表 ├── com.rental.util # JdbcUtil 等工具类 └── com.rental.filter # 登录拦截过滤器 src/main/resources └── db.properties # 数据库连接配置 src/main/webapp ├── WEB-INF │ ├── lib # mysql 驱动 jar 放在这里 │ └── web.xml ├── login.jsp ├── register.jsp └── house_list.jsp这个结构不是随便分的Servlet 层每张核心业务表对应一个类HouseServlet 只管房源相关请求OrderServlet 只管租约相关请求避免一个 Servlet 里堆几百行代码。Service 层处理业务规则比如下单时先查房源是否已出租、再扣减库存对租房来说就是改状态。DAO 层只做最朴素的 JDBC 操作返回实体对象或 List。这样分层之后每一层都能单独测试出问题了也能快速定位。目录结构里容易忽略的是 src/main/resources 这个资源目录db.properties 放在这里程序里通过类加载器读取。这样改数据库密码只改配置文件不用重新编译。2.3 servletjsp 直接开发还是用 Maven 管理依赖这里有一个经常让新手纠结的问题要不要引入 Maven。如果你已经有 Maven 基础那自然是用 Maven 管理依赖最省事pom.xml 里声明 servlet-api、jstl、mysql-connector-java 三个依赖就够了版本由 Maven 统一拉取。但如果你对 Maven 还不太熟课程设计又只需要一个能跑的工程那不用 Maven 反而更稳。不使用 Maven 时你需要手动下载 servlet-api.jar、jstl.jar 和 mysql-connector-java.jar一股脑丢进 WEB-INF/lib 目录IDEA 会把这些 jar 自动打包进最终的 war 包。这种做法最直观也最适合新手排查问题——哪个 jar 缺失打开 lib 目录一眼就能看到。等后面想做 Spring Boot 迁移时再把项目结构转成 Maven 也不迟反正代码逻辑是一样的变化的只是依赖管理方式。3. 从零跑通房屋租赁系统JDK、Tomcat、MySQL 与 IDEA 的配置细节3.1 版本匹配表JDK 8 Tomcat 9 MySQL 8别拿 Tomcat 10 硬碰环境配置是 servletjspmysql 项目翻车率最高的环节而且多数问题都出在版本不匹配。这里直接给出一套验证过的稳定组合照这个版本装能省掉大量排查时间组件推荐版本关键说明JDK1.8即 Java 8兼容性最好所有教材和代码示例都是基于它写的Tomcat9.0.x最后一代使用 javax.servlet 命名的容器MySQL8.0 或 5.78.0 需要用对应的新驱动IDEA2021 以上版本社区版即可功能完全够用驱动 jarmysql-connector-java 8.0.33与 MySQL 8.0 配套这里要特别强调一条血泪经验不要用 Tomcat 10 或 11。Tomcat 10 开始把 javax.servlet 包名改成了 jakarta.servlet网上绝大多数 servletjsp 教程和课设源码都是基于 javax 包名写的拿到 Tomcat 10 上要么编译报错要么运行直接 404。如果你不想一上来就花两小时跟包名较劲就老老实实用 Tomcat 9。JDK 版本也不需要追新JDK 8 足够支撑 Tomcat 9 和 MySQL 8 驱动。高版本 JDK 在某些环境下反而会触发模块化访问限制给新手添乱。3.2 IDEA 中创建 JavaWeb 项目把 servletjspmysql 的最小骨架搭出来在 IDEA 里创建一个支持 servlet 和 jsp 的 JavaWeb 项目常见做法是直接新建普通 Java 项目然后手动添加 Web 框架支持。以较新版本的 IDEA 为例路径是新建项目时选择 Jakarta EE 或 Java Enterprise 模板勾选 Web Application服务器选择之前配置好的 Tomcat 9。如果你用的版本模板入口有变化也可以先建一个空项目然后右键模块选择添加框架支持勾选 Web Application效果一样。项目创建之后有三个配置步骤缺一不可第一步把 WEB-INF/lib 目录建出来并把 mysql-connector-java-8.0.33.jar 放进去。注意这个目录一旦建立IDEA 会自动把它识别为依赖目录不需要额外在 Project Structure 里配置。第二步配置 Tomcat。点击右上角下拉框选择 Edit Configurations点加号找到 Tomcat Server 里的 Local在 Application server 里选择 Tomcat 9 的安装目录。切到 Deployment 标签页点加号选择 Artifact把项目 war 包加进去Application context 一般填/house_rental这样访问路径就是http://localhost:8080/house_rental/。第三步验证环境。新建一个最简单的 Servlet重写 doGet 方法输出一行文字配置好 web.xml 或注解映射部署后访问。能正常在浏览器看到输出说明整条链路已经通了后面写业务代码只是往这个骨架里填充。3.3 连接 MySQL驱动 jar 放对位置项目才不黑匣子很多同学的项目卡在「能启动但一访问数据库就报错」最典型的就是 ClassNotFoundException 和数据库连不上。前者十有八九是驱动 jar 没打进 war 包后者大概率是连接配置写错了。前置条件是先把 MySQL 装好。MySQL 安装时要注意选择字符集为 utf8mb4记住 root 密码。安装完成后新建一个数据库名字随意这里用一个有业务含义的 house_rental。建库之后不需要在命令行里手动建表后续可以通过项目的 SQL 脚本批量导入。连接数据库的配置集中在 db.properties 文件里这是整个项目里最值得反复检查的文件drivercom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/house_rental?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse userroot password你的密码driver 指的是驱动类路径MySQL 8 必须用 com.mysql.cj.jdbc.Driver用旧版 com.mysql.jdbc.Driver 会提示已过时。url 里有三个关键参数不能省characterEncodingutf8 保证中文写入不乱码serverTimezoneAsia/Shanghai 避免时区报错useSSLfalse 跳过 SSL 握手本地开发完全够用。user 和 password 按实际环境改不建议用 root 之外的账号本地学习场景 root 最省事。配置写好之后在代码里用 Class.forName 加载驱动然后 DriverManager.getConnection 拿连接通过这个流程就能确认驱动 jar 和连接参数是否都正常。4. 数据库设计用五张表撑起房屋租赁的完整业务闭环4.1 表结构设计用户、房源、租约、收藏、预约房屋租赁系统的核心业务可以拆成五个实体用户、房源、租约、收藏、看房预约。表结构设计是整套系统的地基字段类型和约束直接决定后续代码好不好写。下面是完整的建库建表脚本可以直接在 MySQL 里执行CREATE DATABASE IF NOT EXISTS house_rental DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE house_rental; -- 用户表一个表同时容纳管理员、房东、租客三种角色 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, real_name VARCHAR(50), phone VARCHAR(20), role INT DEFAULT 2 COMMENT 0-管理员 1-房东 2-租客, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 房源表 CREATE TABLE t_house ( id INT PRIMARY KEY AUTO_INCREMENT, landlord_id INT NOT NULL, title VARCHAR(100) NOT NULL, area DECIMAL(8,2) COMMENT 面积单位平方米, rent DECIMAL(10,2) NOT NULL COMMENT 月租金, address VARCHAR(255) NOT NULL, description TEXT, status INT DEFAULT 0 COMMENT 0-未出租 1-已出租, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (landlord_id) REFERENCES t_user(id) ); -- 租约表 CREATE TABLE t_contract ( id INT PRIMARY KEY AUTO_INCREMENT, house_id INT NOT NULL, user_id INT NOT NULL COMMENT 租客, start_date DATE NOT NULL, end_date DATE NOT NULL, total_amount DECIMAL(10,2) COMMENT 合同总金额, status INT DEFAULT 0 COMMENT 0-进行中 1-已完成 2-已退租, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (house_id) REFERENCES t_house(id), FOREIGN KEY (user_id) REFERENCES t_user(id) ); -- 收藏表 CREATE TABLE t_favorite ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, house_id INT NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_house (user_id, house_id) ); -- 看房预约表 CREATE TABLE t_appointment ( id INT PRIMARY KEY AUTO_INCREMENT, house_id INT NOT NULL, user_id INT NOT NULL, appoint_time DATETIME NOT NULL, status INT DEFAULT 0 COMMENT 0-待确认 1-已确认 2-已取消, remark VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP );关键字段的设计意图要说一下t_user 表复用同一张表存三种角色用 role 字段区分而不是拆三张表因为三种角色的公共字段远多于差异字段这样登录逻辑只需查一张表。t_house 表的 status 用 INT 而不是 BOOLEAN是为了以后扩展状态比如下架、审核中。面积用 DECIMAL(8,2) 而不是 INT因为面积经常带小数。rent 用 DECIMAL(10,2) 是资金字段的标配避免浮点精度问题。t_contract 表里没有直接冗余房东信息需要时通过 house_id 关联查询保持数据规范。4.2 核心 SQL登录校验、房源分页、租约到期统计表建好只是第一步真正检验设计是否合理的是业务 SQL。登录校验要防 SQL 注入房源列表要支持分页和条件搜索租约到期要有提醒。这三条 SQL 是系统里最高频的查询写法和参数都值得单独过一遍。登录校验的 SQL 看起来简单但写法上有讲究SELECT id, username, real_name, role FROM t_user WHERE username ? AND password ?这里的 ? 是占位符对应 PreparedStatement 的 setString 方法。千万不要用字符串拼接的方式写 SQL否则用户输入 or 11直接就能绕过登录。这条 SQL 查出来的是登录用户的身份信息后续 Session 里存的就是这条记录页面是否显示「房东管理入口」「租客下单按钮」全靠 role 字段判断。房源分页是最典型的高频查询。房屋租赁系统里房源数量可能上百条一次全查出来页面会很长。常用的是 LIMIT 分页SELECT id, title, area, rent, address, status FROM t_house WHERE status 0 ORDER BY create_time DESC LIMIT ?, ?第一个问号是偏移量 offset第二个是每页条数 size。前端页码从 1 开始所以代码里计算偏移量时是(currentPage - 1) * pageSize。如果要加关键词搜索就在 WHERE 后面加AND title LIKE CONCAT(%, ?, %)同样使用占位符。注意 MySQL 的 LIMIT 第一个参数从 0 开始页面第 1 页对应数据库里偏移 0这是个很容易想当然写错的细节。租约到期提醒适合放在管理员首页用一个 DATE_SUB 或 BETWEEN 实现七天预警SELECT c.id, h.title, h.address, u.real_name, c.end_date FROM t_contract c JOIN t_house h ON c.house_id h.id JOIN t_user u ON c.user_id u.id WHERE c.status 0 AND c.end_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 7 DAY) ORDER BY c.end_date这条 SQL 展示了三表关联的写法课程设计答辩时经常被问到。考试容易拿到的分数点是为什么不用WHERE DATEDIFF(end_date, NOW()) 7这种写法因为 DATEDIFF 写法会让 MySQL 无法使用 end_date 上的索引。BETWEEN 的范围查询在数据量大时优势明显这个点说出来能让老师对你的 SQL 功底刮目相看。4.3 初始化数据让项目一启动就有东西可看表结构设计完还得往里面塞一些测试数据否则页面打开一片空白既不好演示也不好排查。这里给一组最小可用的初始化数据覆盖三种角色和两种房源状态INSERT INTO t_user (username, password, real_name, phone, role) VALUES (admin, admin123, 系统管理员, 13800000001, 0), (landlord, 123456, 张房东, 13800000002, 1), (tenant, 123456, 李租客, 13800000003, 2); INSERT INTO t_house (landlord_id, title, area, rent, address, description, status) VALUES (2, 阳光花园两室一厅, 89.5, 2800.00, 市南区幸福路 12 号, 精装修近地铁 3 号线, 0), (2, 翠湖苑单间带阳台, 35.0, 1500.00, 市北区湖滨路 8 号, 拎包入住押一付一, 0), (2, 书香华府三室两厅, 118.0, 4500.00, 高新区文汇路 66 号, 学区房带车位, 1);前面四条 INSERT 语句里的密码默认是明文这只用于本地课程设计演示。如果要往正式项目方向走密码务必要做哈希存储可以用 JDK 自带的 MessageDigest 做 SHA-256也可以引入工具类。数据量不需要太多每张表几条就够演示了真正重要的是一套能走到完整流程的数据用户能登录、有可租和已租的房源各一条、有一条快过期的租约演示时才有东西可讲。初始化数据还有一种常见做法写一个 JSP 页面或者一个 InitDataServlet访问一次自动执行初始化。但更简洁的做法是直接用 Navicat 或 MySQL 命令行执行 SQL 脚本一次导入后面再需要重置数据就重新执行一遍脚本。README 里把这条命令写上队友和老师照着做就能跑起来。5. 核心功能代码落地登录拦截、房源展示与租约下单5.1 BaseServlet用反射把 URL 映射到方法省掉一功能一 Servlet如果每个功能都写一个 Servlet 类项目里会出现十几个类而且每个类里只有一两个方法非常冗余。更常见的工程做法是每个业务模块只写一个 Servlet不同操作靠 action 参数区分再用反射把请求分发到对应方法。这就是 BaseServlet 的作用。package com.rental.web; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.lang.reflect.Method; public abstract class BaseServlet extends HttpServlet { Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, java.io.IOException { req.setCharacterEncoding(UTF-8); String action req.getParameter(action); if (action null || action.trim().isEmpty()) { action list; // 默认执行 list 方法 } try { Method method this.getClass().getDeclaredMethod( action, HttpServletRequest.class, HttpServletResponse.class); method.setAccessible(true); method.invoke(this, req, resp); } catch (Exception e) { throw new ServletException(action 方法分发失败: action, e); } } }核心逻辑都在 service 方法里先从请求参数里取出 action 的值然后通过反射在当前类里找到同名方法方法签名固定为方法名(HttpServletRequest, HttpServletResponse)找到后调用。这样子类里只需要写业务方法比如 HouseServlet 里写 list、detail、add、delete 方法URL 写成/house?actionadd就能对应到 add 方法。参数说明action 是约定的参数名可以换成 op 或其他名称但前后端必须一致。method.setAccessible(true) 是处理子类方法声明为 private 的情况。action 找不到对应方法时抛出的是 NoSuchMethodException被统一包进 ServletException 之后Tomcat 会显示 500 错误排查时看异常堆栈第一行就能知道具体是哪个方法名写错了。5.2 JDBC 工具类配置外部化密码和地址不再写死在类里每个 DAO 都需要获取数据库连接和关闭连接这两段重复代码大量复制粘贴会显得代码冗长也不利于统一修改。常见做法是写一个 JdbcUtil 工具类把连接的创建和关闭统一封装。package com.rental.util; import java.io.InputStream; import java.sql.*; import java.util.Properties; public class JdbcUtil { private static String url; private static String user; private static String password; static { try { InputStream in JdbcUtil.class.getClassLoader() .getResourceAsStream(db.properties); Properties props new Properties(); props.load(in); url props.getProperty(url); user props.getProperty(user); password props.getProperty(password); Class.forName(props.getProperty(driver)); } catch (Exception e) { throw new ExceptionInInitializerError(读取 db.properties 失败请检查 resources 目录); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } public static void close(ResultSet rs, Statement stmt, Connection conn) { try { if (rs ! null) rs.close(); } catch (SQLException ignored) {} try { if (stmt ! null) stmt.close(); } catch (SQLException ignored) {} try { if (conn ! null) conn.close(); } catch (SQLException ignored) {} } }这段代码的关键设计静态代码块在类加载时执行一次完成驱动注册和配置读取之后所有线程共享一份连接参数配置从 classpath 下的 db.properties 读取避免把账号密码硬编码在 Java 类里close 方法接受三个参数ResultSet、Statement、Connection 都可能是 null所以每个都判空且异常吞掉因为关闭阶段的异常不影响业务结果。写 DAO 时的常规套路是getConnection 获取连接PreparedStatement 预编译setXxx 填充参数executeQuery 或 executeUpdate最后在 finally 里调 close 释放资源。这套写法学过一次之后增删改查全部是同一个套路。5.3 登录与 Filter 拦截Session 丢了就回登录页登录功能本身不复杂登录页面提交用户名密码UserServlet 验证成功就写入 Session失败就返回错误提示。难的是登录之后所有受保护页面都要判断「当前有没有人登录」这个判断不该每个 Servlet 重复写而是交给过滤器统一处理。先看登录验证的 Servlet 核心代码package com.rental.web; import com.rental.dao.UserDao; import com.rental.entity.User; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; WebServlet(/login) public class LoginServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String password req.getParameter(password); UserDao dao new UserDao(); User user dao.findByUsernameAndPassword(username, password); if (user null) { req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); } else { HttpSession session req.getSession(); session.setAttribute(loginUser, user); resp.sendRedirect(req.getContextPath() /house?actionlist); } } }这里有一个容易踩的细节登录失败用 forward 转发回登录页这样 req 里的 error 属性还能带到页面上显示登录成功用 sendRedirect 重定向到首页这样浏览器地址栏会变成项目地址刷新页面时不会重复提交表单。登录成功后 User 对象放进 Session后续所有页面都可以通过${sessionScope.loginUser.realName}拿到当前用户。有了 Session 里的登录标记再写一个 LoginFilter 拦截受保护的路径package com.rental.filter; import javax.servlet.*; import javax.servlet.http.*; import java.io.IOException; public class LoginFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; HttpSession session req.getSession(false); if (session null || session.getAttribute(loginUser) null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; } chain.doFilter(request, response); } }过滤器里的细节都在 req.getSession(false) 这个调用上参数传 false 表示如果没有 Session 就返回 null而不是新建一个空 Session。如果误用了 getSession()未登录用户访问受保护页面时会被强行创建 Session过滤器判断就失效了。拦截路径通过 web.xml 配置把需要登录的 URL 模式都配进去。5.4 租约下单事务保证「房被订走」和「订单生成」同时成功租约下单是整个系统里业务最完整的操作租客确认要租某套房系统先把房源状态改成已出租再插入一条租约记录。两步操作任何一个失败都不能让数据库停留在中间状态——房源标记了已出租但没有租约或者有租约但房源还是空闲。这里必须用数据库事务。Connection conn null; try { conn JdbcUtil.getConnection(); conn.setAutoCommit(false); String updateSql UPDATE t_house SET status 1 WHERE id ? AND status 0; PreparedStatement ps1 conn.prepareStatement(updateSql); ps1.setInt(1, houseId); int rows ps1.executeUpdate(); if (rows ! 1) { throw new RuntimeException(该房源已被租出请刷新后重试); } String insertSql INSERT INTO t_contract (house_id, user_id, start_date, end_date, total_amount, status) VALUES (?, ?, ?, ?, ?, 0); PreparedStatement ps2 conn.prepareStatement(insertSql); ps2.setInt(1, houseId); ps2.setInt(2, userId); ps2.setDate(3, java.sql.Date.valueOf(startDate)); ps2.setDate(4, java.sql.Date.valueOf(endDate)); ps2.setBigDecimal(5, totalAmount); ps2.executeUpdate(); conn.commit(); } catch (Exception e) { if (conn ! null) conn.rollback(); throw new ServletException(下单失败事务已回滚, e); } finally { if (conn ! null) conn.close(); }这段代码里有两个细节值得在答辩时重点讲。第一UPDATE 语句里带着AND status 0条件这是乐观锁思想只有当房源当前是空闲状态时才允许更新为已出租。executeUpdate 返回值为 1 说明成功修改了一行为 0 说明房源的 status 已经不是 0说明被并发抢租了。第二setAutoCommit(false) 之后必须手动 commit任何一个 SQL 报错都会进入 catch 块执行 rollback把前面已经执行成功的 UPDATE 也撤销掉。还需要注意 Date 类型转换t_contract 表的 start_date 是 DATE 类型Java 里如果用 java.util.Date 去 setDate 会报类型不匹配必须先转成 java.sql.Date。这个是新手特别容易卡住的细节编译期不报错运行期抛异常排查半天才发现是类型问题。6. 避坑排查servletjspmysql 项目最常见的五个翻车点6.1 Tomcat 10 导致 404/500javax.servlet 已改名 jakarta.servlet现象项目在 IDEA 里能编译但部署到 Tomcat 后访问 Servlet 全部 404或者启动直接报ClassNotFoundException: javax.servlet.http.HttpServlet。原因Tomcat 10 开始把 Java EE 迁移到 Eclipse Jakarta EE包名从 javax.* 整体改成了 jakarta.*。所有基于旧包名写的 servlet 代码在 Tomcat 10 上加载不到对应的类。解决把 Tomcat 换回 9.0.x这是成本最低的方案。不要试图把代码里的 import 全部改成 jakarta因为 JSP 内置对象和大量底层依赖仍然是旧包名改了也未必能全跑起来。如果你必须用 Tomcat 10那整个项目要从 servlet-api 到 JSP API 全部换成 Jakarta 版本代码量小的项目可以试但对课设来说不值得。建议在 IDEA 的 Tomcat 配置里直接指定 Tomcat 9 的安装目录一劳永逸。6.2 MySQL 8 驱动加载失败Class.forName 和 URL 时区都要换现象项目启动后第一次访问数据库就报ClassNotFoundException: com.mysql.jdbc.Driver或者报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized。原因MySQL 8 之后官方推荐驱动类名换成了 com.mysql.cj.jdbc.Driver旧类名虽然还存在但需要依赖旧版本驱动。时区错误则是连接 URL 里没带 serverTimezone 参数MySQL 8 默认要求客户端提供时区信息。解决驱动 jar 换成 mysql-connector-java 8.0.xdb.properties 里 driver 改成 com.mysql.cj.jdbc.Driver然后 URL 末尾加上serverTimezoneAsia/Shanghai。改完之后重建部署清除 Tomcat 的 work 目录再启动。这个问题的排查思路是看异常第一行如果是 ClassNotFound多半是 jar 没放好或类名写错如果是时间相关的异常就是 URL 参数问题。6.3 中文乱码pageEncoding、request 编码、Tomcat URI 三处一起改现象页面显示中文变成问号或者表单提交的中文到 Servlet 里取出来是乱码而数据库里存的也是乱码。原因乱码问题的本质是编解码不一致。JSP 页面按一种编码输出Servlet 按另一种编码读取请求参数Tomcat 内部可能又用了默认的 ISO-8859-1。三处只要有一处不匹配就在某一环节出现乱码。解决三个地方一起改。JSP 页面顶部确保有% page contentTypetext/html;charsetUTF-8 %这是告诉 Tomcat 输出页面用 UTF-8。Servlet 里在 service 方法第一行写request.setCharacterEncoding(UTF-8)这能解决 POST 请求参数乱码。对于 GET 请求查询字符串的编码由 Tomcat 控制在 server.xml 的 Connector 节点加URIEncodingUTF-8。三个地方都对齐之后中文才能从浏览器到 Servlet 再到数据库全程保持一致。如果数据库里已经存了乱码需要先清掉数据再重新导入已经乱码的数据很难自动修复。6.4 IDEA 部署后 ClassNotFoundExceptionArtifact 没带 lib 目录现象代码在 IDEA 里编译通过运行也不报错但部署到 Tomcat 后一访问数据库就报 ClassNotFoundException而这个 jar 明明就在项目的 lib 目录下。原因IDEA 的 Artifact 配置里默认可能没有把 WEB-INF/lib 目录里的 jar 打进部署包。代码编译时 IDEA 能找到 jar是编译期 classpath 的问题运行时 Tomcat 加载不到是部署包的问题。两个环节互相独立容易出现编译过了但运行报错的情况。解决打开 File 菜单下的 Project Structure进入 Artifacts在输出布局的 WEB-INF/lib 节点上检查是否包含了外部的 jar。如果没有选择项目里的 lib 目录右键点 Put into Output Root。改完之后重新构建项目再清理 Tomcat 部署目录。最快的验证方式是打开部署后的 WEB-INF 目录直接看里面有没有 lib 文件夹和对应 jar。6.5 MySQL 8 认证插件报错caching_sha2_password 让老驱动直接拒绝连接现象数据库连接时报错Unable to load authentication plugin caching_sha2_password或者提示Public Key Retrieval is not allowed。原因MySQL 8 默认的认证插件是 caching_sha2_password而老版本驱动或老版本连接参数不兼容这种认证方式。解决两个方案选一个。第一个方案是在连接 URL 上加allowPublicKeyRetrievaltrue这能让客户端自动获取公钥完成加密密码传输本地开发安全性要求不高直接用。第二个方案是把数据库用户的认证方式改回 mysql_native_password执行 SQLALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码;然后刷新权限。两种方案都试过之后重启 Tomcat 再看效果。这个错误的特点是一上来就是 500堆栈里关键词非常明显搜索报错信息很容易定位。7. 让这个项目脱胎换骨从「能跑」到「像样」的四个进阶技巧按上面的步骤把系统跑通之后如果想在答辩里拿高分或者让代码真正配得上「源码」两个字可以在四个方向做低成本高回报的改进。第一给密码做加盐哈希。目前系统里密码是明文存储的答辩时老师多半会问安全性。最简单的做法是引入org.apache.commons.codec.digest.DigestUtils.md5Hex(password salt)salt 可以用注册时间或随机字符串。只用 MD5 不加盐容易被查表破解加盐之后安全性明显上升而且改动只集中在 UserDao 里不动其他代码。第二分页参数做校验。LIMIT 的 offset 如果被恶意传成负数或巨大的数字可能引发性能问题。在 HouseServlet 的 list 方法里做个判断页面小于 1 强制改成 1每页条数设定上限比如最大 50。这个逻辑只需要几行代码但能体现你对异常输入的敏感度。第三房源图片用虚拟路径映射。如果给你的房源加图片图片不应该放进项目里打包进 war因为重新部署会丢失。更稳的做法是存到磁盘的某个目录然后在 Tomcat 里加虚拟路径映射或者写一个 ImageServlet 从磁盘目录读文件输出流。课程设计阶段用虚拟路径配置最简单Approve 老师演示时添加房源和显示图片的链路完整效果很加分。第四把项目结构向 Maven 靠拢。这不是让你立刻用 Spring Boot而是引入 pom.xml 管理驱动和依赖版本同时把目录调整为 Maven 标准结构。这步做完后续想迁移到 Spring Boot只需把 Servlet 变成 Controller、把 DAO 换成 MyBatisJSP 原样搬到 Spring Boot 的资源目录下就能用迁移成本比从零开始低得多。最后说一个我自己的习惯每完成一个功能先打开浏览器重新走一遍全流程再打开数据库看关键表格的数据变化。登录、发布房源、下单、退租每条路径都验证过才算做完。这套「改代码—重启—走流程—查数据」的循环看起来慢但它能帮你在答辩前把绝大多数隐藏问题都解决掉。希望这篇文章能帮你少走一些弯路把时间花在真正理解这套技术栈上。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站