简介:这是一套基于Servlet+Jsp+JDBC的房屋租赁管理系统完整毕业设计资料包,面向计算机相关专业正在准备毕设的学生,以及需要JavaWeb实战练习的开发者。系统功能覆盖前台会员登录、免费注册、忘记密码、信息检索、留言板和公告浏览,以及后台出租求租、出售求购、交易记录、统计报表和房屋租金报表等模块,技术栈涉及JavaWeb、Servlet、JSP、jQuery、CSS,运行于JDK8,支持IDEA或Eclipse配合Navicat连接SQL Server数据库。包体包含项目源码、数据库脚本、毕业论文、系统详细配置指导以及PPT,压缩包约13.69MB。其中源码经调试可导入开发工具直接运行,数据库脚本用于初始化房屋与交易数据,配置指导帮助快速搭建环境,论文与PPT便于完成毕业设计文档与答辩展示,整体结构清晰,可直接作为毕业设计基础或项目实战练手。目前已有250人学习/下载,对于需要一套可运行、易扩展的房屋租赁管理系统参考方案的读者而言,具有很高的实用价值。
1. 基于 Servlet+JSP+JDBC 的房屋租赁管理系统:这套老技术栈为什么还有人在做
房屋租赁管理系统是 JavaWeb 方向最常见的毕业设计题之一,也是新手能真正跑通“浏览器 → Servlet → JDBC → MySQL 数据库”全链路的最小完整项目。这套系统要管的不只是“把房源信息存进数据库”,而是围绕「房东、租客、房屋、合同」四个核心对象,完成信息发布、租赁登记、合同生成和统计查询。对正在选毕设题或者刚学完 JavaWeb 的读者来说,它最大的价值在于:不需要接触 Spring 全家桶,只要弄懂 HTTP 请求、Servlet 生命周期、JSP 标签、JDBC 增删改查这四件事,就能把系统完整落地。本文按一个能交付验收的标准来讲这套系统的选型理由、数据库设计、核心代码写法、环境配置和上线打包的完整路径。
2. 为什么还在用 Servlet+JSP+JDBC:技术选型与开发环境搭建
2.1 这套技术栈的真实边界在哪里
很多人一听到 Servlet+JSP 就觉得过时,但实际做这套租赁系统,它反而是最能讲清楚原理的选择。Spring Boot 能帮你省掉配置,但也会把请求怎么进 Servlet、参数怎么绑定、连接怎么管理这些关键环节全部封装成黑匣子。毕业设计答辩时老师问“登录状态怎么保持的”“查询条件怎么拼接的”,如果你从 Spring Security 和 MyBatis-Plus 写起,大概率只能背术语;用 Servlet+JSP+JDBC,你自己写的每一行代码都能成为答辩素材。
这套技术栈的边界也很明确:适合并发量不大、业务逻辑清晰、表数量在十张以内的管理系统。房屋租赁系统恰好符合——没有高并发秒杀,没有复杂支付,没有分布式需求。最重的操作也就是房东发布房源、租客检索房源、生成租赁合同、管理员统计这几个动作。如果未来要扩展,业务层可以换成 Spring,但 Servlet 层和数据库层的设计仍然可以复用。
2.2 开发环境版本搭配与下载清单
做这套系统前,先把版本对齐,否则后面排查环境问题的时间比写代码还长。我建议的版本组合是:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 | 最稳妥,兼容 Tomcat 8.5 和 9.x |
| Tomcat | 8.5.x 或 9.0.x | 支持 Servlet 3.1/4.0 |
| MySQL | 5.7 或 8.0 | 5.7 对新手友好,8.0 需要配置驱动参数 |
| 开发工具 | IDEA 或 Eclipse | IDEA 社区版足够 |
| MySQL 驱动 | mysql-connector-java 5.1.49(对应 MySQL 5.7)/ 8.0.x(对应 MySQL 8.0) | 经典坑:驱动版本与数据库版本不匹配 |
常见做法是直接用 IDEA 新建一个普通的 Java Web 工程,而不是 Maven 工程。原因很实际:Maven 会自动联网下载依赖,如果网络不稳或者私服配置有问题,经常出现 IDEA 自动下载 Maven 依赖失败。传统方式只要手动把 mysql-connector-java 的 jar 包拷进 WEB-INF/lib,就不会有这类问题。
提示:开发环境不是越新越好。JavaWeb 老项目用 JDK 8 + Tomcat 8.5 + MySQL 5.7 是能少踩很多坑的组合,尤其是做毕业设计时间紧张的同学,别在环境上折腾太久。
2.3 创建工程的标准目录结构
一个规范的 JavaWeb 工程应该这样组织,后面扩展时不会乱:
house-rental/ ├── src/ │ ├── com/house/rental/ │ │ ├── servlet/ // 控制层:LoginServlet、HouseServlet... │ │ ├── service/ // 业务层:HouseService、ContractService │ │ ├── dao/ // 数据访问层:HouseDao、UserDao │ │ ├── entity/ // 实体类:House、User、Contract │ │ └── util/ // 工具类:DBUtil、StringUtil │ └── com/house/rental/filter/ // 编码、登录拦截过滤器 ├── web/ │ ├── static/ // CSS、JS、图片 │ ├── jsp/ // house-list.jsp、house-add.jsp... │ ├── WEB-INF/ │ │ ├── web.xml │ │ └── lib/ // mysql-connector-java.jar 等 └── sql/ └── house_rental.sql // 建库建表脚本这个分层方式对应 MVC 思想:Servlet 接收请求并控制页面跳转,Service 写业务规则(比如发布房源前校验房东身份),DAO 用 JDBC 操作数据库,JSP 只负责展示。把分层做好,后面答辩时“为什么这么设计”这一问就稳了。
3. 核心功能实现:从用户登录到房屋检索与合同生成
3.1 登录功能与 Session 状态管理:servlet 生命周期的一次完整演示
登录功能是所有管理系统的入口。这里你会发现 servlet 生命周期不是纯理论——用户点“登录”按钮之后,一个请求先经过 Tomcat,Tomcat 判断没有对应实例就调用构造器和 init(),然后执行 service() 分发到 doPost(),你在这里验证用户名密码,最后用完进入 service() 返回响应。整个流程走一遍比背十遍“init—service—destroy”都管用。
登录逻辑代码示例:
@WebServlet("/login") public class LoginServlet extends HttpServlet { private 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"); // 调用 DAO 层验证用户,注意密码比对要在 Service 层做 MD5 加密后比对 User user = userDao.findByUsernameAndPassword(username, MD5Util.md5(password)); if (user == null) { request.setAttribute("errorMsg", "用户名或密码错误"); request.getRequestDispatcher("/jsp/login.jsp").forward(request, response); return; } // 登录成功,将用户信息存入 Session,供 JSP 页面用 HttpSession session = request.getSession(); session.setAttribute("loginUser", user); // 重定向到首页而不是转发,避免刷新页面重复提交表单 response.sendRedirect(request.getContextPath() + "/house/list"); } }这段代码里有三个细节值得记住。第一,密码不能明文比对,Service 层里统一用 MD5(再加盐更好)处理后再查库。第二,request.getParameter 取到的全是字符串,涉及金额、日期时要注意类型转换。第三,登录成功后用 sendRedirect 而不是 forward——因为重定向是让浏览器重新发起一次 GET 请求到 /house/list,地址栏变化了,F5 刷新时不会把登录表单重新提交一遍,这是新手很容易踩的“刷新页面导致重复登录/重复下单”坑。
3.2 房屋发布与多表关联查询:JDBC 增删改查的完整业务载体
房屋发布是租赁系统的核心动作。房源上架时要同时写入房屋基本信息和房东信息,状态还要设置为“待审核”。这一步背后是 service 层事务控制——房屋表和房东表要同时成功,要么都失败。来看一个带事务的 DAO 写法:
public boolean addHouseAndLandlord(House house, Landlord landlord) { Connection conn = null; PreparedStatement pstmtHouse = null; PreparedStatement pstmtLandlord = null; try { conn = DBUtil.getConnection(); // 手动开启事务,JDBC 默认自动提交,多条 SQL 必须关掉这个默认值 conn.setAutoCommit(false); String sqlLandlord = "INSERT INTO landlord(name, phone, id_card) VALUES(?, ?, ?)"; pstmtLandlord = conn.prepareStatement(sqlLandlord, Statement.RETURN_GENERATED_KEYS); pstmtLandlord.setString(1, landlord.getName()); pstmtLandlord.setString(2, landlord.getPhone()); pstmtLandlord.setString(3, landlord.getIdCard()); pstmtLandlord.executeUpdate(); // 取出自增主键作为房屋的外键 landlord_id ResultSet rs = pstmtLandlord.getGeneratedKeys(); int landlordId = 0; if (rs.next()) { landlordId = rs.getInt(1); } rs.close(); String sqlHouse = "INSERT INTO house(address, area, price, landlord_id, status) VALUES(?, ?, ?, ?, 'PENDING')"; pstmtHouse = conn.prepareStatement(sqlHouse); pstmtHouse.setString(1, house.getAddress()); pstmtHouse.setBigDecimal(2, house.getArea()); pstmtHouse.setBigDecimal(3, house.getPrice()); pstmtHouse.setInt(4, landlordId); pstmtHouse.executeUpdate(); conn.commit(); // 两条 SQL 都成功才提交 return true; } catch (SQLException e) { if (conn != null) { try { conn.rollback(); // 任何一条出错回滚全部 } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { // 资源释放顺序:ResultSet → Statement → Connection // Connection 如果是连接池的,这里不是真正关闭,而是归还给池子 DBUtil.close(pstmtHouse, pstmtLandlord, conn); } }这段代码的要点在于:一,setAutoCommit(false) 是事务开始标志,很多人漏掉这一行,导致第一条 SQL 执行完自动提交,第二条失败时无法回滚;二,用 getGeneratedKeys 回填自增主键,避免查两次数据库;三,finally 块里资源释放顺序不能乱,如果先把 Connection 释放了再关 Statement,连接池中的连接会残留未关闭的语句对象,数据库连接池一段时间后就被“占满”。
多表关联查询是租赁系统检索功能的基础。前台检索“某区域可租房源+房东联系方式”时,一条 SQL 就能带出全部信息:
SELECT h.id, h.address, h.area, h.price, h.status, l.name AS landlord_name, l.phone AS landlord_phone FROM house h INNER JOIN landlord l ON h.landlord_id = l.id WHERE h.status = 'AVAILABLE' AND h.address LIKE CONCAT('%', ?, '%') AND h.price BETWEEN ? AND ? ORDER BY h.create_time DESC LIMIT ?, ?;这条 SQL 里的 LIMIT 是分页的实现核心。传入三个参数:关键词、价格下限、价格上限。JDBC 层用 PreparedStatement 的 setString/setBigDecimal 填充这三个问号,注意 LIKE 语句在 PreparedStatement 里不能用 %?% 这样写,必须用 CONCAT('%', ?, '%'),这是很多新手直接拼字符串后碰到的第一个 SQL 注入漏洞点。
3.3 JSP 页面与分页组件:让租客能按条件筛选房源
JSP 页面负责展示和收集数据。一个合格的房源列表页要完成三件事:展示分页数据、保留查询条件、管理按钮按角色显示。用 JSTL 拼接 HTML 是传统 JSP 项目打包 war 后最常用的做法。
<%-- house-list.jsp 核心部分 --%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <div class="filter-bar"> <form action="${pageContext.request.contextPath}/house/list" method="get"> <input type="text" name="keyword" value="${param.keyword}" placeholder="输入区域关键字"/> <input type="number" name="minPrice" value="${param.minPrice}" placeholder="最低价"/> <input type="number" name="maxPrice" value="${param.maxPrice}" placeholder="最高价"/> <button type="submit">搜索</button> </form> </div> <table class="house-table"> <tr> <th>地址</th> <th>面积</th> <th>月租</th> <th>状态</th> <th>房东</th> <th>操作</th> </tr> <c:forEach items="${page.list}" var="house"> <tr> <td>${house.address}</td> <td>${house.area}㎡</td> <td><fmt:formatNumber value="${house.price}" pattern="¥#,##0.0"/></td> <td> <c:choose> <c:when test="${house.status == 'AVAILABLE'}">可租</c:when> <c:when test="${house.status == 'RENTED'}">已租</c:when> <c:otherwise>待审核</c:otherwise> </c:choose> </td> <td>${house.landlordName} / ${house.landlordPhone}</td> <td> <c:if test="${sessionScope.loginUser.role == 'ADMIN'}"> <a href="house/edit?id=${house.id}">编辑</a> <a href="house/delete?id=${house.id}" onclick="return confirm('确定删除该房源?')">下架</a> </c:if> <a href="contract/create?houseId=${house.id}">签约</a> </td> </tr> </c:forEach> </table> <%-- 分页组件 --%> <div class="pagination"> <c:if test="${page.currentPage > 1}"> <a href="house/list?page=${page.currentPage - 1}&keyword=${param.keyword}">上一页</a> </c:if> <span>第 ${page.currentPage} / ${page.totalPages} 页</span> <c:if test="${page.currentPage < page.totalPages}"> <a href="house/list?page=${page.currentPage + 1}&keyword=${param.keyword}">下一页</a> </c:if> </div>分页参数是这套系统的必调参数。页码 page 从前端传入后必须做边界校验——小于 1 就置为 1,大于总页数就置为总页数,否则用户手动改 URL 参数 ?page=999 会得到一个空白页甚至 SQL 异常。每页条数我一般设为 10,如果房源数据量大到上千条,要靠索引和延迟加载配合,而不是单纯调大每页条数。JSP 里用 param.keyword 回显搜索条件才能保证翻页时条件不丢,这个细节做不好就会让用户点第二页时搜索条件重置。
4. 数据库设计与 JDBC 优化:表结构、连接池与多数据源注意点
4.1 五张核心表的字段设计与外键约束
房屋租赁系统的数据模型要围绕“租赁合同”这个核心业务来设计。最少需要五张表,表结构直接决定 Servlet 和 DAO 的编写量。
| 表名 | 关键字段 | 说明 |
|---|---|---|
| user | id, username, password, role | 系统登录用户,role 区分 ADMIN/AGENT |
| landlord | id, name, phone, id_card | 房东信息,一个房东可有多套房 |
| house | id, address, area, price, status, landlord_id, create_time | 核心房源表,status 用枚举字符串 |
| tenant | id, name, phone, id_card, create_time | 租客信息,签约前先建档 |
| contract | id, house_id, tenant_id, landlord_id, start_date, end_date, monthly_rent, deposit, status | 租赁合同表,外键最多 |
设计时要注意三个点。第一,金额字段用 DECIMAL(10,2) 而不是 FLOAT,避免租金计算出现 0.0000001 的精度问题。第二,外键约束要建,但要分清楚逻辑外键和物理外键——正式项目里所有关联都建物理外键会影响性能和迁移,毕设项目建物理外键反而能在答辩时展示你懂数据完整性。第三,status 字段用字符串 VARCHAR 加 CHECK 约束,比如 'AVAILABLE'、'RENTED'、'PENDING',不要用 0/1 数字,可读性差而且扩展第三种状态时不直观。
初始化建库脚本这样写:
CREATE DATABASE IF NOT EXISTS house_rental DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE house_rental; CREATE TABLE landlord ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, phone VARCHAR(20) NOT NULL, id_card VARCHAR(18), create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE house ( id INT AUTO_INCREMENT PRIMARY KEY, address VARCHAR(200) NOT NULL, area DECIMAL(6,2) NOT NULL, price DECIMAL(10,2) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'PENDING', landlord_id INT NOT NULL, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_house_landlord FOREIGN KEY (landlord_id) REFERENCES landlord(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- tenant、contract 表结构按相同规范继续加这里 ENGINE=InnoDB 必须写清楚。InnoDB 支持外键和事务,而 MyISAM 不支持——如果建表时没写,MySQL 5.5 之前默认是 MyISAM,事务代码就废了。utf8mb4 用四字节存储,租房合同里如果出现生僻字或 emoji 才会正常显示,这是 jdbc 连接项里的 characterEncoding 写入 SQL 脚本时最容易出现的字符集口径不一致问题。
4.2 数据库连接池参数与 JDBC API 封装
传统 JDBC 的 Connection 每次创建和销毁开销很大。在 Tomcat 里用连接池只需要配置 context.xml,不需要额外引入 jar 包。下面是一套适配 MySQL 8.0 的最小 Database 配置:
<?xml version="1.0" encoding="UTF-8"?> <Context> <Resource name="jdbc/houseRentalDB" auth="Container" type="javax.sql.DataSource" driverClassName="com.mysql.cj.jdbc.Driver" url="jdbc:mysql://localhost:3306/house_rental?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&characterEncoding=UTF-8" username="root" password="your_password" maxTotal="20" maxIdle="10" minIdle="5" maxWaitMillis="10000" initialSize="5" validationQuery="SELECT 1" /> </Context>关键参数里,maxTotal=20 是最大连接数,超过后请求会排队,如果线上并发超过这个数就要关注慢 SQL,不能盲目加大。maxWaitMillis=10000 是拿到连接的等待超时,超过 10 秒抛异常,配太短高峰期会被误杀,配太长四五个慢查询就能占满线程。validationQuery 配置成 SELECT 1,作用是每次从池里取出连接时验证可用性,防止 MySQL 默认的 8 小时超时断开导致拿到的 Connection 变成“死连接”。
配套的工具类封装核心是“把连接归还池子而不是关闭”这个语义。DBUtil 的 close 方法要注意这个细节:
public class DBUtil { private static DataSource dataSource; static { try { Context ctx = new InitialContext(); dataSource = (DataSource) ctx.lookup("java:comp/env/jdbc/houseRentalDB"); } catch (NamingException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { // 从连接池借出连接,不是 new 一个新连接 return dataSource.getConnection(); } public static void close(ResultSet rs, Statement stmt, Connection conn) { if (rs != null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (stmt != null) { try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }解释一下为什么 close 方法里 Connection 的关闭是安全的归还:连接池返回的 Connection 是代理对象,它的 close() 不是真的断开 TCP,而是把这条连接标记为空闲并放回池中。新手最容易犯的错是在 Service 层里把 conn 暴露给两个 DAO 方法顺序使用,第一个方法调用 close 后第二个方法操作一个已经“归还”的连接,就会报 Connection is closed。
4.3 多数据源场景下的“数据库同步”问题
一个系统同时连接多个库(比如日志库和业务库分离),或者用数据库同步工具做主从读写分离,尤其是毕设答辩时老师极可能追问的一套接口。这里只提跟 jdbc 最相关的一点:多数据源时一个 Connection 对应一个库,不要让 DAO 自己决定用哪条数据源——事务应该控制在 Service 方法内部。具体做法是在 Service 里传入数据源名称参数,DAO 方法签名必须接受 Connection 作为参数,而不是内部隐藏数据源切换,这样同一个事务方法里两条 SQL 才能保证用的是同一个 Connection,主从切换不会造成“空事务”。
5. 常见问题排查:驱动加载失败、中文乱码、事务与连接池踩坑记录
5.1 现象:Class.forName("com.mysql.jdbc.Driver") 报 ClassNotFoundException
这是用 MySQL 8.0 数据库但驱动 jar 还停留在 5.x 时的经典报错。MySQL 8.0 的驱动类名变成了 com.mysql.cj.jdbc.Driver,旧名称不再生效。另一种情况是 jar 包放在了 /lib 目录而不是 WEB-INF/lib——Tomcat 的类加载器只认自己环境下的 lib 目录。解决方法是去 mysql-connector-java 8.0.x 版本里找 mysql-connector-java-8.0.26.jar 这类文件,拷进 WEB-INF/lib 并重新构建 Artifact。如果用的 IDEA,还要记得在项目结构里把 jar 包“添加到库”,否则编译能过、运行时报错。
5.2 现象:页面中文全部显示为“???”
字符集有三道关口:JSP 文件本身的编码、Servlet 中 request 与 response 的编码、MySQL 表结构和 JDBC URL 的编码。四者只要有一个不一致,中文就乱。我现在的习惯是建立文件时全部选 UTF-8,web.xml 里再加一个强制过滤器,不给漏网之鱼。
<!-- web.xml 中配置字符集过滤器,位置要在所有 Servlet 之前 --> <filter> <filter-name>encodingFilter</filter-name> <filter-class>com.house.rental.filter.EncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>对应的 Filter 类实现里必须同时设置 request 和 response 的编码。很多教程只写了 request.setCharacterEncoding,结果从数据库读出来中文正常,但 through response 写回浏览器的 JSON 或页面文字乱码。另外 GET 请求的 query string 编码是 Tomcat 在 URI 层面处理的,要在 Tomcat 的 server.xml 里给 Connector 加 URIEncoding="UTF-8" 才能真正解决 URL 参数乱码,很多人改了 filter 没用就是因为漏了这一步。
5.3 现象:更新或删除房源后数据没有变化
这类“修改不生效”的问题十有八九是事务没提交。排查顺序三条:第一,确认代码里有没有 conn.setAutoCommit(false) 和 conn.commit() 成对出现;第二,看是否在方法中途 try-catch 吞掉了 SQLException,一旦 catch 住没往外抛,rollback 不会执行事务也不会提交;第三,如果是连接池环境,确认 close 方法是不是放在 finally 里,如果提前 close,事务被自动回滚。
一个容易被忽略的原因:PreparedStatement 复用了同一个 Connection 但没用同一个 transaction 边界。比如一个 Service 方法调用了两个 DAO 方法,每个 DAO 方法内部都调用了 DBUtil.getConnection(),这实际上是两条连接,事务失效。要解决必须把 Connection 从 Service 层传入 DAO 方法内部,所有操作共用一段事务。
5.4 现象:IDEA 自动下载 Maven 依赖失败(plugin 报 download from maven failed)
如果用了 Maven 管理依赖,这是最常见的“环境怪现象”。原因是中央仓库地址网络慢或证书问题。解决方法是换阿里云镜像,在 settings.xml 里加镜像,或者在项目 pom.xml 里拉下来的依赖缺少时先手动安装了 .m2 缓存。但我的习惯是毕设项目不用 Maven,手动管理 jar。将 Tomcat 的 lib 目录下自带的一些 jar(比如 servlet-api.jar、jsp-api.jar)复制到项目的 WEB-INF/lib,再在 IDE 里“添加为库”,编译运行时完全不用去网上下东西,这是最不受环境影响的做法。
5.5 现象:intellij idea 里用 SQL Server 数据源的 JDBC 自动下载同样报错
换了一个库就报相同的 Failure 现象,根本原因是 Maven 布鲁克坐标解析出意外版本导致下载失败时 IDEA 会多次自动重试,网络不给力就卡死。我们的项目是 MySQL,如果在写作业时交叉用了 SQLServer,正确做法是手动下载对应版本的 mssql-jdbc 包,通过项目结构导入而非让 IDE 网络解析。不管什么数据库,jdbc targetServertype 这类新驱动参数在旧项目里根本不存在,不要照抄别人 Oracle 的配置——不同数据库驱动 URL 写法完全不同,这属于“看帖不认版本”造成的翻车。
6. 打包上线与维护:从 War 部署到性能参数的进阶技巧
6.1 传统 JSP 项目打包 War 并部署到 Tomcat
开发完成后,最常见的交付方式是打包成 War 文件直接丢到 Tomcat 的 webapps 目录。步骤分四步:
第一步,在 IDEA 里选择 Build → Build Artifacts → 选择你的项目 Artifact(如果没配置,在 Project Structure → Artifacts 里新建一个 Web Application Exploded,再点 Fix 把所有依赖 jar 打包进去)。第二步,点击 Build Artifact 后选择 Build,生成的 War 文件在 out/artifacts/ 目录下。第三步,把这个 War 拷到 Tomcat 的 webapps/ 下,启动 Tomcat 后它会自动解压部署。第四步,浏览器访问 http://localhost:8080/项目名/jsp/login.jsp。
部署前的三件检查事项:第一,确认 lib 下的 mysql 驱动 jar 没有和 Tomcat 自带的 servlet-api.jar 冲突——把 servlet-api 从 lib 中删掉只留 Tomcat 的;第二,检查 context.xml 里的数据库密码是不是明文且写对的;第三,确认 web.xml 中欢迎页列表配置了 login.jsp。
War 包名尽量用短英文,不要带版本号和中文。如果 Tomcat 启动后页面 404,在 logs 目录下的 catalina.out 里搜索 Exception,大多数情况要么是连接数据库失败,要么是 jar 冲突。
6.2 上线后的三个必调参数
阶段目标是把“能跑起来的项目”变成“能稳定跑的项目”,三个关键调整项值得先做。
第一,JVM 内存参数。Tomcat 的 bin/setenv.sh(Windows 是 setenv.bat)里添加 CATALINA_OPTS="-Xms256m -Xmx102m",Xms 等于初始堆大小,Xmx 是最大堆,毕设系统 256m 起步。
第二,数据库连接池连接数上限。maxTotal 默认是 8,对房屋租赁这种并发不高的系统,20 就够了;调大意味着数据库要预留相应连接数,MySQL 默认 max_connections 151,别盲目调大。
第三,定期清理临时文件。Tomcat 的 webapps 下每次启动都会创建临时缓存目录,长期运行会产生大量 session 文件和日志,用 crontab 定期把 work/Catalina 目录下超过 7 天的缓存文件做清理。
6.3 从“能跑”到“可用”的验证思路与维护习惯
完成基本功能后,验证系统是否达到交付标准,可以从用户体验和代码健康度两个维度自检:
| 验证维度 | 检查项 | 预期结果 |
|---|---|---|
| 功能完整性 | 房东发布→管理员审核→租客签约→合同到期退租 | 全链路数据一致 |
| 数据正确性 | 合同结束日期等于退租日期,押金冻结状态正常释放 | 字段更新正确 |
| 安全基础 | 未登录直接访问 /house/list 会被拦截到登录页 | 重定向生效 |
| 并发基础 | 5 个用户同时浏览房源 | 页面不卡,连接池不爆 |
安全的拦截器很少有人写。写一个 LoginFilter,对所有 /jsp/ 下的页面和 /house/、/contract/这些 servlet 路径做校验,session 里没有 loginUser 就重定向回 login.jsp。这一步代码量很少但答辩加分极大,因为业务系统最忌讳未登录操作后台接口。
维护层面我的习惯是每次改数据库结构都在 sql/ 目录下留一份增量脚本,命名为 db_2024_v2.sql,不直接改原表。租房合同这类核心数据每月导出一次备份 SQL,放在本地。这样即使 Tomcat 崩溃重装,半小时内能恢复全部数据。
整套系统里最值得反复看的不是彩色页面,而是 Servlet 中每个 try-catch 的 rollback 和 finally 的资源释放代码。我把“事务要么全成功要么全失败、连接只借不永不关闭”这两条规则刻进了自己的编码习惯里。这个道理在 Spring 的 @Transactional 里也适用,只是框架帮你隐藏了细节。希望帮到你,下次处理这类管理系统时,先把数据库表结构和连接池参数定下来,再动手写第一行 Servlet 代码,你会发现整个开发流程顺很多。
本文还有配套的精品资源,点击获取