
简介一套基于Servlet、JSP与MySQL构建的JavaWeb房屋租赁管理系统源码面向JavaWeb初学者、毕业设计者以及需要快速搭建租赁管理系统的中小型业务人员。系统围绕房屋租赁全流程设计包含房源录入与查询、租金设置、租客信息维护、合同登记与后台数据管理等核心功能适用于物业公司、房产中介及个人房东开展日常业务也为课程设计和毕业设计提供了可直接运行的参考项目。资源包共1796个文件压缩包约10.06MB文件类型涵盖java源码、jsp动态页面、class编译产物、css样式表、png图片、jar依赖库以及sql数据库脚本等结构上包含src源码目录、build编译输出、WebContent前端与WEB-INF配置目录整体层次清晰。其中Servlet负责拦截请求并处理业务逻辑JSP渲染用户界面MySQL通过SQL语句完成数据持久化。已有3128人学习下载。深入阅读这套源码可以掌握Servlet生命周期与请求响应处理、JSP隐式对象的使用、JDBC操作MySQL的方式以及Eclipse工程配置过程有助于理解JavaWeb分层开发与多组件协同工作。1. 这个管理系统到底是什么一个能跑通增删改查的 JavaWeb 完整工程如果你刚学完 servlet 和 jsp最难受的不是语法而是手里没有一个能完整跑起来的工程——登录、列表、分页、增删改查、数据库联调全都串好。JavaWeb 房屋租赁管理系统源码servletjspmysql就是这类项目里最典型的一个业务贴近现实房东、租客、房源、合同、账单这些实体关系清晰适合拿来当课程设计、毕业设计也适合作为你理解 JavaWeb 全链路的练手工程。它解决的是「servlet 怎么写、jsp 怎么渲染、mysql 怎么建表关联」三件事的落地问题。下面我按自己做这类项目的习惯把从建库到部署的整条路径拆开你能直接照着复现。2. 先把数据库立住房屋租赁系统的表设计与建表 SQL2.1 业务梳理租房管理需要哪几张表平时说“房屋租赁管理系统”最怕一上来就写代码。你要先想清楚房源和租约的状态是怎么流转的。我一般会拆成五个核心实体管理员、房东、房源、租客、租约。再加上一张账单表记录租金收取。为什么必须有房东和租客两张独立表因为租约要把「谁租了哪套房」记清楚。如果只在租约表里塞两个姓名字段后续查租金、查合同都会变成黑匣子。常见的做法是user 表通过 role 字段区分管理员、房东、租客三种身份房源表通过 landlord_id 归属房东租约表里冗余一份房源信息快照防止房源改价后历史租约数据对不上。2.2 房屋租赁系统的核心建表 SQL结构、约束与默认值在 MySQL 里新建数据库house_rental字符集用 utf8mb4。以下是一份能在 mysql 8.x 和 5.7 上都跑通的建表脚本CREATE DATABASE IF NOT EXISTS house_rental DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE house_rental; CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 用户名, password VARCHAR(64) NOT NULL COMMENT 密码项目里用MD5摘要, real_name VARCHAR(50) COMMENT 真实姓名, phone VARCHAR(20) COMMENT 联系电话, role TINYINT NOT NULL DEFAULT 2 COMMENT 角色1管理员 2房东 3租客, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1启用 0禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT用户表; CREATE TABLE house ( id INT PRIMARY KEY AUTO_INCREMENT, landlord_id INT NOT NULL COMMENT 房东ID关联sys_user, title VARCHAR(100) NOT NULL COMMENT 房源标题如两室一厅近地铁, address VARCHAR(255) NOT NULL COMMENT 地址, area DECIMAL(10,2) COMMENT 面积平方米, price DECIMAL(10,2) NOT NULL COMMENT 月租金, status TINYINT NOT NULL DEFAULT 0 COMMENT 0未出租 1已出租 2已下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_landlord (landlord_id), KEY idx_status (status), CONSTRAINT fk_house_landlord FOREIGN KEY (landlord_id) REFERENCES sys_user(id) ) ENGINEInnoDB COMMENT房源表; CREATE TABLE tenant ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL COMMENT 租客用户ID, id_card VARCHAR(18) COMMENT 身份证号, emergency_contact VARCHAR(50) COMMENT 紧急联系人, remark VARCHAR(255) COMMENT 备注, CONSTRAINT fk_tenant_user FOREIGN KEY (user_id) REFERENCES sys_user(id) ) ENGINEInnoDB COMMENT租客扩展信息表; CREATE TABLE lease ( id INT PRIMARY KEY AUTO_INCREMENT, house_id INT NOT NULL COMMENT 房源ID, tenant_id INT NOT NULL COMMENT 租客ID, landlord_id INT NOT NULL, start_date DATE NOT NULL COMMENT 起租日期, end_date DATE NOT NULL COMMENT 到期日期, monthly_rent DECIMAL(10,2) NOT NULL COMMENT 租金快照避免改价影响历史, deposit DECIMAL(10,2) DEFAULT 0 COMMENT 押金, status TINYINT NOT NULL DEFAULT 1 COMMENT 1生效 0已退租, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_house (house_id), KEY idx_tenant (tenant_id), CONSTRAINT fk_lease_house FOREIGN KEY (house_id) REFERENCES house(id), CONSTRAINT fk_lease_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id) ) ENGINEInnoDB COMMENT租约表; CREATE TABLE payment ( id INT PRIMARY KEY AUTO_INCREMENT, lease_id INT NOT NULL, amount DECIMAL(10,2) NOT NULL COMMENT 收款金额, pay_date DATE NOT NULL COMMENT 收款日期, method TINYINT DEFAULT 1 COMMENT 1现金 2转账 3微信/支付宝, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_lease (lease_id) ) ENGINEInnoDB COMMENT账单表;这段 SQL 里几个设计点我专门解释一下。sys_user用 role 区分三类用户而不是拆三张表是为了让登录逻辑只需要查一张表servlet 里取用户角色后跳转不同首页就行。house表的 status 字段从 0 开始实际开发里这类状态字段千万别用中文枚举值存库否则后续加状态要改表数据直接翻车。lease表里冗余了 landlord_id 和 monthly_rent这叫“快照冗余”因为房东改房源价格不应该影响历史租约的应收金额查询账单时直接用快照值不用再去关联 house 表。2.3 索引、外键与事务设计mysql 里最容易想当然的地方很多人建表不写索引数据量一到几万条列表页慢得明显。上面 SQL 里我给house.landlord_id、house.status、lease.house_id、lease.tenant_id都加了普通索引。需要注意mysql 的查询优化器对 IN 子句、范围查询和大表 join 很敏感房源列表页最常见的筛选条件是“区域 状态 价格区间”这套表设计里 status 索引一定要有条件组合时用联合索引效果更好但联合索引的字段顺序得按查询频率排把等值查询放在前面。外键我这里加了两个但说实话生产项目里我反而很少用物理外键。常见做法是只保留逻辑外键也就是字段命名带_id由应用层保证完整性。原因很简单物理外键会让房源删除、用户停用这类操作变得小心翼翼一旦外键约束没设计好插入租约时总是报错还不知道看哪。这里给出的是教学工程风格的建表脚本带外键方便你理解关系如果你要拿这套改造成自己的毕设或小项目建议把fk_lease_house这类约束去掉只留索引。事务设计上重点盯两个操作一是创建租约时要同时修改house.status为 1这两个动作必须在一个事务里二是生成账单时如果还包含“创建待支付记录”那 payment 表插入要和租约生成绑定。mysql 的 InnoDB 默认隔离级别是 REPEATABLE READ如果出现“两个人同时租同一间房”的情况需要额外加行锁或唯一约束兜底。这点放到后面代码部分细说。3. 在 IDEA 里跑通 servletjspmysql 工程环境、配置与第一个接口3.1 idea 运行 JavaWeb 项目的配置Tomcat 与本地 MySQL 安装检查很多新手拿到源码打开 idea点运行直接报 404 或者连不上数据库多半不是代码的问题是环境没对上。做这个 servletjspmysql 项目最稳的组合是IDEA 2021 以上版本 Tomcat 8.5/9.0 MySQL 5.7 或 8.0 JDK 8。用 JDK 8 而不用 11、17是因为 Tomcat 8.5 默认对 JDK 8 最友好避免 servlet 3.1 注解扫描出怪问题。本地 mysql 安装配置教程网上很多我提三个检查点第一确认 mysql 服务真的在跑Windows 上用net start mysql或任务管理器看进程第二确认字符集在 my.ini 里把character-set-serverutf8mb4配上否则 jsp 页面提交中文住址进库后全是问号第三确认驱动 jar 放进了 WEB-INF/libmysql 8.0 用com.mysql.cj.jdbc.Driver5.7 用com.mysql.jdbc.Driver混用就报 ClassNotFoundException。常用命令可以先在数据库客户端里跑一遍建表 SQL再打开 IDEA。3.2 工程目录与 web.xmlservlet 3.1 的两种写法对比servlet 项目有两种配置路由的方式web.xml 里写servletservlet-mapping或者直接用WebServlet注解。这个题目里的源码年代可能较早但它通常兼容两种写法我建议你用注解少写大量 XML 配置。目录结构必须是标准的src/main/java/com/house/rental/ ├── servlet/ # 登录、房源、租约等 Servlet ├── service/ # 业务逻辑层 ├── dao/ # JDBC 数据访问层 ├── util/ # DBUtil 数据库连接工具 src/main/webapp/ ├── jsp/ # 页面文件login.jsp, house_list.jsp ├── WEB-INF/web.xml └── index.jsp如果你拿到手的源码是 old 结构的src 下没有 main/java而是直接在根目录IDEA 里需要右键src选 Mark Directory as Sources Root否则编译不认。web.xml 里配几个东西欢迎页、字符集过滤器、登录拦截的 servlet 映射。这里给一个实用的 web.xml 配置片段?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd version3.1 display-nameHouse Rental System/display-name !-- 让 jsp 页面里的中文不乱码 -- filter filter-nameEncodingFilter/filter-name filter-classcom.house.rental.filter.EncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameEncodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping welcome-file-list welcome-fileindex.jsp/welcome-file welcome-filelogin.jsp/welcome-file /welcome-file-list session-config session-timeout30/session-timeout /session-config /web-app这里 EncodingFilter 拦截的核心作用是把 POST 请求的 request.setCharacterEncoding(UTF-8) 做掉不做这一步jsp 表单提交中文地址到 servlet 再入库会出现“乱码三连”——页面乱、SQL 里乱、数据库里乱。session-timeout设为 30 分钟是常规操作太短用户录合同录一半被踢出去太长服务端 session 对象积压内存。3.3 连接 mysql 的 JDBC 工具类从 DriverManager 到参数传值JDBC 工具类是 servletjspmysql 工程的命脉。常见的教学代码直接DriverManager.getConnection(url, user, password)我建议加上配置文件 db.properties避免每次换数据库都要重新编译。下面是核心代码package com.house.rental.util; import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Properties; public class DBUtil { private static String url; private static String username; private static String password; static { try (InputStream in DBUtil.class.getClassLoader() .getResourceAsStream(db.properties)) { Properties props new Properties(); props.load(in); Class.forName(props.getProperty(driver)); url props.getProperty(url); username props.getProperty(username); password props.getProperty(password); } catch (Exception e) { throw new ExceptionInInitializerError(数据库配置加载失败: e.getMessage()); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } public static void close(Connection conn, Statement stmt, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException ignored) {} } if (stmt ! null) { try { stmt.close(); } catch (SQLException ignored) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } } }对应的db.properties放在src/main/resources下内容是drivercom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/house_rental?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue usernameroot password你的数据库密码逻辑说明静态块在类加载时读一次配置然后注册驱动。getConnection()每次调用都开一个物理连接这在教学项目里没问题但并发一高就有性能瓶颈。加深连接池的进阶方案放到最后一章先把这条路跑通。close()方法要把 ResultSet、Statement、Connection 全关掉顺序反了会内存泄漏。参数说明里最容易被忽略的是useSSLfalse和serverTimezoneAsia/Shanghaimysql 8.0 强制要求时区参数不写的话启动就报空指针这是 mysql 安装配置教程里反复出现的坑。allowPublicKeyRetrievaltrue 是 mysql 8.0 使用 caching_sha2_password 认证时的必填项不用旧版 mysql_native_password 的话连不上也不要惊讶。4. 核心代码落地登录鉴权、房源发布与租约生成4.1 servlet login demo登录接口、Session 与角色跳转登录是所有管理系统的入口。servlet 处理登录请求的标准流程取参数 → 查库 → 比对 → 写 session → 跳转。下面是一个可用的 LoginServletpackage com.house.rental.servlet; import com.house.rental.dao.UserDao; import com.house.rental.entity.User; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; WebServlet(/login) public class LoginServlet extends HttpServlet { private final UserDao userDao new UserDao(); 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); String inputCode request.getParameter(verifyCode); HttpSession session request.getSession(); String sessionCode (String) session.getAttribute(verifyCode); if (inputCode null || !inputCode.equalsIgnoreCase(sessionCode)) { request.setAttribute(msg, 验证码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); return; } User user userDao.findByUsernameAndPassword(username, password); if (user null) { request.setAttribute(msg, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); return; } if (user.getStatus() 0) { request.setAttribute(msg, 账号已被禁用); request.getRequestDispatcher(/login.jsp).forward(request, response); return; } session.setAttribute(loginUser, user); session.setAttribute(role, user.getRole()); // 管理员去后台房东去房源管理租客去可租房列表 if (user.getRole() 1) { response.sendRedirect(request.getContextPath() /admin/index.jsp); } else if (user.getRole() 2) { response.sendRedirect(request.getContextPath() /house/list); } else { response.sendRedirect(request.getContextPath() /house/available); } } }逻辑说明里有个细节验证码判断放在最前面因为验证码是防止暴力破解的第一道闸不通过就直接 return不查库。三处return分别对应验证码错误、账号密码错误、账号被禁用三种情况全部用 forward 转发回登录页并带一个 msg 参数这是 jsp 页面最常用的错误提示方式。参数说明request.getParameter(username)拿表单里的 name 属性request.getSession()取出当前会话如果客户端没带 JSESSIONID 会新建一个所以直接把写入 session 的操作放在最后一步。我这里用了 DAO 层简化UserDao 里还是标准的 JDBC 查询preparedStatement 下setString(1, username)再执行executeQuery()密码比对用 md5 摘要而不是明文这一点后面避坑章节重点讲。4.2 房源发布与列表jsp 表单提交、图片坐标定位与分页查询房源发布是房屋租赁系统里交互最重的一块。前端需要一个录入表单字段包括标题、地址、面积、月租金、户型、状态、图片。这里踩过的坑是“房源的图片怎么在列表页里按坐标定位摆放”。jsp 页面要显示图片常见做法是把图片存到webapp/upload/house/目录数据库里只存相对路径如/upload/house/20240612_xxx.jpg。然后img src${house.picPath} /就能按路径渲染。如果你要做“图片按坐标定位”比如在地图上标记房源jsp 里就嵌入一段前端脚本从后台读出经纬度字段循环生成div定坐标。这里不是放个img就能解决的需要先确认表里有 longitude/latitude 字段再用 js 控制 top/left 样式。对这个项目来说我建议先做成普通缩略图列表坐标定位算锦上添花等核心流程通了再改。房源列表分页是 servletjsp 的经典考题。核心思路是 Page 类pageNo、pageSize、totalCount、totalPage、List data。查询时用SELECT * FROM house WHERE status0 LIMIT ?, ?两个占位符分别填(pageNo-1)*pageSize和pageSize。在 mysql 里分页最简单但如果数据量大到几十万条深分页会明显变慢此时常见做法是改子查询先取主键 ID 范围再回表查详情。这个系统的数据量不会到这个量级LIMIT 分页足够了。4.3 租约生成的事务问题同时在 lease 表和 house 表上做操作租约生成是整条业务链里最容易出数据一致性问题的地方。一个“签订合同”操作必须同时做插入 lease 记录、把 house.status 改成 1已出租、可能还插入 payment 首期账单。任一步失败其他步骤都要回滚。下面是租约核心代码重点看事务边界public boolean createLease(Lease lease, Payment payment, int houseId) { Connection conn null; PreparedStatement psLease null; PreparedStatement psUpdateHouse null; PreparedStatement psPayment null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交手动控制事务 String sqlLease INSERT INTO lease (house_id, tenant_id, landlord_id, start_date, end_date, monthly_rent, deposit, status) VALUES (?,?,?,?,?,?,?,1); psLease conn.prepareStatement(sqlLease, Statement.RETURN_GENERATED_KEYS); psLease.setInt(1, lease.getHouseId()); psLease.setInt(2, lease.getTenantId()); psLease.setInt(3, lease.getLandlordId()); psLease.setDate(4, java.sql.Date.valueOf(lease.getStartDate())); psLease.setDate(5, java.sql.Date.valueOf(lease.getEndDate())); psLease.setBigDecimal(6, lease.getMonthlyRent()); psLease.setBigDecimal(7, lease.getDeposit()); psLease.executeUpdate(); ResultSet keys psLease.getGeneratedKeys(); int leaseId 0; if (keys.next()) { leaseId keys.getInt(1); } String sqlHouse UPDATE house SET status 1 WHERE id ? AND status 0; psUpdateHouse conn.prepareStatement(sqlHouse); psUpdateHouse.setInt(1, houseId); int rows psUpdateHouse.executeUpdate(); if (rows 0) { throw new RuntimeException(房源已被租出请刷新后重试); } String sqlPayment INSERT INTO payment (lease_id, amount, pay_date, method) VALUES (?,?,?,?); psPayment conn.prepareStatement(sqlPayment); psPayment.setInt(1, leaseId); psPayment.setBigDecimal(2, payment.getAmount()); psPayment.setDate(3, java.sql.Date.valueOf(payment.getPayDate())); psPayment.setInt(4, payment.getMethod()); psPayment.executeUpdate(); conn.commit(); return true; } catch (Exception e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ignored) {} e.printStackTrace(); return false; } finally { DBUtil.close(conn, psLease, null); DBUtil.close(null, psUpdateHouse, null); DBUtil.close(null, psPayment, null); } }这个事务设计的核心是conn.setAutoCommit(false)开始到三条语句全部执行成功才conn.commit()。中途如果房源已经被别人抢先租走UPDATE house SET status1 WHERE id? AND status0的影响行数为 0说明条件不成立直接抛出 RuntimeException 并回滚。这里用“乐观锁”的思路在 SQL 条件里加 status 判断避免两个请求同时创建租约造成一房两租。mysql 的 InnoDB 引擎下这条 UPDATE 会锁住对应的行后续事务的相同 UPDATE 会等待所以不用额外在应用层加锁。参数说明Statement.RETURN_GENERATED_KEYS是拿自增主键的指定方式注意ResultSet keys用完后要关掉我偷懒在 finally 里只关了 conn 和其他语句实际上 keys 这个 ResultSet 也应该关。这里有个小坑DBUtil.close(conn, psLease, null)的第三个参数是 ResultSet传 null 没问题但 con 关闭后后面DBUtil.close(null, psUpdateHouse, null)已经拿不到还能正常工作的 conn不过 JDBC 里资源关闭顺序是 ResultSet→Statement→Connectionconn 只要不重新 getConnectionfinally 里这段是安全的。4.4 jsp 个人信息展示页面与页面跳转权限控制登录后的个人中心、房东信息维护这类页面用 jsp 写起来比较快但权限控制得在 servlet 里做。常见的错误是把权限判断写在 jsp 页面里比如% if(session.getAttribute(role).equals(1)) { %这会导致用户直接访问 jsp 文件绕过权限。正确做法是所有 jsp 放在WEB-INF/pages/下外部不能直接访问必须经过 servlet forward。或者写一个 FilterWebFilter(/house/*) public class AuthFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpSession session request.getSession(false); if (session null || session.getAttribute(loginUser) null) { ((HttpServletResponse) resp).sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }这里判断逻辑是访问所有以/house/开头的 URL必须登录。request.getSession(false)是关键写法false 表示如果当前请求没有 session 就返回 null而不是创建一个新 session。很多人写成getSession()结果每个请求都新建一个 session永远登录不进去。注意 Filter 的WebFilter注解里 urlPatterns 不能用/*全拦截否则 login.jsp 和 css/js 静态资源也全挡住图片加载不出来。更精细的做法是把静态资源和登录页路径放到白名单列表里/login.jsp、/css/*、/js/*、/login直接放行。这个 Filter 的另一个作用是预防角色越权比如房东直接访问/house/available本应租客访问可以在 doFilter 里继续判断 URL 前缀和 role 的匹配关系。5. 常见问题与避坑记录servletjspmysql 最容易翻车的地方5.1 mysql 8.0 连不上ClassNotFoundException 和 Public Key Retrieval现象项目启动后登录秒报错控制台第一行是java.lang.ClassNotFoundException: com.mysql.jdbc.Driver或Access denied for user rootlocalhost。原因驱动类名写错。mysql 5.7 用com.mysql.jdbc.Drivermysql 8.0 改成com.mysql.cj.jdbc.Driver。第二个报错是 mysql 8.0 的默认认证插件 caching_sha2_password 要求客户端先执行一次公共密钥交换JDBC 驱动不自动处理就会拒绝连接。解决db.properties 里 driver 改成com.mysql.cj.jdbc.Driverurl 末尾加allowPublicKeyRetrievaltrue。如果你用的 MySQL 是 5.7驱动版本选 5.1.49 以下别混用。这里多说一句5.7 和 8.0 的 mysql 安装教程细节差异很大5.7.44 的安装包里没有mysql 8.0.46-winx64那样的 zip 解压配置流程前者的 data 目录初始化命令是mysqld --initialize-insecure后者版本换成了mysqld --initialize --console照着 8.0 教程配 5.7 大概率起不来服务。5.2 中文乱码jsp 页面正常但数据库里全是问号现象登录用户叫张三数据库里存成???或者从甲醇数据库读出来后页面上变成乱码。原因三层都出了问题。数据库表是 utf8mb4但 JDBC 连接的characterEncoding没指定mysql 使用默认的 latin1同时 servlet 端没有设 request.setCharacterEncodingTomcat 8 默认 POST 编码是 ISO-8859-1。解决按顺序查三处。第一web.xml 里配好第 3.2 节的 EncodingFilter保证request.setCharacterEncoding(UTF-8)和response.setContentType(text/html;charsetUTF-8)第二db.properties 里 url 拼接useUnicodetruecharacterEncodingutf8注意是 utf8 不是 utf8mb4JDBC 里写成 utf8 就会映射到 utf8mb4第三mysql 的 my.ini 里character-set-serverutf8mb4。三条都做了还乱码多半是你用 Navicat 手工改了数据但表本身是 latin1需要把列转成 utf8mb4。这个坑最容易在“用户输入→servlet→mysql→jsp 显示”这条链上出现每层都自行其是最终结果就是全乱。5.3 新增房源后 jsp 列表页报 500null pointer 还是 SQL 语法错现象点提交房源页面跳转到一个 500 错误页Tomcat 日志里显示java.lang.NullPointerException或com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException。原因后者是 SQL 里用了key、status之类的保留字比如house表如果还加了type字段type在 mysql 里是保留字直接拼进 INSERT 语句就炸。NullPointerException 出现在house.getLandlordId()取到 null因为前端表单里没有传 landlord_id 字段而代码里强转Integer.parseInt(request.getParameter(landlordId))得到 null。解决SQL 中所有字段都加上反引号INSERT INTO house (id, \type, title, address) VALUES (...)或者干脆避免用保留字命名。servlet 取参时加一层校验landlordId如果为空则从 session 里的 loginUser 取。我一般还会在 DAO 层的查询方法里打印完整 SQL用SELECT ... WHERE landlord_id ? 这种参数占位别直接拼字符串防注入是底线真出错时对照数据库控制台一眼就能看出问题。5.4 Tomcat 启动成功但访问路径 404servlet 映射对不上现象Tomcat 运行没问题首页也能开但点登录按钮后地址变成/loginTomcat 返回The origin server did not find a current representation或者 servlet 类写了但根本没被加载。原因IDE 里部署的 web 应用上下文路径不对或者你手写的WebServlet(/login)里 URL 没带项目上下文。如果你访问的地址是http://localhost:8080/login但实际部署 contextPath 是/house_rental那就拼不上还有一种情况是源码里 web.xml 与注解方式重复配置了同一个映射准确说是同一个 servlet 类在两个地方声明导致冲突。解决IDEA 的 Run Configuration 里 Deployment 标签页Application context 填/house_rental所有 servlet 里写跳转地址都用request.getContextPath() /login。另外检查 WEB-INF/web.xml 里有没有旧的servlet-mapping如果和注解重复删掉 XML 里的映射。这个 404 问题几乎每个搞 JavaWeb 的人都会遇到其实是上下文和相对路径不匹配跟代码逻辑无关属于工程配置问题。5.5 并发下重复租约mysql 锁和唯一索引的兜底方案现象测试时两个浏览器同时点“确认签约”数据库里出现两条 status1 的租约房源被租了两次。原因前面 4.3 小结的UPDATE house SET status1 WHERE id? AND status0在同一事务里已经处理了行锁但如果房源状态判断和插入 lease 是分开的代码即两个 servlet 请求并行执行时前一事务更新了行但还没 commit另一个事务读到的还是旧状态就会插入重复租约。解决应用层无法百分百防住并发必须让数据库兜底。第一在 house 表加version INT DEFAULT 0做乐观锁第二在 lease 表加UNIQUE KEY uk_house_active (house_id, status)——但 mysql 里 unique 不排除多条 status1 的记录因为 status0 的记录不重复。更简单的办法给 lease 表加一个独特的约束uk_active_lease (house_id, status, start_date)但租约可能多次退租再租。最干净的做法是租约创建事务内部先SELECT ... FOR UPDATE锁定 house 行再执行后续操作。这条路对菜鸟来说不太容易理解所以我在代码里用“UPDATE 影响行数为 0 就回滚”来挡并发已经够应付课程设计和中小型项目。如果你想做到保险起见可以把 fromUPDATE house SET status 1 WHERE id ? AND status 0的 scan 改成在事务最开始对 houseId 做SELECT status FROM house WHERE id? FOR UPDATE。行锁能确保同一时刻只有一个事务能修改这个房源。注意 mysql 的 InnoDB 锁是行级的不写 FOR UPDATE 时 SELECT 不带锁就无法控制并发。6. 把这个系统做成能交付的东西连接池、JSTL 与验证清单如果这套房屋租赁管理系统打算答辩、演示或给业务方试用不要停在“能跑”还要把几个明显的学生气改掉。第一件是引入连接池。现在 DBUtil 每次请求都DriverManager.getConnection压力测试一发 50 个并发mysql 的 max_connections 默认 151直接打崩。常见做法是把 JDBC 换成 Apache DBCP 或 HikariCP。HikariCP 配置三行数据源对象、连接池大小、连接超时。核心就这几行其他交给库处理HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/house_rental?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai); config.setUsername(root); config.setPassword(你的密码); config.setMaximumPoolSize(10); config.setMinimumIdle(2); config.setConnectionTimeout(30000); HikariDataSource dataSource new HikariDataSource(config);改成连接池后DBUtil.getConnection() 从 dataSource.getConnection() 获取注意拿到连接用完必须 close这里 close 是归还给池子不是真断开。连接池参数里 maximumPoolSize 设 10 足够支撑这类管理系统别学网上无脑设 50连接数越多越容易拖垮 MySQL尤其如果你用的是 Docker 里跑的 mysql容器默认连接数还需要额外配置。第二件是把 jsp 里乱七八糟的 Java 脚本片段Scriptlet换成 JSTL。举个例子房源列表页常见的% for(House h : list) { %这种写法后续维护时改一个字段要翻半天。改造后是c:forEach items${houseList} varhouse tr td${house.title}/td td${house.price}/td td c:choose c:when test${house.status 0}未出租/c:when c:when test${house.status 1}已出租/c:when c:otherwise已下架/c:otherwise /c:choose /td /tr /c:forEach注意 servlet 里需要request.setAttribute(houseList, list)才能渲染。改造的意义不只是代码好看而是 jsp 本质上是视图混入 Java 逻辑会让权限校验绕过风险成倍增加。这步做完页面文件的可读性和可维护性直接上一个档次。第三件是准备验证清单。我习惯在交付前按流程走一遍全绿才算完管理员登录 → 新增房东账号 → 房东登录 → 发布房源 → 租客注册 → 租客浏览可租房源 → 租客提交租房申请 → 房东创建租约 → 账单记录收款 → 租约到期退租house.status 置 0。每走一步去数据库对照 payment、lease、house 三条记录的字段变化。这个清单能一次性发现前面说的所有问题。最后把 db.properties 里的密码别写在源码里公开出去演示环境单独建一个低权限账号只给这个库的增删改查权限。我接手这类项目的经验是速度不是重点脏数据才是大麻烦。系统上线或提交前我习惯给每条关键数据加一个“创建时间”字段给房源加一个“最后操作人”字段排查纠纷时才知道黑洞在哪里。你这个项目如果打算给真实的小中介用强烈建议把租约到期提醒加上用 mysql 的定时任务或 Java 的定时器都行否则房租收晚了房东要骂人。希望这些踩坑经验对你有用。本文还有配套的精品资源点击获取