简介这是一套基于Servlet房屋租赁管理信息系统完整项目源码采用JSPServletJQueryAjax经典分层架构搭配MySQL数据库与Tomcat服务器运行适合JavaWeb初学者、毕业设计选题或租房类小型项目二次开发。包内共681个文件主要包含91个JSP页面、181个JS交互脚本、18个CSS样式、29个Java源码及对应的class文件并附带SQL脚本、XML配置与登录说明文档等压缩包大小仅2.6MB结构较清晰。系统前后台功能完整前台支持会员注册登录、密码找回、新闻资讯、出租求租/出售求购信息发布与检索、留言板等后台涵盖新闻与公告管理、会员管理、出租出售及求租信息管理、预订定金与合同收付款管理、房屋和销售租金统计报表、管理员设置及登录日志等功能。已有261人学习浏览适合作为课程设计参考或学习ServletJSP整合开发的实战案例可用于理解MVC思想及房屋租赁业务数据流转。1. 基于Servlet房屋租赁管理信息系统在中型课程设计与真实小项目之间的真实位置每年到课程设计季或毕业设计选型时总有同学问“还有必要用ServletJSP写项目吗”。我的答案是如果你要在一个学期内跑通一套含前后台的业务系统同时还要能讲清楚每个请求去了哪、数据库为什么这么设计那基于Servlet房屋租赁管理信息系统(含前后台)(JavaServletjspmysql)这套组合仍然是最稳妥的骨架之一。它没有Spring Boot那样的自动装配黑匣子但正因如此它的每个环节都暴露在你面前请求怎么进Servlet、SQL怎么执行、页面怎么渲染、Session怎么维持这些东西恰恰是面试时被反复问到的Servlet生命周期、JSP九大隐式对象、MySQL事务隔离级别的出处。这套系统主要解决三类诉求一是房屋租赁业务的线上化包括房源发布、租客登记、合同签订、租金收退、到期提醒这些核心流程二是权限上的前后台分离即管理员后台管房源和合同租客或前台用户只查房和提交求租意向三是数据可追溯所有合同和流水都落到MySQL表里方便统计账目。适合的人群是正在做课程设计或毕设的在校生、想巩固Java Web基础的一线开发新人以及业务量在百级房源以内、不需要微服务的小团队快速落地内部管理工具。2. 技术选型拆解Servlet、JSP、MySQL的分工与这套组合仍然经得起推敲的原因2.1 Servlet到底在项目里负责哪一层在正式编码前先明确Servlet在整个系统里的位置。它不是一个独立的业务框架而是Java Web规范里定义的、运行在Tomcat这类容器中的服务端组件。一个HTTP请求从浏览器发出后经过Tomcat的线程池处理会按web.xml或注解中的映射规则找到对应的Servlet然后调用它的doGet或doPost方法。在这个过程中Servlet承担的是控制器和门面角色解析请求参数、校验登录状态、调用Service/DAO层查询数据库、把结果放进request作用域或session作用域再转发到JSP页面渲染。很多初学者容易把Servlet写成一个大杂烩——查数据、拼HTML、处理业务全塞在一个方法里。这样跑的起来但后期改需求时你会异常痛苦。比如房租计算规则变了你需要同时改Servlet里的逻辑和JSP里的展示还容易漏改。常见做法是Servlet只做参数接收、调用服务和结果分发Service层处理租金计算、合同状态流转这些业务规则DAO层用JDBC访问MySQL。这样每个类职责单一答辩时也能清楚地讲出每一层做了什么。2.2 JSP负责展示的边界在哪里JSP的本质是一个在服务端动态生成HTML的模板Java代码可以嵌入其中但我不建议你在JSP里写大段Java逻辑。它的强项是使用EL表达式和JSTL标签快速遍历集合、条件渲染。房屋租赁系统里典型的JSP页面有这几类房源列表页展示所有可租房源、房源详情页展示配套设施和租金、租客信息填写页表单提交、合同预览页打印用、后台管理页分页表格。这里有个容易翻车的地方很多课程设计把登录校验逻辑写在JSP页面顶部的scriptlet里导致每个页面都重复一段判断Session是否存在的代码。更好的方式是统一拦截。我在后面第4章会给出基于Filter的登录校验实现这比在每个JSP里复制同一段代码靠谱得多。另外需要注意JSP第一次被访问时会翻译成Servlet再编译所以线上环境如果改了JSP文件需要等容器重新编译或重启Tomcat才会生效这也是后面避坑章节要讲的缓存问题。2.3 MySQL在数据模型上的关键设计原则房屋租赁系统看起来表不少但核心其实是围绕“房源”和“合同”两条主线的。MySQL负责的不仅是存储还有事务保证和数据一致性。比如登记一份新合同时要做两件事往合同表插入一条记录同时把房源表的状态从“空闲”改成“已出租”。这两步必须在一个事务里完成否则会出现合同已经登记但房源显示空闲或者房源被锁死但合同没建好的脏数据。在隔离级别上默认的REPEATABLE READ对于这个项目够用不需要刻意调低。真正要留意的是数据类型选型金额字段不要用FLOAT要用DECIMAL(10,2)因为房租计算涉及精确金额浮点数误差在累计后可能变成大问题面积字段用DECIMAL(8,2)状态字段用TINYINT加注释比如0空闲、1已租、2下架比直接存字符串更省空间也更好做索引。2.4 选型误区看到Spring Boot就觉得Servlet过时不得不承认现在很多公司直接上Spring Boot导致初学者觉得学Servlet是浪费时间。但一个现实是Spring Boot的底层就是内嵌Tomcat Servlet规范你连Servlet处理流程都不清楚出问题时根本定位不到是过滤器链的问题还是拦截器的问题。另一个现实是很多课程设计和毕业设计的评审大纲里明确要求使用JSP Servlet JDBC这套纯Java Web技术不允许直接用Spring Boot。在这种情况下与其东拼西凑“没用过”的框架代码不如把这份基于Servlet房屋租赁管理信息系统的Servlet、DAO、JSP分层吃透答辩时能扛住提问代码量也完全可控。3. 数据库设计与前后台模块划分从建表开始把业务边界锁死3.1 核心表结构设计五张表覆盖一条完整业务链在设计表结构前先把系统中的实体理清楚管理员、房源、租客、合同、租金流水。房源和租客之间是多对多关系一个租客可以更换多次房源一个房源也可以在不同时间段租给不同人所以中间必须由合同表来建立关联。合同再对应多笔租金流水形成一套完整链。下面的SQL脚本是这套系统的核心建表语句包含必需的字段设计和外键约束。CREATE DATABASE IF NOT EXISTS house_rent DEFAULT CHARACTER SET utf8mb4; USE house_rent; -- 管理员表登录后台使用 CREATE TABLE t_admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, real_name VARCHAR(32), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; -- 房源表覆盖基础信息和状态流转 CREATE TABLE t_house ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(128) NOT NULL, address VARCHAR(255) NOT NULL, area DECIMAL(8,2) COMMENT 户型面积平方米, price DECIMAL(10,2) COMMENT 月租金元, status TINYINT DEFAULT 0 COMMENT 0空闲 1已租 2下架, owner_name VARCHAR(32) COMMENT 房东姓名, owner_phone VARCHAR(20) COMMENT 房东电话, description TEXT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; -- 租客表登记承租人信息 CREATE TABLE t_tenant ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(32) NOT NULL, id_card VARCHAR(18) COMMENT 身份证号, phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; -- 合同表关联房源和租客 CREATE TABLE t_contract ( id INT PRIMARY KEY AUTO_INCREMENT, house_id INT NOT NULL, tenant_id INT NOT NULL, start_date DATE COMMENT 合同开始日期, end_date DATE COMMENT 合同结束日期, monthly_rent DECIMAL(10,2) COMMENT 约定月租金, deposit DECIMAL(10,2) COMMENT 押金金额, status TINYINT DEFAULT 1 COMMENT 1生效 0已结束, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_contract_house FOREIGN KEY (house_id) REFERENCES t_house(id), CONSTRAINT fk_contract_tenant FOREIGN KEY (tenant_id) REFERENCES t_tenant(id) ) ENGINEInnoDB; -- 租金流水表记录每一次收款 CREATE TABLE t_payment ( id INT PRIMARY KEY AUTO_INCREMENT, contract_id INT NOT NULL, period VARCHAR(16) COMMENT 账期如2024-03, amount DECIMAL(10,2), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_payment_contract FOREIGN KEY (contract_id) REFERENCES t_contract(id) ) ENGINEInnoDB;参数说明utf8mb4字符集是必须的它能完整存储四字节Emoji和特殊符号避免租客姓名或地址里出现生僻字导致入库报错。金额字段全部使用DECIMAL如果后面对账出现分差回查起来会容易得多。status字段用TINYINT而不用VARCHAR一方面节省空间另一方面在SQL里写WHERE status 1比写WHERE status 已租性能更好。外键约束在建表时加上保证合同表里不会出现不存在的房源ID或租客ID这是数据一致性最基础的防线。3.2 前后台功能边界管理员能做的不等于租客能做的这套系统的“含前后台”是需求上最容易让人困惑的点。我的划分方式是这样的后台面向管理员管理入口通常是独立/admin路径功能包括房源信息维护增删改查、上下架、租客档案管理、合同登记与续签、收租金记录前台面向租客和访客页面路径是普通/house列表页功能包括浏览房源、看详情、在线提交求租预约。两者共用同一套数据库但访问入口、权限级别、可操作范围完全不同。实际操作时前后台最容易混掉的是Servlet的包结构。我建议从一开始就按模块分包controller.admin存放后台Servletcontroller.house存放前台Servletservice统一放业务逻辑dao统一放JDBC操作。这样分包之后哪怕页面文件都丢在webapp目录下也能通过路径前缀清晰区分功能归属。后台删除房源这个高危操作需要二次确认前台页面上绝不能出现这类入口。3.3 初始化数据与测试账号让项目启动即有内容系统建完表后如果没有一点初始化数据启动进去空白一片不仅不好演示也不方便自测。在插入测试数据时至少要准备好两个管理员账号和一个状态的房源数据。下面给出最小初始化脚本-- 初始化管理员账号密码为123456的MD5值 INSERT INTO t_admin (username, password, real_name) VALUES (admin, e10adc3949ba59abbe56e057f20f883e, 系统管理员); -- 初始化房源示例数据 INSERT INTO t_house (title, address, area, price, status, owner_name, owner_phone) VALUES (两室一厅近地铁, 江城区幸福路12号, 78.50, 2800.00, 0, 王先生, 13800138001), (单间带独立阳台, 滨江区科技园旁, 35.00, 1200.00, 1, 李女士, 13800138002), (三居室整租, 老城区桥梁村, 105.00, 4500.00, 2, 陈先生, 13800138003);建议把管理员密码统一使用MD5加密后再入库这样项目部署时不会出现明文密码被同学或同事看到后尴尬的情况。至于为什么是“123456”因为演示系统方便记忆即可真正上线时再换成BCrypt。生成MD5的方式很多Java里使用MessageDigest即可后面登录校验时我会给出对应代码。4. 核心功能落地登录会话、房源分页、合同事务的ServletJSP完整实现4.1 构建项目骨架并配置Servlet映射项目采用标准Maven War包结构时源码目录是这样组织的src/main/java存放Java源码src/main/webapp存放JSP和静态资源pom.xml声明依赖。既然是ServletJSP项目依赖项不需要太多javax.servlet-api、javax.servlet.jsp-api、mysql-connector-java三个即可JSTL根据页面是否用到标签库而决定是否额外引入。在Servlet 3.0规范里可以不用web.xml而直接通过WebServlet(/admin/login)注解完成URL映射。但为了兼容性和让评审老师看清路由配置我建议保留web.xml文件把核心Servlet及初始化参数集中管理。下面是一个典型的登录Servlet入口package com.rent.controller.admin; 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 java.io.IOException; // 后台登录接口映射路径为 /admin/login WebServlet(/admin/login) public class AdminLoginServlet 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); String inputCaptcha request.getParameter(captcha); // 简单校验非空 if (username null || password null || .equals(username.trim())) { request.setAttribute(error, 用户名或密码不能为空); request.getRequestDispatcher(/admin/login.jsp).forward(request, response); return; } // 调用服务层验证账号密码verifyLogin 返回管理员ID或null Integer adminId new AdminService().verifyLogin(username, password); if (adminId ! null) { // 登录成功把管理员信息放入 Session request.getSession().setAttribute(adminId, adminId); request.getSession().setAttribute(adminName, username); response.sendRedirect(request.getContextPath() /admin/house/list); } else { request.setAttribute(error, 用户名或密码错误); request.getRequestDispatcher(/admin/login.jsp).forward(request, response); } } }这段代码里最值得关注的是request.setCharacterEncoding(UTF-8)和response.sendRedirect的区别。setCharacterEncoding必须放在读取任何参数之前否则中文用户名会乱码验证失败后使用forward转发可以保留request里的error提示并在JSP中显示验证成功则使用sendRedirect重定向这样能避免用户刷新浏览器时重复提交表单。AdminService().verifyLogin方法内部会调用DAO层把用户提交的密码先做MD5再和数据库里的密文比对这里不直接在Servlet里写SELECT语句是为了把业务和数据访问隔离开。4.2 基于Filter的统一登录校验不用在每个JSP里复制Session判断许多课程设计项目里每个后台页面顶部都有一行% if(session.getAttribute(adminId) null) response.sendRedirect(/login.jsp); %。这段代码分散在几十个页面里改一次路径要全局替换属于典型血泪经验。用一个Filter做统一拦截既减少重复代码又不会漏掉某个页面导致未授权访问。下面是完整的过滤器实现package com.rent.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; // 拦截所有 /admin/ 开头的请求但放行登录接口本身 WebFilter(/admin/*) public class AdminAuthFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; // 获取请求路径用于放行不需要登录的接口 String uri request.getRequestURI(); if (uri.endsWith(/admin/login) || uri.endsWith(/admin/login.jsp)) { chain.doFilter(request, response); return; } // 核心判断Session里是否存在用户 HttpSession session request.getSession(); Object adminId session.getAttribute(adminId); if (adminId null) { // 未登录跳转到后台登录页 response.sendRedirect(request.getContextPath() /admin/login.jsp); return; } chain.doFilter(request, response); } }这段代码的逻辑很直白所有/admin/*路径的请求都被它拦截登录页和登录接口本身放行其余任何请求若在Session中找不到adminId则强制重定向。好处有两点第一新增加的后台Servlet只要路径以/admin/开头就自动纳入权限保护第二完全不依赖JSP页面自觉。如果后续需要做更细粒度的权限控制比如区分超级管理员和普通管理员只需要在这个Filter里再加一个角色判断分支即可。参数说明WebFilter(/admin/*)的/*通配符覆盖全部子路径所以/admin/house/list会被拦截/house/list则不会。这也要求后台所有页面和Servlet必须统一放在admin路径下布局不规范会导致权限漏洞。4.3 房源分页列表LIMIT和OFFSET的正确用法房源列表是展示高频页面数据量小的时候可以一次性查出所有数据但房源一多几百条以上不做分页会让页面渲染明显变慢所以必须做分页查询。分页的核心在于两点前端传入当前页码后端计算出OFFSET再查询总记录数生成总页数。下面是一个完整的查询Servletpackage com.rent.controller.admin; import com.rent.dao.HouseDao; import com.rent.entity.House; 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 java.io.IOException; import java.util.List; WebServlet(/admin/house/list) public class HouseListServlet extends HttpServlet { private HouseDao houseDao new HouseDao(); Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); // 获取页码参数默认第一页 int page 1; int pageSize 10; String pageParam request.getParameter(page); if (pageParam ! null !.equals(pageParam.trim())) { try { page Integer.parseInt(pageParam); } catch (NumberFormatException e) { page 1; // 参数非法时回落到第一页 } } // 计算起始行偏移量 int offset (page - 1) * pageSize; // 查询总数和当前页数据 int totalCount houseDao.countAll(); ListHouse houseList houseDao.findByPage(offset, pageSize); int totalPages (int) Math.ceil((double) totalCount / pageSize); if (page totalPages totalPages 0) { page totalPages; // 页码越界保护 } // 存入Request并转发到JSP渲染 request.setAttribute(houseList, houseList); request.setAttribute(currentPage, page); request.setAttribute(totalPages, totalPages); request.getRequestDispatcher(/admin/house_list.jsp).forward(request, response); } }配合这个Servlet的DAO层查询方法也很简单public int countAll() { String sql SELECT COUNT(*) FROM t_house; // 使用JDBC执行查询结果为单行单列 } public ListHouse findByPage(int offset, int pageSize) { String sql SELECT id, title, address, area, price, status FROM t_house ORDER BY create_time DESC LIMIT ? OFFSET ?; // PreparedStatement设置参数后执行查询 }这里有两个细节值得说。第一LIMIT ? OFFSET ?这两个参数必须使用PreparedStatement设置而不能拼接字符串否则一旦offset被构造为0; DROP TABLE t_house这种恶意内容数据库就会出大问题。这是Web安全里最典型的SQL注入场景之一也是面试必问点。第二LIMIT和OFFSET的顺序不能写反MySQL正确语法是LIMIT 条数 OFFSET 起始偏移量写成OFFSET 0 LIMIT 10在某些数据库里虽然兼容但在MySQL里会直接报语法错误。分页参数默认值设为page1、pageSize10是根据实际场景定的。后台查询场景里一页10条足够管理端查看如果想做成前台房源列表可以调整为12条或20条符合不同的卡片布局。页码越界保护这段代码建议保留否则用户手动把URL上的?page999改大后端会查出一个空列表JSP如果没用条件渲染会显示一个空表格。4.4 合同登记与事务控制两件事必须同时成功合同登记是整个系统中风险最高的操作因为它要把“插入合同”和“更新房源状态”两条SQL放在同一个事务中。想象一下这样的场景如果先更新房源状态为已租再插入合同记录插入时外键校验发现租客ID不存在事务回滚后房源状态恢复原样这是正确的但如果把事务关掉两条SQL各自独立提交就可能出现房源已锁死但合同尚未成立的垃圾数据。下面给出的是带事务控制的DAO层核心代码import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; public boolean createContractWithHouseUpdate(Contract contract) { String insertContractSql INSERT INTO t_contract (house_id, tenant_id, start_date, end_date, monthly_rent, deposit, status) VALUES (?, ?, ?, ?, ?, ?, 1); String updateHouseSql UPDATE t_house SET status 1 WHERE id ? AND status 0; // 数据库连接来自DBUtil工具类 try (Connection conn DBUtil.getConnection()) { // 关闭自动提交开启手动事务 conn.setAutoCommit(false); try (PreparedStatement ps conn.prepareStatement(insertContractSql)) { ps.setInt(1, contract.getHouseId()); ps.setInt(2, contract.getTenantId()); ps.setDate(3, new java.sql.Date(contract.getStartDate().getTime())); ps.setDate(4, new java.sql.Date(contract.getEndDate().getTime())); ps.setBigDecimal(5, contract.getMonthlyRent()); ps.setBigDecimal(6, contract.getDeposit()); int insertRows ps.executeUpdate(); if (insertRows ! 1) { conn.rollback(); return false; } } try (PreparedStatement ps conn.prepareStatement(updateHouseSql)) { ps.setInt(1, contract.getHouseId()); int updateRows ps.executeUpdate(); if (updateRows ! 1) { // 房源状态可能不是0说明已被其他人租走 conn.rollback(); return false; } } // 两条SQL都成功提交事务 conn.commit(); return true; } catch (SQLException e) { // 异常触发回滚避免部分成功 try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return false; } }这段代码里最关键的逻辑在updateHouseSql中加了一个AND status 0条件。它的作用是当系统并发处理多个用户同时签订同一套房源时数据库层面会保证只有一个事务能成功更新状态。这是比在Java代码里加锁更可靠的做法因为Java单机锁只能在单实例内生效而SQL条件更新是数据库层面的行级锁控制。如果更新影响行数为0说明房源已经被其他请求先行签约了此时回滚并提示“该房源已被租出”是合理的业务反馈。关于try-with-resources语法Java 7以后的所有项目都应该使用它来管理数据库资源它能在代码块结束时自动关闭PreparedStatement和Connection省去手工close()的冗长代码。后面避坑章节里提到的连接泄漏问题绝大多数就是因为早期的try-catch-finally写法里漏掉了资源关闭。5. 必踩的坑与排查清单部署、并发、编码那些事5.1 MySQL连接池耗尽报错“Too many connections”但查不出SqlSource现象系统运行几天后后台列表页面偶尔白屏Tomcat日志里出现getConnection() timeout或Too many connections重启Tomcat后恢复正常过几天又复发。原因这是Java Web项目最经典的连接泄漏问题。DAO层类里的Connection没有在finally块中被关闭每次请求都从MySQL的连接池拿一条新连接用完没归还最终把MySQL的最大连接数默认151耗尽。很多课程设计项目的DAO代码里conn.close()只写在正常路径上一旦SQL执行抛出异常就跳过关闭逻辑。解决换用try-with-resources语法或确保finally块关闭连接。排查时先执行SHOW PROCESSLIST;看是否有大量Sleep状态的连接堆积然后逐个检查DAO方法里是否有异常路径未关闭连接。我通常会写一个DBUtil.close(conn, ps, rs)工具方法在finally中统一关闭三个资源。5.2 中文乱码出现在三个不同位置修改方式各不相同现象录入中文房源地址后列表页显示一串问号或者控制台输出乱码。原因中文乱码有三个独立入口。第一是请求参数乱码POST请求体里的编码需要request.setCharacterEncoding(UTF-8)处理而GET请求的查询参数编码则需要修改Tomcat的server.xml中Connector节点添加URIEncodingUTF-8属性第二是响应输出乱码需要response.setCharacterEncoding(UTF-8)并设置text/html;charsetUTF-8第三是数据库乱码建库时没有指定utf8mb4字符集或JDBC连接字符串缺少characterEncodingutf8参数。解决项目里统一在Filter里对请求和响应设置UTF-8编码数据库连接串改为jdbc:mysql://localhost:3306/house_rent?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai。注意serverTimezone参数必须加MySQL 8.x驱动版本对时区敏感不加会报CLIENT_PLUGIN_AUTH相关的时区异常。5.3 表单重复提交导致合同重复登记现象用户点了一次“提交合同”按钮页面卡了一下用户又点了几次数据库里出现多条相同合同记录。原因表单提交后服务器处理慢用户在等待期间重复点击提交按钮。这是所有管理系统的共性问题根源在于HTTP协议本身无法感知用户是不是有意重复点击需要应用层做防重控制。解决前端改法简单粗暴在提交按钮的onclick事件里把按钮置灰禁用后端改法更可靠在Session中存放一个一次性令牌Token表单里渲染这个TokenServlet处理完合同登记后立即将其失效下一次重复请求携带的Token在Session中已不存在直接拒绝。两个方案同时做比较稳妥。5.4 JSP修改刷新不生效页面还是旧版本现象修改了JSP文件里的提示文案或样式重启Tomcat后访问还是旧页面。原因JSP首次访问会被容器编译成Java类并缓存在Tomcat的work目录里。如果项目部署时复制的产物不完整或者使用的是打包后的War包没有解压清理干净Tomcat可能会直接复用旧的编译缓存。另外一个常见场景是浏览器本地缓存把旧页面缓存住了尤其是带?v1这样的查询参数请求更容易被缓存。解决开发阶段定期清理Tomcat的work/Catalina/localhost目录部署时如果改过JSP删除work目录再重启Tomcat。浏览器侧使用CtrlF5强制刷新排查。更优雅的做法是给JSP里静态资源的引用加上版本号参数比如style.css?v20240601这样每次发版修改版本号就能规避浏览器缓存问题。5.5 时间字段差8小时日期落库偏了一天现象数据库里记录的合同开始日期比页面上选的日期少一天或者前后台显示时间不一致。原因MySQL驱动版本较新时useSSL和时区参数的处理方式有变化尤其是serverTimezone没设置或设置为UTC时数据库会把本地时间按UTC存进库查询出来再转换一次造成8小时偏移。从MySQL 8.0开始连接时区参数不显式配置会直接抛出异常。解决JDBC连接串加上serverTimezoneAsia/Shanghai并保证数据库服务端时区也是08:00风格。要注意的是MySQL数据库可以通过SELECT NOW();查询当前时间来判断数据库自身时区是否正确如果数据库时间和系统时间不一致需要在MySQL配置文件的[mysqld]部分加default-time-zone 08:00。6. 上线前验证与进阶方向从能跑到能答辩、能讲原理6.1 用JMeter做一次最基础的登录并发测试系统开发完不能只看“能跑”还要验证它在大一点的并发下不会立刻崩溃。使用JMeter做一次测试门槛很低几步就能完成在“测试计划”里添加线程组线程数设为50、循环次数设为10此时模拟的是500个请求在线程组下添加HTTP请求配置服务器地址、端口、路径/admin/login请求方法选POST在参数列表里填入username和password最后添加查看结果树和聚合报告组件运行后重点看错误率和响应时间分布。如果测试结果里出现大量5xx错误回到第5章的连接池排查如果响应时间线性增长而没返回错误大概率是数据库查询缺少索引。此时在MySQL执行EXPLAIN SELECT * FROM t_house WHERE status 1 ORDER BY create_time DESC LIMIT 10;查看type字段是否为ALL全表扫描是的话就需要为t_house.status加索引。这个分析和调优过程本身就是答辩时很好的加分素材。6.2 日志与监控t_log表记录关键操作合同登记、租金收款、房源删除这几个操作属于业务敏感操作系统里最好有一个t_log表记录操作日志。每条日志包含操作人ID、操作类型、操作内容、操作时间、操作IP五个字段。在Servlet里调用日志Service把日志写入数据库即使页面不展示以后排查问题也有据可查。这个细节在很多课程设计中是被忽略的而你做了在评审眼中就是“有工程意识”。日志表的数据量会逐渐增大设计时不需要加索引太重按时间字段建一个普通索引即可条件允许的话定期清理几个月前的数据。6.3 想往生产走连接池和分层必须先升级如果这套系统不只是交作业而是要放到真实环境服务几十个用户有两点必须先做一是把JDBC直连换成连接池组件比如Druid或HikariCP。连接池的核心作用是复用连接避免每次请求都经历TCP握手和MySQL认证的开销。在pom.xml引入依赖后把DBUtil.getConnection()内部实现从DriverManager.getConnection()换成从连接池获取即可调用方完全不用改动这正好体现出分层的好处。二是为密码加密升级策略MD5已经不适合安全要求较高的场景密码存储改成BCrypt加盐校验逻辑同步调整。6.4 从课设到面试把“技术栈”讲成“方案”做完这套基于Servlet房屋租赁管理信息系统之后不要急着把它改成Spring Boot先把这几个问题想清楚Servlet实例化和销毁的时机是什么单例多线程模式下如何保证成员变量线程安全Session和Cookie的关系是什么forward和redirect的本质区别在哪里WHERE status 0加条件更新为什么能防并发重复签约这些问题能答清楚才真正消化了这套小系统的价值。如果后面想接Spring Boot迁移路径也不是推翻重写而是用Spring MVC的Controller替代Servlet的WebServlet用JdbcTemplate替代手写JDBC业务逻辑和数据库设计仍然可以复用。我自己的习惯是每个项目交付前都拿测试数据把核心流程重新走一遍每次都能发现一两个没覆盖到的边界场景。最后再说一句把Servlet、JSP、MySQL这套基本功吃透再去看Spring Boot里的自动配置、内嵌容器你会觉得豁然开朗。希望帮到你。本文还有配套的精品资源点击获取