ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

JavaWeb停车场管理系统实战:从数据库到并发优化

JavaWeb停车场管理系统实战:从数据库到并发优化 简介基于JavaWeb的停车场管理系统课程设计资源包面向需要完成相关大作业或课设的在校生提供从源码到报告的全套方案。系统覆盖管理员登录、停车记录查看与修改、停车场使用情况及数据统计、预计收入查看以及车辆驶入驶出结算、照片添加查看、余量判断和车位自动分配等完整功能基于MVC架构并综合运用Servlet、JSP、JavaBean、JS与数据库技术可帮助读者理解WEB应用分层设计与停车计费规则落地。压缩包共1086个文件以js、jpg、less、xml、class、css、java、jar等类型为主其中JS与CSS负责前端交互和样式less为样式预编译文件java与class对应后端源码及编译输出jar为依赖库jpg/png等为界面切图与文档图片另有docx格式的实验报告和操作说明整体约92.05MB目录结构清晰便于按模块查阅。已有4395人学习下载配套报告和傻瓜式安装步骤即使新手也能按步骤独立完成部署与演示。通过实际部署与调试还能深入掌握JavaWeb项目从环境搭建到功能实现的全过程。1. 为什么课程设计都爱拿停车场管理系统练手多数人在课程设计选题表上勾下“停车场管理系统”那一刻以为是在选一个“容易过”的项目真正动手时才发现它既要处理车辆入场、出场、计费、会员充值还要应对管理员权限和统计报表。这不是一个纯增删改查的玩具而是一套典型的 JavaWeb 业务闭环——从浏览器发请求到 Tomcat再到 Servlet 处理逻辑最后通过 JDBC 读写 MySQL 数据。IDEA 里新建一个 Maven 工程后你会同时面对前端页面、后端 Servlet、数据库表设计和部署配置四条线。正因为它覆盖了 JavaWeb 教学案例里最常被考核的模块才被当作 javaweb 课程设计的常青树。这篇文章会从数据库表设计开始逐步拆到 Servlet 业务逻辑、前端交互和答辩前的验证适合正在做 javaweb 大作业的在校生也想用这个项目练手并准备面试的转行者。2. 从表结构到 JDBC 连接池数据层设计与数据源配置数据层是整个系统的地基很多人的停车场大作业在验收时被评委问垮第一问就是“你的车位、订单、用户之间什么关系”。如果答不上来后面的功能演示再花哨也会被打折。这章先把表结构逻辑讲清楚再给出可复用的 Druid 连接池配置最后用一段规范的 JDBC 代码把数据访问层落地。2.1 四张核心表如何设计才能支撑计费和统计一个能应付答辩的停车场系统至少要有用户表、车位表、订单表和车辆表。用户表区分管理员与普通用户车辆表记录车牌与用户归属车位表维护车位状态订单表存储每次入场的开始时间、结束时间、费用和状态。这样设计是为了让每次计费都能追溯到一条订单记录而不是在程序里临时计算。下面是一份可直接执行的 MySQL 建表脚本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 密码建议存储MD5或加盐值, role TINYINT DEFAULT 0 COMMENT 0管理员1普通用户, real_name VARCHAR(50) COMMENT 真实姓名, phone VARCHAR(20) COMMENT 联系电话, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) COMMENT 系统用户表; CREATE TABLE vehicle ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 车辆ID, user_id INT NOT NULL COMMENT 所属用户ID, plate_no VARCHAR(20) NOT NULL COMMENT 车牌号, vehicle_type TINYINT DEFAULT 0 COMMENT 0小型车 1大型车, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_vehicle_user FOREIGN KEY (user_id) REFERENCES sys_user(id) ) COMMENT 车辆信息表; CREATE TABLE parking_space ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 车位ID, space_no VARCHAR(10) NOT NULL UNIQUE COMMENT 车位编号如 A-01, location VARCHAR(50) COMMENT 区域位置, status TINYINT DEFAULT 0 COMMENT 0空闲 1占用, vehicle_id INT COMMENT 当前占用车辆ID空闲为NULL, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) COMMENT 车位表; CREATE TABLE park_order ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 订单ID, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 订单编号, plate_no VARCHAR(20) NOT NULL COMMENT 车牌号, space_id INT NOT NULL COMMENT 车位ID, user_id INT COMMENT 用户ID临停可为空, start_time DATETIME NOT NULL COMMENT 入场时间, end_time DATETIME COMMENT 出场时间未出场为NULL, amount DECIMAL(10,2) DEFAULT 0.00 COMMENT 应收金额, status TINYINT DEFAULT 0 COMMENT 0进行中 1已完成 2取消, CONSTRAINT fk_order_space FOREIGN KEY (space_id) REFERENCES parking_space(id) ) COMMENT 停车订单表;字段类型的选择有讲究plate_no留 20 位是为了兼容新能源车牌amount用DECIMAL(10,2)而不是FLOAT因为计费涉及金额浮点运算会产生精度误差这在答辩时是加分点。status用TINYINT存储状态码扩展状态位时可以只改代码不重建表。外键约束在课程设计里可以保留它能保证车位和订单数据的一致性但实际互联网项目中外键往往被拆掉这里按业务完整性优先。2.2 Druid 连接池参数与 JDBC 工具类纯 JDBC 直接DriverManager.getConnection()在每次请求时创建连接压力测试十次就会看到 Tomcat 报连接超时。javaweb 项目完整案例里普遍使用连接池最常见的方案是阿里 Druid。在 Maven 的pom.xml中引入依赖dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency然后在src/main/resources下创建druid.propertiesdriverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/parking_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse usernameroot passwordyour_password initialSize5 maxActive20 minIdle5 maxWait60000 validationQuerySELECT 1参数说明参数默认值当前配置作用与建议initialSize05启动时创建的物理连接数课程设计环境设 3~5 足够maxActive820最大活跃连接数并发量不高时 20 已绰绰有余minIdle05保底空闲连接防止突然无连接可用maxWait-160000获取连接的最大等待毫秒数超时抛出异常避免线程池耗尽validationQuery无SELECT 1用来检测连接是否存活至少保证池中的连接可用maxActive不是越大越好每个连接都占用一份内存和 TCP 资源Tomcat 默认线程数通常是 200如果maxActive也配 200数据库会先扛不住。课程设计的校验场景20 以内完全够用。接着封装一个工具类所有 DAO 层都从这里拿连接public class DBUtil { private static DataSource dataSource; static { try (InputStream in DBUtil.class.getClassLoader() .getResourceAsStream(druid.properties)) { Properties props new Properties(); props.load(in); dataSource DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(ResultSet rs, PreparedStatement ps, Connection conn) { if (rs ! null) { try { rs.close(); } catch (SQLException ignored) {} } if (ps ! null) { try { ps.close(); } catch (SQLException ignored) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } } }这段静态块在类加载时初始化连接池后续所有线程都复用这组连接。注意close方法里调用的conn.close()实际上是归还连接池而不是真的断开连接这是连接池代理对象的行为。手动关闭顺序必须从ResultSet到PreparedStatement再到Connection反了会导致结果集未完全释放时报错。2.3 数据访问层的 try-with-resources 写法Java 7 之后推荐用try-with-resources替代手动closeDBUtil.close可以留作备用但在新代码中直接这样写更干净public ParkOrder findByOrderNo(String orderNo) { String sql SELECT * FROM park_order WHERE order_no ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, orderNo); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { ParkOrder order new ParkOrder(); order.setId(rs.getInt(id)); order.setOrderNo(rs.getString(order_no)); order.setPlateNo(rs.getString(plate_no)); order.setStartTime(rs.getTimestamp(start_time)); order.setEndTime(rs.getTimestamp(end_time)); order.setAmount(rs.getBigDecimal(amount)); order.setStatus(rs.getInt(status)); return order; } } } catch (SQLException e) { e.printStackTrace(); } return null; }try括号里的资源会自动关闭不需要finally。PreparedStatement传参用?占位而不是字符串拼接这是最基础的 SQL 注入防线。课程设计答辩时老师常问“为什么用PreparedStatement而不是Statement”你可以答两点预编译让 SQL 结构固定传入参数不会改变执行计划同时避免拼接造成的注入风险。3. 登录鉴权与车位状态流转Servlet Filter 的业务实现数据层搭好后真正的业务逻辑在 Servlet 层展开。本章以“登录验证”和“车辆入出场”两个场景为切入点讲解 Session 会话管理、Filter 拦截器和状态流转如何配合这也是 javaweb 项目完整案例里最容易被追问的部分。3.1 基于 Session 的用户认证与登录态保持HTTP 是无状态协议用户登录后再次请求时服务器不知道“你是谁”。最常见做法是登录成功后将用户对象存入 HttpSession并用一个 Cookie 保存会话 ID。这里给出登录 Servlet 的核心代码WebServlet(/login) public class LoginServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String password MD5Util.md5(req.getParameter(password)); UserDao dao new UserDao(); User user dao.findByUsernameAndPassword(username, password); if (user ! null) { HttpSession session req.getSession(); session.setAttribute(loginUser, user); session.setMaxInactiveInterval(30 * 60); resp.sendRedirect(req.getContextPath() /index.jsp); } else { req.setAttribute(errorMsg, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); } } }这里有两个容易在答辩时被问到的点。第一密码存的是 MD5 加盐后的值数据库里不保存明文盐值可以取用户名的一部分防止预计算彩虹表破解第二setMaxInactiveInterval(30 * 60)设置 30 分钟无操作就失效避免长期占用服务端内存。在doPost里校验参数前还要先判断用户名是否为空否则MD5Util.md5(null)会抛空指针。3.2 车位状态机的流转设计与入场业务一个车位有“空闲”和“占用”两种状态入场时把状态置为占用并创建订单出场时计算费用并释放车位。但数据库里不能只改一个status字段因为计费需要入场时间、订单号而车位表里需要记录是哪辆车占用了当前车位。下面是入场操作的 Service 方法Transactional public boolean enterParking(String plateNo, int spaceId, int userId) { try (Connection conn DBUtil.getConnection()) { conn.setAutoCommit(false); // 防止超卖先查询车位是否仍为空闲 String checkSql SELECT status FROM parking_space WHERE id ? AND status 0 FOR UPDATE; try (PreparedStatement ps conn.prepareStatement(checkSql)) { ps.setInt(1, spaceId); try (ResultSet rs ps.executeQuery()) { if (!rs.next()) { conn.rollback(); return false; } } } String updateSpace UPDATE parking_space SET status 1, vehicle_id ? WHERE id ?; try (PreparedStatement ps conn.prepareStatement(updateSpace)) { ps.setInt(1, userId); // 简单场景用userId关联再查车牌 ps.setInt(2, spaceId); ps.executeUpdate(); } String orderNo P System.currentTimeMillis(); String insertOrder INSERT INTO park_order(order_no, plate_no, space_id, user_id, start_time, status) VALUES(?, ?, ?, ?, NOW(), 0); try (PreparedStatement ps conn.prepareStatement(insertOrder)) { ps.setString(1, orderNo); ps.setString(2, plateNo); ps.setInt(3, spaceId); ps.setInt(4, userId); ps.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { // 这里应回滚实际代码中需要用 try-catch 包裹并回滚 e.printStackTrace(); return false; } }SELECT ... FOR UPDATE是行级锁锁住车位记录后其他并发请求必须等待当前事务提交才能读到最新状态。这能防止两个请求同时看到“空闲”并都成功占位。事务必须手动setAutoCommit(false)否则每一步都会自动提交一旦后续插入订单失败车位状态已经变更为占用数据就脏了。实际项目中这个逻辑通常会放在 Service 层统一管理事务而不是在 DAO 里散落操作课程设计如果能把事务边界讲清楚已经比大部分人的大作业高一个档次。3.3 用 Filter 做全局登录拦截和编码处理如果每个 Servlet 都写一遍“判断是否登录”代码会冗余且容易漏判。用 Filter 可以统一拦截未登录请求WebFilter(/*) public class AuthFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; req.setCharacterEncoding(UTF-8); resp.setCharacterEncoding(UTF-8); String uri req.getRequestURI(); if (uri.endsWith(/login.jsp) || uri.endsWith(/login) || uri.endsWith(.css) || uri.endsWith(.js) || uri.endsWith(.png)) { chain.doFilter(request, response); return; } HttpSession session req.getSession(false); if (session ! null session.getAttribute(loginUser) ! null) { chain.doFilter(request, response); } else { resp.sendRedirect(req.getContextPath() /login.jsp); } } }这个 Filter 干了两件事第一统一设置请求和响应的 UTF-8 编码解决中文乱码这个最常见的问题第二检查 Session 中是否有登录用户没有就跳回登录页。注意/login接口本身必须放行否则用户永远无法登录。静态资源也要放行否则登录页的 CSS 和图片会被拦截。req.getSession(false)中的false表示如果当前请求没有绑定的 Session 就返回 null而不是新建一个——这样可以避免未登录用户也被创建大量无意义的 Session 对象属于内存优化的小细节。4. 前端交互与统计报表JSP 渲染、AJAX 请求与 MySQL 查询优化业务逻辑完成后前端页面和统计报表是决定验收印象分的部分。这一章讲 JSP 如何正确渲染车位数据再用 AJAX 处理入出场操作最后给出几条统计 SQL 和索引优化建议这也是“三步点金指标源码”这类检索词背后真正需要的落地能力——把数据变成能看懂的指标。4.1 JSP JSTL 渲染车位状态传统 JSP 里避免直接用% %写散碎的 Java 代码而是用 EL 表达式和 JSTL 标签。在pom.xml引入 JSTL 后车位列表页面可以这样写% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table classtable table-bordered thead trth车位编号/thth位置/thth状态/thth操作/th/tr /thead tbody c:forEach items${spaceList} varspace tr td${space.spaceNo}/td td${space.location}/td td c:choose c:when test${space.status 0} span classbadge badge-success空闲/span /c:when c:otherwise span classbadge badge-danger占用/span /c:otherwise /c:choose /td td c:if test${space.status 0} button classbtn btn-primary enter-btn>document.querySelectorAll(.enter-btn).forEach(function(btn) { btn.addEventListener(click, function() { const spaceId this.getAttribute(data-id); const plateNo document.getElementById(plateNoInput).value.trim(); if (!plateNo) { alert(请输入车牌号); return; } fetch(contextPath /park/enter, { method: POST, headers: { Content-Type: application/x-www-form-urlencoded }, body: spaceId spaceId plateNo encodeURIComponent(plateNo) }) .then(function(resp) { return resp.json(); }) .then(function(data) { if (data.code 1) { alert(入场成功); window.location.reload(); } else { alert(data.msg || 入场失败); } }) .catch(function() { alert(网络异常); }); }); });请求被后端 Servlet 处理后返回 JSON前端根据code判断是否成功。encodeURIComponent是必须的车牌号如果是中文或者带有特殊字符不编码会导致请求体乱码。在 Servlet 中通过request.getParameter获取参数时Tomcat 7 以上会按 UTF-8 解码 POST 请求但前提是前面 Filter 里设置了req.setCharacterEncoding(UTF-8)这一行放在获取任何参数之前才有效。4.3 统计报表 SQL 与索引优化报表页通常需要展示“今日收入”“今日停车次数”“当前占用率”三个指标。手写 SQL 时最常见的坑是把所有数据一次查出来再在 Java 内存里统计当数据量到万级别就会卡顿。正确做法是让 MySQL 先做聚合再返回结果-- 今日收入与订单数 SELECT COUNT(*) AS order_count, IFNULL(SUM(amount), 0) AS total_amount FROM park_order WHERE start_time CURDATE() AND status 1; -- 当前各区域车位占用率 SELECT p.location, COUNT(*) AS total_spaces, SUM(CASE WHEN p.status 1 THEN 1 ELSE 0 END) AS occupied_spaces FROM parking_space p GROUP BY p.location;第二条 SQL 在parking_space上按location分组统计每个区域的车位总数和占用数量。为了让这类查询走索引而不是全表扫描建议给频繁查询和过滤的字段加索引ALTER TABLE park_order ADD INDEX idx_start_time (start_time); ALTER TABLE park_order ADD INDEX idx_status (status); ALTER TABLE parking_space ADD INDEX idx_location (location);设计索引时要克制不要每个字段都加因为索引会拖慢插入和更新速度。停车场系统的写入操作远低于查询所以给查询字段建索引收益明显。在答辩演示时可以先用EXPLAIN SELECT ...展示加索引前后type列从ALL变成range或ref这一个细节就能让评委相信你不是只会跑 CRUD 的。5. 答辩前必做的三轮验证并发扣费、事务隔离与部署排错课程设计答辩时功能演示往往只占两分钟剩下时间都在提问。最后一章我分享自己递交给老师前必做的三个验证动作这些动作能提前暴露项目里最隐蔽的问题。5.1 并发场景下用多线程模拟入场抢车位在浏览器里点两次入场第二次大概率因为状态已更新而提示失败但这是串行请求。真正的并发问题要用多线程模拟我通常写一个临时的测试类一次性起 20 个线程同时请求同一个车位的入场接口ExecutorService pool Executors.newFixedThreadPool(20); CountDownLatch latch new CountDownLatch(1); for (int i 0; i 20; i) { final int index i; pool.submit(() - { try { latch.await(); String result HttpUtil.post( http://localhost:8080/parking/park/enter, spaceId1plateNoTestA index); System.out.println(index - result); } catch (Exception e) { e.printStackTrace(); } }); } latch.countDown(); pool.shutdown();如果代码里的车位状态更新不是原子的你会看到多个线程都返回成功同一个车位被多辆车占用。正确的结果是只有一个成功其余返回“车位已被占用”。这里的CountDownLatch是为了让 20 个线程尽可能在同一时刻发起请求模拟峰值冲击。建议在本地启动 Tomcat 后运行这个验证不用写太复杂结果能证明你考虑过线程安全就足够。5.2 部署到 Tomcat 时的压测排错清单最常见的部署问题集中在端口、驱动和编码三处。先列出排查清单现象可能原因处理办法Tomcat 启动失败提示Port 8080 already in use端口被占用用netstat -ano找出占用 PID杀掉进程或改 Tomcat 端口ClassNotFoundException: com.mysql.cj.jdbc.Drivermysql-connector 依赖缺失检查WEB-INF/lib是否包含 jar或 Maven 是否package成功页面中文全部变成问号JSP 页面或 Servlet 响应编码未设置检查pageEncodingUTF-8并确认 Filter 设置了request.setCharacterEncodingCommunications link failureMySQL 没启动或连接串写错先在本机用命令行mysql -u root -p测试连通性其中Communications link failure是最容易漏掉的环境坑尤其 MySQL 8 以上必须要带serverTimezoneAsia/Shanghai否则 JDBC 连接抛时区异常。这些内容在 javaweb 环境配置 idea 相关教程里都能查到但真正动手部署时把异常堆栈第一行读明白往往比从头搜文章更快。5.3 提交前给评委看的三个亮点验证除了让系统跑通建议在报告里附上三组截图和说明。第一组是数据库表结构截图旁边标注外键关系和索引设计展示数据建模能力第二组是打开druid监控页面或打点日志的截图证明在真实场景观察过连接池状态第三组是并发测试的结果哪怕只有一张成功失败结果的输出也能体现你做过压力验证的思考。这三个点不需要远比别人多但都比单纯展示页面能加分。最终别忘了对照自己报告里的“功能描述”逐条打勾避免出现源码里有但报告没写、报告写了但代码没实现的情况。本文还有配套的精品资源点击获取
返回列表