☰
JavaWeb共享租车管理系统设计:JSP+Servlet+JDBC核心实现解析
2026/10/2 2:06:32 网站建设 项目流程

最近帮朋友捋一个毕业设计,正是标题里这套基于 javaweb + mysql 的 jsp + servlet 共享租车信息管理系统。说实话,这两年大家一窝蜂去卷 SpringBoot、微服务、前后端分离,真到了课程设计、毕业设计、入门练手这种场景,反而很多人绕回来重新用 JSP + Servlet + JDBC 这套经典组合。我自己带过几个类似项目,也在上面踩过不少坑,今天就把整个项目的设计思路、数据库表结构、核心代码实现、部署过程和排查记录一次性讲清楚。

这套项目虽然技术栈看着老,但它恰恰是理解 JavaWeb 底层原理的最佳路径:Servlet 怎么处理请求、JSP 怎么渲染页面、JDBC 怎么和 MySQL 交互、Session 怎么管理登录状态,每一环都是明文可见的,没有框架帮你把细节藏起来。对新手来说,跑通这样一个完整项目,比照着 SpringBoot 模板抄十个 CRUD 都管用。对参加课设、毕设的同学来说,这套结构也完全能撑起一篇像样的论文和答辩。我下面讲的所有内容,都是按照实际可运行、可演示的标准来写的,你可以直接照着做。

1. 项目整体设计与技术选型思路

1.1 为什么还选 JSP + Servlet + JDBC

先回答一个最常被问到的问题:都 2025 年了,为什么还要用 JSP?

我的观点很直接:不是因为它先进,而是因为它简单、透明、适合教学和演示。SpringBoot 帮你自动装配了 Tomcat、自动处理了 JSON 序列化、自动管理了 Bean,你在 controller 里写两行注解就能跑通接口,但底层为什么能跑通,很多人完全不理解。而 JSP + Servlet 这套组合,请求进来先到 Servlet,Servlet 调 DAO,DAO 用 JDBC 连 MySQL,数据拿出来放进 request 域,再 forward 到 JSP 渲染,整条链路清清楚楚,没有黑盒。

从答辩角度讲,评委大概率会问几个问题:登录状态怎么保持的?数据库连接怎么管理的?订单和车辆状态怎么保证一致性?你如果用 SpringBoot + MyBatis-Plus,可能几句话就解释完了,但用 JSP + Servlet + JDBC,你需要手写 Session、手写 Connection、手写事务,这反而成了你展示基本功的最好素材。我见过不少用最新框架做答辩讲不清楚原理的,也见过用这套老技术栈把底层原理讲得头头是道的,后者拿高分的机会要大得多。

从学习角度讲,JDBC 是 MyBatis、Hibernate 这些 ORM 框架的底层基础,Servlet 是 SpringMVC 的核心模型,JSP 虽然被 Thymeleaf 和前后端分离替代了,但它背后的 request 域、Session、Cookie 机制到今天依然没变。把这套组合吃透,再看任何 JavaWeb 框架都会轻松很多。

1.2 共享租车系统到底要做什么

很多同学拿到这种题目,第一反应是“不就是个增删改查吗”。这话一半对一半不对。租车系统确实绕不开增删改查,但它比普通的信息管理系统多了一个核心难点:资源(车辆)是有限的、独占的,同一辆车在同一时间段只能被一个人租用。这和图书管理系统有本质区别,书可以复印,车不能一车两租。

所以这个系统的核心业务要围绕三个角色和三个状态来设计。

三个角色分别是:管理员、普通用户、游客。游客只能浏览车辆列表,普通用户可以登录、租车、查看自己的订单、取消订单,管理员可以管理车辆信息、审核订单、处理还车结算、管理用户。

三个状态分别是:车辆状态(空闲、已租、维修中)、订单状态(待取车/租赁中、已完成、已取消)、用户登录态(在线、离线)。

