ARTICLE DETAIL

资讯详情

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

基于JSP+Servlet+JDBC的共享租车信息管理系统实战解析

基于JSP+Servlet+JDBC的共享租车信息管理系统实战解析 做JavaWeb项目很多人一上来就想着Spring Boot、微服务反而把最基础的东西丢了。其实用JSP Servlet JDBC MySQL写一个共享租车信息管理系统才是真正能把JavaWeb整条调用链吃透的方式。这套技术组合看起来传统但它恰好把HTTP请求、Servlet生命周期、JSP页面渲染、JDBC连接数据库、SQL设计、事务控制全部串了一遍。这个系统解决的是共享租车场景里最核心的问题用户注册登录、浏览车辆、下单租车、还车结算、后台管理车辆和订单。如果你是做课程设计、毕业设计或者刚学完Servlet想找一个完整项目练手这个项目非常适合拿来当参考模板。下面我把整个项目的设计思路、数据库建模、环境配置、核心代码实现和排查记录一次性写清楚。1. 项目整体设计与技术选型解析1.1 为什么选 JSPServletJDBC 这套组合先聊一个很多人会纠结的问题现在Spring Boot这么火为什么还要用JSPServlet这种“老古董”我的答案是因为这套组合能让你看到JavaWeb最真实的运行过程。JSP负责页面展示Servlet负责接收请求和调度JDBC负责操作数据库MySQL负责存储数据。这四样东西组合起来就是最朴素的MVC三层架构JSP是ViewServlet是ControllerJDBC操作DAO层是Model的一部分。没有框架帮你遮遮掩掩一个请求从浏览器发出经过Tomcat找到对应的ServletServlet再调用JDBC查询数据库最后把结果通过转发或重定向送回页面每一步都可以在代码里找到对应位置。这就像学车先学手动挡虽然换挡踩离合很麻烦但你能理解汽车是怎么运作的。等以后再学Spring MVC、MyBatis你反而会觉得轻松因为你知道框架底层在帮你做什么。所以我个人强烈建议毕设或课程设计选这种传统技术栈不是落后而是最能体现基础掌握程度。而且这类项目网上参考资料多部署包小运行环境也好配。1.2 系统功能模块拆解共享租车信息管理系统核心业务其实不复杂平台上有车辆用户注册登录后可以租车、还车管理员负责维护车辆和处理订单。我把功能拆成两个端方便后面做数据库设计和代码分层。模块功能点说明用户端注册、登录、个人信息查看Session保存登录状态密码需要加密存储用户端车辆列表浏览、关键字查询按车型、品牌、日租金筛选用户端下单租车、还车结算核心流程涉及事务控制用户端我的订单、取消订单订单状态流转管理端账号登录、权限校验区分管理员和普通用户管理端车辆信息管理增删改查上下架维护状态管理端订单管理查看所有订单执行审核、完成操作管理端用户管理查看用户列表禁用异常账号功能不算多但覆盖了一个JavaWeb项目该有的全部环节表单传参、文件上传车辆图片可选、列表分页、条件查询、事务操作、Session会话管理、Filter过滤器登录验证、Listener统计在线人数等基础能力。等会儿我会重点讲几个关键实现因为这些都是面试和答辩时最容易被打听的地方。2. 数据库设计与核心表结构2.1 MySQL 建模思路这次项目用的数据库是MySQL版本我推荐8.0后面会专门讲驱动问题。先聊设计思路。共享租车场景涉及三个核心实体用户、车辆、订单。用户和车辆之间是“多对多”关系一个用户能租多辆车一辆车也会被多个用户租过但这个多对多通过订单表关联起来了。所以最核心的表就是这三张用户表t_user、车辆表t_car、订单表t_order。另外为了区分普通用户和管理员我最初的想法是建一个用户表加一个role字段但这样在代码里需要自己判断角色。更清晰的做法是再加一张管理员表t_admin不过从权限设计角度统一用用户表加角色字段其实更合理适合后期扩展。我最后采用了“用户表 角色字段”的方式表结构清爽也方便以后对接更复杂的权限系统。2.2 核心表结构设计下面是我实际建表用的SQL关键片段删减了注释方便直接看字段。用户表CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(64) NOT NULL COMMENT 建议MD5或SHA-256后存储, phone VARCHAR(11) DEFAULT NULL, role TINYINT DEFAULT 1 COMMENT 0表示管理员1表示普通用户, status TINYINT DEFAULT 1 COMMENT 1正常0禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );车辆表CREATE TABLE t_car ( id INT PRIMARY KEY AUTO_INCREMENT, car_no VARCHAR(20) UNIQUE NOT NULL COMMENT 车牌号, brand VARCHAR(50) NOT NULL, model VARCHAR(50), price_per_day DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 1 COMMENT 1可用2已租出3维修4下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );订单表CREATE TABLE t_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) UNIQUE NOT NULL, user_id INT NOT NULL, car_id INT NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, total_price DECIMAL(10,2) DEFAULT 0.00, status TINYINT DEFAULT 1 COMMENT 1已下单2租赁中3已还车4已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user_id (user_id), KEY 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) );有几个设计细节值得说明金额字段用DECIMAL而不是Float或Double。凡是涉及钱的都要用定点数避免浮点数精度丢失。这是Finance业务的基本常识放到租车项目里同样适用。状态字段用TINYINT存数字代码里定义一个常量类或枚举去对应不要直接在JSP里写死数字否则后期维护很痛苦。订单号order_no用UUID或时间戳随机数生成不用数据库自增主键直接给用户看主要出于安全考虑也方便未来对接第三方支付。2.3 表关系与索引设计要点外键约束在互联网公司实际生产环境里经常被禁用但学习项目里保留外键有好处能理清关系防止插入脏数据。我在这个项目里保留了两条外键订单表关联用户表和车辆表。这样在删除用户或车辆时数据库层面就能阻止误删。不过要注意外键会带来额外的数据库开销而且对插入性能有影响。所以我在订单表里额外手动建立了index_user_id和index_car_id因为列表查询经常按user_id查“我的订单”按car_id查“该车辆使用记录”。如果数据量增大这个索引就是命根子。另外车辆列表页经常按status筛选可用车辆所以在车辆表的status字段也加了一个普通索引。这些设计在答辩时讲出来面试官会认为你真的设计过系统而不是简单建了几张表。3. 环境配置与项目运行实操3.1 JDK、Tomcat、MySQL 版本搭配环境配置是新手最容易卡住的地方我先把推荐组合列出来这些我都实测过。组件推荐版本说明JDK1.8JavaWeb项目最稳定的版本IDEA2022自带Maven和Tomcat集成Tomcat9.0兼容Servlet 4.0JSP支持好MySQL8.0新版驱动类名和时区配置需要注意Maven3.6用Maven创建war项目管理依赖更方便如果你喜欢用MySQL 5.7那驱动用5.x系列也行但网上教程混杂容易踩坑。个人感觉直接上MySQL 8.0更省心只要注意驱动名称和URL参数就好。3.2 IDEA 创建 JavaWeb 项目和导入依赖推荐用Maven结构创建JavaWeb项目IDEA里选择“Java Web”骨架或者直接创建普通Maven项目后加上war包插件。关键是在pom.xml里配置下面几个依赖!-- Servlet APITomcat容器提供所以scope用provided -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- JSP API同样由容器提供 -- dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency !-- JSTL标签库方便JSP中使用forEach等标签 -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency !-- MySQL 8连接器 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependencyMaven会帮你自动下载依赖但有时IDEA下载依赖会失败尤其是公司网络受限的时候。遇到“Cannot download from Maven Central”这类问题我一般先把IDEA的Maven仓库镜像改成阿里云镜像再在File - Settings - Build Tools - Maven里设置好settings.xml。改完基本就顺了。3.3 JDBC 连接 MySQL 的完整配置与常见坑项目里我一般会把数据库连接信息放到db.properties配置文件中然后写一个工具类统一获取连接。这样避免代码里到处写死数据库地址。db.propertiesjdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/car_share?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456工具类简化版public class DBUtil { private static String url; private static String user; private static String password; static { try { InputStream in DBUtil.class.getClassLoader() .getResourceAsStream(db.properties); Properties props new Properties(); props.load(in); Class.forName(props.getProperty(jdbc.driver)); url props.getProperty(jdbc.url); user props.getProperty(jdbc.username); password props.getProperty(jdbc.password); } catch (Exception e) { throw new ExceptionInInitializerError(数据库配置加载失败); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } }几个必须要注意的坑MySQL 8的驱动类名是com.mysql.cj.jdbc.Driver不是5.x的com.mysql.jdbc.Driver。如果看到ClassNotFoundException十有八九是版本和类名不匹配。URL里一定要加useSSLfalse。MySQL 8默认开了SSL本地开发环境没有证书不加这个参数会报Communications link failure或SSL connection error。加serverTimezoneAsia/Shanghai否则会因为时区差异报The server time zone value ???ú±ê׼ʱ¼ä is unrecognized尤其是当时数据库是默认时区的时候。中文乱码问题URL里需要characterEncodingutf8并且后续会结合Filter统一设置请求编码。MySQL的安装配置也是热词里很多人卡的地方。Windows下安装MySQL 8我用的是免安装版ZIP解压后配置my.ini然后以管理员身份执行mysqld --initialize-insecure net start mysql如果net start mysql提示服务无法启动最常见的两个原因是my.ini路径配置错误、以及data目录没有初始化。用mysqld --initialize-insecure初始化后再启动基本就能解决。4. 核心功能实现详解4.1 Servlet 控制器设计与请求流转传统JavaWeb项目Servlet负责接收请求。我不建议一个功能写一个Servlet类否则类会爆炸。我的做法是在一个模块下用一个Servlet类通过action参数区分操作。比如用户相关的UserServletWebServlet(/user) public class UserServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String action request.getParameter(action); if (login.equals(action)) { login(request, response); } else if (register.equals(action)) { register(request, response); } else if (logout.equals(action)) { logout(request, response); } // ... 更多操作 } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doPost(request, response); } private void login(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username request.getParameter(username); String password request.getParameter(password); User user userService.login(username, password); if (user ! null user.getStatus() 1) { request.getSession().setAttribute(loginUser, user); response.sendRedirect(request.getContextPath() /index.jsp); } else { request.setAttribute(errorMsg, 用户名或密码错误或账号已被禁用); request.getRequestDispatcher(login.jsp).forward(request, response); } } }这里有个细节doGet里直接doPost这样无论用户用GET还是POST访问同一个逻辑都走同一种编码设置。如果你只重写doPost用GET方式提交表单会空白这是个很常见的低级错误。4.2 JSP 页面渲染与表单交互JSP页面里我强烈建议使用EL表达式和JSTL而不是直接在写% Java代码 %。作为早期JavaWeb项目JSP中夹杂大量脚本代码会让页面非常难维护。车辆列表展示核心片段% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table tr th车牌号/th th品牌/th th车型/th th日租金/th th状态/th th操作/th /tr c:forEach items${carList} varcar tr td${car.carNo}/td td${car.brand}/td td${car.model}/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 td a href${pageContext.request.contextPath}/car?actiondetailid${car.id}详情/a a href${pageContext.request.contextPath}/order?actionrentcarId${car.id}租车/a /td /tr /c:forEach /table页面里所有链接都要加上${pageContext.request.contextPath}也就是项目的上下文路径。否则部署到Tomcat后如果路径带了项目名你的所有静态资源、超链接都会404。这是我做项目时踩过最多的坑。关于热词里提到的“jsp页面让加载完后刷新一次”其实很多人遇到的问题是提交表单后按F5刷新浏览器弹出“确认重新提交表单”的对话框导致重复下单。正确做法是在Servlet处理完业务后使用重定向而不是转发// 错误转发会让刷新时重新执行表单提交 request.getRequestDispatcher(orderList.jsp).forward(request, response); // 正确Redirect After Post 模式刷新只重新请求列表页 response.sendRedirect(request.getContextPath() /order?actionmyOrders);重定向的原理是浏览器先收到一个302响应然后自动发出新的GET请求。用户刷新时刷新的是GET请求的页面订单不会重复插入这个技巧非常实用。4.3 JDBC 增删改查与事务处理JDBC的基础功能是增删改查网上有大量模板。但租车下单这个业务必须使用事务这是整个项目中最核心、最值得写的部分。租车流程简单说有三步检查车辆状态是否可用创建订单状态设为“已下单”把车辆状态改成“已租出”。如果第2步成功了第3步失败那么会出现“有订单但车辆仍可租”的数据不一致情况。解决方式就是事务。public boolean createOrder(int userId, int carId) { Connection conn null; PreparedStatement ps1 null; PreparedStatement ps2 null; ResultSet rs null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 1. 查询车辆并锁定行防止并发下订单 String checkSql SELECT status FROM t_car WHERE id ? FOR UPDATE; ps1 conn.prepareStatement(checkSql); ps1.setInt(1, carId); rs ps1.executeQuery(); int status -1; if (rs.next()) { status rs.getInt(status); } if (status ! 1) { conn.rollback(); return false; } // 2. 插入订单 String insertOrder INSERT INTO t_order (order_no, user_id, car_id, start_time, status) VALUES (?,?,?,NOW(),1); ps2 conn.prepareStatement(insertOrder); ps2.setString(1, generateOrderNo()); ps2.setInt(2, userId); ps2.setInt(3, carId); ps2.executeUpdate(); // 3. 更新车辆状态 String updateCar UPDATE t_car SET status 2 WHERE id ?; ps2 conn.prepareStatement(updateCar); ps2.setInt(1, carId); ps2.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } throw new RuntimeException(创建订单失败, e); } finally { DBUtil.close(conn, ps1, rs); DBUtil.close(null, ps2, null); } }这里最关键的是第1步里的SELECT ... FOR UPDATE。这是一个悲观锁在事务提交之前其他事务执行同样的查询会阻塞等待。如果不用这个锁两个用户同时请求同一辆车可能会都查询到“可用”然后都创建订单最终车辆状态被后来的更新覆盖但库里出现了两笔成功订单。这个并发问题在答辩时讲出来老师会觉得你确实理解数据一致性。还车结算稍微简单一点根据当前时间和订单里start_time计算天数乘以金额更新总费用并把车辆状态改回“可用”订单状态改成“已还车”。这里同样需要使用事务道理一样。5. 常见问题与排查技巧5.1 问题速查表我把实际运行过程中遇到的典型问题整理成一张速查表基本覆盖了所有新手会踩的坑。现象可能原因解决办法浏览器访问localhost:8080白屏或404项目没部署成功或访问路径缺少项目名检查IDEA中Artifacts是否已部署访问http://localhost:8080/项目名/Tomcat启动报端口被占用Tomcat的8080端口被其他程序占用命令行netstat -anoClassNotFoundException: com.mysql.jdbc.Driver驱动jar包版本不对或加载类名写错MySQL8使用com.mysql.cj.jdbc.Driver检查Maven依赖是否真正下载成功SSL connection errorMySQL8默认开启SSL但本地没有配置证书JDBC URL加useSSLfalse插入中文数据后乱码数据库、表、连接URL编码不一致统一使用UTF-8连接URL加characterEncodingutf8Tomcat配置URIEncoding前端页面表单提交后刷新重复下单用了forward转发导致POST请求被重复提交改用response.sendRedirect重定向也就是PRG模式Access denied for user rootlocalhost数据库密码错或MySQL8默认root密码策略确认密码或通过ALTER USER重置密码jsp页面里null直接显示后端某个字段赋值空值在JSP用c:if或${empty xxx}做判空mysql服务无法启动my.ini配置错误或data目录未初始化删除data目录执行mysqld --initialize-insecure再用net start mysql5.2 Filter 过滤器与登录状态校验用户和管理员有些页面需要权限控制。我在项目里写了一个LoginFilter拦截所有.jsp和Servlet请求把不需要过滤的页面登录页、注册页、车辆列表等排除掉。WebFilter(/*) public class LoginFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; String uri req.getRequestURI(); if (uri.endsWith(login.jsp) || uri.contains(/user?actionlogin) || uri.contains(register.jsp) || uri.endsWith(.css) || uri.endsWith(.js) || uri.endsWith(.png)) { chain.doFilter(request, response); return; } HttpSession session req.getSession(); Object loginUser session.getAttribute(loginUser); if (loginUser null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; } chain.doFilter(request, response); } }注意Filter的执行顺序以及在web.xml里配置的顺序如果用了WebFilter注解顺序一般按类名排序不稳定。我习惯在web.xml里手写Filter配置顺序可控。5.3 三个独家避坑技巧第一个技巧每次修改Java代码或配置文件后一定要重建并重启Tomcat。IDEA有时只是简单地热更新class但配置文件不一定生效。我踩过最惨的一次是改了db.properties里密码后没有重启结果一直报数据库密码错误排查了半小时。第二个技巧JSP页面调试时如果报500先去看Tomcat的日志不要只在浏览器里刷新。IDEA控制台底部Tomcat Localhost Log和Catalina Log里会有完整异常堆栈。很多时候是空指针或者是SQL语法问题一看堆栈就知道。第三个技巧数据库连接千万别忘记关闭。我最初开发时写了个DAO方法没关ResultSet连续跑了几次测试后数据库连接数直接满了Tomcat卡死老半天。后来我在DBUtil里统一封装了close方法所有DAO都用同一个关闭工具类才彻底杜绝资源泄漏。6. War 包打包与部署经验6.1 传统 JSP 项目如何打包 War课程设计或小组作业经常需要把项目部署到别的电脑上。传统JSP项目打包成war其实很简单Maven项目直接执行mvn clean package然后target目录下会生成xxx.war。把这个war文件丢到Tomcat的webapps目录下启动Tomcat后war会自动解压部署。如果用的是IDEA的Artifacts也可以手动添加Web Application Archive构建效果一样。打包之前记得检查pom.xml里有没有加打包插件。如果不加Maven默认只能打jar这个jar无法直接被Tomcat运行。6.2 数据库初始化脚本与一键部署思路为了让项目在另一台电脑上快速跑起来我写了一个init.sql脚本包含建库建表语句和基础测试数据。部署时只需要先安装MySQL然后用命令执行mysql -u root -p init.sql然后在db.properties里改好数据库连接信息启动Tomcat就能用了。我也建议把MySQL的字符集、排序规则在创建数据库时指定CREATE DATABASE IF NOT EXISTS car_share DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;使用utf8mb4而不是utf8是因为utf8mb4才能完整存储某些生僻字和表情符号虽然租车系统的车辆备注里一般不会用到但这是好习惯。7. 后续扩展与优化方向把基础功能跑通之后这个项目的可扩展性其实很强。我自己接下来想去加的东西有这么几个方向。第一个是数据源连接池。现在每次获取连接都用DriverManager直接创建一个新连接在高并发下性能很差。切换到Druid或HikariCP连接池配置一个数据源性能会有肉眼可见的提升。而且连接池还能监控慢SQL非常实用。第二个是密码加密。现在用户表里存的密码还是MD5加密后的结果但其实MD5不够安全推荐用BCrypt或SHA-256加盐。虽然这个项目只是课设但养成安全意识很重要。第三个是把车辆列表的分页和查询优化。现在如果车多一次性查出来页面加载会慢。我打算改成用LIMIT分页查询条件用动态SQL拼接再配一个PageBean类这样系统就更有“成品”的样子。第四个是前端升级。传统JSP页面确实不够美观可以引入Bootstrap或者Vue Element UI把展示层做得更现代。JSP后端保持不动前端通过Ajax请求Servlet返回JSON接口风格会更清晰。这些优化如果时间充裕完全可以一个个往项目里加。每次加完一个功能你会对整个JavaWeb体系理解更深一层。8. 一些真实的项目心得体会这个共享租车信息管理系统我从零开始写到现在大概用了两周的业余时间。最大的心得是别小看JSPServletJDBC这套老旧组合它让我对HTTP请求的生命周期、Servlet容器的工作原理、JDBC的事务边界有了很扎实的认知。我在自己电脑上跑通第一笔租车订单的时候那种感觉比用Spring Boot做一百个接口还有成就感。因为每一步都是自己写的SQL自己控制的连接自己处理的异常。尤其是用SELECT ... FOR UPDATE解决并发下单的那一刻我真正理解了为什么框架要封装那么多东西。如果你现在正是被JavaWeb课设折磨的阶段我的建议是不要一上来就复制网上的完整项目代码。先自己把数据库表建好再从用户登录开始写每一个功能能自己实现就自己实现实在卡住了再去看参考写法。这个项目代码量并不大认真写一遍你对JavaWeb的掌握程度会上升一个台阶。最后再分享一个调试小技巧如果页面报500错误或者数据加载不出来先看Tomcat的logs目录下的catalina.out文件里面会有完整的异常堆栈。很多时候你甚至不用百度看一眼第一行“Exception”就定位到问题了。遇到奇怪的数据库问题就用Navicat或命令行客户端手动执行一遍SQL看看是不是SQL本身的问题分清楚是前端、后端还是数据库的问题排查速度会快很多。
返回列表