拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Servlet+JSP+JDBC实战:房屋租赁管理系统从搭建到部署完整指南

Servlet+JSP+JDBC实战:房屋租赁管理系统从搭建到部署完整指南

简介:这是一套基于Servlet+Jsp+JDBC开发的房屋租赁管理系统毕业设计资料包,面向计算机相关专业正在做毕设的学生,以及需要JavaWeb项目实战练习的学习者。系统功能覆盖前台会员登录注册、新闻中心、出租求租信息管理,后台出售求购信息、交易管理、统计报表等模块,界面简洁、操作流畅,适合作为课程设计或毕业设计的参考原型。压缩包约13.69MB,内含项目源码、数据库脚本、论文、系统详细配置指导及PPT演示文稿,可直接导入Eclipse或IDEA运行,配合SQL Server数据库即可快速启动项目。目前已有250人学习下载,资料完整度和实用性得到初步验证。通过这套资料,读者不仅能获得可运行的完整项目代码,还能学习Servlet/JSP/JDBC的整合思路、JQuery前端交互以及数据库设计方法,对完成毕设答辩和积累实际开发经验很有帮助。

1. 房屋租赁管理系统为什么值得用 Servlet+JSP+JDBC 这套组合重写一遍

很多人在做课程设计或者毕业设计时,看到 Servlet+JSP+JDBC 这种组合第一反应是「太老了」,Spring Boot 一把梭不香吗。但如果你想真正弄懂一个 Web 系统从请求到数据库再回显页面的完整链路,Servlet+JSP+JDBC 恰恰是最短路径:没有框架帮你把一切都包好,每一个请求如何被接收、每一个 SQL 如何被拼出来、每一行数据如何被塞进 HTML,全都在你眼皮底下。房屋租赁管理系统这个选题也正好卡在这个技术段的舒适区里——业务表不过三五张,权限模型简单,CRUD 占大头,做出来的东西既能覆盖课程设计要求的全部功能点,又不会因为业务太复杂而让你被框架和中间件拖到怀疑人生。

这篇笔记会从项目目录结构讲到数据表设计,从 JDBC 连接池写到登录态保持,再给你几条最常见的翻车记录。适合三类人:一是正在选课设题目的在校生,二是想补 Java Web 底层功底的转行者,三是需要一套能快速改造成自己毕设骨架的在职党。全程都是可以直接抄作业的命令和代码,参数我会标清楚为什么这么设,坑我也会直接告诉你踩在哪。

2. 把技术栈和项目结构先立住:三层架构到底在分什么

2.1 为什么是 Servlet 做控制层,而不是直接 JSP 里写 Java

很多初学者会问:JSP 里也能写 Java 代码,为什么还要 Servlet 先接请求再转发给 JSP?这个问题想通了,整个项目你就懂了一半。早期确实有人直接在 JSP 里写<%代码块,一个页面既查数据库又渲染表格,写完第一版感觉挺爽,等到要加一个「下架房源」的功能,你发现同样的查询逻辑在三个页面里各写了一遍,改一个字段名要全文搜索替换,这就是把业务逻辑和页面展示焊死在一起的后果。

Servlet+JSP+JDBC 这个组合的经典分工是这样的:Servlet 只做三件事——接收 HTTP 请求、解析参数、调用业务方法,最后把结果塞进 request 域对象里转发给 JSP。JSP 只做一件事——从 request 里拿数据,用 JSTL 或者原生表达式渲染成 HTML。JDBC 则封装在最底层,负责和 MySQL 对话,返回结果集给 Servlet 层的业务代码。

这套分工落到房屋租赁系统里,最典型的场景就是登录:LoginServlet 拿到用户名和密码,调用一个 UserDao 的 queryUserByUsernameAndPassword 方法,这个方法内部用 JDBC 执行一条 SELECT,返回 true 就request.getSession().setAttribute("loginUser", user)然后转发到index.jsp,返回 false 就转发回login.jsp并带一个 error 参数。整个过程没有任何框架魔法,你调试时用浏览器开发者工具看 Network 面板,每一个跳转都能对应到代码里的一次forward或sendRedirect,出了问题一眼就能定位。

