ARTICLE DETAIL

资讯详情

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

Servlet+JSP+JDBC房屋租赁系统实战:从环境搭建到状态机设计

Servlet+JSP+JDBC房屋租赁系统实战:从环境搭建到状态机设计 简介本资源是一套完整的JavaWeb毕业设计实战项目——基于ServletJSPJDBC开发的房屋租赁管理系统面向计算机专业本科生、Java初学者及毕设选题需求者解决课程设计与毕业实践中的系统开发能力训练问题。压缩包为ZIP格式大小13.69MB包含可直接运行的源码工程、SQL Server数据库脚本、系统详细配置指导文档、毕业论文含需求分析、系统设计与实现、答辩用PPT等核心内容覆盖开发、部署、演示全流程。已有250人学习下载资源经Eclipse或IDEA严格调试支持JDK8环境一键启动附带前台会员/管理员双角色功能模块如出租求租发布、信息检索、收藏留言、统计报表等及后台数据管理能力。读者可获得完整可运行系统、规范论文写作范例、环境配置避坑指南与前后端协同开发逻辑梳理切实提升Web应用开发与工程交付能力。1. 为什么用 ServletJSPJDBC 做房屋租赁系统现在还值得投入这不是一个“过时技术堆砌”的毕设应付项目——而是JavaWeb 工程能力的最小闭环验证场。当你在 IntelliJ IDEA 或 VS Code 里敲下第一个HttpServlet#doGet配置好web.xml或注解路由再把ResultSet一行行映射成 JSP 表格最后用 JDBC 连上 SQL Server 的那一刻你真正踩到的是 Web 开发最底层的三块基石请求响应生命周期、服务端模板渲染、关系型数据持久化。很多 Spring Boot 新手写不好事务控制不是因为注解不熟而是没亲手写过Connection.setAutoCommit(false)和conn.rollback()不少人在 MyBatis 动态 SQL 里翻车根源是没在 JSPc: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.1apache-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 17JVM 拒绝加载低版本编译器生成的类。正确组合只有两个JDK 版本Tomcat 版本Servlet 规范关键适配点JDK 8u361Tomcat 8.5.90 / 9.0.83Servlet 3.1 / 4.0最稳教学案例默认选型SQL Server JDBC Driver 9.4 兼容JDK 17.0.1Tomcat 10.1.12Servlet 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.batWindows或./version.shLinux双验证。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.jarJDK 11 兼容或mssql-jdbc-12.4.2.jre17.jarJDK 17 专用。将其放入WEB-INF/lib/目录后在context.xml中配置连接池时必须显式关闭 SSL 验证仅开发环境!-- META-INF/context.xml -- Resource namejdbc/houseRent authContainer typejavax.sql.DataSource factoryorg.apache.tomcat.jdbc.pool.DataSourceFactory driverClassNamecom.microsoft.sqlserver.jdbc.SQLServerDriver urljdbc:sqlserver://localhost:1433;databaseNameHouseRentDB;encryptfalse;trustServerCertificatetrue; usernamesa passwordYourStrongPass1 maxActive20 maxIdle10 minIdle5 initialSize5 removeAbandonedOnBorrowtrue removeAbandonedTimeout60 logAbandonedtrue/注意encryptfalse;trustServerCertificatetrue是开发阶段绕过证书校验的合法配置生产环境必须部署有效证书并设encrypttrue。trustServerCertificatetrue解决的是“驱动无法验证服务器证书链”的核心报错不是玄学开关。2.3 VS Code 配置 Servlet 开发环境比 IDEA 更轻量但需手动补全构建链VS Code 本身不带 JavaWeb 支持依赖三个扩展协同工作Extension Pack for Java必装含 Language Support、Debugger、Test RunnerTomcat for Java由 Red Hat 提供提供服务器管理界面Project Manager for Java快速切换 Maven/Gradle 项目关键步骤是手动配置pom.xml或build.gradle的 Web 资源路径。若用 Mavenpom.xml必须声明warpackaging 并添加maven-war-pluginpackagingwar/packaging build plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-war-plugin/artifactId version3.3.2/version configuration failOnMissingWebXmlfalse/failOnMissingWebXml !-- 允许无 web.xml -- warSourceDirectorysrc/main/webapp/warSourceDirectory /configuration /plugin /plugins /build然后在 VS Code 中按CtrlShiftP→ 输入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两张表必须引入中间状态控制。以下是经过三次迭代验证的最小可行表结构表名字段说明关键约束househouse_idPK,titleNVARCHAR(100),areaDECIMAL(5,2),price_monthlyMONEY,statusCHAR(1) CHECK(status IN (A,R,L)) — A待租,R已租,L下架status默认A禁止 NULLtenanttenant_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])校验身份证格式contractcontract_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 内置的数字辅助表typeP表示正整数类型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 SetString allowedNext; ContractStatus(String code, String desc, SetString 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:forEachpageContext实现无 JS 分页JSP 分页不是靠前端 JS而是服务端计算页码后生成a href?page2链接。关键在PageUtil工具类和 JSTL 的配合% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % % // 从 request 获取分页参数Servlet 中已 setAttribute Integer currentPage (Integer) request.getAttribute(currentPage); Integer totalPage (Integer) request.getAttribute(totalPage); ListHouse houseList (ListHouse) request.getAttribute(houseList); % table classtable table-striped thead tr th房源标题/th th面积(㎡)/th th月租(元)/th th状态/th th操作/th /tr /thead tbody c:forEach items${houseList} varhouse tr td${house.title}/td tdfmt:formatNumber value${house.area} pattern#.##//td tdfmt: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 classpagination c:if test${currentPage 1} a href${pageContext.request.contextPath}/house/list?page${currentPage-1}laquo; 上一页/a /c:if c:forEach begin1 end${totalPage} vari c:choose c:when test${i currentPage} span classcurrent${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${currentPage1}下一页 raquo;/a /c:if /div逻辑说明fmt:formatNumber格式化数字避免12345.0显示为12345.00c:choose替代 Java 的 if-else语义清晰pageContext.request.contextPath确保路径不硬编码适配不同部署上下文分页链接用?page2传递参数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(); MapString, String errors (MapString, String) pageContext.getRequest().getAttribute(errors); if (errors ! null errors.containsKey(field)) { JspWriter out pageContext.getOut(); out.write(div classerror errors.get(field) /div); } } // getter/setter... }在register.jsp中使用form action${pageContext.request.contextPath}/tenant/register methodpost label姓名input typetext namename required/label jsp:useBean idvalidator classcom.rent.tag.ValidatorTag / validator:validate fieldname errorKeyname/ label身份证input typetext nameidCard required pattern\d{17}[\dXx]/label validator:validate fieldidCard errorKeyidCard/ label手机号input typetel namephone required pattern1[3-9]\d{9}/label validator:validate fieldphone errorKeyphone/ input typesubmit value注册 /form参数说明pattern属性做前端正则校验required做非空校验validator:validate标签在服务端渲染时检查request.getAttribute(errors)中对应字段的错误消息有则输出div classerror。血泪经验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-namemonthsLeft/function-name function-classcom.rent.el.ContractEL/function-class function-signatureint getMonthsLeft(java.util.Date, java.util.Date)/function-signature /el-function el-function function-nametotalPaid/function-name function-classcom.rent.el.ContractEL/function-class function-signaturejava.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} patternyyyy-MM-dd//p逻辑说明fn:length()是 JSTL 的内置函数获取paidRecords列表长度totalPaid()和monthsLeft()是自定义 EL 函数参数类型必须严格匹配function-signaturefmt:formatNumber确保金额显示千分位。注意EL 函数的function-signature中的类名必须用完整包路径且参数顺序、类型必须与 Java 方法完全一致否则 JSP 编译时报Unable to find method。6. 避坑指南这 5 个问题占了 83% 的调试时间按顺序排查能省 3 天6.1 现象JSP 页面中文显示为???但数据库里是正常汉字原因JSP 文件保存编码不是 UTF-8或 Tomcat 的URIEncoding未设置。JSP 文件用记事本另存为时默认 ANSIVS Code 默认 UTF-8 但未声明% page contentTypetext/html;charsetUTF-8 %。解决VS Code 中右下角点击编码 → 选择Save with Encoding→UTF-8在conf/server.xml的Connector标签中添加URIEncodingUTF-8Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 URIEncodingUTF-8 /所有 JSP 文件顶部必须有% page contentTypetext/html;charsetUTF-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 descriptionSQL Server Datasource/description res-ref-namejdbc/houseRent/res-ref-name res-typejavax.sql.DataSource/res-type res-authContainer/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;本文还有配套的精品资源点击获取
返回列表