简介:本资源是一套完整的JavaWeb毕业设计项目——房屋租赁管理系统,面向计算机专业本科生毕设开发与Java初学者实战训练,覆盖需求分析、编码实现到部署运行全流程。压缩包为ZIP格式,大小13.69MB,包含可直接运行的Servlet+JSP源码、SQL Server数据库脚本、系统配置指导文档、毕业论文(含开题报告、摘要、正文、参考文献)、答辩用PPT及功能说明,文件结构清晰,模块划分明确,涵盖前台会员中心、房源发布检索、后台信息管理与多维统计报表等核心业务。项目经Eclipse/IDEA严格调试,配套详细环境配置指南,支持JDK8+Navicat快速部署,降低新手搭建门槛。目前已有250人学习下载,适合需要高质量毕设参考、理解MVC分层架构、掌握JDBC数据交互与JSP动态页面开发的学习者,提供从代码到文档的一站式交付方案。
1. 这不是“过时技术堆砌”,而是 JavaWeb 工程能力的实体化验证:Servlet+JSP+JDBC 搭建房屋租赁系统,本质是训练你对请求生命周期、视图分离逻辑与数据库事务边界的完整掌控
很多人看到 Servlet+JSP+JDBC 就下意识划走,觉得这是“2005 年的技术栈”。但现实是:全国仍有超 37% 的政企内部管理系统、高校实训平台、中小房产中介后台,仍在稳定运行基于这套组合的 JavaWeb 应用——它们不追求微服务弹性伸缩,但要求部署极简、维护可控、数据强一致。本项目标题里的“房屋租赁管理系统”不是 Demo,它强制你面对真实业务约束:租约起止时间校验必须在 Service 层拦截、房源状态变更需同步更新数据库行锁、用户登录态要跨 JSP 页面保持且不能明文存 session。你写的不是 Hello World,而是带租期重叠检测、押金流水记账、合同 PDF 生成触发点的最小可行业务闭环。适合两类人:刚学完《Java Web 编程技术》教材想落地验证的在校生;或需要快速交付一个可演示、可审计、无外部依赖(如 Spring Boot 自动配置)的轻量后台给甲方的技术支持工程师。它不教你怎么写 REST API,但教会你怎么用HttpServletRequest#getParameterMap()安全解析表单、怎么用JspWriter避免 XSS 输出、怎么用PreparedStatement#setObject()统一处理 SQL Server 的datetime2和decimal(18,2)类型。
2. 从零构建可运行环境:SQL Server 数据库建模 + Tomcat 本地部署 + JDBC 驱动精准匹配
2.1 为什么必须用 SQL Server 而非 MySQL?关键在日期精度与事务隔离级别
房屋租赁业务对时间字段有严苛要求:合同生效时间需精确到毫秒(datetime2(3)),续租操作必须保证“同一房源在同一时段不可被重复签约”——这依赖 SQL Server 的SERIALIZABLE隔离级别与UPDLOCK, ROWLOCK提示。MySQL 默认REPEATABLE READ在高并发下易产生幻读,导致两个租客同时提交对同一房源的预约。本项目数据库脚本中所有CREATE TABLE语句均显式声明:
CREATE TABLE lease_contract ( id BIGINT IDENTITY(1,1) PRIMARY KEY, house_id BIGINT NOT NULL, tenant_id BIGINT NOT NULL, start_time DATETIME2(3) NOT NULL, -- 精确到毫秒,避免时区转换歧义 end_time DATETIME2(3) NOT NULL, deposit_amount DECIMAL(18,2) NOT NULL, status VARCHAR(20) DEFAULT 'ACTIVE', created_time DATETIME2(3) DEFAULT GETDATE() );提示:SQL Server 2019 及以上版本才原生支持
DATETIME2(3)的毫秒级存储。若使用 SQL Server 2008 R2,需降级为DATETIME并在 Java 层用SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")手动截断,否则插入时会丢失毫秒位。
2.2 Tomcat 9.0.x 是当前最稳妥的选择:兼容 Servlet 4.0 规范且无 JDK 版本陷阱
项目未标注 Tomcat 版本,但根据web.xml中<web-app>声明的version="4.0",必须选用 Tomcat 9.0.62 或更高版本(Tomcat 10+ 已迁移到 Jakarta EE 命名空间,javax.servlet.*包名失效)。安装后需确认两处配置:
conf/context.xml中添加资源定义:
<Resource name="jdbc/HouseRentalDB" auth="Container" type="javax.sql.DataSource" factory="org.apache.tomcat.jdbc.pool.DataSourceFactory" driverClassName="com.microsoft.sqlserver.jdbc.SQLServerDriver" url="jdbc:sqlserver://localhost:1433;databaseName=HouseRentalDB;encrypt=false;trustServerCertificate=true;" username="sa" password="YourStrong@Pass123" maxActive="20" minIdle="5" maxWait="10000"/>lib目录下必须放置mssql-jdbc-9.4.1.jre11.jar(对应 JDK 11)或mssql-jdbc-8.4.1.jre8.jar(对应 JDK 8)。严禁使用sqljdbc4.jar——该驱动已废弃,不支持 SQL Server 2016+ 的datetime2类型,会导致ResultSet#getTimestamp()返回 null。
2.3 JDBC 连接字符串参数必须显式关闭加密协商,否则连接失败
SQL Server 默认启用 TLS 加密,但本地开发环境常无有效证书。若连接时抛出SSL Provider: The certificate chain was issued by an authority that is not trusted,需在 URL 中强制禁用证书校验:
# 正确(开发环境) jdbc:sqlserver://localhost:1433;databaseName=HouseRentalDB;encrypt=false;trustServerCertificate=true; # 错误(生产环境绝对禁止) jdbc:sqlserver://localhost:1433;databaseName=HouseRentalDB;encrypt=false; # 缺少 trustServerCertificate=true| 参数 | 必填 | 说明 | 生产环境建议 |
|---|---|---|---|
encrypt | 是 | 启用/禁用 SSL 加密 | true |
trustServerCertificate | 是(当encrypt=false时) | 跳过证书链验证 | false,改用有效证书 |
sendStringParametersAsUnicode | 否 | 是否将 String 作为 Unicode 发送 | false(避免中文乱码) |
applicationName | 否 | 标识连接来源,便于 SQL Server Profiler 追踪 | 填写HouseRentalWebApp |
3. Servlet 层实现业务路由与状态控制:从doPost到RequestDispatcher的完整流转
3.1 登录 Servlet 必须完成三件事:凭证校验、Session 绑定、角色跳转
LoginServlet.java不是简单比对密码,而是体现 JavaWeb 的核心安全实践:
@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"); // 1. 使用 PreparedStatement 防止 SQL 注入(关键!) String sql = "SELECT id, username, role, status FROM users WHERE username = ? AND password = ?"; try (Connection conn = DataSourceUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); // 自动转义特殊字符 ps.setString(2, DigestUtils.md5Hex(password)); // 密码必须哈希存储 try (ResultSet rs = ps.executeQuery()) { if (rs.next() && "ACTIVE".equals(rs.getString("status"))) { // 2. Session 绑定用户上下文(非仅存 username) User user = new User(); user.setId(rs.getLong("id")); user.setUsername(rs.getString("username")); user.setRole(rs.getString("role")); req.getSession().setAttribute("currentUser", user); // 3. 根据角色重定向,而非统一跳转 String redirectPath = "ADMIN".equals(user.getRole()) ? "/admin/dashboard.jsp" : "/tenant/myLeases.jsp"; resp.sendRedirect(req.getContextPath() + redirectPath); return; } } } req.setAttribute("errorMsg", "用户名或密码错误,或账户已被禁用"); req.getRequestDispatcher("/login.jsp").forward(req, resp); } }注意:
resp.sendRedirect()与RequestDispatcher#forward()有本质区别。前者触发客户端 302 重定向,URL 变更且原 request 属性丢失;后者是服务器端内部跳转,URL 不变且 request 属性可传递。登录成功后必须用sendRedirect(),防止用户刷新页面重复提交表单。
3.2 房源列表 Servlet 需支持分页与状态过滤,且避免 N+1 查询
HouseListServlet.java不能直接SELECT * FROM houses,而要结合前端传参动态构建 SQL:
// 获取分页参数(默认第1页,每页10条) int page = Integer.parseInt(req.getParameter("page") != null ? req.getParameter("page") : "1"); int pageSize = 10; int offset = (page - 1) * pageSize; // 构建带条件的 COUNT 查询(用于计算总页数) String countSql = "SELECT COUNT(*) FROM houses WHERE status = ?"; String dataSql = "SELECT id, title, address, price, status FROM houses WHERE status = ? ORDER BY created_time DESC OFFSET ? ROWS FETCH NEXT ? ROWS ONLY"; try (Connection conn = DataSourceUtil.getConnection(); PreparedStatement psCount = conn.prepareStatement(countSql); PreparedStatement psData = conn.prepareStatement(dataSql)) { psCount.setString(1, "AVAILABLE"); // 只查可租房源 try (ResultSet rsCount = psCount.executeQuery()) { rsCount.next(); int totalRecords = rsCount.getInt(1); req.setAttribute("totalPages", (int) Math.ceil((double) totalRecords / pageSize)); } psData.setString(1, "AVAILABLE"); psData.setInt(2, offset); psData.setInt(3, pageSize); try (ResultSet rs = psData.executeQuery()) { List<House> houses = new ArrayList<>(); while (rs.next()) { House h = new House(); h.setId(rs.getLong("id")); h.setTitle(rs.getString("title")); h.setAddress(rs.getString("address")); h.setPrice(rs.getBigDecimal("price")); h.setStatus(rs.getString("status")); houses.add(h); } req.setAttribute("houseList", houses); req.setAttribute("currentPage", page); } } req.getRequestDispatcher("/house/list.jsp").forward(req, resp);3.2.1 分页 SQL 写法必须用OFFSET-FETCH而非LIMIT
SQL Server 2012+ 支持标准OFFSET-FETCH,严禁使用TOP N+ 子查询模拟分页,因为后者在大数据量时性能急剧下降。OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY是唯一推荐语法。
3.2.2DataSourceUtil.getConnection()必须复用 Tomcat JNDI 数据源
public class DataSourceUtil { private static DataSource dataSource; static { try { Context initCtx = new InitialContext(); Context envCtx = (Context) initCtx.lookup("java:comp/env"); dataSource = (DataSource) envCtx.lookup("jdbc/HouseRentalDB"); } catch (Exception e) { throw new RuntimeException("JNDI DataSource lookup failed", e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); // 自动参与 Tomcat 连接池管理 } }4. JSP 页面层实现安全渲染与交互增强:从 EL 表达式到自定义标签的渐进式升级
4.1 租约详情页必须用 JSTL<c:forEach>安全遍历,禁用 scriptlet
lease-detail.jsp中展示租约条款时,传统<% while(rs.next()) { %>写法已淘汰,必须用 JSTL 标签库:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <!-- 从 Servlet 传入的 lease 对象 --> <c:if test="${not empty lease}"> <h2>合同编号:${lease.contractNo}</h2> <p>出租方:${lease.landlordName} | 承租方:${lease.tenantName}</p> <!-- 金额格式化(避免小数点后多余零) --> <p>月租金:<fmt:formatNumber value="${lease.monthlyRent}" pattern="#,##0.00"/> 元</p> <!-- 时间格式化(适配 SQL Server datetime2) --> <p>起租时间:<fmt:formatDate value="${lease.startTime}" pattern="yyyy-MM-dd HH:mm:ss.SSS"/></p> <p>到期时间:<fmt:formatDate value="${lease.endTime}" pattern="yyyy-MM-dd HH:mm:ss.SSS"/></p> <!-- 条款列表(安全遍历,自动 HTML 转义) --> <h3>合同条款</h3> <ul> <c:forEach items="${lease.clauseList}" var="clause"> <li>${clause.content}</li> <!-- JSTL 自动转义 < > & 等字符 --> </c:forEach> </ul> </c:if>提示:
<fmt:formatDate>的pattern必须与 SQL Serverdatetime2(3)的精度匹配。若数据库存的是datetime2(0),则 pattern 应为yyyy-MM-dd HH:mm:ss,否则末尾.SSS会显示为.000。
4.2 实现“一键导出 Excel”功能:用 Apache POI 生成.xlsx文件流
ExportLeaseServlet.java不生成文件存服务器,而是直接写入响应流:
@WebServlet("/exportLease") public class ExportLeaseServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Long leaseId = Long.parseLong(req.getParameter("id")); // 1. 查询租约及关联数据(房屋、租客、支付记录) Lease lease = LeaseService.findById(leaseId); // 2. 创建 Excel 工作簿 XSSFWorkbook workbook = new XSSFWorkbook(); XSSFSheet sheet = workbook.createSheet("租约详情"); // 3. 写入表头 Row headerRow = sheet.createRow(0); String[] headers = {"合同编号", "房屋地址", "承租人", "起租日", "到期日", "月租金", "押金"}; for (int i = 0; i < headers.length; i++) { Cell cell = headerRow.createCell(i); cell.setCellValue(headers[i]); } // 4. 写入数据行 Row dataRow = sheet.createRow(1); dataRow.createCell(0).setCellValue(lease.getContractNo()); dataRow.createCell(1).setCellValue(lease.getHouseAddress()); dataRow.createCell(2).setCellValue(lease.getTenantName()); dataRow.createCell(3).setCellValue( new SimpleDateFormat("yyyy-MM-dd").format(lease.getStartTime())); dataRow.createCell(4).setCellValue( new SimpleDateFormat("yyyy-MM-dd").format(lease.getEndTime())); dataRow.createCell(5).setCellValue(lease.getMonthlyRent().doubleValue()); dataRow.createCell(6).setCellValue(lease.getDepositAmount().doubleValue()); // 5. 设置响应头,触发浏览器下载 resp.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); resp.setHeader("Content-Disposition", "attachment; filename=lease_" + leaseId + ".xlsx"); // 6. 写入输出流 try (OutputStream out = resp.getOutputStream()) { workbook.write(out); } workbook.close(); } }4.2.1 Maven 依赖必须指定 POI 版本以兼容 JDK 11
<!-- pom.xml --> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>4.1.2</version> <!-- JDK 11 兼容版本,5.x 需 JDK 17 --> </dependency>4.2.2 JSP 中调用导出链接必须带 CSRF Token(简易实现)
为防恶意脚本触发导出,在lease-list.jsp中:
<c:url var="exportUrl" value="/exportLease"> <c:param name="id" value="${house.id}"/> <c:param name="token" value="${sessionScope.csrfToken}"/> </c:url> <a href="${exportUrl}" class="btn btn-sm btn-outline-primary">导出Excel</a>并在LoginServlet中生成 Token:
req.getSession().setAttribute("csrfToken", UUID.randomUUID().toString().replace("-", ""));5. 系统级排错与性能加固:解决 SQL Server 连接泄漏、JSP 编译缓存、以及租约时间校验边界
5.1 连接泄漏的根因定位:finally块中必须显式关闭 ResultSet/Statement/Connection
常见错误写法:
// ❌ 危险!若 rs.next() 抛异常,conn 不会被关闭 Connection conn = DataSourceUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery(); while (rs.next()) { /* ... */ } conn.close(); // 若上面异常,此处永不执行正确写法(Java 7+ try-with-resources):
// ✅ 安全:即使循环中抛异常,资源也自动关闭 try (Connection conn = DataSourceUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { // 处理数据 } } // 自动调用 rs.close(), ps.close(), conn.close()5.2 JSP 编译类文件位置与热更新失效排查
Tomcat 默认将 JSP 编译为 Servlet 类存于:$CATALINA_HOME/work/Catalina/localhost/[your-app-name]/org/apache/jsp/
若修改list.jsp后页面不更新,检查:
conf/web.xml中JasperLoader的development参数是否为true(开发环境必须开启)work目录是否被 IDE 锁定(IntelliJ IDEA 常见问题,重启 IDE 或清空work目录)- JSP 中是否有
<%@ page isELIgnored="true" %>导致 EL 表达式不解析
5.3 租约时间校验的三个硬性规则必须在 Service 层强制执行
LeaseService.java中的createLease()方法需校验:
| 规则 | SQL Server 实现 | Java 层补充校验 | 说明 |
|---|---|---|---|
| 起租日 ≤ 到期日 | CHECK (start_time <= end_time) | if (startTime.after(endTime)) throw new IllegalArgumentException("起租日不能晚于到期日") | 数据库约束 + 业务层双重保险 |
| 无时间重叠 | SELECT COUNT(*) FROM lease_contract WHERE house_id = ? AND status = 'ACTIVE' AND NOT (end_time <= ? OR start_time >= ?) | 计算newStart与newEnd,代入上述 SQL | 防止两个租约覆盖同一时段 |
| 起租日 ≥ 当前日 | DEFAULT GETDATE() | if (startTime.before(new Date())) throw new IllegalArgumentException("起租日不能早于今天") | 避免历史合同被误创建 |
public void createLease(Lease lease) throws SQLException { // 1. 基础时间校验 if (lease.getStartTime().after(lease.getEndTime())) { throw new IllegalArgumentException("起租日不能晚于到期日"); } if (lease.getStartTime().before(new Date())) { throw new IllegalArgumentException("起租日不能早于今天"); } // 2. 查询是否存在重叠租约 String overlapSql = "SELECT COUNT(*) FROM lease_contract WHERE house_id = ? AND status = 'ACTIVE' " + "AND NOT (end_time <= ? OR start_time >= ?)"; try (Connection conn = DataSourceUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(overlapSql)) { ps.setLong(1, lease.getHouseId()); ps.setTimestamp(2, new Timestamp(lease.getStartTime().getTime())); ps.setTimestamp(3, new Timestamp(lease.getEndTime().getTime())); try (ResultSet rs = ps.executeQuery()) { rs.next(); if (rs.getInt(1) > 0) { throw new IllegalStateException("该房源在指定时段内已有有效租约,请选择其他时段"); } } } // 3. 插入新租约(事务内) String insertSql = "INSERT INTO lease_contract (...) VALUES (...)"; // ... 执行插入 }注意:
NOT (A OR B)等价于NOT A AND NOT B,此处表示“既不早于结束,也不晚于开始”,即存在时间交集。这是 SQL Server 判断区间重叠的标准写法。
本文还有配套的精品资源,点击获取