news 2026/10/2 14:11:33

JSP+Servlet+JDBC+MySQL共享租车系统完整开发实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JSP+Servlet+JDBC+MySQL共享租车系统完整开发实践

最近接手维护一个老牌的课程设计项目——基于 JSP + Servlet + JDBC + MySQL 的共享租车信息管理系统,技术栈一看就是典型的 JavaWeb 教学案例:没有 Spring、没有 MyBatis,甚至连 Maven 都没用,就是最原始的 Java + JSP + Servlet + JDBC 直连数据库。

说实话,这几年大家一上来就学 Spring Boot,JSP 这套"上古组合"反而成了稀缺技能。但恰恰是这种项目,把 Servlet 生命周期、HTTP 状态管理、JDBC 事务、Tomcat 部署这些 JavaWeb 底层原理暴露得最彻底。对于正在做课程设计、毕业设计,或者想补一补 JavaWeb 基础的同学来说,这东西比一百个 CRUD 的 Spring Boot 教程都值得啃。

这篇文章我就从业务建模、环境配置、核心代码链路、部署踩坑四个维度,把这个共享租车系统彻底拆开讲清楚,让你拿到的不是一段能跑但看不懂的代码,而是一套能自己复现、能在答辩时讲明白的完整项目。

1. 共享租车系统的业务边界:为什么四张核心表就能撑起整套流程

很多同学拿到项目第一件事就是看代码,这其实是个坏习惯。老项目最怕"没有注释直接看逻辑",你根本不知道每一层在干什么。我习惯先理清业务模型,再去看表结构,最后才去对代码,这样效率高得多。

1.1 用户视角下的核心需求

共享租车和传统租车有个本质区别:传统租车是"按天租",共享租车是"按小时租",而且取车和还车可能不在同一个地点。不过作为课程设计级别的项目,往往会把业务收敛到最核心的闭环,避免功能过于分散后代码失控。

我这个系统里实际跑的业务闭环是这样:

  • 用户注册并登录系统,注册信息包括手机号、密码、姓名、身份证号(用于租车资质校验)。
  • 用户浏览车辆列表,按车型筛选,查看车辆的实时状态(可租/已租)。
  • 用户发起租车请求,系统锁住车辆,记录租用开始时间。
  • 用户还车,系统根据用车时长计算费用,并更新车辆状态为"可租"。
  • 管理员登录后台,维护车辆信息(上架、下架、修改价格),查看所有订单。

这个流程看起来简单,但如果你把每一步对应的数据访问都拆出来,涉及的JSP页面、Servlet类、DAO方法加起来有五六十个文件。这正是JSP+Servlet项目的典型特征:没有框架帮你省事,所有请求映射、参数获取、数据库操作都必须手写,也因此每一行代码都值得细看。

1.2 数据库表的字段设计

我在设计数据库时遵循"最少表、但字段不偷懒"的原则。整个系统只用了四张表:

表名用途核心字段
t_user用户信息id, username, password, phone, idcard, realname, create_time
t_car车辆信息id, car_no, brand, model, price_per_hour, status, create_time
t_order租车订单id, user_id, car_id, start_time, end_time, total_fee, status
t_admin管理员id, username, password

其中车辆表里的 status 字段是关键,我用了 int 类型做状态机,0 表示可租,1 表示已租,2 表示维修中。订单表的 status 字段同样用 int 表达:待取车、使用中、已完成、已取消。

这种"用数字表达状态"的做法看起来朴素,但好处是 JSP 页面里做状态展示时判断逻辑非常干净,类似<c:if test="${order.status == 2}">,也方便后续扩展。我见过很多同学喜欢用字符串存状态,今天写个"可用",明天写个"可用中",后天写个"AVALIABLE",数据一乱,前端全崩,这点必须要控制住。

1.3 建表 SQL 的几个实用细节

共享租车系统涉及费用和时间,有两个字段细节容易被忽略:

  • 车辆表的 price_per_hour 用 DECIMAL(10,2) 而不是 DOUBLE。因为 DOUBLE 在涉及浮点累加时容易出现精度误差,计费场景不能用浮点。
  • 订单表的 start_time 和 end_time 用 DATETIME,不要用 TIMESTAMP。TIMESTAMP 有 2038 年问题,虽然现在看起来遥远,但课程设计里经常直接拿now()测试,DATETIME 的可读性更好,存储范围更大。

