坦白讲,最初接到“基于JavaWeb和MySQL的JSP+Servlet共享租车信息管理系统”这个需求时,我心里第一反应是:现在谁还从零写JSP+Servlet+JDBC?直接上个Spring Boot不香吗?但真正动手把这套技术栈从建库建表到Tomcat部署完整跑通之后,我反而觉得这种“复古”组合的课设/毕设项目,才是理解Web后端最好的教材。它把浏览器请求到MySQL返回结果之间的每一步都摆在明面上,没有框架替你兜底,每一个环节出问题都必须自己找到答案。所以这篇博文我不会讲太多花哨的东西,就结合一个完整的共享租车系统,把技术选型逻辑、业务建模、表结构设计、JDBC事务控制、Servlet与JSP的请求流转,以及运行环境配置和实战踩坑,一条条说清楚。无论你是正在做类似课设、毕设,还是第一次接触这类老项目的开发,这篇文章应该都能帮你避开我踩过的坑。
1. 为什么是JSP+Servlet+JDBC:这套“老技术栈”的真实定位
1.1 不是技术落后,而是链路够透明
我在项目里看到的第一行需求说明是“基于JavaWeb和MySQL,采用JSP+Servlet+JDBC技术实现”。当时我觉得这要求有点落后,但做完一个完整模块后,我理解了这种选型在教学场景里的价值。
使用Spring Boot + MyBatis时,你在Controller里写一个方法,框架自动帮你完成参数绑定、请求分发、数据库连接管理、事务提交回滚。效率确实高,但如果你还没建立完整的Web开发心智模型,出了问题会非常痛苦,因为你根本不知道请求是怎么被处理的、连接是什么时候关闭的、事务是什么时候提交的。
而JSP+Servlet+JDBC这套组合,整个请求链路是完整暴露的:
- 浏览器发送HTTP请求到Tomcat。
- Tomcat根据URL映射找到对应的Servlet。
- Servlet调用业务方法,内部通过JDBC向MySQL发起SQL。
- 结果返回后,Servlet把数据塞进request作用域。
- 最后forward到JSP,由JSP渲染成HTML响应给浏览器。
中间每一个环节都需要开发者自己写代码、自己管理资源。初看是负担,仔细想其实是把整条链路“透明化”了。我后来去维护一些老项目时,就是因为当年搞清楚了这套机制,才能快速定位问题。
1.2 与主流框架的差异对比
经常有人问我:招行测这种东西到底有没有用?我的看法是分场景。如果你是为了快速交付生产系统,Spring Boot当然更合适;但如果你是为了理解Web工作原理,或者老师指定了技术栈,那JSP+Servlet+JDBC就是绕不开的一课。
| 技术方案 | 开发效率 | 学习成本 | 链路可见性 | 常见场景 |
|---|---|---|---|---|
| Spring Boot + MyBatis | 高 | 中高 | 低,框架封装配了大量细节 | 企业快速业务开发 |
| JSP + Servlet + JDBC | 中低 | 低 | 高,步骤全部显式 | 课程设计、毕业设计、老项目维护 |
| Python Flask + SQLAlchemy | 中高 | 低中 | 中 | 原型、中小型系统 |
这套项目适合的读者,我认为有三类:一是学校指定了SSH时代或JSP+Servlet时代的课设题目、又不想用框架糊弄的同学;二是想彻底搞懂HTTP请求、Servlet生命周期、JDBC事务的同学;三是工作中要维护老系统的朋友。我的建议是:别急着嫌它土,认真写完一个完整系统,你对“请求-响应”这件事的理解会扎实很多。
2. 共享租车业务建模:从需求到状态流转
2.1 三类角色与权限边界
拿到这个题目后,我没有马上写代码,而是先把系统里的角色划分清楚。一个共享租车信息管理系统,至少要有三类用户角色:
- 游客:只能浏览车辆列表和车辆详情,不能下单。一旦发起预约操作,就要引导到登录页。
- 注册用户:可以登录系统、预约车辆、取消预约、确认取车、发起还车、查看历史订单和个人信息。
- 管理员:负责车辆管理(增删改查、上下架维护)、预约订单审核、还车结算确认、查看租车数据和基础统计。
权限边界如果不提前划清楚,后面会出很多安全问题。比如用户直接访问管理员的Servlet地址,结果发现没有做任何校验,就能操作别人的订单,这是我最常看到课设项目里翻车的地方。解决思路很简单:公共页面和放行的接口公开访问,其余所有业务操作都必须在Session里校验登录用户身份,管理员操作还要额外校验角色。
2.2 一条完整的主业务链路
系统的主链路其实不复杂,我用一条线就能串起来:
用户注册账号 → 登录系统 → 浏览车辆列表 → 选中目标车辆查看详情 → 发起预约(车辆状态从“可用”变为“已预约”) → 管理员确认预约 → 用户到店取车(车辆状态变“租用中”) → 用车结束发起还车 → 系统按天数计算费用 → 结算完成,车辆恢复“可用”。
这里有个容易被忽略的点:什么是“共享租车”和普通租车系统的区别?我理解它的核心是“车辆资源的重复利用”。同一辆车可以被不同用户分时段租用,所以车辆状态管理特别重要。如果状态设计得不好,就很容易出现同一辆车被两个人同时预约成功的情况,这在课设答辩里属于致命逻辑错误。
2.3 车辆与订单的状态机:状态不可乱跳
我在代码里对车辆和订单的状态做了严格约定。车辆状态用整数枚举:
- 1 = 可用
- 2 = 已预约
- 3 = 租用中
- 4 = 维护中
订单状态则独立设计:
- 1 = 待确认(用户刚提交预约)
- 2 = 已确认(管理员审核通过)
- 3 = 租用中(用户已取车)
- 4 = 已完成(还车并结算)
- 5 = 已取消
为什么要单独维护一套状态机,而不是在车辆状态里直接体现订单?因为一个订单要经历完整的生命周期,而车辆状态只是车辆当前资源的镜像。比如订单到了“已确认”但用户还没取车,车辆显示“已预约”,可如果单看车辆状态,你并不知道是哪个订单造成的。把两张表状态字段分开管理,配合订单表的冗余字段,查起来才方便。
状态跳转也要在代码里做控制,不是写个setStatus就行。比如订单状态从“待确认”不能直接跳到“租用中”,必须经过“已确认”;已取消的订单不能再激活。我会在Service层用if判断前置状态是否合法,保证逻辑不自相矛盾。
3. 数据库表结构设计:共享租车系统的落库方案
3.1 三张核心表的建表SQL
建模完成后,我先把数据库表结构定了下来。共享租车系统最核心的就是三张表:用户表、车辆表、订单表。下面是我实际用的建表脚本,你可以直接参考:
CREATE DATABASE IF NOT EXISTS car_sharing DEFAULT CHARACTER SET utf8mb4; USE car_sharing; CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID', username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(64) NOT NULL COMMENT '密码哈希值', phone VARCHAR(20) DEFAULT '', real_name VARCHAR(50) DEFAULT '', id_card VARCHAR(20) DEFAULT '' COMMENT '身份证号', role TINYINT NOT NULL DEFAULT 1 COMMENT '1普通用户 2管理员', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB COMMENT '用户表'; CREATE TABLE car ( id INT PRIMARY KEY AUTO_INCREMENT, car_no VARCHAR(20) NOT NULL UNIQUE COMMENT '车牌号', brand VARCHAR(30) NOT NULL COMMENT '品牌', model VARCHAR(50) NOT NULL COMMENT '车型', price_per_day DECIMAL(10,2) NOT NULL COMMENT '日租金', status TINYINT NOT NULL DEFAULT 1 COMMENT '1可用 2已预约 3租用中 4维护中', description VARCHAR(500), create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB COMMENT '车辆表'; CREATE TABLE rent_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '订单号', user_id INT NOT NULL COMMENT '租车用户ID', car_id INT NOT NULL COMMENT '车辆ID', start_day DATE NOT NULL COMMENT '预计取车日期', end_day DATE NOT NULL COMMENT '预计还车日期', actual_return_date DATE COMMENT '实际还车日期', total_price DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '结算金额', status TINYINT NOT NULL DEFAULT 1 COMMENT '1待确认 2已确认 3租用中 4已完成 5已取消', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_user_id (user_id), KEY idx_car_id (car_id), KEY idx_status (status), CONSTRAINT fk_order_user FOREIGN KEY (user_id) REFERENCES sys_user(id), CONSTRAINT fk_order_car FOREIGN KEY (car_id) REFERENCES car(id) ) ENGINE=InnoDB COMMENT '租车订单表';这三张表已经能支撑整个系统的核心运转了。实际项目里可能还会加支付流水表、车辆图片表等,但课设级别先用这三张把逻辑跑通,后续再加表不影响结构。
3.2 字段类型的设计考量
很多同学建表很随意,后面做钱和日期计算的时候就开始后悔。我总结几个共享租车这种业务场景里特别需要注意的点:
第一,金额字段必须用DECIMAL。我在系统里统计租金时用过float,结果某条订单显示费用是99.999999元这类尴尬数字。二进制浮点数根本表示不精确,租金、押金这种跟钱有关的字段,一律用DECIMAL(10,2)。这就是我强调“钱不是用来看的,是用来算的”这句话的由来。
第二,日期字段要区分DATE和DATETIME。预估的取车、还车日期用DATE就够了,订单创建时间用DATETIME。如果你把日期类型混用,在JSP页面用SimpleDateFormat或者JSTL格式化的时候会非常别扭。
第三,状态字段用TINYINT而不是VARCHAR。用字符串’可用’、’租用中’确实一眼能看懂,但存数据库占空间、查询慢,而且拼SQL容易写错。用TINYINT配合Java里常量类或者枚举类,是更标准的方式。我在代码里定义一个CarStatus类和OrderStatus类存放常量,页面上显示时再映射成中文文案。
第四,一定给高频查询字段建索引。我在rent_order表上建了idx_user_id,因为用户查“我的订单”是最频繁的SQL;车辆表的car_no建了唯一索引,因为同一辆车的车牌不能重复。索引不是越多越好,而是根据你的查询习惯来建。热搜里经常有人问“mysql创建索引”怎么正确规划,其实思路就是:先列出你最常用的查询条件,再决定建什么索引。
3.3 关于MySQL版本选择的碎碎念
这个项目用MySQL 8.0还是5.7,我建议环境允许就直接上8.0。原因很实际:8.0默认字符集是utf8mb4,不需要额外配置中文就正常;5.7及更早版本默认utf8mb3,存生僻字或表情符可能出问题。如果你装的是MySQL 8.x,要注意JDBC驱动要用mysql-connector-java 8.x版本,驱动类名也变了,我后面专门有一节聊这个问题。
另外,数据库可视化工具我用Navicat比较多,建库建表、看数据都很方便。虽然直接用命令行也能完成,但做课设项目时,一个直观的图形工具能省很多时间。网上那些“Navicat安装”教程很多,安装完记得连一次测试下连接,确认账号密码、端口都没问题。
4. JDBC连接与事务:把“下单扣库存”做成可靠操作
4.1 连接管理:从裸连到连接池
JSP+Servlet项目里最基础的数据访问方式就是用JDBC。最原始的写法是这样的:
Class.forName("com.mysql.cj.jdbc.Driver"); String url = "jdbc:mysql://localhost:3306/car_sharing?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; Connection conn = DriverManager.getConnection(url, "root", "password");每次需要操作数据库就创建一次连接,用完关闭。这样写在小项目里似乎没问题,但仔细想想:每次连接都要经过TCP握手、MySQL认证,几十个并发请求同时打过来,系统很快就卡住了。所以真实项目里几乎都会用数据库连接池。
我在这套系统里引入的是Druid连接池,用配置文件维护连接参数:
// druid.properties driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/car_sharing?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 username=root password=你的密码 initialSize=5 maxActive=20 maxWait=3000然后写一个工具类来获取连接,全项目统一从这里拿连接:
public class DBUtil { private static DruidDataSource dataSource; static { try { Properties props = new Properties(); props.load(DBUtil.class.getClassLoader().getResourceAsStream("druid.properties")); dataSource = (DruidDataSource) DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new RuntimeException("初始化连接池失败", e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }这样每次拿到的都是连接池里复用好的连接,性能比DriverManager方式高很多。关于“mysql的数据库连接池”这个话题,Druid、HikariCP、C3P0都行,但对课设项目来说Druid的资料最多、最容易出结果。
4.2 PreparedStatement:SQL注入防线
我发现很多课设里还在用Statement拼字符串,这是非常危险的习惯。比如用户输入手机号作为查询条件,如果写成:
Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM sys_user WHERE phone = '" + phone + "'");一旦用户在输入框里传一段' OR '1'='1,SQL语义就完全变了,整张表的数据都能被查出来。正确做法是永远使用PreparedStatement占位符。
PreparedStatement ps = conn.prepareStatement("SELECT * FROM sys_user WHERE phone = ?"); ps.setString(1, phone); ResultSet rs = ps.executeQuery();PreparedStatement不仅防注入,还因为预编译可以提升执行效率。所以我在这套系统里规定:所有访问数据库的代码,一律不准用Statement拼SQL,只准用PreparedStatement或者后期如果引入MyBatis时的#{}参数绑定。
4.3 事务边界:一个连接干完一件事
共享租车系统里最需要事务控制的业务有两个:一个是用户下单预约,一个是还车结算。
先看下单预约的逻辑。用户预约一辆车时,需要做三步操作:
- 查询车辆的当前状态,必须是“可用”。
- 把车辆状态更新为“已预约”。
- 插入一条订单记录,状态为“待确认”。
如果这三步之间没有事务保护,第二步把车锁了、第三步插入订单时数据库报错,那车就永远卡在“已预约”状态,用户却连订单都查不到。这种逻辑错误在答辩时被老师一问就能问出来。
正确写法是在Service层手动控制事务边界,并且自始至终使用同一个Connection:
Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 1. 检查车辆状态 String checkSql = "SELECT status FROM car WHERE id = ? FOR UPDATE"; // 注意FOR UPDATE:先对这张行加排他锁,避免并发场景下两个事务同时读到“可用” // 2. 更新车辆状态为已预约 // 3. 插入订单记录 conn.commit(); } catch (Exception e) { if (conn != null) conn.rollback(); throw e; } finally { if (conn != null) conn.setAutoCommit(true); conn.close(); // 这里关闭是归还给连接池,不是真正断开 }特别注意:事务里所有操作必须复用同一个连接对象。如果中途去调用一个内部会重新DBUtil.getConnection()的方法,那个方法使用的是连接池里的另一个连接,事务边界就被割裂了。有时候你会发现明明写了commit却什么都没有生效,八成就是这个问题。
4.4 连接URL参数的坑与解决
连接MySQL时经常碰到的幺蛾子,我一次说清。
如果你用的MySQL 8.x,驱动类是com.mysql.cj.jdbc.Driver而不是老版本的com.mysql.jdbc.Driver。驱动类找不到的报错,九成是jar包没引对,或者类是拼错了。
连接串里建议加上这三个参数:
useSSL=false:MySQL 8默认开启SSL验证,但本地开发环境通常没有证书配置,不关掉会报SSL连接错误。serverTimezone=Asia/Shanghai:否则驱动会报时区错误,时间字段也可能差8小时。characterEncoding=utf8:配合数据库字符集utf8mb4,防止中文乱码。
热搜里那些“mysql ssl连接错误”“mysql 8.0安装”“mysql安装配置教程”我基本都翻过一遍,大部分问题的根源就是这三项没设置好。如果你跑的是5.7,没加serverTimezone可能还能凑合,但升级MySQL 8后大概率会直接爆错。我建议一开始就按标准格式写,避免后期各种奇怪异常。
5. Servlet+JSP协作:登录、预约、还车的完整链路
5.1 Servlet的路由组织
这个系统里我没有用一个Servlet包打天下,而是按模块划分:UserServlet、CarServlet、OrderServlet。每个Servlet内部再根据请求参数action分发具体方法。以OrderServlet的doPost为例:
@WebServlet("/order") public class OrderServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String action = request.getParameter("action"); if ("submit".equals(action)) { submitOrder(request, response); } else if ("cancel".equals(action)) { cancelOrder(request, response); } else if ("confirmReturn".equals(action)) { confirmReturn(request, response); } else { response.sendError(404); } } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 列表查询、详情跳转等GET请求,同样按action分发 } }这样的好处是代码可读性高,后续维护时不需要在几十个Servlet之间跳来跳去。如果你把系统做得很小,也可以只写一个IndexServlet统一转发,但我觉得按模块拆更清晰。
5.2 Session与登录过滤器
登录成功之后,我会把用户对象放进Session:
HttpSession session = request.getSession(); session.setAttribute("loginUser", user);然后在所有需要用户身份的后台路径上增加一个登录校验过滤器。这样不管用户直接访问哪个Servlet的URL,都会先经过过滤器检查Session里有没有loginUser。如果没有,就重定向到login.jsp。
@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("register.jsp") || uri.contains("/user") || uri.contains("/car")) { chain.doFilter(req, resp); return; } // 检查登录状态 HttpSession session = request.getSession(false); if (session != null && session.getAttribute("loginUser") != null) { chain.doFilter(req, resp); } else { response.sendRedirect(request.getContextPath() + "/login.jsp"); } } }这个过滤器要小心写,不然很容易把CSS、静态资源也拦住。URI判断的规则自己提前设计好,博客里给的只是示例,实际项目按自己的路径规则调整。
5.3 数据交付:request转发与JSP渲染
Servlet处理完业务后,把结果存到request作用域,再forward到JSP:
List<Car> carList = carService.listAvailableCars(); request.setAttribute("carList", carList); request.getRequestDispatcher("/carList.jsp").forward(request, response);JSP页面用EL表达式和JSTL标签取数据,不要在JSP里写一堆Java脚本片段。比如车辆列表页的核心部分:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <table> <tr> <th>车牌</th> <th>品牌</th> <th>日租金</th> <th>状态</th> </tr> <c:forEach var="car" items="${carList}"> <tr> <td>${car.carNo}</td> <td>${car.brand}</td> <td>${car.pricePerDay}</td> <td> <c:choose> <c:when test="${car.status == 1}">可用</c:when> <c:when test="${car.status == 2}">已预约</c:when> <c:otherwise>不可用</c:otherwise> </c:choose> </td> </tr> </c:forEach> </table>EL和JSTL是JSP项目的核心技能。很多入门同学刚上手时喜欢在JSP里写<%= request.getAttribute("xxx") %>甚至Java循环,搞得页面乱成一锅粥。我建议一定花半小时学一下c:forEach和c:if标签,页面整洁度提升好几个档次,答辩观感也会更好。
5.4 中文乱码一揽子解决方案
JSP+Servlet项目里的中文乱码,通常是三个节点没有对齐:
第一,Servlet接收POST请求参数前,要先调用request.setCharacterEncoding("UTF-8")。注意:这个方法必须在读取任何参数之前调用,放在doPost/doGet第一行。
第二,Servlet响应JSP页面时,JSP头部要保证pageEncoding是UTF-8:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>第三,MySQL连接串里带上characterEncoding=utf8,同时建库时指定utf8mb4字符集。这一步我会把环境变量、数据库连接、Servlet编码对齐成同一个UTF-8,基本就不会再出乱码。
GET请求的中文参数在Tomcat 8及以上默认按UTF-8解码,不会出问题;如果你用的是老版本Tomcat,则需要去server.xml里加URIEncoding="UTF-8"。这属于比较少见的坑,知道有这回事就行。
6. 运行配置与实战踩坑:从IDEA到Tomcat再到MySQL
6.1 环境清单与IDEA搭建
这个项目在我本机跑起来的环境组合是:JDK 1.8、Tomcat 8.5、MySQL 8.0.33、IDEA 2023.3。也可以用Maven来管理jar包,但如果你是纯手工JavaWeb项目,也可以把mysql-connector-java直接放到WEB-INF/lib下。两种方式各有优劣,Maven好维护依赖版本,手工lib直观但容易漏jar。
在IDEA里运行JavaWeb项目,我强烈建议在Run Configuration里配置Tomcat Server,使用war exploded方式部署。这样修改代码后热部署快,不会每次都要重新打war包。IDE配置Tomcat时,注意指定你的本机Tomcat安装目录,选择Application Server时别选成JDK目录。
热搜里有个高频词是“idea运行javaweb项目配置”,很多人卡在这一步。我的建议是:先不要想着一步到位,按这个顺序排查——IDEA有没有配置Tomcat Server、Project Structure里的Artifact有没有选war exploded、Dependencies里是否包含Servlet-api和JSP-api(这两个Tomcat自带,编译时如果没引入会报错,运行时又不需要放进war里)。
6.2 高频错误排查表
我把自己运行项目时遇到的高频错误整理成了表格,应该能覆盖大部分同学的启动问题:
| 现象 | 原因 | 解决办法 |
|---|---|---|
| ClassNotFoundException: com.mysql.cj.jdbc.Driver | MySQL驱动包缺失或类名写错 | 检查WEB-INF/lib下是否放了mysql-connector-java 8.x jar包 |
| Communications link failure | MySQL服务没启动、端口被占用或防火墙拦截 | 先netstat查看3306端口,再确认连接串IP/端口 |
| Access denied for user | 用户名或密码错误,或者该账号没有远程访问权限 | 用命令行mysql -u root -p验证账号,确认授权 |
| Table doesn't exist | 数据库库名选错或表名拼错 | Navicat里确认表名和SQL中的库名完全一致 |
| 404:Servlet路径找不到 | @WebServlet注解URL或web.xml配置错误 | 检查注解值和前端页面提交的action路径 |
| 500:空指针 | 从数据库查出的对象为null,或request.getParameter返回null | 看Tomcat localhost日志定位具体行号 |
| 端口占用8080 | 旧Tomcat实例未关闭,或其他程序占用 | netstat -ano查找PID后结束进程,或改Tomcat端口 |
排查思路我一般遵循“从外到内”:先确认MySQL能不能连,再确认Tomcat日志有没有异常,最后才去看具体代码。很多同学一看到500就翻业务代码,实际上错误在页面提交的路径根本没配好,方向反了。
6.3 并发抢单:行锁怎么用
共享租车场景里,并发抢同一辆车是绕不开的问题。两个人同时看到同一辆车“可用”,同时点了预约,怎么办?单纯靠事务还不足以保证正确,因为两个事务同时读取到“可用”状态,然后先后更新车辆状态,第二个事务也会认为自己操作成功。
解决办法是在查询车辆状态时加上FOR UPDATE,对车辆行加排他锁(X锁)。我前面事务代码里已经写了:
SELECT status FROM car WHERE id = ? FOR UPDATE第一个事务执行这条SQL后持有该行排他锁,第二个事务再执行同样的SQL会阻塞等待,等第一个事务提交后才能继续执行,此时它读取到的status已经是“已预约”,就可以判定预约失败。这就是MySQL里“共享锁和排他锁”在实际业务中的典型应用。课设答辩时能主动讲出这个细节,绝对是加分项。
6.4 资源释放与代码习惯
最后说一个容易被忽略但很重要的习惯。虽然用了连接池,JDBC的Connection、PreparedStatement、ResultSet依然要在finally或try-with-resources里显式关闭。连接池关闭Connection只是归还给池,但ResultSet和Statement如果不释放,长时间跑下来会造成游标泄漏,最终拖垮数据库。
我在这套项目里的统一写法是用try-with-resources,Java 7以后简洁可靠:
try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, carId); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { // 处理结果 } } }这种写法不仅代码量少,而且无论有没有抛异常,资源都会自动释放。我自己后来在公司维护老项目时也保持着这个习惯,排查过几次别人留下的连接泄漏问题后,更确定这个前期习惯有多值钱。
整套系统从设计到跑通,我最深的体会是:开发顺序一定不要乱。先把三张表建好,再写JDBC工具类,然后是Service层,再是Servlet,最后才是JSP页面。每一步都以前一步的结果为基础,出错时也能迅速定位到具体层。如果你现在正因为课设、毕设卡在这种项目上,别慌,踏踏实实按“数据库建模 -> 事务链路 -> 请求流转”的顺序来,一条条解决。这套东西虽然老,但每个知识点都能在你以后写任何Web系统时派上用场。