围绕这些角色和状态,功能模块拆成前台和后台两块。前台面向普通用户,包括注册登录、车辆列表浏览、按品牌或价格条件检索、车辆详情、下单租车、我的订单、取消订单。后台面向管理员,包括管理员登录、车辆信息的增删改查、订单列表审核、订单状态变更、租车统计概览。模块不需要做得很花哨,但每个功能都要能闭环,比如用户下单后,管理员能看到这笔订单并确认,用户能在自己的订单列表里看到状态变化,车还完以后车辆自动回到空闲状态,这个闭环比功能数量更重要。

1.3 三层结构如何在代码里落地

这套项目的代码分层我建议严格按 JSP + Servlet + Service/DAO 来拆,不要把所有代码堆在 Servlet 里。很多同学的课设最后变成“一个 Servlet 干所有事”,几百行代码挤在一起,出问题根本没法调。

我习惯的分层是:

  • 展示层:JSP 页面,负责接收数据并渲染,不在 JSP 里写 Java 业务逻辑,最多用 JSTL 或 EL 表达式取值。
  • 控制层:Servlet,负责接收请求参数、调用业务逻辑、分发页面。一个 Servlet 对应一类业务,比如 UserServlet 管登录注册,CarServlet 管车辆列表和详情,OrderServlet 管下单和订单查询。
  • 业务/数据层:DAO(Data Access Object),负责和数据库打交道,每个表对应一个 DAO 类,比如 UserDao、CarDao、OrderDao。事务控制在 Service 层或者直接在 Servlet 里调用 DAO 时处理,看项目复杂度,这个项目我建议在关键操作如“下单”里手动控制事务。

这样一个 Servlet + 一个 DAO + 若干 JSP 的结构,职责清晰,出了问题能快速定位是参数错了、业务错了还是 SQL 错了。更重要的是,后来如果你要换框架,从这套结构往 SSM 迁移是非常自然的,因为 DAO 对应 Mapper,Servlet 对应 Controller,JSP 对应 View,几乎是一一映射的关系。

2. 数据库设计与核心表结构

2.1 建模思路:围绕三张核心表展开

数据库是整个系统最不能偷懒的部分。很多人的课设表结构随便写,字段不全、没有外键关联、时间字段用字符串存,结果写代码的时候到处碰壁。我建议先想清楚实体关系再动手建表。

这个项目最核心的实体有三张表:用户表 t_user、车辆表 t_car、订单表 t_order。用户和车辆之间是多对多的关系,一个用户可能租过很多辆车,一辆车也可能被很多用户租过,所以必须通过订单表来关联。如果你只做简单的“用户表 + 车辆表 + 订单表”三张表,逻辑上基本够了,但如果你想做得更完整,可以再加一张维修记录表或系统日志表,不过课设阶段不建议把范围铺太大,三张主表加两张辅助表就够了。

设计表结构时,有几点要特别注意:

第一,主键全用自增 INT,不要用字符串主键,字符串主键在联表查询时效率低,代码里也要多写一层处理。

第二,价格字段用 DECIMAL(10,2),绝对不用 DOUBLE。DOUBLE 是浮点数,累加和比较精度都有坑,涉及钱一律用定点数。

第三,时间字段区分“日期”和“时间戳”。租车的取车日期、还车日期用 DATE 类型,订单创建时间用 DATETIME,别全用字符串。

第四,状态字段用 TINYINT 数字枚举,不用 VARCHAR 存中文状态。比如车辆状态 0 表示空闲、1 表示已租、2 表示维修中,代码里定义常量,不要散落魔法数字。

2.2 建表 SQL 与关键字段说明

下面这套建表语句是这个项目的基础,我直接贴出来,你可以在 Navicat 或命令行里执行。