另外我建议在 user 表里给 username 加唯一索引。你永远不知道答辩演示的时候,会不会有同学手滑注册了两次,然后系统里出现两条一模一样的账号。这种边界情况提前用数据库约束挡掉,比在 Servlet 里做逻辑判断省心得多。

2. 跑通项目的环境组合:从 IDEA 配置到 MySQL 连接串

老项目的第一道坎通常不是代码,而是环境。JSP+Servlet 项目对工具链版本特别敏感,JDK 版本太高编译不过,Tomcat 版本太新又不兼容 Servlet 规范,MySQL 8.0 的连接串写法跟 5.7 又不一样。我先把这套组合讲清楚,你照着搭基本不会翻车。

2.1 版本组合怎么选才稳定

我实际验证过最稳的一套组合是:

  • JDK 1.8(不要用 11 或 17,很多老项目的 javax.servlet 包在新 JDK 下会报警告甚至编译失败)
  • Tomcat 8.5(兼容 Servlet 3.1,对 JSP 2.3 支持好)
  • MySQL 5.7 或 8.0(后面连接串写法略有差异,我会单独说明)
  • IDEA(社区版就够,不需要旗舰版)

有人会问,为什么不直接用 Tomcat 9 或 10?很简单,Tomcat 10 开始把javax.servlet改成了jakarta.servlet,老项目的 import 全是 javax 开头,部署上去直接 NoClassDefFoundError。所以如果你的项目是 2020 年之前写的,老老实实用 Tomcat 8.5,别折腾。

2.2 MySQL 连接串:一个字符都不能错

连接串是 JDBC 项目的高频事故区。我见过太多次"账号密码明明对了,但项目就是连不上数据库"的情况,十次有九次是连接串参数写法问题。

MySQL 5.7 推荐的写法是:

private static final String URL = "jdbc:mysql://localhost:3306/car_share?useUnicode=true&characterEncoding=utf8&useSSL=false";

MySQL 8.0 则要加上时区参数,并且驱动类变了:

private static final String URL = "jdbc:mysql://localhost:3306/car_share?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai"; private static final String DRIVER = "com.mysql.cj.jdbc.Driver";

驱动包也要对应:5.7 用 mysql-connector-java 5.1.47,8.0 用 8.0.x 版本,驱动 jar 包必须放进项目的WEB-INF/lib目录下,而不是只放在 IDEA 的 library 里。后面部署到 Tomcat 的时候,tomcat 认的是 WEB-INF/lib,这一点我见过无数次有人踩坑。

2.3 IDEA 里配置 Tomcat 的正确姿势

在 IDEA 中运行 JSP+Servlet 项目,很多新手会卡在 404 或页面无法访问。正常流程是这样的:

  1. 在项目结构里找到 Artifacts,确保输出的类型是 Web Application Exploded,并且加了WEB-INF/lib下所有依赖。
  2. 添加 Tomcat Server -> Local,选择本地 Tomcat 安装目录。
  3. Deployment 里把项目的 Artifacts 加进去,Application context 建议设为/,不要带项目名。这样访问地址就是http://localhost:8080/,省得每次输一长串路径。
  4. 修改 Tomcat 的端口时,要同时检查 HTTP port 和 JMX port,很多冲突报错其实是 JMX 端口占用引起的。

这一步做完,Run 起来如果浏览器直接能看到登录页,说明环境跑通了。跑不通的时候不要急着看业务代码,先看 IDEA 控制台的 Tomcat 日志,90% 的问题在日志里写得明明白白。

3. 核心业务链路拆解:从注册登录、车辆查询到租车还车的事务处理

项目跑起来之后,就到了大家最喜欢问的环节:"代码是怎么写的?"这节我会按照系统的三条核心业务链——用户登录、租车下单、还车计费——逐一拆开,把 Servlet 和 JDBC 的关键代码逻辑讲透。

3.1 JDBC 工具类的封装:省掉九九八十一行重复代码

JSP+Servlet 项目中,几乎每个 DAO 都要拿连接、执行 SQL、关资源。如果不封装,一个项目里到处都是Class.forName、DriverManager.getConnection、conn.close(),代码量爆炸且极易漏关连接。

我习惯封装一个DBUtil类,用静态代码块加载驱动,用ThreadLocal管理连接,这样后续做事务时也能保证同一个线程用的是同一个 Connection:

