ARTICLE DETAIL

资讯详情

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

JavaWeb房屋租赁管理系统:Servlet+JSP+MySQL源码与部署全解析

JavaWeb房屋租赁管理系统:Servlet+JSP+MySQL源码与部署全解析 简介一份基于Servlet、JSP与MySQL构建的JavaWeb房屋租赁管理系统源码面向JavaWeb学习者与毕业设计者可完整演示从页面交互、后端处理到数据库存储的开发流程覆盖房源管理、租客资料、租赁合同等核心业务。资源包共1796个文件压缩后约10.06MB包括119个java源码、102个jsp页面、119个class文件、146个css样式以及png图片、jar依赖库和sql数据库脚本等目录沿用Eclipse标准工程结构便于导入运行。已有3127人浏览学习。借助这套源码可以看清Servlet如何处理请求、JSP如何渲染数据、MySQL如何持久化记录配套部署描述符、SQL脚本与项目配置也有助于理解JavaWeb工程的完整发布与运行流程。1. 一套老技术栈的JavaWeb房屋租赁管理系统源码为什么还在被课设和毕设反复选择如果你正在被课程设计要交一个完整网站或者毕设选题选了信息管理系统这类需求卡住大概率会在Gitee、GitHub上搜到一类关键词组合JavaWeb房屋租赁管理系统源码、servletjspmysql。这套组合看起来老实际却是国内高校JavaWeb教学案例的标准答案Servlet负责处理请求JSP负责页面展示MySQL负责数据落地。相比Spring Boot全家桶它没有复杂的依赖注入和自动配置每个文件的功能直白到能在半小时内读完这对需要向评委讲解代码、回答这行代码在干什么的场景极为友好。这篇笔记会按照一个能跑的完整项目来拆解先讲为什么用ServletJSPMySQL而不直接上框架再给数据库设计、JDBC封装、登录鉴权、房源发布、租约状态流转这些核心代码最后集中讲部署到Tomcat时最容易翻车的几个坑。读者里如果有调了两天连不上数据库的直接跳到第5章血泪经验基本都在那里。2. 用ServletJSPMySQL做房屋租赁系统为什么这套组合才是课设优先项2.1 JavaWeb三层结构里Servlet、JSP各自站在哪一层理解这个系统的源码最高效的方式是先记住一句话JSP负责长得什么样Servlet负责接下来干什么数据库连接代码负责数据放哪里。一个请求进来流程是浏览器把表单POST给ServletServlet调用DAO层读写MySQL再把结果转发forward或重定向redirect到JSP页面由JSP把数据渲染成HTML。这种分法叫Model2也是JavaWeb教学案例里最常见的组织方式它的核心思想是不要让业务逻辑散落在页面标签里。在房屋租赁管理系统这个具体项目里Servlet的典型职责包括校验用户登录、查询在租房源列表、创建租赁订单、变更房源状态。JSP的职责则是把房源卡片、订单状态、用户信息展示到浏览器。如果发现源码里某个JSP页面里写了大量的Java代码块处理业务比如在页面里直接操作数据库那说明这个版本的写法比较老维护时建议把逻辑挪进Servlet或JavaBean里。2.2 这套技术选型的边界什么时候该用它什么时候该换Spring Boot实话实说ServletJSPMySQL不是眼下企业生产的主流Spring Boot Vue这类前后端分离方案才是。但它有两个Spring Boot替代不了的优势第一它把HTTP请求、Session、请求转发这些Web底层原理全部暴露在明面上一套源码读下来你对浏览器到服务器再到数据库这条链路会有比用框架扎实得多的理解第二毕业答辩时这行代码是做什么的这个问题非常好回答不存在框架封装的黑匣子效应。如果你的项目需求里有大量异步交互、移动端接口、分布式部署这套组合就会很吃力那应该直接换Spring Boot。但如果需求就是房屋信息发布、用户注册登录、租赁订单管理这种标准CRUDJavaWebServletJSPMySQL完全扛得住代码量也不夸张。判断标准就一条有没有多端接口需求没有就用Servlet有就换框架。2.3 运行环境准备IDEA里配置Tomcat与MySQL的最小步骤拿到一套源码第一步不是读代码而是先把环境跑起来。常见的坑是版本不匹配导致Tomcat起不来或页面报404下面给出我验证过多次的最小配置路径。第一步把源码用IDEA以Web Application方式导入确认Project SDK为JDK 8或JDK 11这两代对Servlet和JSP兼容性最稳第二步确认项目里存在WEB-INF/web.xml老版本源码依赖它做映射和过滤器注册第三步在IDEA的Run/Debug配置里添加Tomcat ServerLocal类型Deployment选项卡中把项目以war exploded方式部署Application context填/house_rent或者空路径这一步决定了你在浏览器里访问的根地址第四步在本地启动MySQL服务执行源码中的init.sql建库建表。这四步里最容易翻车的是Application context配置。填了/house_rent那么访问登录页面就是http://localhost:8080/house_rent/login.jsp如果源码里所有的超链接都按这个路径写而你把context填成了/所有页面跳转都会404节奏马上就乱了。3. 房屋租赁数据库设计四张核心表与JDBC连接参数的设置3.1 用户表、房源表、订单表、合同表的设计边界房屋租赁业务的核心可以拆成三个角色和一条状态线。三个角色是租客、房东、管理员一条状态线是房源从待出租到已预约再到已签约最终到已退租或已下架。围绕这个模型我会把数据库拆成四张表比常见的三张表方案多出一张合同表理由是退租和续租需要留存独立的合同记录不能跟着订单一起被覆盖。用户表字段设计要点id主键自增username唯一索引password存加盐后的MD5摘要而不是明文role用tenant、landlord、admin三个字符串值代码里用常量比对比用数字更直观增加phone和create_time前者用于联系后者用于后台展示注册时间。房源表字段设计要点核心字段是landlord_id关联用户表、title、address、price用DECIMAL(10,2)、area、room_num、status。status建议用0待审核、1已上架、2已出租、3已下架四个值中间状态越少代码分支越简单。订单表和合同表的设计是这套系统里最容易出问题的地方。订单表记录的是行为谁在什么时间对哪套房源发起了预约或签约请求所以字段是house_id、tenant_id、status0预约中、1已签约、2已退租、3已取消、start_date、end_date合同表则是签约时生成的正式记录包含order_id、contract_no唯一编号、sign_date、deposit押金、rent月租金。合同表为什么要独立出来因为一次签约后后续续租是在订单表上加一条新订单还是在合同表上改结束日期这个逻辑只有在两表分离时才能说清楚。3.2 建库建表SQL脚本一次性跑通的初始数据下面是这套系统初始化时的核心SQL脚本。执行顺序要严格按删库→建库→建表→插初始数据否则外键关联会报错。CREATE DATABASE IF NOT EXISTS house_rent DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE house_rent; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, phone VARCHAR(20) DEFAULT , role VARCHAR(20) NOT NULL DEFAULT tenant, salt VARCHAR(32) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE t_house ( id INT PRIMARY KEY AUTO_INCREMENT, landlord_id INT NOT NULL, title VARCHAR(100) NOT NULL, address VARCHAR(200) NOT NULL, price DECIMAL(10, 2) NOT NULL, area DECIMAL(8, 2) DEFAULT 0, room_num INT DEFAULT 1, hall_num INT DEFAULT 1, status TINYINT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_house_landlord FOREIGN KEY (landlord_id) REFERENCES t_user(id) ) ENGINEInnoDB; CREATE TABLE t_order ( id INT PRIMARY KEY AUTO_INCREMENT, house_id INT NOT NULL, tenant_id INT NOT NULL, status TINYINT DEFAULT 0, start_date DATE, end_date DATE, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_order_house FOREIGN KEY (house_id) REFERENCES t_house(id), CONSTRAINT fk_order_tenant FOREIGN KEY (tenant_id) REFERENCES t_user(id) ) ENGINEInnoDB; CREATE TABLE t_contract ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, contract_no VARCHAR(32) NOT NULL, deposit DECIMAL(10, 2) DEFAULT 0, rent DECIMAL(10, 2) NOT NULL, sign_date DATE, CONSTRAINT fk_contract_order FOREIGN KEY (order_id) REFERENCES t_order(id) ) ENGINEInnoDB;这段SQL里有几个参数值得说明。字符集用utf8mb4而不是utf8因为MySQL的utf8最多存3字节遇到生僻字或特殊符号会报Incorrect string value错误现在新建库我无脑用utf8mb4password字段长度设为64是为了容纳MD5摘要32位再加前缀盐的拼接结果status用TINYINT对应Java里的int比用字符串省空间但代码里要写注释说明每个数字的含义。create_time用DEFAULT CURRENT_TIMESTAMP后插入语句里就不需要手动传时间了。3.3 封装JDBC工具类驱动加载、连接创建与资源关闭所有Servlet和DAO都需要获取数据库连接如果每个类里都写一遍DriverManager.getConnection代码冗余还容易漏关连接。我一般会在util包下放一个DBUtil类把连接参数集中管理。下面是核心代码。package com.house.util; import java.sql.*; public class DBUtil { private static final String DRIVER com.mysql.cj.jdbc.Driver; private static final String URL jdbc:mysql://localhost:3306/house_rent?characterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue; private static final String USERNAME root; private static final String PASSWORD 123456; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(MySQL驱动加载失败请检查jar包是否放入WEB-INF/lib); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USERNAME, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException ignored) {} } if (stmt ! null) { try { stmt.close(); } catch (SQLException ignored) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } } }这段代码有三个参数值得展开。第一个是URL里的serverTimezoneAsia/ShanghaiMySQL 8.0以后必须显式指定时区否则报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized这是新人必踩的坑第二个是allowPublicKeyRetrievaltrueMySQL 8.0默认使用caching_sha2_password认证不开启这个参数第一次连接会报Public Key Retrieval is not allowed第三个是静态块里的Class.forName(DRIVER)MySQL 8.0之后驱动类名是com.mysql.cj.jdbc.Driver如果是老版本驱动则要写成com.mysql.jdbc.Driver写错会出现ClassNotFoundException。资源关闭为什么单独写成一个方法是因为Connection、Statement、ResultSet三者都可能为空每层都做null判断会让业务代码变得很长放在工具类里统一收口DAO层每次只需要一行DBUtil.close(conn, pstmt, rs)调用就够了。这里没有使用连接池属于裸JDBC方案适合课设讲解生产级改进方案在第6章给出。4. 核心功能实现登录鉴权、房源发布与租约状态流转4.1 使用Filter统一做登录状态校验避免每个页面重复判断一个房屋租赁系统里除了登录页和注册页其余页面几乎都需要校验当前用户是否已登录。如果每个Servlet里都写一段判断Session的代码重复劳动多漏一个就能造成权限漏洞。常见的做法是把校验逻辑抽到一个Filter里对所有请求统一拦截。下面是核心实现。package com.house.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; WebFilter(/*) public class LoginFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String uri request.getRequestURI(); if (uri.endsWith(/login.jsp) || uri.endsWith(/login) || uri.endsWith(/register.jsp) || uri.endsWith(/register) || uri.contains(/static/) || uri.endsWith(.css) || uri.endsWith(.js)) { chain.doFilter(req, resp); return; } HttpSession session request.getSession(false); if (session null || session.getAttribute(loginUser) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }这段Filter的逻辑分三段。第一段是放行规则登录页、注册页、静态资源请求不需要Session校验否则会陷入没登录就跳登录页登录页又被拦截的死循环第二段是拦截规则request.getSession(false)传了false意思是如果当前没有Session就返回null而不是创建一个新的这个细节很重要如果传true会被客户端每次请求都种一个空Session浪费内存第三段是跳转规则request.getContextPath()会动态获取项目上下文路径这样即使Application context配置改动了重定向路径也能自动适配。Filter注解里的WebFilter(/*)是Servlet 3.0以后的写法如果源码比较老没有注解支持就要在web.xml里对应位置注册Filter和映射。判断依据很简单如果源码里Filter类上找不到注解就去web.xml里找filter和filter-mapping标签。4.2 PreparedStatement防注入的登录与注册Servlet实现登录和注册是这套系统里被用户亲手测试得最多的功能也是SQL注入重灾区。比较常见的反面教材是把用户名直接拼接进SQL语句例如SELECT * FROM t_user WHERE username username 输入一个admin --就能绕过密码校验。下面给出PreparedStatement的标准写法。package com.house.servlet; import com.house.dao.UserDao; import com.house.entity.User; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; WebServlet(/login) public class LoginServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String username req.getParameter(username); String password req.getParameter(password); UserDao dao new UserDao(); User user dao.findByUsernameAndPassword(username, password); if (user ! null) { HttpSession session req.getSession(); session.setAttribute(loginUser, user); resp.sendRedirect(req.getContextPath() /house/list); } else { req.setAttribute(errorMsg, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); } } }对应的DAO层查询方法是这样的。public User findByUsernameAndPassword(String username, String password) { String sql SELECT * FROM t_user WHERE username ? AND password ?; try (Connection conn DBUtil.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql)) { pstmt.setString(1, username); pstmt.setString(2, password); try (ResultSet rs pstmt.executeQuery()) { if (rs.next()) { User user new User(); user.setId(rs.getInt(id)); user.setUsername(rs.getString(username)); user.setPhone(rs.getString(phone)); user.setRole(rs.getString(role)); return user; } } } catch (SQLException e) { e.printStackTrace(); } return null; }这里有两个容易被忽略的细节。第一setString方法的作用是告诉JDBC驱动这个值是一个参数不是SQL片段它会自动转义单引号和反斜杠这是PreparedStatement防注入的原理比任何字符串过滤都可靠第二这段代码里的密码校验存在一个隐患password字段在数据库里应当存的是加盐后的MD5值所以在调用这个DAO方法前Servlet里要先通过用户名查一次用户表拿到盐再对输入的密码做同样的MD5计算最后把两个摘要字符串做比较第6章会详细展开这个改进。登录成功后的resp.sendRedirect里用了/house/list这个路径它对应房源列表的Servlet映射。用重定向而不使用forward是因为重定向会让浏览器地址栏变成/house/list用户刷新页面不会重复提交登录表单forward则会导致刷新时浏览器提示确认重新提交表单。4.3 房源发布与列表查询分页参数的编码与页面回显约定房源发布是房东角色的核心操作而房源列表是租客的第一入口。这一节重点实现两个功能房东提交房源表单租客分页浏览房源。下面是发布房源的Servlet接收数据并写入数据库的代码。package com.house.servlet; import com.house.dao.HouseDao; import com.house.entity.House; import com.house.entity.User; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; import java.math.BigDecimal; WebServlet(/house/add) public class HouseAddServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); User loginUser (User) req.getSession().getAttribute(loginUser); if (loginUser null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; } House house new House(); house.setLandlordId(loginUser.getId()); house.setTitle(req.getParameter(title)); house.setAddress(req.getParameter(address)); house.setPrice(new BigDecimal(req.getParameter(price))); house.setArea(new BigDecimal(req.getParameter(area))); house.setRoomNum(Integer.parseInt(req.getParameter(roomNum))); house.setHallNum(Integer.parseInt(req.getParameter(hallNum))); HouseDao dao new HouseDao(); boolean ok dao.insertHouse(house); if (ok) { resp.sendRedirect(req.getContextPath() /house/list?page1size5); } else { req.setAttribute(errorMsg, 发布失败请重试); req.getRequestDispatcher(/house/publish.jsp).forward(req, resp); } } }列表分页的Servlet实现里核心是页码和每页条数的参数解析。下面给出代码。WebServlet(/house/list) public class HouseListServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int page 1; int size 5; String pageParam req.getParameter(page); String sizeParam req.getParameter(size); if (pageParam ! null !pageParam.isEmpty()) { page Integer.parseInt(pageParam); } if (sizeParam ! null !sizeParam.isEmpty()) { size Integer.parseInt(sizeParam); } // 防御性处理page不能小于1 if (page 1) { page 1; } HouseDao dao new HouseDao(); int total dao.countByStatus(1); int totalPages (total size - 1) / size; ListHouse houses dao.findByPage(page, size); req.setAttribute(houses, houses); req.setAttribute(currentPage, page); req.setAttribute(totalPages, totalPages); req.getRequestDispatcher(/house/list.jsp).forward(req, resp); } }分页参数这里有个编码约定需要强调page表示第几页从1开始计数size表示每页显示多少条totalPages的计算公式(total size - 1) / size是整数除法向上取整的技巧因为整数除法会直接舍去小数所以要先加size - 1再除比如total12, size5计算结果是(124)/53刚好是3页如果不加12/52最后一页的3条数据就丢了。DAO层对应的分页SQL长这样SELECT id, title, address, price, area, room_num, hall_num, status FROM t_house WHERE status 1 ORDER BY create_time DESC LIMIT ?, ?;LIMIT的第一个问号是偏移量等于(page - 1) * size第二个问号是每页条数。偏移量为什么是(page-1)*size因为第1页要跳过0条第2页要跳过前5条展示第6到第10条。用PreparedStatement给LIMIT赋值时两个参数都要setInt不能拼接进SQL否则会被注入。4.4 租约状态流转预约、签约、退租的代码路径房源状态的流转是房屋租赁系统区别于普通CRUD的核心逻辑。一个完整流程是租客看到在租房源发起预约房东确认后生成合同房源状态从已上架变为已出租租期结束后租客申请退租房东确认后房源恢复已上架。下面用订单状态的update语句示意核心流转逻辑。// 租客点击预约看房 public boolean createOrder(int houseId, int tenantId) { String checkSql SELECT status FROM t_house WHERE id ?; String insertSql INSERT INTO t_order(house_id, tenant_id, status) VALUES(?, ?, 0); try (Connection conn DBUtil.getConnection(); PreparedStatement checkStmt conn.prepareStatement(checkSql); PreparedStatement insertStmt conn.prepareStatement(insertSql)) { checkStmt.setInt(1, houseId); ResultSet rs checkStmt.executeQuery(); if (rs.next() rs.getInt(status) 1) { insertStmt.setInt(1, houseId); insertStmt.setInt(2, tenantId); return insertStmt.executeUpdate() 0; } return false; } catch (SQLException e) { e.printStackTrace(); return false; } }这段代码里最关键的是一开始的checkSql。为什么不直接插入订单而是先查一次房源状态这是为了避免同时有两个租客预约同一套房的并发问题。虽然用代码检查不是最严格的方案真正的强约束应该是在t_order表上对house_id加唯一索引限制未完成订单但课程设计层面做一次状态预检已经能挡住绝大多数误操作。签约和退租的SQL流转类似签约是把t_order的status从0改为1同时在t_contract插入合同记录退租是先把t_order.status改为2再把t_house.status改回1。这里有一个新手容易忽略的细节状态的变更要放在同一个方法里顺序执行如果先改订单状态再改房源状态中间某个SQL抛异常就会出现订单已退租但房源仍显示已出租的数据不一致。更稳妥的做法是用事务把两条更新包起来conn.setAutoCommit(false)全部执行成功后再conn.commit()。5. 部署到Tomcat避坑指南WAR包发布与MySQL连接常见问题排查5.1 从IDEA导出WAR包并在本地Tomcat目录下发布的完整步骤在IDEA里通过Run按钮跑项目只能说明开发模式下没问题评委要验收时机房电脑往往没有IDEA所以学会打WAR包是必须的技能。操作路径是Project Structure - Artifacts - 点加号 - Web Application: Archive - 勾选Build on make然后Build - Build Artifacts - 选择生成的war包。产出物通常位于项目目录/out/artifacts/项目名_war/项目名.war。拿到war包后把它复制到Tomcat的webapps目录下启动Tomcat它会自动解压成一个同名目录访问路径就是http://localhost:8080/项目名/。注意WAR包解压后项目名默认就是WAR文件名如果文件名里带中文或空格访问URL会非常别扭建议打包前就把Artifact名称改成house_rent这种纯小写名称。配置文件里的MySQL连接参数在这一步容易出问题。开发环境连接的是localhost:3306如果部署到机房服务器或实验室局域网要确认MySQL的bind-address配置允许远程访问同时操作系统的防火墙要放行3306端口否则客户端连接会超时。5.2 五个高频部署与运行问题按现象定位原因这一节整理了部署这套JavaWebMySQL项目时最常遇到的五个问题每条都按现象、原因、解决展开可以直接对照排查。问题一访问JSP页面浏览器中文全部变成乱码现象页面标题、房源地址、用户名里的中文显示成房东或????。 原因三层字符集设置不一致。JSP页面编码、Servlet请求编码、数据库连接编码、数据库表编码四个环节只要有一个是ISO-8859-1或GBK就会出现乱码。 解决统一走utf8链路。JSP顶部确认pageEncodingUTF-8Servlet里在读取参数前调用req.setCharacterEncoding(UTF-8)JDBC的URL上加characterEncodingutf8建表时已用utf8mb4最后web.xml里可选配置CharacterEncodingFilter一次性覆盖所有Servlet的请求编码。问题二启动Tomcat后访问项目报Cannot connect to MySQL server或Access denied for user现象页面报500错误控制台堆栈显示Communications link failure或Access denied。 原因三种情况最常见。MySQL服务没启动连接账号密码和DBUtil里写的不一致数据库名写错或者不存在。 解决先在命令行用mysql -uroot -p验证能否登录MySQL再执行SHOW DATABASES;确认house_rent库存在最后检查DBUtil的USERNAME和PASSWORD。如果MySQL装在另一台机器上还要确认jdbc:mysql://里写的IP不是localhost而是那台机器的局域网IP。问题三所有请求都被Filter拦截登录后仍然跳回登录页现象输入正确的用户名和密码登录接口返回成功但跳转列表页后又被重定向到登录页。 原因Session丢失。最典型的原因是Tomcat的Session Cookie名被改过或者浏览器禁用了Cookie。还有一种可能是request.getSession(false)在Filter里拿不到Session因为登录成功后执行了response.sendRedirect重定向请求带了新的JSESSIONID但Filter判断Session为null。 解决先确认浏览器开启Cookie检查loginUser是否存的是Java对象如果对象没有实现Serializable接口部分Tomcat会话序列化配置下可能存储失败。还有一种做法是把登录状态简化成存user.getId()而不是整个对象减少序列化复杂度。问题四部署环境JDK版本升级后报ClassNotFoundException: com.mysql.jdbc.Driver现象本地开发环境跑得好好的换一台只有JDK 17的机器后数据库操作全部报驱动类找不到。 原因MySQL驱动jar包在Java 9模块化系统下访问受限老版本的com.mysql.jdbc.Driver类被标记为不安全或者项目使用的高版本驱动改用新类名com.mysql.cj.jdbc.Driver。 解决换用MySQL Connector/J 8.0以上版本并把驱动类改为com.mysql.cj.jdbc.Driver。检查WEB-INF/lib目录下jar包是否被打进WARIDEA的Artifact配置里如果没有把jar包关联到WEB-INF/lib部署后运行时就会ClassNotFound。问题五日期字段写入数据库后时间比实际少了8个小时现象订单创建时间在页面上显示正常但数据库里查询结果比当前时间早8小时或者反过来。 原因JDBC连接参数没有指定时区MySQL 8.0默认使用服务器的系统时区而Java虚拟机的默认时区是Asia/Shanghai两者有偏移时就会出现8小时的差距。 解决在JDBC的URL末尾追加serverTimezoneAsia/Shanghai同时CREATE TABLE里create_time字段不指定默认值而是用DATETIME DEFAULT CURRENT_TIMESTAMP让数据库自己记录当前时间。提示排查任何部署问题第一件事是看Tomcat的logs/catalina.out日志文件。很多新人习惯盯着浏览器看错误但500错误的真正原因只在服务端日志里。6. 安全加固与性能优化把课设项目升级成能写进简历的作品6.1 用Druid连接池替换DriverManager提升并发下的响应速度裸JDBC每次获取连接都要经历建立TCP连接、MySQL认证、断开连接三步一个请求耗时几十毫秒并发一高数据库连接数会打满。把连接池引入后连接建立完成后不关闭而是放回池里复用响应速度提升明显。常见的做法是引入阿里巴巴的Druid因为它自带监控页面便于答辩时展示连接数变化。接入步骤分成两步。第一步把druid-1.2.20.jar复制到WEB-INF/lib第二步在src目录下新建druid.properties配置文件driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/house_rent?characterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai usernameroot password123456 initialSize5 maxActive10 maxWait3000然后修改DBUtil类的getConnection方法private static DruidDataSource dataSource; static { try { InputStream in DBUtil.class.getClassLoader().getResourceAsStream(druid.properties); Properties props new Properties(); props.load(in); dataSource (DruidDataSource) DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(Druid连接池初始化失败: e.getMessage()); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); }这段代码里initialSize5表示启动时预创建5个连接maxActive10表示连接池最多容纳10个连接超过这个数的新请求会等待maxWait设置的时间单位毫秒到期获取不到连接就抛异常maxWait3000能避免并发峰值时响应无限挂起。引入连接池后业务代码里的DBUtil.getConnection()和DBUtil.close()调用方式完全不用改因为连接池返回的Connection对象在调用close时不是真关闭而是归还到池里这对已有的DAO层代码是零侵入的改进。6.2 密码加盐与MD5摘要替换掉明文存储的隐患在4.2节的登录实现里有一段隐患需要处理数据库的password字段如果直接存明文任何人只要看一眼数据库就能拿到所有账号的密码。常见的做法是注册时对原始密码随机盐做MD5摘要把摘要和盐一起存入用户表登录时取出盐对输入密码做同样的拼接和摘要然后与库里的摘要比对。注册场景的核心代码String salt UUID.randomUUID().toString().replace(-, ).substring(0, 16); String rawPassword req.getParameter(password); String encodedPassword MD5Util.md5(rawPassword salt); User user new User(); user.setUsername(username); user.setPassword(encodedPassword); user.setSalt(salt);MD5Util.md5这个工具方法内部是把字符串转成MD5摘要后输出32位十六进制字符串。这里的关键是盐一定要随机生成且每个用户不同复制粘贴固定盐的写法虽然能防明文存储但无法防彩虹表攻击。登录时把查询SQL改成SELECT salt FROM t_user WHERE username?先拿到盐再做摘要比较比较操作放在Java里而不是SQL里这样即使查询结果为空也能保持同样的响应时间减少用户枚举风险。6.3 代码健壮性验收清单答辩和自测前过一遍这四件事项目做完不等于能交最后一步应该过一遍验收清单。第一注册一个普通租客账号、一个房东账号分别验证权限隔离租客不能访问房东的发布接口房东不能给自己下单。第二重复点击发布按钮确认不会插入两条相同的房源记录这一步在服务端要做防重提交判断。第三打开两个浏览器同时预约同一套房源看第二次预约是否被拒绝这验证并发控制逻辑。第四把数据库的字符集改成utf8mb4后往房源地址里输入一个生僻字看页面和数据库是否都正常排查乱码隐患。做完这些再回头看这套源码的边界心里要有数ServletJSPMySQL的学习价值在于原理清晰但这套方案没有前后端分离、没有Redis缓存、没有消息队列距离企业级生产项目还有很长距离。它的正确用法是作为理解JavaWeb原理的垫脚石把Filter、Session、PreparedStatement这些底子打好之后再切换到Spring Boot时你会明显感觉到框架帮我封装了什么这层理解力是别人直接上手框架学不到的。我自己带过的几个项目组里凡是老老实实把Servlet这套源码彻底读透的人后续接触Spring MVC时几乎不需要刻意学一眼就能看出DispatcherServlet就是那个统一入口的Filter。反过来直接上Spring Boot的人往往连请求是怎么路由到Controller的都答不清楚。做这个系统不要只求跑通把Filter、Session、PreparedStatement、事务这四个点挨个追一遍才算真正吃透了。希望上面这些踩坑记录和代码习惯能帮到你至少让你在答辩前少熬两个通宵。本文还有配套的精品资源点击获取
返回列表