2.2 房屋租赁系统的目录结构:一个能直接照着建包的骨架

我一般做这种课设项目,不会用什么 Maven 标准目录之外的花活,直接用 Eclipse 或 IDEA 的 Dynamic Web Project 结构,因为最终交付时要跑在 Tomcat 上,这个结构最不容易出幺蛾子。下面是推荐的项目根目录组织方式,你照着建就行:

HouseRentalSystem/ ├── src/ │ ├── com/rental/ │ │ ├── servlet/ // 控制层:LoginServlet, RoomServlet, ContractServlet... │ │ ├── dao/ // 数据访问层:UserDao, RoomDao, ContractDao │ │ ├── model/ // 实体类:User, Room, Contract, Bill │ │ ├── util/ // 工具类:DBUtil, DateUtil │ │ └── filter/ // 过滤器:LoginFilter, EncodingFilter ├── WebContent/ │ ├── index.jsp // 登录后首页 │ ├── login.jsp // 登录页 │ ├── room/ │ │ ├── roomList.jsp // 房源列表 │ │ ├── roomAdd.jsp // 新增房源 │ │ └── roomEdit.jsp // 编辑房源 │ ├── contract/ │ │ ├── contractList.jsp │ │ └── contractAdd.jsp │ ├── WEB-INF/ │ │ ├── web.xml // Servlet 映射、过滤器配置 │ │ └── lib/ // mysql-connector.jar 放这里 │ └── css/ js/ images/

为什么把 web.xml 里的 Servlet 映射单独强调一下?因为 Servlet 3.0 之后可以用注解@WebServlet("/login")替代 XML 配置,但课设答辩时老师很可能让你现场改 URL 映射,如果你只会在注解里改value属性,而 web.xml 里其实还残留着旧配置,就会出现改了半天不生效的灵异事件。我的习惯是:如果项目用了 web.xml,就全部在 XML 里配映射,别混着来;如果用注解,就全用注解。混用是新手最容易踩的坑之一。

实体类这块没什么好说的,一个 Room 类对应 room 表里的字段,写 getter/setter 就完事。但有一个细节值得提:房屋租赁里有个「状态」字段,我在数据库里用status TINYINT存(1=已出租,0=未出租,2=已下架),在实体类里也用 Integer 而不是 boolean。原因很简单,boolean 只有两种状态,撑不起「下架」这种业务语义,而且 JDBC 从ResultSet.getInt到 boolean 的转换容易埋 NPE 隐患。

2.3 数据库设计:五张表搞定房屋租赁的核心业务

房屋租赁管理系统的表设计,核心逃不开「人、房、租、账」四个字。我推荐的库名是house_rental,字符集统一用utf8mb4,排序规则用utf8mb4_general_ci。这里给出一套经过验证的最小表结构,总共五张表:

表名用途关键字段
user系统用户(管理员/员工)id, username, password, real_name, role, create_time
room房源信息id, room_no, area, rent_price, status, community, address, description
tenant租客信息id, name, phone, id_card, rent_start_date, rent_end_date
contract租赁合同id, room_id, tenant_id, start_date, end_date, monthly_rent, deposit, sign_date
bill账单/缴费id, contract_id, type(租金/押金/物业费), amount, status(已缴/未缴), due_date, pay_date

建表 SQL 这里只给 room 表的示例,其余表结构大同小异,重点是字段类型和默认值的设置思路:

CREATE DATABASE IF NOT EXISTS house_rental DEFAULT CHARSET utf8mb4; USE house_rental; CREATE TABLE room ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', room_no VARCHAR(20) NOT NULL UNIQUE COMMENT '房号,唯一约束', area DECIMAL(5,2) COMMENT '面积,单位平米', rent_price DECIMAL(8,2) NOT NULL COMMENT '月租金', status TINYINT DEFAULT 0 COMMENT '0未出租 1已出租 2已下架', community VARCHAR(50) COMMENT '小区名', address VARCHAR(100) COMMENT '详细地址', description VARCHAR(255) COMMENT '房源描述', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB COMMENT='房源表';