public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/car_share?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PASSWORD = "123456"; private static ThreadLocal<Connection> connHolder = new ThreadLocal<>(); static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError("MySQL驱动加载失败,请检查WEB-INF/lib下是否有驱动jar包"); } } public static Connection getConnection() throws SQLException { Connection conn = connHolder.get(); if (conn == null) { conn = DriverManager.getConnection(URL, USER, PASSWORD); connHolder.set(conn); } return conn; } public static void closeAll(Connection conn, Statement stmt, ResultSet rs) { if (rs != null) { try { rs.close(); } catch (SQLException e) { } } if (stmt != null) { try { stmt.close(); } catch (SQLException e) { } } if (conn != null) { try { conn.close(); } catch (SQLException e) { } } connHolder.remove(); } }

这个封装里有三个设计点值得注意:第一,驱动加载放进静态代码块,只执行一次;第二,资源关闭用三个 try-catch 独立包裹,避免一个关闭异常影响后面资源的释放;第三,关闭连接后要remove(),防止 ThreadLocal 在 Web 容器线程池中造成内存泄漏。

3.2 用户登录流程:JSP 表单、Servlet 控制、JDBC 查询的一整套串联

登录功能是 JSP+Servlet 项目中最经典的"教科书链路",但往往也是同学们第一个写糊涂的地方。我把它拆成四个环节:

  • JSP 页面:登录表单的action指向UserServlet,method为post。
  • Servlet 控制层:doPost方法里先获取参数,再判断是"登录"还是"注册"操作(我习惯通过隐藏字段action区分)。
  • DAO 层:findUserByUsernameAndPassword方法执行查询。
  • Session 管理:登录成功后把 user 对象放入 session,页面跳转。

Servlet 核心代码大概是这个形态:

@WebServlet("/user") public class UserServlet extends HttpServlet { private UserDao userDao = new UserDao(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String action = req.getParameter("action"); if ("login".equals(action)) { String username = req.getParameter("username"); String password = req.getParameter("password"); User user = userDao.login(username, password); if (user != null) { HttpSession session = req.getSession(); session.setAttribute("loginUser", user); resp.sendRedirect("index.jsp"); } else { req.setAttribute("msg", "用户名或密码错误"); req.getRequestDispatcher("login.jsp").forward(req, resp); } } else if ("register".equals(action)) { // 注册逻辑,省略 } } }

注意这里我用了resp.sendRedirect做登录成功跳转,用req.getRequestDispatcher().forward做失败跳转。两者的区别在于:sendRedirect 是重定向,地址栏会变化,且 request 域中的属性会丢失;forward 是转发,地址栏不变,request 域可以传递数据。出错时用 forward 把错误信息塞进 request 域带到 JSP 页面,这是最常规的写法。

3.3 租车下单与还车计费:一次必须用事务的完整场景

很多人写 JDBC 的增删改查很溜,但一到"多表操作"就翻车了——因为不知道事务什么时候该用。这个租车流程里就有一个教科书级的例子:租车下单时,既要插入订单记录,又要修改车辆状态。这两步必须同时成功或者同时失败,否则就会出现"订单生成了但车辆状态还是可租"的数据不一致。

下单的核心流程是:

  1. 根据 vehicleId 查出车辆信息,确认 status == 0(可租)。
  2. 插入一条订单记录,状态为"使用中",记录租车开始时间。
  3. 更新车辆 status 为 1(已租)。

这三步我放在同一个方法里,用手动事务控制:

public void rentCar(int userId, int carId) throws SQLException { Connection conn = DBUtil.getConnection(); try { conn.setAutoCommit(false); // 第1步:检查车辆状态 Car car = carDao.findById(carId); if (car == null || car.getStatus() != 0) { throw new RuntimeException("车辆不存在或已被租出"); } // 第2步:插入订单 Order order = new Order(); order.setUserId(userId); order.setCarId(carId); order.setStartTime(new Date()); order.setStatus(1); orderDao.insert(order); // 第3步:更新车辆状态 carDao.updateStatus(carId, 1); conn.commit(); } catch (Exception e) { conn.rollback(); throw new RuntimeException("租车失败,事务已回滚", e); } finally { conn.setAutoCommit(true); DBUtil.closeAll(conn, null, null); } }

关键点在于:setAutoCommit(false)之后,所有操作都在同一个事务里;一旦任何一步抛出异常,rollback()会撤销已执行的 SQL。这是 JSP+Servlet 项目里最实用也最容易被忽视的事务写法。很多同学的代码明明从"表设计到功能实现"都对,就是这里没开事务,导致做一步错一步,越做越怀疑人生。

还车计费同理。还车时要计算end_time - start_time的时长,并按小时价格算钱,再更新订单状态和车辆状态。计算逻辑不难,但同样要放进事务,确保"费用算对了 + 订单状态改了 + 车辆恢复可租"这三件事原子生效。

3.4 车辆列表分页查询:LIMIT 参数在 Servlet 和 JSP 之间怎么传

共享租车的车辆数量多了以后,列表不能一次全查。我用的方案是最经典的下拉分页:Servlet 接收pageNow和pageSize参数,DAO 层执行带 LIMIT 的查询,JSP 页面用PageBean对象渲染分页按钮。

SELECT * FROM t_car WHERE status = 0 ORDER BY id LIMIT ?, ?

注意 JDBC 中 LIMIT 的参数占位符要用setInt来设置,因为LIMIT后面的值是数字类型,不能当字符串处理。我见过有人写成setString,MySQL 直接报语法错误。这个细节看起来小,排错时却很烦人。

分页查询还需要一个总数统计:SELECT COUNT(*) FROM t_car WHERE status = 0,然后根据"总记录数"和"每页条数"计算出总页数。Idea 里调试分页时,最常遇到的问题就是:翻到第 5 页之后再点下一页,pageNow超过总页数导致界面空白。解决办法是在 Servlet 里做一个保护:如果 pageNow 大于总页数,强制重置为总页数。

4. 部署打包与那些必然踩到的坑:war 包、驱动版本、乱码与端口占用

环境配好了,代码也写通了,最后一步是把项目打成 war 包部署到 Tomcat——很多人到了这一步又开始抓狂。这里把高频问题汇总成一张表,并给出具体的处理建议。

4.1 传统 JSP 项目怎么打 war 包

在 IDEA 里打包 war 包很简单:进入 Project Structure -> Artifacts,新建一个 Web Application Archive 类型,名称取car_share,然后 Build -> Build Artifacts。生成出来的 war 包直接复制到 Tomcat 的webapps目录下,启动 Tomcat 后它会自动解压并发布。

如果是命令行环境,也可以用jar -cvf car_share.war *在编译好的 Web 根目录下打,但要注意保证目录结构是WEB-INF/classes和WEB-INF/lib这对标准形态。

有一点要特别提醒:war 包部署后,Tomcat 访问路径默认是http://localhost:8080/car_share/,如果你开发时用的 Application context 是/,那么部署后的所有 internal 相对路径都要多带一层项目名,否则资源加载全是 404。这个问题开发时不容易发现,一到部署就炸。解决方案是在所有 JSP 页面里都用${pageContext.request.contextPath}来拼绝对路径。

4.2 高频报错排查表:照着查就能定位问题

报错现象根本原因解决办法
java.lang.ClassNotFoundException: com.mysql.jdbc.Driver驱动 jar 未放入 WEB-INF/lib把 mysql-connector-java 复制到 WEB-INF/lib 目录
Access denied for user 'root'@'localhost'数据库账号或密码错误核对 DBUtil 中的账号密码,注意 MySQL8 默认认证插件
Unknown system variable 'query_cache_size'MySQL 8 连接串未加时区参数改用serverTimezone=Asia/Shanghai并升级驱动
Tomcat 启动端口冲突8080 被其他进程占用改 Tomcat 端口,或用任务管理器杀掉占用进程
中文乱码请求参数编码和 JSP 编码不一致JSP 页面pageEncoding="UTF-8",Servlet 里req.setCharacterEncoding("UTF-8")
页面加载后 CSS/图片失效使用相对路径导致资源加载失败统一换成${pageContext.request.contextPath}

这张表里的前四条我都实际踩过,尤其是 MySQL 8 连 5.7 项目的驱动包,那个报错信息极其迷惑,光看日志完全不知道是驱动版本的问题。所以我在做这个项目时直接就把驱动固定成com.mysql.cj.jdbc.Driver,给定一个统一标准。

4.3 为什么"明明代码没问题,Tomcat 就是热部署不生效"

最后一个常见困惑是:我改了 JSP 页面,刷新浏览器没变化;我删了某个 Servlet,Tomcat 还继续执行旧类。这在 IDEA 的 Debug 模式里尤其明显。

原因是 Tomcat 默认的 reloadable 参数是 false,或者 IDEA 的 On Upate Action 设置不对。建议把 Tomcat 配置里的 On Update Action 改成 Update resources,这样改 JSP 后按 Ctrl+F10 就会生效;如果是改 Java 文件,需要 Update classes and resources。如果怎么热部署都不生效,那就干脆重启 Tomcat,尤其是我们这种纯 Servlet 项目,注解注册的 Servlet 在热部署时经常出现旧类残留。

4.4 一点关于"老技术栈"的思考

最后说点务虚的感受。很多人对 JSP+Servlet 嗤之以鼻,觉得这是已经被时代淘汰的技术,做课程设计纯粹是为了交差。我的看法不太一样:如果你已经是 Spring Boot 的老手,回来看这种项目确实会觉得繁琐;但如果你还没完整地做过一个 JavaWeb 项目,从 JSP+Servlet 起步反而是最快的路径。

因为它把 Web 开发的所有"中间层"都撕掉了:没有 Spring IoC 帮你管理依赖,没有 MyBatis 帮你映射 SQL,没有模板引擎帮你渲染数据。你被迫一步步手动完成"客户端请求 -> Servlet 解析 -> JDBC 操作 -> 结果返回"的完整过程,这会让你对 HTTP、Session、事务这些概念的理解到达另一个层次。

等到你能徒手写出一个能跑、能部署、能处理事务的 JSP+Servlet 项目,再去看 Spring Boot 的封装,你会看得懂它到底替你省了什么、又藏了什么。反过来,一上来就 Spring Boot 的同学,遇到依赖冲突和数据源配置问题,往往连从哪里排查都不知道。

我的实操建议是:把这个共享租车系统做出来之后,再自己加两个功能练手——比如"用户查看历史订单"和"管理员导出订单报表"。加功能的过程会逼你重新梳理刚才提到的状态机、事务和多表查询,这才是这门课真正的收获。这套 JSP+Servlet 的组合虽然老,但它值得你花时间把它彻底吃透。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 14:08:12

TCP文件传输实战:从协议设计到C语言实现与疑难排查

TCP 文件传输这个题目&#xff0c;看起来像是课程设计或者面试前突击的项目&#xff0c;但真把它写好&#xff0c;比你想象中要挖得深。我在实际开发里被 TCP 的“字节流”特性坑过不止一次&#xff0c;也见过不少把 send 和 recv 当成“一次发完、一次收全”导致文件损坏的…

作者头像 李华
网站建设 2026/10/2 14:08:09

深度学习隐写分析系统落地实战:从论文到可交互GUI

简介&#xff1a;本资源是一套基于深度学习的图像隐写分析与去除系统完整实现&#xff0c;面向计算机、人工智能、信息安全等专业本科生及研究生&#xff0c;适用于毕业设计、课程实践与算法复现学习。项目涵盖隐写分析&#xff08;SRNet模型&#xff09;与隐写去除&#xff08…

作者头像 李华
网站建设 2026/10/2 14:06:37

插件化Agent开发:Cordis运行时与Harness编排实战解析

如果你这两天在刷 Agent 相关的技术讨论&#xff0c;大概率见过这几个词连在一起出现&#xff1a;deepseek harness、harness anything、node cordis、dsh harness。看起来像几个不同项目&#xff0c;其实指向同一件事——社区正在把 Agent 从“一个脚本干一件事”&#xff0c;…

作者头像 李华
网站建设 2026/10/2 14:05:46

机器学习预测A股:从数据采集到LSTM回测的完整源码解析

简介&#xff1a;一份基于机器学习算法预测A股走势的完整系统压缩包&#xff0c;面向对量化交易与数据建模感兴趣的投资者、金融从业者及数据科学学习者&#xff0c;覆盖从数据预处理到模型训练、回测的完整流程。包内共12个文件&#xff0c;以6个Jupyter Notebook为核心&#…

作者头像 李华
网站建设 2026/10/2 14:01:51

C# OpenVINO裂缝分割源码实战:从模型加载到推理优化

简介&#xff1a;本资源为基于C#与Intel OpenVINO工具包的裂缝分割与检测项目源码&#xff0c;面向具备一定C#基础、希望入门深度学习推理部署的开发者&#xff0c;可应用于建筑结构健康监测、道路巡检等计算机视觉场景。压缩包共272个文件&#xff0c;约244.76MB&#xff0c;以…

作者头像 李华