
简介这是一套基于ServletJspJDBC开发的房屋租赁管理系统毕业设计资料包面向计算机相关专业正在做毕设的学生以及需要JavaWeb项目实战练习的学习者。系统功能覆盖前台会员登录注册、新闻中心、出租求租信息管理后台出售求购信息、交易管理、统计报表等模块界面简洁、操作流畅适合作为课程设计或毕业设计的参考原型。压缩包约13.69MB内含项目源码、数据库脚本、论文、系统详细配置指导及PPT演示文稿可直接导入Eclipse或IDEA运行配合SQL Server数据库即可快速启动项目。目前已有250人学习下载资料完整度和实用性得到初步验证。通过这套资料读者不仅能获得可运行的完整项目代码还能学习Servlet/JSP/JDBC的整合思路、JQuery前端交互以及数据库设计方法对完成毕设答辩和积累实际开发经验很有帮助。1. 房屋租赁管理系统为什么值得用 ServletJSPJDBC 这套组合重写一遍很多人在做课程设计或者毕业设计时看到 ServletJSPJDBC 这种组合第一反应是「太老了」Spring Boot 一把梭不香吗。但如果你想真正弄懂一个 Web 系统从请求到数据库再回显页面的完整链路ServletJSPJDBC 恰恰是最短路径没有框架帮你把一切都包好每一个请求如何被接收、每一个 SQL 如何被拼出来、每一行数据如何被塞进 HTML全都在你眼皮底下。房屋租赁管理系统这个选题也正好卡在这个技术段的舒适区里——业务表不过三五张权限模型简单CRUD 占大头做出来的东西既能覆盖课程设计要求的全部功能点又不会因为业务太复杂而让你被框架和中间件拖到怀疑人生。这篇笔记会从项目目录结构讲到数据表设计从 JDBC 连接池写到登录态保持再给你几条最常见的翻车记录。适合三类人一是正在选课设题目的在校生二是想补 Java Web 底层功底的转行者三是需要一套能快速改造成自己毕设骨架的在职党。全程都是可以直接抄作业的命令和代码参数我会标清楚为什么这么设坑我也会直接告诉你踩在哪。2. 把技术栈和项目结构先立住三层架构到底在分什么2.1 为什么是 Servlet 做控制层而不是直接 JSP 里写 Java很多初学者会问JSP 里也能写 Java 代码为什么还要 Servlet 先接请求再转发给 JSP这个问题想通了整个项目你就懂了一半。早期确实有人直接在 JSP 里写%代码块一个页面既查数据库又渲染表格写完第一版感觉挺爽等到要加一个「下架房源」的功能你发现同样的查询逻辑在三个页面里各写了一遍改一个字段名要全文搜索替换这就是把业务逻辑和页面展示焊死在一起的后果。ServletJSPJDBC 这个组合的经典分工是这样的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_timeroom房源信息id, room_no, area, rent_price, status, community, address, descriptiontenant租客信息id, name, phone, id_card, rent_start_date, rent_end_datecontract租赁合同id, room_id, tenant_id, start_date, end_date, monthly_rent, deposit, sign_datebill账单/缴费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 ) ENGINEInnoDB COMMENT房源表;注意三个细节room_no加了UNIQUE约束避免重复录入房号status设置了默认值 0这样插入时少写一个字段create_time用DEFAULT CURRENT_TIMESTAMP连插入时间都不用程序生成。还有一个容易被忽略的点金额一律用DECIMAL而不是FLOAT/DOUBLE否则经过多轮计算后会出现 0.10.20.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.DriverURL 里必须加上serverTimezoneAsia/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?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; 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 里的useSSLfalse是在本机调试时关掉 SSL 握手省去证书配置的麻烦characterEncodingutf8保证中文不乱码——这里要注意这个参数对参数化查询生效但如果你的表或连接字符集不一致还是会乱码。参数方面如果是在 IDEA 里直接 run 一个带 main 方法的测试类记得在Run Configuration里确认 working directory 没问题如果是部署到 Tomcatmysql-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?id1如果能看到一个最简单的 HTML 表格里显示出一条房源记录恭喜你这个项目的地基已经打完了。下面要做的所有业务模块无非是照着这个模式往里面填东西。4. 业务模块落地登录鉴权、房源 CRUD 与账单状态流转4.1 登录与 Session 保持过滤器是权限管理的命门登录功能是课设的标配但大部分人的实现方式都有同一个漏洞只在 LoginServlet 里判断「用户名密码对不对」然后就把用户丢进 index.jsp至于用户是不是真的登录过后续每个页面都不管。这意味着别人只要直接访问contractList.jsp的 URL就能绕过登录看到所有合同数据。虽然课设项目没有什么隐私价值但这个漏洞一旦被答辩老师指出来印象分会很难看。正确做法是用 Filter 做一个全局的登录拦截。在web.xml里配置 Filter 的映射路径为/*然后在过滤器里统一放行登录页和静态资源其余请求全部检查 Sessionfilter filter-nameLoginFilter/filter-name filter-classcom.rental.filter.LoginFilter/filter-class /filter filter-mapping filter-nameLoginFilter/filter-name url-pattern/*/url-pattern /filter-mappingpackage 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 中文乱码的三种表现和统一解决方式乱码问题在 JSPServlet 时代几乎是每个项目必踩的坑而且诡异的是——它有时候只在特定页面出现。三种典型表现页面显示???、数据库里存进去的是乱码、查询条件传中文查不到。原因各不相同页面显示问号通常是 JSP 文件头没写编码声明或者 Tomcat 默认用 ISO-8859-1 解析请求。JSP 文件头必须有一行% page contentTypetext/html; charsetUTF-8 pageEncodingUTF-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 节点上加URIEncodingUTF-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 参数里加allowPublicKeyRetrievaltrue或者更推荐的做法在 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 relstylesheet 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-serverutf8mb4已经设置好但新机器的 MySQL 默认可能是latin1导致表创建时继承了这个字符集。项目代码里虽然是 UTF-8但客户端和服务器字符集不一致存储时就乱套了。解决到新机器 MySQL 命令行执行SHOW VARIABLES LIKE character_set%;查看结果。如果character_set_server不是utf8mb4在my.ini的[mysqld]段加character-set-serverutf8mb4重启 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 namejdbc/houseRental authContainer typejavax.sql.DataSource maxTotal20 maxIdle8 maxWaitMillis5000 usernameroot password123456 driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/house_rental?useSSLfalseserverTimezoneAsia/Shanghai/ /Context然后在代码里用Context ctx new InitialContext(); DataSource ds (DataSource) ctx.lookup(java:comp/env/jdbc/houseRental);拿连接。maxTotal20是最常见的在线人数假设maxWaitMillis5000表示连接等待最多 5 秒就报错——这两个参数是课设演示时最容易被老师问到的答得出来说明你真理解连接池的边界而不是背配置。第二个建议是把密码改成加密存储。现在的 user 表里直接存明文密码这是个答辩时容易被追问「安全性」的短板。你只需要引入 JDK 自带的MessageDigest做 MD5 加盐即可不用引任何第三方库。常见做法是注册时把salt password拼起来做一次md5登录时用同样的盐再算一次比对。盐一般取用户名的前两个字符 固定串保证同一密码不同用户的密文不同。这段代码大约二十几行但写上去之后整个项目的完成度会明显不一样。最后一个改造也是最能体现「工程思维」的给 servlet demo 加一个统一的BaseServlet基类用反射做 action 分发这样你就不用一个 URL 一个 Servlet 地建文件了。比如RoomServlet继承 BaseServlet通过?actionaddroomNo101这种参数路由到对应的 add 方法。这个模式是 Java Web 老框架 Struts 的简化原型写出来后代码量骤减也方便维护——当然缺点是对新手来说反射机制的理解门槛高一些一旦传入的 action 字符串写错报错也难查一些。我自己的经验是一开始老老实实每个功能一个 Servlet 确实能让你把「请求 → 处理 → 跳转」的肌肉记忆练出来但项目写到第五个 Servlet 时你会发现重复代码太多getParameter、封装对象、forward 这三步几乎一模一样那时候再往上抽 BaseServlet手感是完全不一样的——因为你是先写了重复代码才体会到为什么要消除重复。所以进阶改造别急等基础版本跑通、逻辑理顺了再动手也不迟。哦对了最后提一个我吃过亏的习惯整个项目做完之后记得在项目根部写一个 README.txt把你的 MySQL 账号密码、Tomcat 版本、JDK 版本、数据库导入步骤写清楚。不是为了给谁看是方便你自己半个月后要交演示视频时不用翻聊天记录找当时的部署细节。这套系统从零到一跑通之后这个习惯的价值比多写一百行代码大得多。希望帮到你。本文还有配套的精品资源点击获取