注意三个细节:room_no加了UNIQUE约束,避免重复录入房号;status设置了默认值 0,这样插入时少写一个字段;create_time用DEFAULT CURRENT_TIMESTAMP,连插入时间都不用程序生成。还有一个容易被忽略的点:金额一律用DECIMAL而不是FLOAT/DOUBLE,否则经过多轮计算后会出现 0.1+0.2=0.30000000000000004 这种浮点误差,账单对不上账就是从这里开始的。

3. 环境准备与跑通最小闭环:从 Tomcat 到第一条 JDBC 查询

3.1 JDBC 连接 MySQL 的驱动、URL 和连接参数设置

这个环节是整个项目里「玄学」浓度最高的地方,80% 的启动失败都发生在 JDBC 连接这一步。开始之前先把版本匹配搞定:Java 8 对应 Tomcat 9 和 MySQL 5.7 或 8.0,驱动用mysql-connector-java-5.1.49.jar或mysql-connector-java-8.0.33.jar。这里有个大坑:MySQL 8.0 的驱动类名改成了com.mysql.cj.jdbc.Driver,URL 里必须加上serverTimezone=Asia/Shanghai,否则会报The server time zone value异常;如果你用的是 5.1.x 驱动连 8.0 的库,大概率会报Unable to load authentication plugin 'caching_sha2_password'——这是因为 8.0 默认认证插件变了,要么换驱动,要么在 MySQL 里把用户插件改回mysql_native_password。

连接参数我直接用DBUtil.java来写,不引连接池框架,先把最基础的跑通。

package com.rental.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/house_rental?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError("MySQL驱动加载失败,请检查lib目录下是否有jar包"); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }

如果你用的是 Java 8 以上,Class.forName这行其实可以去掉,因为 DriverManager 会自动加载 SPI 配置里的驱动类,但保留它有两个好处:一是如果你用的 Tomcat 版本较旧可能触发 ClassLoader 问题,二是课设答辩时老师问「驱动是怎么加载的」,这行代码可以让你直接回答到DriverManager的 SPI 机制上。URL 里的useSSL=false是在本机调试时关掉 SSL 握手,省去证书配置的麻烦;characterEncoding=utf8保证中文不乱码——这里要注意,这个参数对参数化查询生效,但如果你的表或连接字符集不一致,还是会乱码。

参数方面,如果是在 IDEA 里直接 run 一个带 main 方法的测试类,记得在Run Configuration里确认 working directory 没问题;如果是部署到 Tomcat,mysql-connector-java-xxx.jar必须放到WEB-INF/lib下,很多人把 jar 丢到 Tomcat 的 lib 里也能跑,但换一台机器部署时就会漏,所以拷进项目里才是最保险的交付方式。

3.2 在 Tomcat 上跑通「查询一条房源记录」的完整链路

环境配好后的最小验证目标,不是登录,而是先跑通一条数据链路:浏览器输入 URL → Servlet 接收请求 → JDBC 查库 → JSP 把结果渲染出来。这个闭环一旦打通,剩下的模块都是复制粘贴改改表名的事。

Servlet 端的代码长这样:

package com.rental.servlet; import java.io.IOException; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.rental.util.DBUtil; public class RoomDetailServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String id = request.getParameter("id"); // 参数校验:不能直接拿字符串去拼接SQL if (id == null || id.trim().length() == 0) { response.sendRedirect("roomList"); return; } String sql = "SELECT id, room_no, area, rent_price, status, community, address FROM room WHERE id = ?"; // try-with-resources 自动关闭连接,避免连接泄漏 try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, Integer.parseInt(id)); ResultSet rs = ps.executeQuery(); if (rs.next()) { // 手动封装成实体对象,不用第三方工具 com.rental.model.Room room = new com.rental.model.Room(); room.setId(rs.getInt("id")); room.setRoomNo(rs.getString("room_no")); room.setArea(rs.getBigDecimal("area")); room.setRentPrice(rs.getBigDecimal("rent_price")); room.setStatus(rs.getInt("status")); room.setCommunity(rs.getString("community")); room.setAddress(rs.getString("address")); request.setAttribute("room", room); request.getRequestDispatcher("room/roomDetail.jsp").forward(request, response); } else { response.sendRedirect("roomList"); } } catch (SQLException e) { // 生产环境这里应该打日志,课设里至少也要 e.printStackTrace() e.printStackTrace(); response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } } }

代码逻辑拆开看:doGet 里先从request.getParameter("id")拿到前端传来的房源 ID,紧接着做空值校验——这一步不是为了防黑客(当然也防注入),更多是为了防止用户手抖点了不带参数的空 URL 导致NumberFormatException。然后PreparedStatement的参数占位符?配合ps.setInt(1, ...)是标准的防 SQL 注入写法,这里不要用字符串拼接 SQL,属于红线。try-with-resources 是 JDK 7 的语法,conn 和 ps 在 try 结束后自动关闭,不用写冗长的 finally 块——很多老代码里忘了关闭连接导致 MySQL 的max_connections被耗尽,就是从这里开始的。

JSP 端的渲染也得提一下,数据库里查出来的时间类型、状态数字都要在页面层做格式化。比如status是 TINYINT,你不能直接把数字贴到页面上,建议写一个小函数或者用 JSTL 的<c:choose>把 0、1、2 映射成「未出租/已出租/已下架」三个中文词。这一步不算技术难点,但做不好,答辩时老师看着一屏 0 和 1,会怀疑你是不是没做 UI 设计。

在 Tomcat 里跑起来之后,浏览器输入http://localhost:8080/HouseRental/roomDetail?id=1,如果能看到一个最简单的 HTML 表格里显示出一条房源记录,恭喜你,这个项目的地基已经打完了。下面要做的所有业务模块,无非是照着这个模式往里面填东西。

4. 业务模块落地:登录鉴权、房源 CRUD 与账单状态流转

4.1 登录与 Session 保持:过滤器是权限管理的命门

登录功能是课设的标配,但大部分人的实现方式都有同一个漏洞:只在 LoginServlet 里判断「用户名密码对不对」,然后就把用户丢进 index.jsp,至于用户是不是真的登录过,后续每个页面都不管。这意味着别人只要直接访问contractList.jsp的 URL,就能绕过登录看到所有合同数据。虽然课设项目没有什么隐私价值,但这个漏洞一旦被答辩老师指出来,印象分会很难看。

