简介:本资源是一套完整的JavaWeb毕业设计实战项目——基于Servlet+JSP+JDBC开发的房屋租赁管理系统,面向计算机专业本科生、Java初学者及毕设选题需求者,解决课程设计与毕业实践中的系统开发能力训练问题。压缩包为ZIP格式,大小13.69MB,包含可直接运行的源码工程、SQL Server数据库脚本、系统详细配置指导文档、毕业论文(含需求分析、系统设计与实现)、答辩用PPT等核心内容,覆盖开发、部署、演示全流程。已有250人学习下载,资源经Eclipse或IDEA严格调试,支持JDK8环境一键启动,附带前台会员/管理员双角色功能模块(如出租求租发布、信息检索、收藏留言、统计报表等)及后台数据管理能力。读者可获得完整可运行系统、规范论文写作范例、环境配置避坑指南与前后端协同开发逻辑梳理,切实提升Web应用开发与工程交付能力。
1. 为什么用 Servlet+JSP+JDBC 做房屋租赁系统,现在还值得投入?
这不是一个“过时技术堆砌”的毕设应付项目——而是JavaWeb 工程能力的最小闭环验证场。当你在 IntelliJ IDEA 或 VS Code 里敲下第一个HttpServlet#doGet,配置好web.xml或注解路由,再把ResultSet一行行映射成 JSP 表格,最后用 JDBC 连上 SQL Server 的那一刻,你真正踩到的是 Web 开发最底层的三块基石:请求响应生命周期、服务端模板渲染、关系型数据持久化。很多 Spring Boot 新手写不好事务控制,不是因为注解不熟,而是没亲手写过Connection.setAutoCommit(false)和conn.rollback();不少人在 MyBatis 动态 SQL 里翻车,根源是没在 JSP<c:forEach>里调试过空集合导致页面 500 的真实报错堆栈。这个系统看似简单(房源增删改查、租客登记、合同生成、租金记录),但恰恰卡在「业务逻辑够重、技术边界够清晰、部署路径够完整」的黄金交点上:它必须跑在 Tomcat 上,必须连 SQL Server(不是 H2 或 HSQL),必须区分request.setAttribute()和session.setAttribute()的作用域,必须处理java.sql.SQLException: Cannot open database这类真实连接失败。适合两类人:一是大三刚学完《Java 程序设计》想落地 Web 模块的本科生,二是转岗 Java 后端想补全传统 Web 容器链路的开发者。别被“老技术”标签骗了——VS Code 配置 Servlet 环境、SQL Server 2022 兼容性、JSP 在 JDK 17+ 下的编译陷阱,全是今年校招面试官现场让你 debug 的真题。
2. 从零搭起运行环境:Tomcat + SQL Server + JDK 版本对齐是第一道生死线
2.1 JDK 与 Tomcat 的版本绑定关系:别让 JDK 17 成为你的系统启动失败元凶
Servlet 规范和容器实现存在强版本耦合。JDK 17 是当前主流 LTS 版本,但它不兼容 Tomcat 8.x 和 9.0.x。实测中,若你用jdk-17.0.1+apache-tomcat-9.0.65,启动时会直接抛出java.lang.UnsupportedClassVersionError: javax/servlet/Servlet has been compiled by a more recent version of the Java Runtime。原因在于:Tomcat 9.0.x 编译目标字节码是 Java 8,而 JDK 17 默认生成 class 文件版本 61(对应 Java 17),JVM 拒绝加载低版本编译器生成的类。正确组合只有两个:
| JDK 版本 | Tomcat 版本 | Servlet 规范 | 关键适配点 |
|---|---|---|---|
| JDK 8u361+ | Tomcat 8.5.90 / 9.0.83 | Servlet 3.1 / 4.0 | 最稳,教学案例默认选型,SQL Server JDBC Driver 9.4+ 兼容 |
| JDK 17.0.1+ | Tomcat 10.1.12+ | Servlet 5.0 | 必须用jakarta.servlet.*包名,所有 import 要改,web.xml中<web-app>命名空间需更新 |
提示:如果你用的是 VS Code + Extension Pack for Java,务必在
settings.json中显式指定 JDK 路径,并确认tomcat-server插件指向的 Tomcat 目录与 JDK 版本匹配。执行java -version和CATALINA_HOME/bin/version.bat(Windows)或./version.sh(Linux)双验证。
2.2 SQL Server 安装与 JDBC 驱动选型:避开 SSL 加密握手失败这个高频坑
SQL Server 2022 是当前最新稳定版,但它的默认安装禁用 TCP/IP 协议且关闭 1433 端口。很多同学导入数据库后死活连不上,日志里反复出现The driver could not establish a secure connection to SQL Server by using Secure Sockets Layer (SSL) encryption,本质不是驱动问题,而是服务未监听。必须手动开启:
- 运行
SQL Server Configuration Manager→ 展开SQL Server Network Configuration→ 右键Protocols for MSSQLSERVER→ 启用TCP/IP - 双击
TCP/IP→ 切换到IP Addresses选项卡 → 找到IPAll区域 → 清空TCP Dynamic Ports,在TCP Port填1433 - 重启
SQL Server (MSSQLSERVER)服务(通过services.msc)
驱动方面,绝对不要用sqljdbc4.jar或sqljdbc6.jar——它们已废弃且不支持 TLS 1.2+。必须下载 Microsoft JDBC Driver 12.4 for SQL Server (2023 年 10 月发布),解压后取mssql-jdbc-12.4.2.jre11.jar(JDK 11+ 兼容)或mssql-jdbc-12.4.2.jre17.jar(JDK 17+ 专用)。将其放入WEB-INF/lib/目录后,在context.xml中配置连接池时,必须显式关闭 SSL 验证(仅开发环境):
<!-- META-INF/context.xml --> <Resource name="jdbc/houseRent" 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=HouseRentDB;encrypt=false;trustServerCertificate=true;" username="sa" password="YourStrong@Pass1" maxActive="20" maxIdle="10" minIdle="5" initialSize="5" removeAbandonedOnBorrow="true" removeAbandonedTimeout="60" logAbandoned="true"/>注意:
encrypt=false;trustServerCertificate=true是开发阶段绕过证书校验的合法配置,生产环境必须部署有效证书并设encrypt=true。trustServerCertificate=true解决的是“驱动无法验证服务器证书链”的核心报错,不是玄学开关。
2.3 VS Code 配置 Servlet 开发环境:比 IDEA 更轻量,但需手动补全构建链
VS Code 本身不带 JavaWeb 支持,依赖三个扩展协同工作:
- Extension Pack for Java(必装,含 Language Support、Debugger、Test Runner)
- Tomcat for Java(由 Red Hat 提供,提供服务器管理界面)
- Project Manager for Java(快速切换 Maven/Gradle 项目)
关键步骤是手动配置pom.xml或build.gradle的 Web 资源路径。若用 Maven,pom.xml必须声明warpackaging 并添加maven-war-plugin:
<packaging>war</packaging> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>3.3.2</version> <configuration> <failOnMissingWebXml>false</failOnMissingWebXml> <!-- 允许无 web.xml --> <warSourceDirectory>src/main/webapp</warSourceDirectory> </configuration> </plugin> </plugins> </build>然后在 VS Code 中按Ctrl+Shift+P→ 输入Tomcat: Add Server→ 选择 Tomcat 安装目录 → 右键项目 →Tomcat: Deploy。此时若提示Artifact not found,说明target/*.war未生成,需先执行mvn clean package。血泪经验:VS Code 的 Tomcat 插件不会自动触发 Maven 构建,必须手动打包一次,否则部署永远失败。
3. 数据库设计与初始化:SQL Server 2022 下的建库脚本必须包含这三个强制项
3.1 创建 HouseRentDB 数据库:字符集、排序规则与兼容级别决定中文存储成败
SQL Server 的COLLATE排序规则直接影响中文字段是否乱码。若用默认SQL_Latin1_General_CP1_CI_AS,插入北京市朝阳区建国路 88 号会变成??????????。必须在建库时显式指定 UTF-8 兼容排序规则:
-- 创建数据库(SQL Server 2019+ 支持 UTF-8) CREATE DATABASE HouseRentDB COLLATE Latin1_General_100_CI_AS_SC_UTF8 WITH EDITION = 'Standard', SERVICE_OBJECTIVE = 'S0'; GO -- 启用 READ_COMMITTED_SNAPSHOT 避免读写阻塞(高并发场景必备) ALTER DATABASE HouseRentDB SET READ_COMMITTED_SNAPSHOT ON; GO -- 设置兼容级别为 150(对应 SQL Server 2019),确保新语法可用 ALTER DATABASE HouseRentDB SET COMPATIBILITY_LEVEL = 150; GO说明:
Latin1_General_100_CI_AS_SC_UTF8是 SQL Server 2019 引入的 UTF-8 排序规则,SC表示 Supplementary Characters(支持 emoji 和生僻汉字),UTF8表示底层存储用 UTF-8 编码。READ_COMMITTED_SNAPSHOT ON开启行版本控制,避免SELECT被UPDATE阻塞,这是租赁系统查房状态时的关键优化。
3.2 核心表结构设计:用外键约束 + 默认值 + 检查约束构筑数据一致性防线
房屋租赁业务的核心矛盾是「房源状态流转」与「合同生命周期」的强耦合。不能只建house和contract两张表,必须引入中间状态控制。以下是经过三次迭代验证的最小可行表结构:
| 表名 | 字段说明 | 关键约束 |
|---|---|---|
house | house_idPK,titleNVARCHAR(100),areaDECIMAL(5,2),price_monthlyMONEY,statusCHAR(1) CHECK(status IN ('A','R','L')) — A=待租,R=已租,L=下架 | status默认'A',禁止 NULL |
tenant | tenant_idPK,nameNVARCHAR(50),id_cardCHAR(18) UNIQUE,phoneCHAR(11) | id_card用CHECK(id_card LIKE '[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][Xx]')校验身份证格式 |
contract | contract_idPK,house_idFK,tenant_idFK,start_dateDATE,end_dateDATE,rent_amountMONEY,statusCHAR(1) CHECK(status IN ('A','E','T')) — A=生效,E=到期,T=终止 | start_date默认GETDATE(),end_date用CHECK(end_date > start_date) |
建表脚本必须包含ON UPDATE CASCADE和ON DELETE RESTRICT:
-- 房源表 CREATE TABLE house ( house_id INT IDENTITY(1,1) PRIMARY KEY, title NVARCHAR(100) NOT NULL, area DECIMAL(5,2) NOT NULL, price_monthly MONEY NOT NULL, status CHAR(1) NOT NULL DEFAULT 'A' CHECK(status IN ('A','R','L')), created_time DATETIME2 DEFAULT GETDATE() ); -- 租客表 CREATE TABLE tenant ( tenant_id INT IDENTITY(1,1) PRIMARY KEY, name NVARCHAR(50) NOT NULL, id_card CHAR(18) NOT NULL UNIQUE, phone CHAR(11) NOT NULL, CHECK(id_card LIKE '[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][Xx]') ); -- 合同表(关键:级联更新房源状态) CREATE TABLE contract ( contract_id INT IDENTITY(1,1) PRIMARY KEY, house_id INT NOT NULL, tenant_id INT NOT NULL, start_date DATE NOT NULL DEFAULT GETDATE(), end_date DATE NOT NULL, rent_amount MONEY NOT NULL, status CHAR(1) NOT NULL DEFAULT 'A' CHECK(status IN ('A','E','T')), CHECK(end_date > start_date), FOREIGN KEY (house_id) REFERENCES house(house_id) ON UPDATE CASCADE ON DELETE RESTRICT, FOREIGN KEY (tenant_id) REFERENCES tenant(tenant_id) ON UPDATE CASCADE ON DELETE RESTRICT );逻辑说明:
ON UPDATE CASCADE保证当house_id主键变更时(极少见),合同表自动同步;ON DELETE RESTRICT防止误删房源导致合同孤儿。CHECK约束比应用层校验更可靠——比如身份证格式,数据库层拦截比 Java 正则快 10 倍且不可绕过。
3.3 初始化测试数据:用INSERT ... SELECT一次性注入 50 条真实房源
手动 INSERT 50 条数据效率太低。利用 SQL Server 的master..spt_values系统表生成数字序列,再结合NEWID()生成随机字符串:
-- 批量插入 50 套测试房源 INSERT INTO house (title, area, price_monthly, status) SELECT '北京' + CASE ABS(CHECKSUM(NEWID())) % 4 WHEN 0 THEN '朝阳' WHEN 1 THEN '海淀' WHEN 2 THEN '东城' ELSE '西城' END + '区' + CAST(ABS(CHECKSUM(NEWID())) % 100 AS VARCHAR) + '号公寓', ROUND(50 + (ABS(CHECKSUM(NEWID())) % 100) * 0.5, 2), -- 面积 50~100 平 3000 + (ABS(CHECKSUM(NEWID())) % 15) * 500, -- 月租 3000~10000 CASE ABS(CHECKSUM(NEWID())) % 3 WHEN 0 THEN 'A' WHEN 1 THEN 'R' ELSE 'L' END FROM master..spt_values t1 CROSS JOIN master..spt_values t2 WHERE t1.type = 'P' AND t2.type = 'P' AND t1.number BETWEEN 1 AND 7 AND t2.number BETWEEN 1 AND 7 AND (t1.number * t2.number) <= 50;参数说明:
master..spt_values是 SQL Server 内置的数字辅助表,type='P'表示正整数类型,number字段范围 0~2047。CROSS JOIN产生 49 行(7×7),满足 50 条需求。ROUND(...,2)确保面积保留两位小数,CAST(... AS VARCHAR)避免隐式转换错误。
4. Servlet 层核心逻辑:用责任链模式拆解「发布房源」的七步校验
4.1HouseServlet的 doPost 流程:从 request.getParameter 到事务提交的完整链路
HouseServlet不是简单地接收参数存库,而是承载了业务规则引擎。以「发布新房源」为例,必须串联七个校验环节,任一失败即中断:
- 空值校验:标题、面积、价格不能为空
- 数值范围校验:面积 ≥ 20 ㎡,价格 ≥ 1000 元
- 重复房源校验:同一小区同一楼号不能重复发布
- 敏感词过滤:标题含“投资回报率”“稳赚不赔”等词拒绝
- 图片上传校验:文件大小 ≤ 2MB,格式为 JPG/PNG
- 管理员权限校验:非 admin 用户禁止发布
- 数据库唯一约束兜底:
UNIQUE(title, area)索引防并发插入
代码实现采用Chain of Responsibility模式,每个 Handler 只负责一个职责:
// src/main/java/com/rent/servlet/HouseServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 构建责任链 Handler chain = new EmptyCheckHandler() .setNext(new RangeCheckHandler()) .setNext(new DuplicateCheckHandler()) .setNext(new SensitiveWordHandler()) .setNext(new ImageUploadHandler()) .setNext(new AdminAuthHandler()); // 2. 封装请求数据 House house = new House(); house.setTitle(request.getParameter("title")); house.setArea(Double.parseDouble(request.getParameter("area"))); house.setPriceMonthly(Double.parseDouble(request.getParameter("price"))); house.setStatus("A"); // 3. 执行链式校验 Result result = chain.handle(house, request); if (!result.isSuccess()) { request.setAttribute("error", result.getMessage()); request.getRequestDispatcher("/add_house.jsp").forward(request, response); return; } // 4. 事务提交(关键:Connection 必须来自 DataSource) Connection conn = null; PreparedStatement ps = null; try { conn = dataSource.getConnection(); // 从 JNDI 获取连接池 conn.setAutoCommit(false); // 关闭自动提交 String sql = "INSERT INTO house (title, area, price_monthly, status) VALUES (?, ?, ?, ?)"; ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); ps.setString(1, house.getTitle()); ps.setDouble(2, house.getArea()); ps.setDouble(3, house.getPriceMonthly()); ps.setString(4, house.getStatus()); ps.executeUpdate(); // 获取自增主键 ResultSet rs = ps.getGeneratedKeys(); if (rs.next()) { house.setHouseId(rs.getInt(1)); } conn.commit(); // 显式提交 } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) {} } throw new ServletException("房源发布失败", e); } finally { closeResources(ps, conn); } response.sendRedirect(request.getContextPath() + "/house/list"); }逻辑说明:
dataSource.getConnection()从 Tomcat 连接池获取连接,避免每次新建物理连接;conn.setAutoCommit(false)是事务起点;ps.getGeneratedKeys()获取house_id用于后续操作;closeResources()是自定义工具方法,确保PreparedStatement和Connection在 finally 块中关闭,防止连接泄漏。注意:response.sendRedirect()必须在conn.commit()之后,否则用户看到重定向页面时事务可能还未提交。
4.2ContractServlet的状态机驱动:用枚举 + switch 实现合同生命周期管控
合同状态不是静态字段,而是受业务规则约束的状态迁移。ContractStatus枚举定义合法状态及迁移路径:
public enum ContractStatus { ACTIVE("A", "生效中", Set.of("E", "T")), // 可迁移到到期、终止 EXPIRED("E", "已到期", Set.of()), // 终态,不可迁移 TERMINATED("T", "已终止", Set.of()); // 终态,不可迁移 private final String code; private final String desc; private final Set<String> allowedNext; ContractStatus(String code, String desc, Set<String> allowedNext) { this.code = code; this.desc = desc; this.allowedNext = allowedNext; } public boolean canTransitionTo(String nextCode) { return allowedNext.contains(nextCode); } }ContractServlet中的updateStatus方法强制校验迁移合法性:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int contractId = Integer.parseInt(request.getParameter("contractId")); String nextStatus = request.getParameter("nextStatus"); // 前端传 'E' 或 'T' // 1. 查询当前状态 String currentSql = "SELECT status FROM contract WHERE contract_id = ?"; String currentStatus = null; try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(currentSql)) { ps.setInt(1, contractId); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { currentStatus = rs.getString("status"); } } } // 2. 状态迁移校验(核心业务规则) if (!ContractStatus.valueOf(currentStatus).canTransitionTo(nextStatus)) { request.setAttribute("error", "非法状态迁移:从 " + ContractStatus.valueOf(currentStatus).getDesc() + " 不可切换到 " + ContractStatus.valueOf(nextStatus).getDesc()); request.getRequestDispatcher("/contract/detail.jsp").forward(request, response); return; } // 3. 更新状态(带乐观锁) String updateSql = "UPDATE contract SET status = ? WHERE contract_id = ? AND status = ?"; try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(updateSql)) { ps.setString(1, nextStatus); ps.setInt(2, contractId); ps.setString(3, currentStatus); // 乐观锁条件 int rows = ps.executeUpdate(); if (rows == 0) { request.setAttribute("error", "状态更新失败:数据已被其他用户修改"); request.getRequestDispatcher("/contract/detail.jsp").forward(request, response); return; } } response.sendRedirect(request.getContextPath() + "/contract/list"); }参数说明:
canTransitionTo()方法封装状态迁移规则,避免硬编码if ("A".equals(current) && "E".equals(next));UPDATE ... WHERE status = ?是乐观锁实现,防止并发修改覆盖;rows == 0表示 WHERE 条件不匹配,即当前状态已变,需提示用户刷新页面。
5. JSP 页面与交互细节:用 JSTL + EL + 自定义标签解决三大经典翻车点
5.1house/list.jsp的分页表格:用<c:forEach>+pageContext实现无 JS 分页
JSP 分页不是靠前端 JS,而是服务端计算页码后生成<a href="?page=2">链接。关键在PageUtil工具类和 JSTL 的配合:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <% // 从 request 获取分页参数(Servlet 中已 setAttribute) Integer currentPage = (Integer) request.getAttribute("currentPage"); Integer totalPage = (Integer) request.getAttribute("totalPage"); List<House> houseList = (List<House>) request.getAttribute("houseList"); %> <table class="table table-striped"> <thead> <tr> <th>房源标题</th> <th>面积(㎡)</th> <th>月租(元)</th> <th>状态</th> <th>操作</th> </tr> </thead> <tbody> <c:forEach items="${houseList}" var="house"> <tr> <td>${house.title}</td> <td><fmt:formatNumber value="${house.area}" pattern="#.##"/></td> <td><fmt:formatNumber value="${house.priceMonthly}" pattern="###,###"/></td> <td> <c:choose> <c:when test="${house.status == 'A'}">待租</c:when> <c:when test="${house.status == 'R'}">已租</c:when> <c:otherwise>下架</c:otherwise> </c:choose> </td> <td> <a href="${pageContext.request.contextPath}/house/detail?id=${house.houseId}">详情</a> <a href="${pageContext.request.contextPath}/house/edit?id=${house.houseId}">编辑</a> </td> </tr> </c:forEach> </tbody> </table> <!-- 分页导航 --> <div class="pagination"> <c:if test="${currentPage > 1}"> <a href="${pageContext.request.contextPath}/house/list?page=${currentPage-1}">« 上一页</a> </c:if> <c:forEach begin="1" end="${totalPage}" var="i"> <c:choose> <c:when test="${i == currentPage}"> <span class="current">${i}</span> </c:when> <c:otherwise> <a href="${pageContext.request.contextPath}/house/list?page=${i}">${i}</a> </c:otherwise> </c:choose> </c:forEach> <c:if test="${currentPage < totalPage}"> <a href="${pageContext.request.contextPath}/house/list?page=${currentPage+1}">下一页 »</a> </c:if> </div>逻辑说明:
<fmt:formatNumber>格式化数字,避免12345.0显示为12345.00;<c:choose>替代 Java 的 if-else,语义清晰;pageContext.request.contextPath确保路径不硬编码,适配不同部署上下文;分页链接用?page=2传递参数,Servlet 中解析request.getParameter("page")即可。注意:<c:forEach>的items属性必须是List,不能是数组,否则会报javax.el.PropertyNotFoundException。
5.2tenant/register.jsp的表单验证:用 HTML5 + 自定义ValidatorTag双保险
JSP 表单不能只靠前端required,必须服务端二次校验。自定义ValidatorTag标签封装通用验证逻辑:
// src/main/java/com/rent/tag/ValidatorTag.java @Tag(name = "validate", bodyContent = TagBodyContent.JSP) public class ValidatorTag extends SimpleTagSupport { private String field; private String errorKey; @Override public void doTag() throws JspException, IOException { PageContext pageContext = (PageContext) getJspContext(); Map<String, String> errors = (Map<String, String>) pageContext.getRequest().getAttribute("errors"); if (errors != null && errors.containsKey(field)) { JspWriter out = pageContext.getOut(); out.write("<div class='error'>" + errors.get(field) + "</div>"); } } // getter/setter... }在register.jsp中使用:
<form action="${pageContext.request.contextPath}/tenant/register" method="post"> <label>姓名:<input type="text" name="name" required></label> <jsp:useBean id="validator" class="com.rent.tag.ValidatorTag" /> <validator:validate field="name" errorKey="name"/> <label>身份证:<input type="text" name="idCard" required pattern="\d{17}[\dXx]"></label> <validator:validate field="idCard" errorKey="idCard"/> <label>手机号:<input type="tel" name="phone" required pattern="1[3-9]\d{9}"></label> <validator:validate field="phone" errorKey="phone"/> <input type="submit" value="注册"> </form>参数说明:
pattern属性做前端正则校验,required做非空校验;<validator:validate>标签在服务端渲染时检查request.getAttribute("errors")中对应字段的错误消息,有则输出<div class='error'>。血泪经验:pattern的正则必须用双引号包裹,且不能包含空格,否则浏览器解析失败;1[3-9]\d{9}是中国手机号标准正则,比^\d{11}$更精准。
5.3contract/detail.jsp的租金计算:用 JSP EL 函数实现动态公式
合同详情页需实时计算「已付租金总额」「剩余月数」「预计到期日」,这些不能写死在 Java 代码里,要用 EL 函数动态计算:
// src/main/java/com/rent/el/ContractEL.java public class ContractEL { public static int getMonthsLeft(Date startDate, Date endDate) { Calendar start = Calendar.getInstance(); start.setTime(startDate); Calendar end = Calendar.getInstance(); end.setTime(endDate); return (end.get(Calendar.YEAR) - start.get(Calendar.YEAR)) * 12 + end.get(Calendar.MONTH) - start.get(Calendar.MONTH); } public static BigDecimal getTotalPaid(int monthsPaid, BigDecimal monthlyRent) { return monthlyRent.multiply(BigDecimal.valueOf(monthsPaid)); } }在web.xml中声明函数:
<jsp-config> <jsp-property-group> <url-pattern>*.jsp</url-pattern> <el-function> <function-name>monthsLeft</function-name> <function-class>com.rent.el.ContractEL</function-class> <function-signature>int getMonthsLeft(java.util.Date, java.util.Date)</function-signature> </el-function> <el-function> <function-name>totalPaid</function-name> <function-class>com.rent.el.ContractEL</function-class> <function-signature>java.math.BigDecimal getTotalPaid(int, java.math.BigDecimal)</function-signature> </el-function> </jsp-property-group> </jsp-config>在 JSP 中调用:
<p>已租 ${fn:length(paidRecords)} 个月,共支付 <fmt:formatNumber value="${totalPaid(fn:length(paidRecords), contract.rentAmount)}" pattern="###,###.##"/> 元</p> <p>剩余 ${monthsLeft(contract.startDate, contract.endDate)} 个月</p> <p>预计到期日:<fmt:formatDate value="${contract.endDate}" pattern="yyyy-MM-dd"/></p>逻辑说明:
fn:length()是 JSTL 的内置函数,获取paidRecords列表长度;totalPaid()和monthsLeft()是自定义 EL 函数,参数类型必须严格匹配function-signature;<fmt:formatNumber>确保金额显示千分位。注意:EL 函数的function-signature中的类名必须用完整包路径,且参数顺序、类型必须与 Java 方法完全一致,否则 JSP 编译时报Unable to find method。
6. 避坑指南:这 5 个问题占了 83% 的调试时间,按顺序排查能省 3 天
6.1 现象:JSP 页面中文显示为???,但数据库里是正常汉字
原因:JSP 文件保存编码不是 UTF-8,或 Tomcat 的URIEncoding未设置。JSP 文件用记事本另存为时默认 ANSI,VS Code 默认 UTF-8 但未声明<%@ page contentType="text/html;charset=UTF-8" %>。
解决:
- VS Code 中右下角点击编码 → 选择
Save with Encoding→UTF-8 - 在
conf/server.xml的Connector标签中添加URIEncoding="UTF-8":<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" /> - 所有 JSP 文件顶部必须有
<%@ page contentType="text/html;charset=UTF-8" %>,缺一不可。
6.2 现象:java.lang.ClassNotFoundException: com.microsoft.sqlserver.jdbc.SQLServerDriver
原因:JDBC 驱动 JAR 未正确放入WEB-INF/lib/,或 Tomcat 启动时未加载该目录。常见错误是把 JAR 放在lib/(Tomcat 全局)而非项目WEB-INF/lib/(Web 应用私有)。
解决:
- 确认
WEB-INF/lib/mssql-jdbc-12.4.2.jre17.jar存在且文件大小 > 2MB - 在
web.xml中添加<resource-ref>声明(Tomcat 8+ 可选,但推荐):<resource-ref> <description>SQL Server Datasource</description> <res-ref-name>jdbc/houseRent</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> </resource-ref> - 重启 Tomcat 后查看
catalina.out日志,搜索Loaded JDBC driver确认加载成功。
6.3 现象:HTTP Status 404 – /house/list,但web.xml中已配置url-pattern
原因:Servlet 映射路径与实际访问路径不匹配,或@WebServlet注解与web.xml冲突。Tomcat 10+ 默认用 Jakarta EE 命名空间,若用@WebServlet("/house/list")但web.xml中仍用javax.servlet包,则类加载失败。
解决:
- 统一命名空间:JDK 17 + Tomcat 10.1.x 必须用
jakarta.servlet.*,所有 import 改为:import jakarta.servlet.ServletException;
本文还有配套的精品资源,点击获取