ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Servlet+JSP+JDBC实战:房屋租赁系统设计与完整开发笔记

Servlet+JSP+JDBC实战:房屋租赁系统设计与完整开发笔记 简介这套基于ServletJspJDBC的房屋租赁管理系统毕业设计资料包面向计算机相关专业正在准备毕设的学生以及需要JavaWeb项目实战练习的开发者。系统功能完善覆盖前台会员登录注册、密码找回、房源出租求租、出售求购、信息检索、留言板与站内公告后台则支持出售/求租/求购/交易信息管理和统计报表界面简洁、操作便捷。压缩包约13.69MB包含完整项目源码、SQL Server数据库脚本、毕业设计论文以及系统详细配置指导PPT可在Eclipse或IDEA中快速导入运行帮助读者理解Servlet、JSP、JDBC的整合开发流程。目前已有250人学习下载适合作为毕设方案参考或JavaWeb项目练手素材。1. 为什么还在用 ServletJspJDBC 写房屋租赁先别急着上 Spring Boot每年毕设季和课设季都会有一批人被要求“用 Java Web 做一个管理系统”但框架又没学透。你说用 Spring Boot答辩老师一句“那你说说 Spring 的自动装配原理”就能把你噎住。ServletJspJDBC 这套组合听起来老却正好是 Java Web 的底层骨架请求怎么进来、数据库连接怎么管、页面怎么渲染全部暴露在你面前。房屋租赁这个业务域也很合适——房源、租客、合同、账单四类数据天然适合做增删改查规模不大刚好能把三层架构讲清楚。这篇笔记就是把你从“拿到一个标题”带到“能跑通、能答辩、能交付”的完整路径。2. 四张表加三层代码房屋租赁系统的数据库设计与包结构2.1 为什么三层架构是这套组合的最稳姿势很多新手拿到这个题目第一反应是“直接在 JSP 里写 JDBC”。一个list.jsp里既能查数据库又能写 HTML看起来能跑但你很快会遇到两个问题一是页面一多改一个字段名要翻十几个文件二是答辩时老师问“你的项目是怎么分层”的你答不上来。常见做法是标准三层com.xxx.house.servlet控制层、com.xxx.house.service业务层、com.xxx.house.dao数据访问层再加上com.xxx.house.entity实体类和com.xxx.house.util工具类。控制层只接收请求、调业务、往 request 里放数据业务层处理逻辑判断DAO 层只做 SQL 和结果集封装。这里业务逻辑不复杂service 层可以写得薄一点但必须有——比如“签合同时要把房屋状态改成已出租”这个动作放 controller 里就是坏味道放 service 里才合理。为什么不用 Spring 的 IOC 来管理这些对象因为题目没让你用框架。手动new对象虽然原始但每行代码都是你写的门儿清。这也是这套组合在课程设计里经久不衰的原因——它逼着你理解“谁调谁”而不是靠注解自动装配。2.2 数据库设计四张核心表的字段与建表 SQL房屋租赁系统要管的核心数据有四块管理员登录用、房源、租客、合同。合同表同时关联房源和租客租金和押金作为合同字段这样既不需要单独的账单表又能满足“按月收租”的展示逻辑。房源的status字段必加用来标记“已出租/未出租”否则合同签完房源还在列表里挂着业务上说不通。CREATE DATABASE IF NOT EXISTS house_rent DEFAULT CHARSET utf8mb4; USE house_rent; -- 管理员表 CREATE TABLE t_admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(50) NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 房源表 CREATE TABLE t_house ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, address VARCHAR(200) NOT NULL, area DOUBLE DEFAULT 0, price DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0未出租 1已出租, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 租客表 CREATE TABLE t_tenant ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, phone VARCHAR(20), id_card VARCHAR(18), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 合同表 CREATE TABLE t_contract ( id INT PRIMARY KEY AUTO_INCREMENT, house_id INT NOT NULL, tenant_id INT NOT NULL, start_date DATE NOT NULL, end_date DATE NOT NULL, rent DECIMAL(10,2) NOT NULL, deposit DECIMAL(10,2) DEFAULT 0, status TINYINT DEFAULT 0 COMMENT 0进行中 1已结束, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_house FOREIGN KEY (house_id) REFERENCES t_house(id), CONSTRAINT fk_tenant FOREIGN KEY (tenant_id) REFERENCES t_tenant(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; INSERT INTO t_admin (username, password) VALUES (admin, 123456);字段类型有个细节金额一律用DECIMAL(10,2)别用DOUBLE。DOUBLE是浮点存 0.1 这种数在计算租金合计时会出精度问题虽然租赁系统金额简单但答辩老师问起来这就是一个加分回答。面积用DOUBLE没关系它不做精确计算。utf8mb4是必选的否则租客姓名里出现生僻字或 emoji 会直接报错——别问我怎么知道的见过太多人用utf8存“”这种字翻车。2.3 包结构与实体类先搭骨架再填肉数据库建完别急着写页面。先把包结构建好每个包放什么一目了然。我一般会建entity、dao、service、servlet、util五个包再加一个webapp目录放 JSP 和静态资源。实体类的字段必须和表字段一一对应名称保持一致否则 DAO 层封装结果集时要写一堆setXxx的映射代码。// entity/House.java package com.house.rent.entity; import java.math.BigDecimal; public class House { private Integer id; private String title; private String address; private Double area; private BigDecimal price; private Integer status; // 0未出租 1已出租 // 无参构造、getter/setter 省略 // 建议加一个 getStatusText() 方法 public String getStatusText() { return status ! null status 1 ? 已出租 : 未出租; } }这里有个小的设计心机在实体类里加getStatusText()JSP 页面直接调这个方法就能显示“已出租/未出租”不用在页面里写if判断。同理合同列表里要显示房源标题和租客姓名也可以在Contract实体里加两个冗余字段houseTitle、tenantName查询时用JOIN带出来。这个做法叫“面向视图的实体扩展”在课程设计里很实用——它能让你的 JSP 页面里没有一行 Java 逻辑答辩时也更像正规项目。3. 从登录到合同ServletJspJDBC 核心流程的落地写法3.1 数据库连接工具类一处配置处处复用JDBC 代码最容易写成“每个 DAO 方法里都DriverManager.getConnection一次”。连接数据库这个动作本身是有开销的但课程设计规模下不需要引入连接池一个工具类统一管理就够了。工具类要做三件事加载驱动、提供连接、安全关闭。// util/DBUtil.java package com.house.rent.util; import java.sql.*; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/house_rent?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(ResultSet rs, PreparedStatement ps, Connection conn) { if (rs ! null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (ps ! null) { try { ps.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn ! null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }参数说明URL 里的serverTimezoneAsia/Shanghai是 MySQL 8.x 的强制要求不加会报时区异常characterEncodingutf8保证写入数据库的中文不乱码useSSLfalse是本地开发关掉 SSL 加密避免 MySQL 8 默认开启 SSL 后控制台刷一堆警告。USER 和 PASSWORD 换成你自己的数据库账号。Class.forName放在静态块里类加载时只执行一次比在每个 DAO 方法里都写一行强得多。3.2 登录功能从 JSP 表单到 Servlet 再到 Session登录是每个系统都有的功能也是理解 Servlet 生命周期最好的例子。用户填表单、点提交请求打到/login映射的LoginServletServlet 里拿到用户名密码调 DAO 查数据库查到了就往 Session 里放一个标志位重定向到主页没查到就回登录页带一个错误提示。// servlet/LoginServlet.java package com.house.rent.servlet; import com.house.rent.dao.AdminDao; import com.house.rent.entity.Admin; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; WebServlet(/login) public class LoginServlet extends HttpServlet { private AdminDao adminDao new AdminDao(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 设置请求编码必须在读取参数之前 req.setCharacterEncoding(UTF-8); String username req.getParameter(username); String password req.getParameter(password); Admin admin adminDao.findByUsernameAndPassword(username, password); if (admin ! null) { HttpSession session req.getSession(); session.setAttribute(admin, admin); resp.sendRedirect(req.getContextPath() /house/list); } else { req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); } } }逻辑说明req.setCharacterEncoding(UTF-8)必须放在getParameter之前否则 POST 提交的中文参数在 Servlet 里读出来就是乱码。登录成功后用sendRedirect而不是forward因为重定向会改变地址栏 URL用户刷新页面时不会重复提交表单失败时用forward带着error属性回登录页登录页用${error}显示。所有业务接口的路径映射统一用WebServlet注解比在web.xml里写一堆servlet-mapping更直观但如果你想在配置指导里体现 web.xml 的存在也可以改成 XML 方式效果一样。登录页login.jsp的主题部分很简单一个form的action属性写loginmethod写post页头加一段c:if test${not empty error}的提示。这里开始引入 JSTL需要把jstl的 jar 也放进WEB-INF/lib。3.3 房源管理列表查询、分页与新增的核心代码房源管理是系统的门面。列表页要展示所有房源还得带分页——这是答辩时最容易加分的功能点。分页的本质是两条 SQL一条SELECT COUNT(*)数总数一条SELECT * FROM t_house LIMIT offset, pageSize。控制层接收page参数算好偏移量传给 DAO。// dao/HouseDao.java 核心方法 package com.house.rent.dao; import com.house.rent.entity.House; import com.house.rent.util.DBUtil; import java.sql.*; import java.util.ArrayList; import java.util.List; public class HouseDao { // 分页查询 public ListHouse findPage(int page, int pageSize) { ListHouse list new ArrayList(); String sql SELECT * FROM t_house ORDER BY id DESC LIMIT ?, ?; int offset (page - 1) * pageSize; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, offset); ps.setInt(2, pageSize); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { House house new House(); house.setId(rs.getInt(id)); house.setTitle(rs.getString(title)); house.setAddress(rs.getString(address)); house.setArea(rs.getDouble(area)); house.setPrice(rs.getBigDecimal(price)); house.setStatus(rs.getInt(status)); list.add(house); } } } catch (SQLException e) { e.printStackTrace(); } return list; } // 统计总数 public int count() { String sql SELECT COUNT(*) FROM t_house; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { if (rs.next()) { return rs.getInt(1); } } catch (SQLException e) { e.printStackTrace(); } return 0; } }参数说明LIMIT ?, ?是 MySQL 的分页写法第一个问号是偏移量第二个是每页条数。(page - 1) * pageSize是标准计算公式——第 1 页从 0 开始取第 2 页从pageSize开始取。这里用了try-with-resources写法JDCB 的Connection、PreparedStatement、ResultSet都实现了AutoCloseable方法结束自动关闭省的自己写 finally。执行完 SQL 后要挨个setXxx从结果集取值注意price是DECIMAL要用getBigDecimal而不是getDouble否则精度会有微妙差异。新增房源新增就是一个INSERTServlet 获取表单参数后封装成House对象调 DAO 的insert方法。这里有个细节新增完成后服务端直接sendRedirect(house/list)回到列表页而不是forward到某个 JSP——避免用户按 F5 时把同一条数据插入两次。这个“提交后重定向”的模式PRG 模式是课程设计里最容易被忽略但最实用的小技巧。3.4 合同签订多表联动与事务处理签合同是房屋租赁系统里最像“业务”的功能选择一套没出租的房子选择一个租客填起止日期、租金和押金提交后要做两件事——往t_contract表插一条记录同时把t_house.status改成已出租。这两步必须在一个事务里否则插了合同但房源状态没改就出现“数据不一致”的脏状态。// service/ContractService.java package com.house.rent.service; import com.house.rent.util.DBUtil; import java.sql.*; public class ContractService { public boolean createContract(int houseId, int tenantId, String startDate, String endDate, double rent, double deposit) { String insertSql INSERT INTO t_contract (house_id, tenant_id, start_date, end_date, rent, deposit, status) VALUES (?, ?, ?, ?, ?, ?, 0); String updateSql UPDATE t_house SET status 1 WHERE id ? AND status 0; Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交开启事务 try (PreparedStatement psInsert conn.prepareStatement(insertSql)) { psInsert.setInt(1, houseId); psInsert.setInt(2, tenantId); psInsert.setDate(3, Date.valueOf(startDate)); psInsert.setDate(4, Date.valueOf(endDate)); psInsert.setDouble(5, rent); psInsert.setDouble(6, deposit); psInsert.executeUpdate(); } // 防止房子被重复签约UPDATE 影响行数为 0 说明状态不是 0 int rows; try (PreparedStatement psUpdate conn.prepareStatement(updateSql)) { psUpdate.setInt(1, houseId); rows psUpdate.executeUpdate(); } if (rows 0) { conn.rollback(); // 房屋已被签约回滚 return false; } conn.commit(); return true; } catch (SQLException e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return false; } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } } }逻辑说明setAutoCommit(false)之后执行的INSERT和UPDATE都在同一个事务里要么一起成功commit要么一起失败rollback。UPDATE语句里带AND status 0是个并发小技巧——如果两单同时签同一个房源只有一单会更新成功另一单rows 0直接回滚。Date.valueOf(startDate)把前端传的2025-08-01字符串转换成java.sql.Date如果你的前端给的是2025/08/01这种格式valueOf会直接抛异常。设置status 0这一步在表单里用 下拉框提交——租客 id 在页面上显示成姓名这里要用JOIN表。合同列表需要展示房源标题和租客姓名JOIN的写法在 DAO 里要注意用别名否则两个表都有id字段取结果时会混淆SELECT c.id, h.title AS house_title, t.name AS tenant_name, c.start_date, c.end_date, c.rent, c.status FROM t_contract c JOIN t_house h ON c.house_id h.id JOIN t_tenant t ON c.tenant_id t.id ORDER BY c.id DESC4. 联调排错避坑JDBC 连不上、中文乱码、路径 404 的 5 个必踩坑4.1 驱动类和 URL 写错导致的通信链路失败现象控制台报ClassNotFoundException: com.mysql.jdbc.Driver或者Communications link failure。原因MySQL 8.x 的驱动类从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.Driver另外mysql-connector-java的 jar 没放到WEB-INF/lib目录Tomcat 加载不到。还有一种情况是 URL 里没写serverTimezoneMySQL 8 直接报The server time zone value Öйú is unrecognized。解决确认你用com.mysql.cj.jdbc.Driverjar 包放在WEB-INF/lib下并且 IDEA 里已经 “Add as Library”URL 里带serverTimezoneAsia/Shanghai。用curl或者 MySQL 客户端先测一下3306端口通不通排除是数据库没启动。4.2 中文乱码三连JSP 页面、Servlet 参数、数据库各乱一遍现象页面上看到中文正常写入数据库成了???或者页面上直接是乱码最诡异的是 JSP 显示正常、Servlet 读参正常、就是查出来的数据库中文是乱码。原因三个环节编码不统一。JSP 页面没写pageEncodingrequest.setCharacterEncoding(UTF-8)放在了getParameter之后JDBC URL 没带characterEncodingutf8。三个地方只要有一个漏了数据链路就会出乱码。解决JSP 第一行写% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %Servlet 的doPost里第一行就设req.setCharacterEncoding(UTF-8)JDBC URL 里加useUnicodetruecharacterEncodingutf8。数据库建库时显式DEFAULT CHARSET utf8mb4三管齐下。4.3 表单提交 404Servlet 映射路径和表单 action 对不上现象点击“新增房源”提交后地址栏变成http://localhost:8080/house_rent/addHouse然后 404。原因JSP 表单actionaddHouse但类上用WebServlet(/house/addHouse)映射或者项目部署路径是house_rent表单里写了绝对路径/addHouse把项目名丢了。actionaddHouse是相对路径浏览器会基于当前 URL 的目录去拼如果你当前在/house/list页面它拼出来是/house/addHouse对不上映射就 404。解决表单里统一用${pageContext.request.contextPath}/house/addHouse这种带上下文路径的写法。JSP 页面里${pageContext.request.contextPath}是 EL 表达式拿项目部署名这样无论部署名怎么改都不会丢。这是个既简单又专业的写法答辩看到不加这个反而会追问。4.4 关联查询显示的是 1、2 而不是房源名和租客名现象合同列表页里house_id列显示 1、2、3住户列显示数字 id看起来像没做联动。原因只查了t_contract表没做 JOIN或者在实体类里没有加houseTitle、tenantName这两个冗余字段。解决SQL 改用JOIN带出关联字段实体类加对应的冗余字段DAO 里用rs.getString(house_title)取值。JSP 直接输出${contract.houseTitle}。同理下拉框里存的是tenant_id的 value但option的显示文本应该循环t_tenant表生成这个循环也要用 DAO 查一次租客列表。4.5 SQL 注入和“忘记关连接”这两个老问题现象登录框输入 OR 11直接登录成功系统跑一会儿后 Tomcat 报Too many connections。原因把用户输入直接拼进了 SQL 字符串——SELECT * FROM t_admin WHERE username username getConnection()开了连接没用close关连接池被耗尽。解决所有 SQL 用PreparedStatement参数用setString等方式传入让 JDBC 自己处理转义每个 DAO 方法用try-with-resources或 finally 块关掉ResultSet、PreparedStatement、Connection。在你这个项目里DBUtil 的close方法就是干这个的。这两个问题不解决代码能跑但答辩时会被直接问住。5. 从能跑到能答辩把项目整理成可交付的源码包5.1 部署顺序一份让新手不迷路的配置指导交付物里那本“系统详细配置指导”文档核心就是环境版本和部署步骤。我一般会把版本固定下来JDK 8、Tomcat 9、MySQL 8.0、IDEA 2021jdbc 驱动用mysql-connector-java-8.0.30。不要用最新版驱动去配 Tomcat 10——Tomcat 10 的包名从javax.servlet换成了jakarta.servlet要改包名和所有注解课程设计没必要自找麻烦。组件版本说明JDK1.8稳定兼容 Tomcat 9Tomcat9.0.x最后一个用javax.servlet的主流版本MySQL8.0.x驱动类用com.mysql.cj.jdbc.DriverIDEIDEA 2021自带 Tomcat 集成模板部署步骤就是六步先启动 MySQL 并执行仓库里的house_rent.sql建库建表IDEA 里创建 Web 项目把源码拷进去把mysql-connector-java-8.0.30.jar放进WEB-INF/lib改DBUtil.java里的数据库账号密码配置 Tomcat 的 Artifacts 并设置Deployment里的 Application context 为/house_rent启动后访问http://localhost:8080/house_rent用admin/123456登录。这六步写清楚别人拿到你的源码包半小时内能跑起来这就是配置指导的价值。5.2 论文与 PPT 怎么和代码对齐论文里最怕出现“系统截图和实装代码不一致”。答辩季见过太多人论文写的是 Spring Boot 架构图代码却摆在 Servlet 里——一眼假。你的论文结构就按代码的层来写第二章需求分析就写四张表和系统角色第三章系统设计把三层架构图画清楚标注 Servlet 是控制层、DAO 是数据层第四章功能实现每个小节对应一个功能点。附录放核心代码片段别贴全文。PPT 控制在 10-12 页封面、需求、架构图、三张核心功能截图、创新点事务处理、分页、防 SQL 注入这五个部分是必有的。5.3 验证链路与最后一道自检交付前花十分钟过一遍这条核心链路登录 → 新增房源 → 新增租客 → 签合同 → 房源状态变“已出租” → 合同列表显示房名和人名。中间任何一个环节断了说明DAO层映射或者JSP取值有问题。我自己的习惯是跑完链路后故意签一套已经出租的房子看系统会不会拒绝——这能验证你在ContractService里写的AND status 0是否生效。这套组合虽然老但能让你把每个环节都摸透等到你哪天真的转去用 Spring Boot会发现它的路由、事务、ORM 不过是这篇文章里每一步的封装。希望这篇能帮你少走几步弯路把项目踏实交掉。本文还有配套的精品资源点击获取
返回列表