正确做法是用 Filter 做一个全局的登录拦截。在web.xml里配置 Filter 的映射路径为/*,然后在过滤器里统一放行登录页和静态资源,其余请求全部检查 Session:

<filter> <filter-name>LoginFilter</filter-name> <filter-class>com.rental.filter.LoginFilter</filter-class> </filter> <filter-mapping> <filter-name>LoginFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
package com.rental.filter; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; public class LoginFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; // 获取当前请求的URI,比如 /HouseRental/login.jsp String uri = request.getRequestURI(); // 放行条件:登录页、登录接口、静态资源、CSS/JS/JPG等 if (uri.endsWith("login.jsp") || uri.endsWith("/login") || uri.contains("/css/") || uri.contains("/js/") || uri.contains("/images/")) { chain.doFilter(req, resp); return; } HttpSession session = request.getSession(false); Object loginUser = (session == null) ? null : session.getAttribute("loginUser"); if (loginUser != null) { // 已登录,继续执行后续的Servlet或JSP chain.doFilter(req, resp); } else { // 未登录,重定向到登录页 response.sendRedirect(request.getContextPath() + "/login.jsp"); } } }

注意request.getSession(false)这个写法:参数为 false 表示「如果没有 Session 就返回 null,而不是新创建一个」。如果你用getSession()不带参数,那么每一个未登录请求都会在服务端创建一个空的 Session 对象,虽然影响不大,但看日志时满屏都是新建 Session,而且这属于一种隐蔽的资源浪费。放行静态资源那一段,我记得第一次写完这个过滤器后,页面上的 CSS 全部失效了,因为所有 css 请求被过滤器拦下来重定向到了 login.jsp,那个场景极其滑稽——登录页刷出来是纯 HTML 裸样式。所以静态资源放行千万不要漏。

4.2 房源 CRUD 的分页与状态联动:为什么不能直接 DELETE

房源管理的增删改查是核心功能,但「删除」在租赁系统里不能是物理删除。因为 contract 表里外键关联了 room_id,你物理删掉一个房间,合同数据就成了一堆孤儿记录;而且从业务上讲,下架比删除更合理——老房子可能有历史账目需要追溯。所以房源「删除」按钮的实际动作是把status字段更新成 2(已下架),SQL 是UPDATE room SET status = 2 WHERE id = ?,而不是 DELETE。

分页查询也是课设加分项里性价比最高的一档。大多数同学的实现是一个roomList.jsp把全部房源一次性查出来,数据少时没问题,但你要在答辩时演示「系统支持大数据量」,就得有分页。JDBC 层面的分页做法是在 SQL 末尾加LIMIT ?, ?,配合计算起始下标:

SELECT id, room_no, area, rent_price, status, community FROM room WHERE status != 2 ORDER BY id DESC LIMIT ?, ?;

这个 SQL 配合 Servlet 里的分页逻辑,注意LIMIT的两个参数都必须用setInt而不是setString,否则某些 MySQL 驱动版本会报错。我一般会把分页参数固定成后端默认值:每页 10 条,页数从 Request 的page参数获取,没有就默认第一页。这里有一个细节很容易出错:LIMIT的起始下标是(currentPage - 1) * pageSize,如果当前页传进来是字符串 "1",你要先转 int 再做减法,而不是直接用(page - 1) * size套一个字符串。

还有个容易忽略的联动:当房源状态是「已出租」时,你应该在 JSP 层面禁用「删除/下架」按钮,否则用户下架一个正在履行合同的房源,账单业务就乱了。这种控制不需要后端做什么,一个<c:if test="${room.status != 1}">包住按钮就行,属于 UI 层面的约束。

账单状态流转是这套系统里稍微有点业务含金量的模块。我把bill表的status设成「已缴/未缴/已逾期」三种状态,当当前日期超过due_date且status = 0时,视为逾期。这个「逾期判断」不能在查询时临时算,因为合同可能续期、租金可能调整,历史的逾期记录要留痕。正确做法是:每次页面加载或者每日定时任务(课设里可以用「每次登录时顺便检查」代替)扫描一次contract表中未结束的合同,把对应的未缴账单从 0 改成 2。这个逻辑用一条 UPDATE 就能实现:

UPDATE bill SET status = 2 WHERE status = 0 AND due_date < CURDATE() AND contract_id IN ( SELECT id FROM contract WHERE end_date >= CURDATE() );

这条 SQL 是你答辩时能讲出「业务闭环」的亮点材料,比起堆砌一堆 CRUD,这一个状态流转就能体现你理解「系统」不只是页面跳转。

4.3 中文乱码的三种表现和统一解决方式

乱码问题在 JSP+Servlet 时代几乎是每个项目必踩的坑,而且诡异的是——它有时候只在特定页面出现。三种典型表现:页面显示???、数据库里存进去的是乱码、查询条件传中文查不到。原因各不相同:

页面显示问号,通常是 JSP 文件头没写编码声明,或者 Tomcat 默认用 ISO-8859-1 解析请求。JSP 文件头必须有一行<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>,这是第一层保险。

POST 请求的中文参数乱码,这个很经典:Tomcat 8 之前默认用 ISO-8859-1 解码 POST 体,所以你要在 web.xml 里配一个 CharacterEncodingFilter 强制 UTF-8,或者更简单,在 Servlet 里拿到参数后做new String(value.getBytes("ISO-8859-1"), "UTF-8")。但在 Tomcat 8 之后,POST 请求的默认编码已经改成了 UTF-8,所以这个乱码场景通常不再发生——除非你手动改了 Tomcat 的配置。

GET 请求的中文乱码是另一个坑,因为 GET 参数在 URL 里走的是 URI 编码,Tomcat 默认对 URI 的解码还是 ISO-8859-1。解决办法是在server.xml的 Connector 节点上加URIEncoding="UTF-8"属性,或者在代码层面转码。我建议直接改 server.xml,一劳永逸,但如果你交作业时要把项目放到另一台机器上跑,对方 Tomcat 没改过配置就会乱码,所以在 Servlet 里做一次兼容转码更稳妥。

乱码问题最省心的策略:代码、JSP、数据库连接、MySQL 表结构全部固定成 UTF-8,任何一环出现其他编码都立刻查出来改掉,不要相信局部调整能解决问题。

5. 配置与部署避坑:5 个高频事故的现象、原因和处理方法

5.1 启动 Tomcat 后访问项目报 404

现象:Tomcat 正常启动,控制台也没有异常堆栈,但浏览器输入项目路径时始终 404,页面显示「源服务器未能找到目标资源的表示或者是不愿公开一个现有的资源」。

原因:绝大多数情况是项目没有成功部署到 Tomcat 的 webapps 目录。在 Eclipse 里运行时不注意看 Server 面板,项目处于「undeployed」状态;或者项目名为HouseRentalSystem但你访问的路径写的是HouseRental。还有一个隐蔽原因:Tomcat 的部署名和工程名不一致。

解决:先用http://localhost:8080/确认 Tomcat 首页能打开,然后看webapps目录下到底解压出来的文件夹叫什么名字,浏览器路径必须跟这个文件夹名完全一致。如果是 IDEA 的 Artifact 配置问题,检查Run Configuration里的 Deployment 标签,确保 Application context 和实际路径匹配。

5.2 数据库连接报错Public Key Retrieval is not allowed

现象:JDBC 连接 MySQL 8.0 时抛java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed。

原因:MySQL 8.0 默认使用caching_sha2_password认证插件,这个插件在数据传输时需要获取服务器的 RSA 公钥,而 JDBC 驱动默认禁止自动获取公钥。

解决:URL 参数里加allowPublicKeyRetrieval=true,或者更推荐的做法:在 MySQL 里创建用户时指定mysql_native_password插件。我一般两种都做,因为客户端环境不可控:CREATE USER 'rental'@'%' IDENTIFIED WITH mysql_native_password BY '你的密码';然后GRANT ALL ON house_rental.* TO 'rental'@'%';。这个坑在课设里出现频率非常高,因为现在新装的 MySQL 几乎都是 8.0,而网上大部分教程停留在 5.7 时代。

5.3 页面正常登录但所有 JS 和 CSS 文件 404

现象:打开 login.jsp 能显示表单,但页面上所有样式丢失,控制台看到 CSS/JS 文件全部 404。

原因:上一节提到的 LoginFilter 把静态资源也拦了并重定向,或者 JSP 里的引用路径写错了。JSP 页面中如果用相对路径css/style.css,当当前 URL 从/HouseRental/login.jsp变成/HouseRental/room/roomList时,相对路径解析会在room/目录下找 css,自然就 404 了。

解决:JSP 里所有资源引用全部用绝对路径,配合 EL 表达式获取上下文路径:<link rel="stylesheet" href="${pageContext.request.contextPath}/css/style.css">。过滤器里放行静态资源那段代码也一并加上,双保险。

5.4 JDBC 连接池耗尽,系统卡死无响应

现象:系统运行一段时间后,操作任何一个页面都会卡住,重启 Tomcat 后恢复正常,过一阵又卡死。查看 MySQL 的SHOW PROCESSLIST能看到大量Sleep状态的连接。

原因:代码里获取了 Connection 但没有 close。常见位置:查询完没有关闭 ResultSet、PreparedStatement 和 Connection;把 Connection 定义成成员变量而不是局部变量;异常路径上忘了关连接。我不止一次看到有人写了try { conn = DBUtil.getConnection(); ... } catch(SQLException e) { ... }却把关闭动作放在 catch 里的写法,那等于每次出错都泄漏一条连接。

解决:所有 JDBC 操作一律用 try-with-resources 写法,把conn,ps,rs全部声明在 try 的小括号里,让 Java 自动帮你关。如果是手动管理连接的老代码,一定要用 finally 块包住关闭逻辑,关之前判断非空。另外可以在 DBUtil 里写一个closeAll()的重载方法,把三种资源全接收进来统一关,代码会清爽很多。

5.5 部署到新机器后数据库中文乱码

现象:在开发机上一切正常,把项目打包成 WAR 拷贝到另一台电脑的 Tomcat 运行,数据库里查出来的中文全部乱码,或者写入的中文变成问号。

原因:开发机的 MySQL 配置文件my.ini里character-set-server=utf8mb4已经设置好,但新机器的 MySQL 默认可能是latin1,导致表创建时继承了这个字符集。项目代码里虽然是 UTF-8,但客户端和服务器字符集不一致,存储时就乱套了。

解决:到新机器 MySQL 命令行执行SHOW VARIABLES LIKE 'character_set%';查看结果。如果character_set_server不是utf8mb4,在my.ini的[mysqld]段加character-set-server=utf8mb4,重启 MySQL。如果已经有表了,用ALTER TABLE room CONVERT TO CHARACTER SET utf8mb4;转换已有表。这句话值得背下来,毕设现场部署时经常靠这一招救人。

6. 把课设升级成毕业设计:三个低成本高回报的进阶改造

先做一个小改造:把 DBUtil 里的连接换成连接池。理由很简单——你的课设如果用了连接池,答辩时就能多讲一个「资源管理」的维度,价值远超多写两百行代码。常见做法是把 DBUtil 内部从DriverManager.getConnection换成HikariCP或者 Tomcat 自带的JNDI DataSource配置。这里我推荐用 Tomcat 的 JNDI 方式,因为不需要额外引入 jar 包,只需要在META-INF/context.xml里写一段配置:

<Context> <Resource name="jdbc/houseRental" auth="Container" type="javax.sql.DataSource" maxTotal="20" maxIdle="8" maxWaitMillis="5000" username="root" password="123456" driverClassName="com.mysql.cj.jdbc.Driver" url="jdbc:mysql://localhost:3306/house_rental?useSSL=false&serverTimezone=Asia/Shanghai"/> </Context>

然后在代码里用Context ctx = new InitialContext(); DataSource ds = (DataSource) ctx.lookup("java:comp/env/jdbc/houseRental");拿连接。maxTotal=20是最常见的在线人数假设,maxWaitMillis=5000表示连接等待最多 5 秒就报错——这两个参数是课设演示时最容易被老师问到的,答得出来说明你真理解连接池的边界,而不是背配置。

第二个建议是把密码改成加密存储。现在的 user 表里直接存明文密码,这是个答辩时容易被追问「安全性」的短板。你只需要引入 JDK 自带的MessageDigest做 MD5 加盐即可,不用引任何第三方库。常见做法是注册时把salt + password拼起来做一次md5,登录时用同样的盐再算一次比对。盐一般取用户名的前两个字符 + 固定串,保证同一密码不同用户的密文不同。这段代码大约二十几行,但写上去之后整个项目的完成度会明显不一样。

最后一个改造,也是最能体现「工程思维」的:给 servlet demo 加一个统一的BaseServlet基类,用反射做 action 分发,这样你就不用一个 URL 一个 Servlet 地建文件了。比如RoomServlet继承 BaseServlet,通过?action=add&roomNo=101这种参数路由到对应的 add 方法。这个模式是 Java Web 老框架 Struts 的简化原型,写出来后代码量骤减,也方便维护——当然缺点是对新手来说,反射机制的理解门槛高一些,一旦传入的 action 字符串写错,报错也难查一些。

我自己的经验是:一开始老老实实每个功能一个 Servlet 确实能让你把「请求 → 处理 → 跳转」的肌肉记忆练出来,但项目写到第五个 Servlet 时你会发现重复代码太多,getParameter、封装对象、forward 这三步几乎一模一样,那时候再往上抽 BaseServlet,手感是完全不一样的——因为你是先写了重复代码才体会到为什么要消除重复。所以进阶改造别急,等基础版本跑通、逻辑理顺了再动手也不迟。

哦对了,最后提一个我吃过亏的习惯:整个项目做完之后,记得在项目根部写一个 README.txt,把你的 MySQL 账号密码、Tomcat 版本、JDK 版本、数据库导入步骤写清楚。不是为了给谁看,是方便你自己半个月后要交演示视频时,不用翻聊天记录找当时的部署细节。这套系统从零到一跑通之后,这个习惯的价值比多写一百行代码大得多。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表