简介一套基于Java Swing与MySQL的停车场管理系统源码面向Java初学者、课程设计及毕业设计人群用于实现车辆进出场登记、车辆信息浏览与查询等常见管理场景可作为Swing界面编程和JDBC操作的入门练习项目。压缩包共49个文件包含12个Java源文件与31个class编译文件便于对照源码和运行结果同时附有SQL数据库脚本、Eclipse项目配置、运行截图及jar依赖压缩后大小约1.96MB结构清晰、便于直接导入IDE启动。当前已有466人学习下载说明项目体量适中且实用适合快速跑通后再按需修改。功能完整覆盖用户登录注册、车辆入场、出场、信息浏览、删除与精确查询数据库设计精简含admin与car两张表SQL脚本可直接导入MySQL 8.0.13结合JDK 1.8环境即可运行。项目结构按源码、编译输出、配置与数据库脚本分区既能帮助理解Swing事件驱动与MySQL持久化操作也为二次开发和答辩展示提供了良好基础。1. 停车场管理系统最容易被忽略的难点在哪里很多刚学完 Java 基础的人拿到“基于 JavaSwingmysql 停车场管理系统2”这个题目时第一反应是把登录窗口画漂亮把车位格子涂成绿色和红色。真正进入开发后才会发现难点不在控件而在“同一车位被两辆车同时占用”和“出场时金额算错”这类数据一致性问题。我一般建议先把数据库表设计定下来再动 Swing 界面因为停车场系统里每一次按钮点击几乎都对应一条 SQL 事务。这篇文章覆盖从 MySQL 安装配置教程到 JDBC 连接参数、从存储过程到 Swing 布局管理的完整落地路径适合正在准备 java 面试综合项目的开发者也适合需要快速搭一个院内停车管理工具的运维工程师。这里写的每一段代码都按“能跑通”的最低标准来不会只给伪代码。2. 先定数据库再写界面停车场管理系统的表结构与连接方式2.1 用 MySQL 设计三张核心表如果把“停车场管理系统2”当作第二次迭代那第一次迭代最常见的毛病是把车辆、车位和订单全塞进一张表。第二次迭代我会拆成三张表parking_space记录车位的静态信息和占用状态vehicle记录车牌和车主parking_order记录一次进场到出场的完整事件。这样拆分以后后续做报表统计、黑名单、月租车位都有地方扩展也方便老员工维护时快速定位问题。2.1.1 车位表和车辆表车位表CREATE TABLE parking_space ( id INT PRIMARY KEY AUTO_INCREMENT, space_no VARCHAR(10) NOT NULL UNIQUE, zone VARCHAR(5) NOT NULL DEFAULT A, type ENUM(STD,LARGE,DISABLED) NOT NULL DEFAULT STD, status ENUM(FREE,OCCUPIED,MAINTENANCE) NOT NULL DEFAULT FREE, occupied_vehicle_id VARCHAR(20) DEFAULT NULL, version INT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;space_no加唯一索引就像门牌号一样不允许重复。occupied_vehicle_id没设外键一是因为车辆可能长期不在库里二是车牌作为业务主键已经足够外键在 InnoDB 里会拖慢高频更新。真正保证强一致的是订单表里对space_id或plate_no的唯一状态判断。车辆表CREATE TABLE vehicle ( plate_no VARCHAR(20) PRIMARY KEY, owner_name VARCHAR(30), phone VARCHAR(20), vehicle_type ENUM(CAR,TRUCK,MOTORCYCLE) NOT NULL DEFAULT CAR, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP );这里把plate_no直接作为主键而不是用自增 ID。停车场系统的车牌是天然的业务主键省掉一次SELECT LAST_INSERT_ID()的查询。vehicle_type在后续月租车计费、大车占位时会用到现在先留好字段。2.1.2 订单表订单表记录一次进场到出场的完整生命周期CREATE TABLE parking_order ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, plate_no VARCHAR(20) NOT NULL, space_id INT NOT NULL, entry_time DATETIME NOT NULL, exit_time DATETIME DEFAULT NULL, duration_minutes INT DEFAULT NULL, fee DECIMAL(8,2) DEFAULT NULL, status ENUM(PARKING,FINISHED,CANCELLED) DEFAULT PARKING, CONSTRAINT fk_order_vehicle FOREIGN KEY (plate_no) REFERENCES vehicle(plate_no) ON UPDATE CASCADE, CONSTRAINT fk_order_space FOREIGN KEY (space_id) REFERENCES parking_space(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;给space_id加上联合索引(status, entry_time)因为出场查询基本都按statusPARKING找订单。订单表里的duration_minutes和fee一开始可以为空出场后一次性更新。业务流程上建议定一个约定一次停车只有一条PARKING状态的订单。如果同车牌连续两次入库先取消旧的未完成订单再插入新订单避免后续统计时同一辆车占两条在停记录。2.2 JDBC 连接池参数与中文字符集Java 连接 MySQL 时最容易遇到中文字符乱码和时区错误。我平时会用这条 URLString url jdbc:mysql://127.0.0.1:3306/parking_db ?useUnicodetruecharacterEncodingUTF-8 serverTimezoneAsia/Shanghai useSSLfalseallowPublicKeyRetrievaltrue rewriteBatchedStatementstrue;useUnicode和characterEncoding必须成对出现只写一个容易出现中文乱码。serverTimezone在 MySQL 8.0 之后是必选项不写启动可能直接抛异常。allowPublicKeyRetrievaltrue解决的是caching_sha2_password认证插件在非加密连接下无法获取公钥的问题本地开发可以开生产环境建议通过受信 SSL 或换用mysql_native_password插件否则这是一处安全隐患。单机 Swing 程序虽然只有一个窗口但线程池里的SwingWorker可能同时执行查询所以连接池仍然需要只是不需要像 Web 服务那样开几十个连接。HikariCP 是常见做法参数建议如下参数建议值说明maximumPoolSize10Swing 单机程序并发不高开得越大越浪费minimumIdle2保留空闲连接减少重复握手connectionTimeout3000数据库挂掉时快速失败maxLifetime1800000避免 MySQL 空闲超时回收连接参数改完要重启验证不能只改一行就认为生效。连接池初始化失败时先检查时间时区参数再看 MySQL 的wait_timeout是否短于连接池的maxLifetime。2.3 存储过程把出场结算做进数据库出场操作横跨订单表、车位表、车辆表如果在 Java 里用三段 SQL 写完再 commit中间任一步失败都会产生脏数据。更稳的做法是写一个 MySQL 存储过程让事务边界发生在数据库层。先写一个计算费用的函数CREATE FUNCTION compute_fee(duration_minutes INT) RETURNS DECIMAL(8,2) DETERMINISTIC BEGIN DECLARE v_fee DECIMAL(8,2); IF duration_minutes 30 THEN SET v_fee 0; ELSEIF duration_minutes 120 THEN SET v_fee 5.00; ELSE SET v_fee 5.00 CEIL((duration_minutes - 120) / 60) * 2.00; END IF; RETURN v_fee; END计费规则是30 分钟内免费2 小时内 5 元超过后每小时加 2 元不足 1 小时按 1 小时算。用CEIL而不是FLOOR否则停了 2 小时 10 分钟会和 2 小时收同样费用。再写出场存储过程注意OUT参数用来返回费用CREATE PROCEDURE sp_exit_vehicle( IN v_plate_no VARCHAR(20), IN v_exit_time DATETIME, OUT v_fee DECIMAL(8,2) ) BEGIN DECLARE v_space_id INT; DECLARE v_entry_time DATETIME; DECLARE v_duration INT; START TRANSACTION; SELECT space_id, entry_time INTO v_space_id, v_entry_time FROM parking_order WHERE plate_no v_plate_no AND status PARKING ORDER BY entry_time DESC LIMIT 1 FOR UPDATE; IF v_space_id IS NULL THEN SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT no active order; END IF; SET v_duration TIMESTAMPDIFF(MINUTE, v_entry_time, v_exit_time); SET v_fee compute_fee(v_duration); UPDATE parking_order SET exit_time v_exit_time, duration_minutes v_duration, fee v_fee, status FINISHED WHERE space_id v_space_id AND status PARKING; UPDATE parking_space SET status FREE, occupied_vehicle_id NULL WHERE id v_space_id; COMMIT; ENDSELECT ... FOR UPDATE会对订单行加锁让同一个车牌的并发出场请求排队执行。这样即使两班值机员同时点同一个车牌的“出场”也不会出现金额不一致。SIGNAL SQLSTATE 45000是显式抛错Java 端的异常处理器可以根据 SQLState 识别到底是没有在场记录还是数据库故障。存储过程写完后用CALL sp_exit_vehicle(京A12345, NOW(), fee); SELECT fee;手动验证别直接接到 Swing 事件里调。3. Swing 界面布局从登录窗口到主控制台3.1 为什么用 Swing 而不是 JavaFX项目标题里已经锁定了 Swing。Swing 对比 JavaFX 的好处是随 JDK 分发不需要额外安装运行时旧操作系统也能跑。对一个停车场管理系统而言UI 形态很简单一个主窗口、几个输入框、一张表格、若干个状态按钮。Swing 的JTable和DefaultTableModel足够完成任务而 JavaFX 的表视图对应的TableView虽然刷新性能更好但引入更多概念对 Java 初学者不友好。另一个原因是排查环境依赖更轻松。JavaFX 在 JDK 11 之后从标准库分离用户机器上还要单独配置模块路径这对一个内部工具来说是不必要的运维负担。Swing 的短板在于外观老旧可以通过设置UIManager的跨平台皮肤解决但不需要为它引入重型 UI 框架。3.2 主窗口的结构菜单、工具栏和状态栏Swing 应用的标准结构是外层JFrame使用BorderLayoutNORTH放工具栏CENTER放主面板SOUTH放状态栏。菜单栏用setJMenuBar()挂到框架上不参与布局。import javax.swing.*; import java.awt.*; public class MainFrame extends JFrame { public MainFrame() { setTitle(停车场管理系统2); setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); setSize(1000, 700); setLocationRelativeTo(null); JMenuBar menuBar new JMenuBar(); JMenu fileMenu new JMenu(系统); fileMenu.add(new JMenuItem(刷新车位)); fileMenu.add(new JMenuItem(数据备份)); menuBar.add(fileMenu); setJMenuBar(menuBar); JToolBar toolBar new JToolBar(); toolBar.add(new JButton(车辆进场)); toolBar.add(new JButton(车辆出场)); toolBar.addSeparator(); toolBar.add(new JButton(查询订单)); add(toolBar, BorderLayout.NORTH); JLabel statusLabel new JLabel( 今日免费车位数: 0 ); add(statusLabel, BorderLayout.SOUTH); setVisible(true); } }这里菜单项和按钮暂时没有绑定事件。Swing 的按钮默认会占满工具栏的宽度所以需要为小图标按钮设置setPreferredSize或直接给按钮设置固定尺寸。状态栏放在SOUTH后主面板变大时状态栏不会跟着挤压。主窗口的布局可以放在一张表里布局位置组件作用NORTHJToolBar车辆进场、出场、查询的入口CENTERJScrollPane 车位按钮面板显示车位占用状态SOUTHJLabel显示当前空余车位数3.3 用面板组合让界面可维护主面板继续用BorderLayout拆分成左右两个区域。左侧放车位状态图右侧放操作面板和订单表格。JPanel mainPanel new JPanel(new BorderLayout(10, 10)); JPanel leftPanel new JPanel(new GridLayout(0, 4, 8, 8)); for (int i 1; i 24; i) { JToggleButton spaceBtn new JToggleButton(A i); spaceBtn.setBackground(Color.GREEN); leftPanel.add(spaceBtn); } mainPanel.add(leftPanel, BorderLayout.WEST); JTable orderTable new JTable(new DefaultTableModel( new Object[]{订单号, 车牌, 进场时间, 金额}, 0 )); mainPanel.add(new JScrollPane(orderTable), BorderLayout.CENTER); add(mainPanel, BorderLayout.CENTER);车位按钮用GridLayout(0, 4)实现自动换行24 个车位分成 6 行。按钮颜色表示状态绿色空闲、红色占用、灰色维修。这个布局的好处是新增加区域时只需调整网格列数不需要重写容器结构。左侧区域宽度固定右侧订单表格自动拉伸不受文字长度影响。3.3.1 刷新车位按钮Swing 的单线程规则导致数据刷新必须回到事件分发线程。后台查询用SwingWorkernew SwingWorkerVoid, Void() { Override protected Void doInBackground() throws Exception { // 执行 SQL 查询得到车位状态列表 return null; } Override protected void done() { // 在这里刷新按钮颜色和表格 reloadParkingSpaceStatus(); } }.execute();更新按钮颜色的核心方法是SwingUtilities.invokeLaterSwingUtilities.invokeLater(() - { for (Component comp : leftPanel.getComponents()) { JToggleButton btn (JToggleButton) comp; btn.setBackground(isOccupied(btn.getText()) ? Color.RED : Color.GREEN); } });如果不调用invokeLater直接在 Swing 事件线程里执行耗时 SQL界面会卡死如果从子线程直接修改按钮背景色则可能抛出ConcurrentModificationException。这也是 Swing 程序在数据库并发访问下的常见出坑点。3.4 登录窗口的模态与校验登录窗口通常用JDialog而不是第二个JFrame因为JDialog可以设置模态JDialog loginDialog new JDialog(mainFrame, 登录, true); loginDialog.setSize(320, 180); loginDialog.setLocationRelativeTo(mainFrame); loginDialog.setVisible(true);模态为true时登录窗口没有关闭之前主窗口无法接收点击事件。登录校验建议把用户名和密码放到PreparedStatement里PreparedStatement ps conn.prepareStatement( SELECT role FROM sys_user WHERE username? AND passwordMD5(?) ); ps.setString(1, username); ps.setString(2, password);密码用MD5只是最低要求真实环境换成SHA-256加盐更稳妥。不要用字符串拼接不然 SQL 注入会把用户表整个导出去。4. 车辆进出与费用计算的实现细节4.1 入场逻辑先查车型再写入订单入场按钮按下后Swing 事件线程调用下面这个方法public boolean carEntry(String plateNo, int spaceId) { try (Connection conn dataSource.getConnection()) { conn.setAutoCommit(false); PreparedStatement ps conn.prepareStatement( SELECT id FROM parking_space WHERE id? AND statusFREE FOR UPDATE); ps.setInt(1, spaceId); ResultSet rs ps.executeQuery(); if (!rs.next()) { conn.rollback(); return false; } PreparedStatement ps2 conn.prepareStatement( INSERT INTO vehicle (plate_no) VALUES (?) ON DUPLICATE KEY UPDATE plate_noplate_no); ps2.setString(1, plateNo); ps2.executeUpdate(); PreparedStatement ps3 conn.prepareStatement( INSERT INTO parking_order (plate_no, space_id, entry_time, status) VALUES (?,?,NOW(),PARKING)); ps3.setString(1, plateNo); ps3.setInt(2, spaceId); ps3.executeUpdate(); PreparedStatement ps4 conn.prepareStatement( UPDATE parking_space SET statusOCCUPIED, occupied_vehicle_id? WHERE id? AND statusFREE); ps4.setString(1, plateNo); ps4.setInt(2, spaceId); int rows ps4.executeUpdate(); if (rows ! 1) { conn.rollback(); return false; } conn.commit(); return true; } catch (SQLException ex) { throw new RuntimeException(ex); } }这里的核心是UPDATE ... WHERE statusFREE这是一层乐观锁保护。即使两个人同时点击同一个空车位的按钮只有rows1的人能拿到执行权。如果只用代码先查再判断两步之间有间隔就会产生“两个查询都看到 FREE两个更新都成功”的竞态条件。ON DUPLICATE KEY UPDATE plate_noplate_no把“插入”和“已存在则忽略”合并成一条语句省掉一次查询。这比 java 面试题里常见的if exists解法更高效因为exists需要两次与数据库的往返且判断和插入中间永远有空隙。4.2 出场调用存储过程并获取费用出场按钮的事件处理器private BigDecimal doExit(String plateNo) { try (Connection conn dataSource.getConnection(); CallableStatement cs conn.prepareCall({CALL sp_exit_vehicle(?,?,?)})) { cs.setString(1, plateNo); cs.setTimestamp(2, new Timestamp(System.currentTimeMillis())); cs.registerOutParameter(3, Types.DECIMAL); cs.execute(); return cs.getBigDecimal(3); } catch (SQLException ex) { if (45000.equals(ex.getSQLState())) { // 业务错误没有在场记录 return null; } throw new RuntimeException(ex); } }这里用registerOutParameter接收存储过程的OUT参数。SQLState 45000对应存储过程里的SIGNAL属于业务异常不用当作系统错误弹出堆栈。费用金额显示给用户之前建议用setScale(2, RoundingMode.HALF_UP)格式化一下。数据库里DECIMAL不会出现浮点误差但 Java 端的BigDecimal如果直接转成String可能保留多余尾数。4.3 车位状态机与异常恢复业务状态变化最好用状态机描述事件订单状态车位状态进场PARKINGOCCUPIED出场FINISHEDFREE取消CANCELLEDFREE当系统里出现“订单状态是PARKING但车位状态是FREE”的不一致时用一个定时任务扫描修复UPDATE parking_space p JOIN parking_order o ON p.id o.space_id AND o.statusPARKING SET p.statusOCCUPIED, p.occupied_vehicle_ido.plate_no WHERE p.statusFREE;定时任务不要做得太频繁5 分钟一次就够。这个修复动作在处理异常重启时很有用常见原因就是 Java 进程在事务提交前被杀掉MySQL 回滚后订单和车位状态不一致。计费过程中有的停车场要求按半小时阶梯计算有的要求月租车不收费这些都可以在compute_fee函数里根据vehicle_type加一层判断。不要把费用计算逻辑散落在 Java 方法里统一放在数据库函数的好处是报表统计和界面显示能复用同一套算法。后续调整计费规则时只改函数不需要重新打包 Swing 客户端。5. 系统上线前的验证和打包技巧5.1 用并发脚本盯住超卖问题写完不等于跑得对。我一般用CountDownLatch模拟多辆车同时抢同一个车位int threadCount 10; CountDownLatch start new CountDownLatch(1); ExecutorService executor Executors.newFixedThreadPool(threadCount); for (int i 0; i threadCount; i) { final int id i; executor.execute(() - { try { start.await(); boolean ok carEntry(TEST id, 1); System.out.println(car id result ok); } catch (InterruptedException ignored) { } }); } start.countDown();正常情况下只有一个resulttrue。如果多个 true就要检查UPDATE ... WHERE statusFREE是否生效。一个容易忽略的问题MySQL 的事务隔离级别默认为REPEATABLE READ在并发测试时可能看到旧数据。可以改为READ COMMITTED降低锁等待但前提是业务不依赖可重复读的复杂范围查询。5.2 用 Maven 打包成可运行 jar如果用的是 Maven需要把 MySQL 驱动和连接池打成一个 jar。在pom.xml里配置maven-shade-pluginplugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId executions execution phasepackage/phase goalsgoalshade/goal/goals configuration transformers transformer implementationorg.apache.maven.plugins.shade.resource.ManifestResourceTransformer mainClasscom.parking.Main/mainClass /transformer /transformers /configuration /execution /executions /plugin打包后执行java -jar parking.jar。如果目标机器没有安装 JDK可以用jpackage生成原生启动器这里不展开。5.3 数据库免安装部署客户端机器不一定要装 MySQL 服务。可以将 MySQL 的 dist 目录拷到本地用免安装版运行。初始化数据目录时需要执行mysqld --initialize-insecure --datadirD:\\mysql-data mysqld --console --datadirD:\\mysql-data --port3306然后在my.ini中设置字符集[mysqld] character-set-serverutf8mb4 [client] default-character-setutf8mb4这两行解决了中文乱码问题。Java 程序这边的 JDBC url 与数据库端字符集必须保持一致否则存储的utf8mb4在读取时仍是乱码。最后提醒一点部署完成别忘用mysqladmin status检查连接数避免超过max_connections。整个系统跑起来后先用手工操作三笔进出一致性数据再交给业务人员使用。本文还有配套的精品资源点击获取