-- 用户表 CREATE TABLE t_user ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主键', username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名', password VARCHAR(100) NOT NULL COMMENT '密码', phone VARCHAR(20) DEFAULT NULL COMMENT '手机号', id_card VARCHAR(20) DEFAULT NULL COMMENT '身份证号', role TINYINT NOT NULL DEFAULT 1 COMMENT '角色 0-管理员 1-普通用户', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 车辆表 CREATE TABLE t_car ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主键', brand VARCHAR(50) NOT NULL COMMENT '品牌', model VARCHAR(50) NOT NULL COMMENT '车型', plate_no VARCHAR(20) NOT NULL UNIQUE COMMENT '车牌号', daily_price DECIMAL(10,2) NOT NULL COMMENT '日租金', status TINYINT NOT NULL DEFAULT 0 COMMENT '状态 0-空闲 1-已租 2-维修中', description VARCHAR(500) DEFAULT NULL COMMENT '车辆描述', image_url VARCHAR(255) DEFAULT NULL COMMENT '车辆图片路径', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='车辆表'; -- 订单表 CREATE TABLE t_order ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主键', order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '订单编号', user_id INT NOT NULL COMMENT '用户ID', car_id INT NOT NULL COMMENT '车辆ID', start_date DATE NOT NULL COMMENT '取车日期', end_date DATE NOT NULL COMMENT '还车日期', total_days INT NOT NULL COMMENT '租用天数', total_price DECIMAL(10,2) NOT NULL COMMENT '订单总金额', status TINYINT NOT NULL DEFAULT 0 COMMENT '状态 0-待取车 1-租赁中 2-已完成 3-已取消', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '下单时间', INDEX idx_user_id (user_id), INDEX idx_car_id (car_id), CONSTRAINT fk_order_user FOREIGN KEY (user_id) REFERENCES t_user(id), CONSTRAINT fk_order_car FOREIGN KEY (car_id) REFERENCES t_car(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

这三张表设计好以后,整个系统的数据关系就清晰了。t_user 和 t_order 是一对多,t_car 和 t_order 也是一对多,订单表的 user_id 和 car_id 字段通过外键约束保证数据完整性。

这里有个细节值得说:order_no 订单编号字段我单独加了一个唯一约束,没有直接用自增 id 做订单号。原因是订单号通常需要展示给用户,自增整数容易被遍历猜测,而且多个系统之间可能还需要对接。我习惯用时间戳加随机数生成,比如yyyyMMddHHmmss + 4位随机数,这样不会重复,看起来也规范。

2.3 租期冲突:这辆车到底能不能租

这是整个数据库设计里最容易忽略、但又是租车系统最关键的问题。普通用户下单租车,选了一个时间段,系统必须校验:这辆车在这个时间段里是不是已经被别人预定了?如果不做这个校验,就会出现很尴尬的情况——用户到了取车那天才发现车被别人开走了。

这个校验的核心就是一条 SQL,逻辑是找当前订单与已有有效订单的租期区间是否重叠。已知的租车区间是 [start_date, end_date],已有订单的区间也是 [start_date, end_date],两个区间不重叠的条件只有一个:已有订单的结束日期小于新订单的开始日期,或者已有订单的开始日期大于新订单的结束日期。所以区间重叠的判断反过来就是:

SELECT COUNT(*) FROM t_order WHERE car_id = ? AND status IN (0, 1) AND NOT (end_date < ? OR start_date > ?)

如果这条 SQL 查出来的 count 大于 0,说明该车辆在目标时间段内已经被租用了,本次下单必须拒绝。这条逻辑建议同时在数据库层面和业务代码层面做双重校验,数据库保证底层数据不出错,业务代码保证用户能看到友好的提示。

另外,我建议在 t_car 表上加一个状态字段来辅助判断:车辆状态为“已租”或“维修中”时直接在前端页面隐藏或标记,这样用户还没点进来就知道这辆车现在不可租。当然状态字段只能表示当下状态,不能表示时间段冲突,所以上面的 SQL 才是真正解决并发冲突的关键。

3. 核心功能的实现与难点拆解

3.1 登录注册与 Session 状态管理

登录这块看似简单,但很多同学会在 Session 和重定向上翻车。整个登录流程应该是:用户在 login.jsp 输入用户名密码,表单提交到 UserServlet,UserServlet 调 UserDao 查数据库,查到就存 Session,查不到就带错误信息转发回登录页。

我贴一个实际用的代码片段:

@WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); UserDao userDao = new UserDao(); User user = userDao.findByUsernameAndPassword(username, password); if (user != null) { HttpSession session = request.getSession(); session.setAttribute("loginUser", user); session.setMaxInactiveInterval(30 * 60); // 30分钟过期 response.sendRedirect(request.getContextPath() + "/index.jsp"); } else { request.setAttribute("error", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); } } }

这里有三个地方要特别提醒。

第一,登录成功用sendRedirect,登录失败用forward。为什么?因为 redirect 是浏览器重新发起一次请求,地址栏会变成目标地址,用户刷新不会重复提交表单。而 forward 是服务器内部转发,地址栏不变,用户如果按 F5 会重新提交一次密码,处理不好就是重复登录。这个区别面试和答辩都爱问。

第二,密码不要明文存数据库。虽然课设里很多人不管这个,但我建议至少做一次 MD5/SHA256 哈希再存,SQL 注入是高危漏洞,明文密码是第二高危漏洞,你写进论文也算是一个亮点。

第三,管理员和普通用户复用同一个登录入口,登录成功后根据 user.role 决定跳转后台还是前台,这比单独再写一套管理员登录要省事得多。

3.2 车辆分页列表与条件检索

车辆列表是用户打开系统第一眼看到的东西,如果一次性把所有车都查出来,数据量一大页面就会卡。所以分页是必须实现的,这也是答辩时评委很喜欢问的一个点。

分页的核心思路很简单:MySQL 的LIMIT offset, size。我定义每页显示 8 辆,当前页码从请求参数 page 获取,然后算 offset = (page - 1) * pageSize,同时需要先查总记录数再算总页码。

int page = 1; String pageStr = request.getParameter("page"); if (pageStr != null && !pageStr.isEmpty()) { page = Integer.parseInt(pageStr); } int pageSize = 8; int total = carDao.countAll(); int totalPages = (int) Math.ceil(total * 1.0 / pageSize); List<Car> carList = carDao.findByPage((page - 1) * pageSize, pageSize); request.setAttribute("carList", carList); request.setAttribute("totalPages", totalPages); request.setAttribute("currentPage", page); request.getRequestDispatcher("/car_list.jsp").forward(request, response);

对应 DAO 里的 SQL 是:

SELECT * FROM t_car ORDER BY create_time DESC LIMIT ?, ?

关于条件检索,我建议至少支持两个维度:品牌和日租金范围。品牌用下拉框,价格用输入框,组合查询时在 SQL 上用WHERE 1=1后面动态拼接条件。这种写法虽然在很多老框架里被吐槽,但在原生 JDBC 里恰恰是最直观可行的方案,用StringBuilder拼条件时注意每个条件前加AND即可。

分页和条件检索还有一个容易踩的坑:条件检索后的分页。用户搜索“宝马”后点第二页,这时候如果页码链接还是不带条件的,就会丢掉搜索结果。我处理的办法是把查询条件作为隐藏参数带在分页链接里,或者在 Servlet 里接收参数时同时接收 page 和 keyword,再在页面拼接 URL 时把条件带上。这个细节虽然小,但操作体验差别很大。

3.3 下单与租金计算:一个完整的事务流程

下单是整个系统里最复杂、最值得深入讲的环节,因为它牵扯到多张表的状态变化,必须用事务保证数据一致性。整个下单流程是这样的:

第一步,用户选择车辆,填写取车日期和还车日期。

第二步,后端计算租用天数。我用LocalDate处理,两个日期相减得到天数。如果用户选的还车日期早于取车日期,直接报错。

第三步,查询车辆日租金,计算总价 = 日租金 × 天数。

第四步,做租期冲突校验,也就是前面说的区间重叠查询,这一步和第五步必须在同一个事务里。

第五步,插入订单记录,同时把车辆状态从“空闲”改成“已租”。

这里最关键的是第四步和第五步不能分开执行。如果先查冲突再插入订单,SQL 执行之间有一个时间窗口,两个用户可能同时通过了冲突检查,都往里插数据,那辆车就被租出去两次。解决办法是使用数据库事务加行锁,把整个流程放在一个Connection里,SELECT ... FOR UPDATE锁住车辆行,再执行冲突检查和订单插入。

Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 1. 锁行查询车辆信息 String lockSql = "SELECT * FROM t_car WHERE id = ? FOR UPDATE"; PreparedStatement lockPs = conn.prepareStatement(lockSql); lockPs.setInt(1, carId); ResultSet rs = lockPs.executeQuery(); // 2. 校验租期冲突 String conflictSql = "SELECT COUNT(*) FROM t_order " + "WHERE car_id = ? AND status IN (0, 1) " + "AND NOT (end_date < ? OR start_date > ?)"; // 3. 插入订单 // 4. 更新车辆状态 conn.commit(); } catch (Exception e) { if (conn != null) conn.rollback(); e.printStackTrace(); } finally { if (conn != null) conn.setAutoCommit(true); }

FOR UPDATE是数据库悲观锁,锁住这行后,另一个事务在提交前查询这一行会被阻塞,从而避免超卖。这个点我建议所有做类似项目的同学都好好理解,答辩时你能讲清楚“为什么下订单要用事务和锁”,这已经是拉开差距的水平了。

关于租期天数计算,还有一个实际业务里容易忽略的细节:租车是不足一天按一天算,还是按自然日算。我做的时候按自然日算,取车当天到还车当天之间相隔的自然日数,比如 5 月 1 日到 5 月 3 日是 3 天。代码里用ChronoUnit.DAYS.between(startDate, endDate)就能拿到,注意这里拿到的其实是 2,要加 1 才是 3,这个加减的逻辑你写的时候最好单独测一下,别想当然。

3.4 订单状态流转与管理员后台

订单状态我用数字枚举表示:0 待取车、1 租赁中、2 已完成、3 已取消。这里面的流转规则要定清楚:

  • 用户下单成功:订单状态为 0,车辆状态为已租。
  • 管理员确认开始租赁:订单状态从 0 变成 1。
  • 管理员处理还车结算:订单状态从 1 变成 2,车辆状态恢复为空闲。
  • 用户取消订单(只有待取车状态下允许取消):订单状态变成 3,车辆状态恢复为空闲。

这个规则在代码里体现为:管理员后台有一个订单列表,操作按钮要根据当前状态动态显示。比如状态为 0 的订单显示“确定取车”和“拒绝”,状态为 1 的订单显示“确认还车”,状态为 2 和 3 的订单不再显示任何操作。前端按钮用 JSTL 或 EL 表达式判断,后端 Servlet 里再收到 action 参数时校验状态是否合法,不能只靠前端。

管理员的车辆管理模块就是标准 CRUD:新增车辆时插入一条 t_car 记录,编辑车辆时更新品牌、车型、日租金、描述,删除车辆前要检查该车是否存在未完成订单,如果有就不能删,防止孤儿数据。这地方我建议把检查写在 DAO 层,SQL 查一下 t_order 里有没有未完成的记录,有就返回错误信息,没有才执行 DELETE。

4. 工程搭建与部署实操记录

4.1 环境与工程结构

说点实操的。我自己的开发环境是:JDK 1.8 + Tomcat 9 + MySQL 8.0 + IDEA,这是目前课设环境最稳妥的搭配,JDK 11 以上搭配老项目偶尔会有兼容问题,没必要冒险。

工程结构我用 Maven 还是传统 Web 目录?这个项目我建议直接手工建传统结构,或者用 IDEA 的 Jakarta EE 模板建,不引入 Maven。为什么?因为引入 Maven 会有依赖下载、打包方式等一堆额外问题,很多同学卡在 Maven 上下载依赖的时间比写代码时间还长。传统 Web 结构无非就是 src 下写 Java 代码,webapp 下放 JSP 和 WEB-INF/web.xml,IDEA 里配置一下 Artifacts 就能跑,对新手最友好。

标准目录结构长这样:

src/ com.carrental.servlet/ com.carrental.dao/ com.carrental.entity/ com.carrental.util/ webapp/ WEB-INF/ web.xml login.jsp register.jsp index.jsp car_list.jsp car_detail.jsp order_add.jsp my_orders.jsp admin/ admin_index.jsp car_manage.jsp order_manage.jsp

entity 里放 User、Car、Order 三个实体类,属性和表字段一一对应,用最简单的私有属性加 getter/setter,不引入任何框架。util 里放 DBUtil 工具类。

4.2 JDBC 工具类与 DAO 层封装

JDBC 的写法是整个项目的底层基础,我强烈建议你写一个 DBUtil 类统一管理连接获取和关闭,别在每个 DAO 里重复写 Class.forName 和 DriverManager。一个规范的工具类长这样:

public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/car_rental" + "?useUnicode=true&characterEncoding=utf8" + "&serverTimezone=Asia/Shanghai&useSSL=false"; private static final String USER = "root"; private static final String PASSWORD = "你自己数据库的密码"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, 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) {} } } }

两个关键点。第一,MySQL 8 的驱动类名是com.mysql.cj.jdbc.Driver,不是老的com.mysql.jdbc.Driver,用老驱动虽然不报错,但会打一大段弃用警告,看着就很难受。第二,URL 里的serverTimezone=Asia/Shanghai必须加,不然 MySQL 8 会因为你没指定时区直接抛异常。useUnicode=true&characterEncoding=utf8是防乱码的,后面专门讲。

DAO 层我建议每个 DAO 类里的方法做成“一个方法对应一条 SQL”:findById、findByPage、countAll、insert、update、delete。代码虽然重复性高,但容易读、容易测、容易改。不要试图写一个万能模板方法,原生 JDBC 里写万能方法只会让你的 SQL 拼接变成灾难。

4.3 Servlet 与 JSP 联动及配置要点

Servlet 这边的请求路径映射有两个方案:注解方式@WebServlet("/login")或者 web.xml 里配置<servlet-mapping>。IDEA 新建的项目默认支持注解方式,课设够用,但我个人建议还是把 web.xml 的配置看一下,因为很多老机器上的 Tomcat 版本和课设需求还是按 web.xml 来的,你如果迁移到其他环境,会配置 web.xml 更稳妥。

web.xml 里必须配置的有两件事:一是所有 Servlet 的映射(如果用注解就省略),二是全局字符编码过滤器。这个过滤器我强烈建议加,否则 GET 和 POST 请求都会出乱码。

<filter> <filter-name>encoding</filter-name> <filter-class>com.carrental.util.EncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encoding</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

过滤器类里最核心的一行代码是request.setCharacterEncoding("UTF-8")。注意这行代码只对 POST 请求的 body 起作用,GET 请求的 URL 参数编码由 Tomcat 的 URIEncoding 决定,所以如果你的 GET 提交带中文还要在 Tomcat 的 conf/server.xml 里给 Connector 加URIEncoding="UTF-8"。这两个地方少配任何一个,都会出现“页面是 UTF-8、数据库是 UTF-8、但中文还是乱码”的诡异状态。

JSP 页面这边,页面顶部一定要写全:

<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" %>

pageEncoding和contentType两个都要,因为 JSP 文件本身的编译解码和响应输出的字符集是两回事,只写一个可能导致编译时报错或页面乱码。

4.4 打包 war 并部署到 Tomcat

我观察到一个现象:很多同学在 IDEA 里点绿色三角按钮跑得飞快,一旦要求演示环境换一台机器,或者要部署到服务器,就懵了。所以打包部署这一步必须自己走一遍。

IDEA 里的操作流程我完整记一下:打开项目结构 Project Structure,选 Artifacts,点加号选 Web Application: Archive,名字取 car_rental,然后在 Output Layout 里确认已经把 webapp 目录下的内容打进去。接着菜单 Build -> Build Artifacts,选择刚才的 Archive 点 Build,几秒钟后项目输出目录下会生成 car_rental.war。

把这个 war 文件复制到 Tomcat 的 webapps 目录下,启动 Tomcat,它会自动解压生成同名目录,然后访问http://localhost:8080/car_rental/index.jsp就能看到项目。注意项目访问路径默认就是 war 包文件名,所以如果包名改过,访问路径也要跟着变。部署前记得把 JDBC 连接中的 localhost 改成目标机器的 IP 或者继续用 localhost,密码改成目标机器的 MySQL 密码。

这一步实操过后,你再也不怕换电脑演示了。

5. 常见问题与排查技巧实录

5.1 数据库连接失败:最常见的三个原因

项目跑不起来,十次里有七次是数据库连接问题,报错信息五花八门,但根因就那么几个。我把这些年见过的连接失败场景按频率排个序。

第一名:Communications link failure。这个报错几乎都是 MySQL 服务没启动,或者你连接的是远程数据库但 IP/端口不通。先检查 MySQL 服务是否在运行,Windows 下services.msc里看 MySQL 服务,Linux 下systemctl status mysql或service mysql status,再用telnet 127.0.0.1 3306测一下端口通不通,基本能定位。

第二名:Access denied for user 'root'@'localhost'。用户名或密码错误,或者当前账号没有从当前主机连接的权限。课设阶段一般用 root 连接本机,多数情况就是密码不对。如果连远程的 MySQL,还要在 MySQL 里授权:GRANT ALL PRIVILEGES ON car_rental.* TO 'root'@'%' IDENTIFIED BY '密码',然后FLUSH PRIVILEGES。

第三名:Unknown database 'car_rental'。连接串里的库名错了,或者 MySQL 里根本还没建这个库。你先看 MySQL 里有没有这个库,没有就执行建库语句:CREATE DATABASE car_rental DEFAULT CHARSET utf8mb4,然后重新导入表结构。

这三类问题我会录成一个排查清单,遇到连接问题先看服务、再看账号、再看库名,不要瞎试。

5.2 中文乱码到底怎么修

乱码问题是传统 JSP 项目的重灾区,而且特别的折磨人,因为“页面正常”和“数据库正常”并不代表“数据链路正常”。乱码的本质是字符在链路各环节的编码不一致,只要有一环用的是 ISO-8859-1 或 GBK,后面全乱。

我把整个链路捋一遍,你要检查的地方一共有四个:

第一,数据库和表的字符集。建表SQL里我写了DEFAULT CHARSET=utf8mb4,建库时也用 utf8mb4。如果你之前建的表已经是 utf8 或 latin1,可以用ALTER TABLE xxx CONVERT TO CHARACTER SET utf8mb4转换。连接串里必须带characterEncoding=utf8,这样 JDBC 驱动会告诉 MySQL 用 UTF-8 收发数据。

第二,JSP 页面声明。前面写过,pageEncoding=UTF-8和contentType="text/html;charset=UTF-8"都必须有,缺一个都有可能出问题。

第三,前端表单提交。在 Servlet 里务必写request.setCharacterEncoding("UTF-8"),最好放在 doPost 的第一行,或者像我前面说的用 Filter 全局设置。

第四,GET 请求 URL。如果<a>链接或 JS 跳转带中文参数,需要在 Tomcat 的 server.xml 里给 Connector 加URIEncoding="UTF-8",否则 Tomcat 默认按 ISO-8859-1 去解码参数。

四步排查逻辑是:先修数据库和连接串,再修 JSP,再修请求入口,最后修 Tomcat 配置。大多数乱码都能解决。

5.3 JDBC 事务与数据一致性怎么保证

这是很多同学写课设时完全没考虑的问题。当你插入订单后还要同时修改车辆状态,这两条 SQL 单独看都能执行,但合在一起就要考虑:如果第一条执行成功、第二条执行失败怎么办?结果的错误形态就是订单存在但车辆状态还是“空闲”,用户明明租了车,管理员看到的车却是可租的,数据就乱了。

解决问题的标准手段是数据库事务,核心代码我前面在下单部分已经贴过。我再强调几个实操关键点。

conn.setAutoCommit(false)必须在执行第一条 SQL 之前调用,它告诉 JDBC 不要每执行一条语句就自动提交。然后是正常的 SQL 执行,全部成功后在 finally 之前调用conn.commit()。任何一步抛出异常,都要在 catch 块里调用conn.rollback(),把已经执行但未提交的 SQL 全部回滚掉。最后在 finally 里把自动提交恢复成 true,方便连接池复用,同时关闭连接。

这里还有一个进阶点:事务的隔离级别和锁。我建议在锁行查询时用SELECT ... FOR UPDATE,因为 MySQL InnoDB 默认情况下普通 SELECT 是非锁定读,你不加锁的话,两个并发请求可能同时读到车辆是空闲状态,然后都往下执行,最终导致一辆车被租给两个人。加FOR UPDATE后,第一个事务没提交前,第二个事务的锁行查询会阻塞等待,这就是数据库层面的串行化保证。

5.4 其他高频问题速查表

我把项目运行过程中容易遇到的零散问题也整理成一个速查表,每条都是实测过的,你可以直接保存备用。

现象可能原因解决办法
500 报错:ClassNotFoundExceptionmysql-connector-java 这个 jar 没有放到 WEB-INF/lib 目录把 jar 复制到 WEB-INF/lib 下,并确认 Artifacts 里已将 lib 打入 war
500 报错:SQLException: No suitable driverJDBC 驱动类名写错或 URL 格式错误检查 URL 前缀是 jdbc:mysql://,驱动类名是 com.mysql.cj.jdbc.Driver
404:请求的 Servlet 找不到@WebServlet 的路径和表单/链接的 action 不一致核对 form action,路径开头加项目上下文/car_rental/
JSP 页面报错:Unable to compile class for JSPJSP 写法有问题或依赖缺失看完整堆栈,多半是缺失 JSTL 标签库,需要导入 jstl jar
中文传入数据库变成问号数据库连接串没加 characterEncoding在 jdbc URL 加useUnicode=true&characterEncoding=utf8
用户登录后刷新就掉线Session 过期时间太短或关闭了 Cookie确认浏览器支持 Cookie,session.setMaxInactiveInterval设置合理
下单提示“车辆不存在或信息异常”车辆刚被管理员删除,或 car_id 传参错误页面传值用隐藏域,后端查询结果为空时给出明确提示
分页点第二页又回到第一页分页链接没带上查询条件参数分页 URL 里把 brand、price 等条件一并携带

这些坑我在实际帮别人调项目时几乎都踩过一遍,最典型的是“jar 包没进 lib”和“路径对不上”这两个,占了 404 和 500 问题的一大半。

这个项目做完之后,我自己最大的感受是:技术栈老不等于没价值,JSP + Servlet + JDBC 这套链路把所有 JavaWeb 的核心原理都摊开放在你面前,你亲手写过一遍锁、事务、Session、分页,后面学任何框架都会快很多。如果你的课设或毕设正好是这个方向,我建议不要满足于“跑通就行”,可以把租期冲突校验、事务控制和并发锁这几个点真正吃透,这些内容写在论文里、讲在答辩上,比任何花哨页面都有说服力。最后再分享一个小技巧:所有 DAO 方法的 SQL 先放到 Navicat 里跑通,确认数据没问题再写 Java 代码,这个习惯能帮你省下至少一半的调试时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询