资讯详情 Java GUI智慧公交管理系统实战:Swing选型与避坑指南
📅 2026/10/25 10:27:29
简介一套基于 Java GUI 与 MySQL 8.0 的智慧公交管理系统完整项目包面向 Java 学习者、数据库课程设计及毕业设计人群覆盖车辆、员工、线路、站点、排班方案和密码修改等核心业务内置管理员、调度员、员工三类角色登录后即可体验完整的数据联动流程。包体共 2 个文件项目源码压缩包与数据库脚本各一个整体约 10.72MB将 SQL 脚本导入 MySQL 即可还原 busmanage 数据库方便快速搭建运行环境并对照学习 JDBC 连接、GUI 界面布局和多表查询设计。当前已有 1137 人学习下载。配套实现百分百可运行数据库表设计覆盖员工表、登录表、排班表、站点表、线路表、车辆表及线路-站点表能帮助理解真实公交管理场景中的信息同步与业务流程项目结构清晰适合作为课程设计参考也可作为 Java Swing 与数据库整合开发的实战范本。1. Java GUI 制作的智慧公交管理系统它不是过时的课程设计Java GUI 制作的智慧公交管理系统听起来像是课程设计的选题但在调度室里它真的在跑一块大屏实时显示车辆位置、发车间隔和司机排班数据存在 MySQL 里前端要看得见、改得动。很多人第一反应是上 Web 前端但内部工具用 Swing 反而更顺——只要装了 JDK双击 jar 包就能启动团队里会 Java 的人远多于会前端框架的人。这套系统常被当成毕设题目但按真实调度业务来做难度并不低角色要分级、表格要实时刷新、排班要算得准。标题里的“智慧”不在界面多炫而在数据与调度逻辑能否落地。这篇文章适合两类人拿它做课程设计或毕设的学生以及要给车队做内部小工具的一线开发者。2. 技术选型为什么 Java GUI 仍选 Swing以及项目怎么分层2.1 Swing vs JavaFX支撑这套系统的三个理由现在的 Java 桌面开发基本在 Swing 和 JavaFX 之间二选一。JavaFX 有 CSS 样式、FXML 布局和硬件加速视觉上确实现代但它的类库在 JDK 8 之后被移出标准 JDK需要手动引入 openjfx 模块打包时还要区分模块化与类路径两种方式。公交公司的后台通常跑在内网机器或旧服务器上JavaFX 的依赖和模块路径问题往往比界面本身更耗时间。Swing 从 JDK 1.2 起就一直在标准库里javax.swing 包在任何装了 JDK 的机器上都能用。智慧公交管理系统的使用场景是调度室里的运营后台交互就是看数据、改数据、盯状态不需要动画和换肤。Swing 的事件分发线程EDT模型虽然老但行为可预测界面卡住时检查是不是在事件线程里干了重活基本能定位问题。第三个理由是团队熟悉度。能写 JDBC 的 Java 工程师比熟悉 JavaFX 生命周期和 FXML 的人多得多。接手成本低、改起来快。我做这类内部工具的选型原则很简单没有复杂动画、没有触屏、没有大屏炫技需求就用 Swing。上 JavaFX 的前提是项目需要对视觉细节做精细控制否则引入的成本大于收益。两个方向的差异可以看这张表维度SwingJavaFXJDK 内置一直内置JDK 8 后需额外引依赖界面样式默认朴素可自绘CSS 定制能力强打包体积jar 即可需附带 jfx 模块表格大数据量常规数据够用虚拟化更强学习曲线组件模型直白FXML、生命周期概念多团队门槛低中高2.2 三层结构不让一个类既画界面又查数据库新手写 Java GUI 项目最常见的问题是把所有逻辑塞进一个 JFrame 子类按钮监听里写 JDBC 查询查询结果直接往 JTable 里塞发车间隔算法也堆在里面。这种写法在 Demo 阶段能跑一旦要加一个“按线路筛选”功能就可能动到查询方法而查询方法又和界面刷新耦合在一起改一处崩一片。我建议从一开始就按“界面-业务-数据”三个层次拆包结构通常是com.example.bus ├── ui # LoginFrame、MainFrame、RoutePanel、BusPanel ├── service # RouteService、ScheduleService、DispatchService ├── dao # RouteDao、BusDao、DriverDao └── model # Route、Bus、Driver、Schedule界面层只做两件事把用户动作转成 service 调用把 service 返回的结果显示到组件上。service 层处理排班、间隔计算、状态判断这些业务规则不依赖任何 JFrame 类。dao 层只做参数化 SQL 和 ResultSet 到 model 的映射。model 是纯数据容器。分层带来的一个隐性好处是 service 和 dao 可以在不打开窗口的情况下用 JUnit 测试。比如调度算法直接喂数据验证不用反复重启界面去手工点按钮。启动失败时也好定位报错堆栈落在 dao 层就是 SQL 问题落在 service 层就是业务逻辑问题落在 ui 层才需要查界面线程或布局。如果所有代码混在一起异常栈四层深每一层都像是一个黑匣子。2.3 主窗口模块划分JTabbedPane 承载四个业务页签主窗口不推荐把 JFrame 的 contentPane 堆成巨型面板。我习惯把主界面拆成四个区域顶部一条状态栏显示登录用户和数据库连接状态中间主区用 JTabbedPane 放四个页签分别是线路管理、车辆管理、调度监控、基础报表每个页签内部再分左表右操作区底部一条提示栏显示最近一次操作的结果。这个划分的逻辑是让每个页签对应一条业务线组件之间不互相引用。线路管理页签只依赖 RouteService 和 RouteDao车辆管理页签只依赖 BusService。调度监控页签复杂一些要同时读线路、车辆和实时位置三张表所以单独抽一个 DispatchPanel 做聚合不在 MainFrame 里写聚合逻辑。JTabbedPane 本身不复杂关键是要给每个页签一个独立的 JPanel 子类而不是在 MainFrame 里匿名 new 出来堆在一起。独立类的好处是每个面板持有自己的 TableModel 和刷新逻辑出问题时互不牵连。后面避坑章节里讲到的几个翻车点很大一部分就出在“所有面板塞在一个类里”这种写法上。如果项目里发现某个文件超过 800 行优先考虑拆面板而不是继续往下堆。3. 从登录到主控台Swing 界面骨架的完整代码3.1 登录窗口JFrame 生命周期与角色判断登录是入口但登录窗口的写法有不少细节。多数教程里用一个 JFrame 就完事我这里给出一个更接近生产环境的做法登录窗口独立运行校验通过后关闭自身并打开主控台窗口。public class LoginFrame extends JFrame { private JTextField userField; private JPasswordField pwdField; private LoginService loginService; public LoginFrame() { loginService new LoginService(); setTitle(智慧公交管理系统 - 登录); setSize(360, 220); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); JPanel panel new JPanel(new GridBagLayout()); GridBagConstraints gbc new GridBagConstraints(); gbc.insets new Insets(5, 5, 5, 5); gbc.gridx 0; gbc.gridy 0; panel.add(new JLabel(用户名:), gbc); userField new JTextField(15); gbc.gridx 1; gbc.gridy 0; panel.add(userField, gbc); gbc.gridx 0; gbc.gridy 1; panel.add(new JLabel(密码:), gbc); pwdField new JPasswordField(15); gbc.gridx 1; gbc.gridy 1; panel.add(pwdField, gbc); JButton loginBtn new JButton(登录); gbc.gridx 1; gbc.gridy 2; panel.add(loginBtn, gbc); loginBtn.addActionListener(e - doLogin()); setContentPane(panel); } private void doLogin() { String user userField.getText().trim(); String pwd new String(pwdField.getPassword()).trim(); User u loginService.authenticate(user, pwd); if (u ! null) { new MainFrame(u).setVisible(true); dispose(); } else { JOptionPane.showMessageDialog(this, 用户名或密码错误, 登录失败, JOptionPane.ERROR_MESSAGE); } } }这段逻辑的关键点有三个第一登录窗口的默认关闭操作设置为 EXIT_ON_CLOSE因为登录是应用的唯一入口直接退出整个进程是合理的。第二密码框用 JPasswordFieldgetPassword() 返回的是 char[] 而不是 String这样做的目的是避免密码字符串长期留在堆内存里。第三登录成功后 new MainFrame(u).setVisible(true) 然后 dispose() 关闭登录窗口主控台拿到当前用户对象后续按钮的可用性按角色判断。LoginService 返回 User 而不是 boolean是为了把角色信息带到主窗口。如果只返回 booleanMainFrame 还得再查一次用户信息多一次数据库往返。角色字段可以用 int 或枚举比如 0 表示调度员、1 表示管理员、2 表示只读用户。界面层拿到角色之后决定哪些页签可见、哪些按钮可点。3.2 主控台窗口JTabbedPane 组装业务面板的骨架登录成功后进入主控台。主控台的主要职责是承载四个业务面板并维护顶部状态栏不在里面写具体增删改查逻辑只负责组装。public class MainFrame extends JFrame { private JLabel statusLabel; private JTabbedPane tabPane; public MainFrame(User user) { super(智慧公交管理系统 - user.getName()); setSize(1024, 700); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); // 顶部状态栏 JPanel topBar new JPanel(new BorderLayout()); topBar.setPreferredSize(new Dimension(0, 40)); statusLabel new JLabel(当前用户: user.getName() 角色: user.getRoleName()); topBar.add(statusLabel, BorderLayout.WEST); // 中间页签每个面板接收 user 用于内部权限控制 tabPane new JTabbedPane(); tabPane.addTab(线路管理, new RoutePanel(user)); tabPane.addTab(车辆管理, new BusPanel(user)); tabPane.addTab(调度监控, new DispatchPanel(user)); tabPane.addTab(基础报表, new ReportPanel(user)); // 底部提示栏 JLabel tipLabel new JLabel(就绪); add(topBar, BorderLayout.NORTH); add(tabPane, BorderLayout.CENTER); add(tipLabel, BorderLayout.SOUTH); } }组装方式看着简单但有两件事值得注意。第一每个业务面板都接收 user 参数权限控制的逻辑可以下沉到面板内部而不是集中在 MainFrame 里用 if 判断。比如只读用户RoutePanel 在构造时就把增删改按钮设为不可用代码职责清晰。第二底部 tipLabel 只是占位实际项目里我会封装一个 StatusBar 类带 setMessage(String msg) 方法供各面板调用这样底部提示栏不会和某个具体面板耦合。顶栏 JPanel 需要显式设置 preferredSize否则 BorderLayout.NORTH 区域的高度可能撑不开按钮和文字会挤在一起。setPreferredSize(new Dimension(0, 40)) 里的宽度 0 表示不限制宽度只限定高度。这是一个经常被忽视的小地方但不设置的话界面在不同系统上的表现会有差异。3.3 自定义 TableModelJTable 与业务数据的桥梁JTable 不直接接受 List 需要经过 TableModel。Swing 自带的 DefaultTableModel 只能存 Object 二维数组泛型和类型安全都缺失实际开发中更推荐继承 AbstractTableModel 自己实现。public class RouteTableModel extends AbstractTableModel { private ListRoute data new ArrayList(); private final String[] columns {线路编号, 线路名称, 起点, 终点, 首班时间, 末班时间, 状态}; public void setData(ListRoute newData) { data newData; fireTableDataChanged(); } Override public int getRowCount() { return data null ? 0 : data.size(); } Override public int getColumnCount() { return columns.length; } Override public String getColumnName(int column) { return columns[column]; } Override public Object getValueAt(int rowIndex, int columnIndex) { Route r data.get(rowIndex); switch (columnIndex) { case 0: return r.getLineNo(); case 1: return r.getName(); case 2: return r.getStartStation(); case 3: return r.getEndStation(); case 4: return r.getFirstTime(); case 5: return r.getLastTime(); case 6: return r.getStatus(); default: return ; } } }整个模型里最重要的方法是 fireTableDataChanged()。setData() 传入一个新的 List 之后调用它通知 JTable 重新拉取数据。不调用这个方法界面上永远显示旧数据很多人在这一步翻车——数据库里改了数据界面上毫无变化还以为是查询失败。为什么不用 DefaultTableModel因为它是基于 Object 二维数组和 Vector 的拿不到 Route 实体对象需要根据行号取线路 ID 时还得从单元格里拼字符串容易出错。自定义 RouteTableModel 可以加一个 getRouteAt(int row) 方法事件监听里直接取当前行对应的实体对象这才是可靠的做法。渲染器、数据、实体三者的类型安全都握在自己手里后面做状态颜色渲染时不用再做无谓的类型转换。4. 数据层与界面联动JDBC、异步刷新与状态渲染4.1 JDBC 与 DAO 层参数化 SQL 是底线智慧公交系统的数据模型通常包括线路表、车辆表、司机表、班次表和实时位置表。数据落在关系型数据库里最常见的是 MySQL。DAO 层的写法决定整个项目的数据访问稳定性。public class RouteDao { private DataSource dataSource; public RouteDao(DataSource dataSource) { this.dataSource dataSource; } public ListRoute listRoutes(String keyword) throws SQLException { String sql SELECT id, line_no, name, start_station, end_station, first_time, last_time, status FROM route WHERE name LIKE ? OR line_no LIKE ? ORDER BY line_no; ListRoute result new ArrayList(); try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { String like % keyword %; ps.setString(1, like); ps.setString(2, like); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Route r new Route(); r.setId(rs.getInt(id)); r.setLineNo(rs.getString(line_no)); r.setName(rs.getString(name)); r.setStartStation(rs.getString(start_station)); r.setEndStation(rs.getString(end_station)); r.setFirstTime(rs.getString(first_time)); r.setLastTime(rs.getString(last_time)); r.setStatus(rs.getInt(status)); result.add(r); } } } return result; } }使用 PreparedStatement 而不是拼接 SQL是这条代码的底线。站名、线路名可能包含引号直接拼接字符串不仅容易出错还会带来注入风险。所有按条件查询都应该参数化。listRoutes 方法里两个占位符都用了 LIKE 的模式串而不是直接拼接前后百分号这样写清晰且不会漏掉转义。try-with-resources 在这里承担了连接关闭的职责。Connection、PreparedStatement、ResultSet 三者都在 try 块里声明方法结束后自动关闭不会出现连接泄漏。连接泄漏是数据库连接池告警里最常见的现象解法没有捷径就是这种写法。数据源从外部传入而不是在 DAO 内部 new 一个 Connection是为了后续接连接池。常见做法是在启动类里用 HikariCP 初始化 DataSource再传给 service 层换数据库或调连接池参数时不用改 DAO。提示HikariCP 的 maximumPoolSize 不用设得很大内部工具 10 个连接足够设置过大会增加数据库端的无效连接开销。4.2 SwingWorker 刷新数据EDT 阻塞导致的界面假死Swing 的所有界面更新都必须在事件分发线程EDT上完成。如果直接在 ActionListener 里执行一个耗时两秒的数据库查询界面上所有按钮都会无响应这就是典型的“界面假死”。JButton refreshBtn new JButton(刷新线路); refreshBtn.addActionListener(e - { refreshBtn.setEnabled(false); new SwingWorkerListRoute, Void() { Override protected ListRoute doInBackground() throws Exception { return routeDao.listRoutes(keywordField.getText().trim()); } Override protected void done() { try { ListRoute routes get(); model.setData(routes); statusBar.setMessage(已加载 routes.size() 条线路); } catch (Exception ex) { statusBar.setMessage(刷新失败: ex.getMessage()); } finally { refreshBtn.setEnabled(true); } } }.execute(); });doInBackground() 里做的事情在 SwingWorker 的后台线程中执行不占用 EDTdone() 方法会自动切回 EDT 线程执行所以可以在里面安全调用 model.setData() 和 statusBar 更新。这个分工是 SwingWorker 的核心特征也是最容易用错的地方。两个常见误用需要特别注意。第一在 doInBackground() 里更新界面组件这不会自动切回 EDT仍然可能触发跨线程访问导致界面不刷新或偶发异常。第二刷新按钮没有禁用用户手快点了十下就会发起十个重复查询数据库压力瞬间上去界面响应更慢。示例里先 setEnabled(false)finally 里恢复这是最简单的防重复提交手段。4.3 TableCellRenderer 做车辆状态颜色标注调度监控页签需要一眼看出车辆处于“运营中”“停运”“维修”还是“事故”JTable 必须能根据行数据给单元格上色。DefaultTableCellRenderer 是这里最常用的工具。class StatusCellRenderer extends DefaultTableCellRenderer { Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); if (!isSelected) { Object raw table.getModel().getValueAt(row, 6); int status (raw instanceof Integer) ? (Integer) raw : -1; switch (status) { case 1: setForeground(new Color(0, 128, 0)); break; // 运营中 case 2: setForeground(new Color(200, 120, 0)); break; // 停运 case 3: setForeground(Color.RED); break; // 维修 default: setForeground(Color.GRAY); break; } } return this; } }使用方式是在对应列上挂载渲染器table.getColumnModel().getColumn(6).setCellRenderer(new StatusCellRenderer())。渲染器每画一个单元格都会被调用一次所以里面不应做耗时操作比如二次查询数据库这会导致滚动表格时明显卡顿。取值时要小心类型。这里从 table.getModel().getValueAt(row, 6) 拿到的可能是 Integer也可能是 String取决于 TableModel 第 6 列的实现。我在改造别人代码时多次遇到 ClassCastException解法是统一让 TableModel 的状态列返回 Integer并在渲染器里做 instanceof 防御。另一个细节是选中状态下不要覆盖前景色否则选中行高亮背景和自定义前景色叠加往往看不清文字所以代码里只在 !isSelected 时调整颜色。5. 避坑指南Swing 开发与智慧公交联调中五个翻车点5.1 刷新后界面不变TableModel 没通知现象数据库里新增了一条线路点了刷新按钮JTable 没有任何变化也没有报错。反复确认查询 SQL 没问题数据确实查出来了但表格就是不动。原因TableModel 的 setData() 里替换了 List但没有调用 fireTableDataChanged()。JTable 不知道数据变了自然不会重绘。另一种隐蔽的子原因是 setData() 里先 clear() 了旧列表再逐条 add()但通知写在了 clear() 之后、add() 之前数据还没加完就通知了界面。解决setData() 里赋值新 List 后必须调用 fireTableDataChanged()。排查时在通知方法前后加日志确认 JTable 的 repaint 有没有被触发。把 add 和更新通知放在一起保持“先改数据后发通知”的固定顺序。以后不管谁来改这个模型只要遵守这个约定就不会出现数据显示与库里不一致的情况。5.2 双击 jar 包没反应启动失败的黑匣子排查现象在 IDE 里点 Run 程序正常跑起来打成 jar 后双击没反应什么弹窗都没有。很多人第一反应是打包工具问题折腾半天下载各种封装工具其实问题根本不在打包这一步。原因常见原因有三个。第一本地编译用的 JDK 版本和运行机器的 JRE 版本不匹配JDK 17 编译的 class 放到 JRE 8 上直接报 UnsupportedClassVersionError。第二MANIFEST.MF 里没写 Main-Class 属性java -jar 不知道从哪个类启动。第三用了第三方依赖如 JDBC 驱动但 jar 包里没有把它们打进去运行时报 NoClassDefFoundError。解决先到命令行执行 java -jar your-app.jar黑匣子立刻打开报错信息会直接指出是版本问题还是缺少主类。然后用 maven-shade-plugin 或类似工具打 fat jar把依赖打进去。MANIFEST.MF 里的 Main-Class 必须写全限定名不带 .class 后缀。如果环境变量配置不当导致 java 命令本身启动失败先到命令行执行 java -version 确认当前默认 JDK 是哪一个再决定是改 PATH 还是改用绝对路径调用 java。5.3 中文乱码贯穿全流程三处编码缺一不可现象界面按钮、表格里的中文显示成问号控制台打印 SQL 也是乱的导出的报表更没法看。原因这不是单点问题而是三处编码不一致叠加的结果。第一源代码文件用 UTF-8 保存但 javac 按平台默认编码Windows 上是 GBK编译字符串字面量在 class 文件里就错了。第二数据库连接 URL 没有指定 characterEncoding写入和读取中文的编码不一致。第三运行环境的 file.encoding 和编译时不匹配部分字符在运行时被转换坏掉。解决统一三处编码。源码保存为 UTF-8在 Maven 的 pom.xml 里设置 project.build.sourceEncoding 为 UTF-8。数据库连接 URL 显式加上 useUnicodetrue 和 characterEncodingutf8以 MySQL 8 驱动为例。运行时若还在 IDE 里乱码给 JVM 加 -Dfile.encodingUTF-8 启动参数。三处全部统一之后乱码问题基本能根除不需要在代码里到处做字符串编码转换。5.4 关窗进程不退出线程和关闭流程的清理顺序现象点窗口右上角 X界面关了但 Java 进程还活着端口和数据库连接一直被占着。开发机上不明显部署到服务器上后重启应用时经常报端口被占用。原因窗口关闭不等于进程退出。JFrame.EXIT_ON_CLOSE 会退出整个进程但如果项目为了做“关闭前确认”改成了 DO_NOTHING_ON_CLOSE就要自己处理退出逻辑。除此之外调度监控里通常有定时刷新用的 Swing Timer或者轮询数据库的后台线程这些非守护线程没有停掉JVM 就不会退出。解决在 windowClosing 事件里按顺序清理先停掉所有 Swing Timer 和 SwingWorker再关闭数据源HikariCP 调 dataSource.close()最后调用 System.exit(0)。如果直接 System.exit(0) 而不关连接池未提交的事务会被强制中断可能产生脏数据。退出前还应该检查有没有未保存的编辑记录弹窗提示用户这个步骤加上以后误关窗口的损失能降不少。5.5 定时轮询错过发车状态刷新时机与业务事件不同步现象调度监控面板每分钟刷新一次车辆状态但屏幕上显示某辆车还在“停运”实际上它五分钟前已经发车了现场广播和界面信息对不上。原因固定间隔轮询是采样逻辑而公交业务有明确的发车时刻、到站时刻。用固定间隔去采样一个随时可能变化的状态必然会错过两次采样之间的状态切换。轮询改成 30 秒、10 秒也只是降低漏报概率不能根除。解决把数据驱动方式反过来让状态变化去通知界面刷新而不是让界面盲目轮询。具体做法是建一个内存事件队列车辆状态变更时写入数据库并在队列里发一条事件界面在 EDT 上监听队列收到事件才刷新对应行。如果现阶段不上队列至少让轮询间隔小于最小业务事件间隔的一半。比如最小发车间隔是 5 分钟轮询间隔就得小于 2.5 分钟否则一定出现丢状态。这个原则在排班系统的所有定时任务里都适用。6. 把“智慧”做实排班算法、站点自绘与验证方法6.1 一个够用的发车间隔计算逻辑排班算法的“智慧”不体现在复杂模型上而在于边界处理。核心逻辑是根据线路车辆数和单趟往返时长计算发车间隔public int calcInterval(int vehicleCount, int cycleMinutes) { if (vehicleCount 0) { throw new IllegalArgumentException(车辆数必须大于 0); } return Math.max(5, (int) Math.ceil(cycleMinutes / (double) vehicleCount)); }两个边界值得注意车辆数为 0 时直接抛业务异常而不是除零间隔不得低于 5 分钟的硬下限。调度面板每次修改车辆数或路况等级时重算这个值把结果显示在界面上。如果要按发车间隔在表格里排序用 TableRowSorter 配一个 Comparator 即可不必把排序逻辑写进 SQL。6.2 用 JPanel 自绘站线图站点地图如果去接在线地图 SDK依赖重且需要网络内部大屏反而不好维护。我建议在调度监控页签里用一个 JPanel 自绘站点画成圆点线路连成折线。Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 (Graphics2D) g; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); for (int i 0; i points.size() - 1; i) { Point p1 points.get(i), p2 points.get(i 1); g2.drawLine(p1.x, p1.y, p2.x, p2.y); } for (Point p : points) { g2.fillOval(p.x - 4, p.y - 4, 8, 8); } }站点坐标不是经纬度而是初始化时算好的相对百分比坐标存到 List 里。收到位置更新事件后调用 repaint() 触发重绘整个站点图就跟着动起来。反锯齿开关放在这里画出来的线比默认效果平滑很多。6.3 验证这套系统的三个技巧第一用 Swing Timer 模拟车辆位置数据每两秒生成一条位置记录喂给调度监控面板验证状态颜色切换、刷新是否及时这是最常用的回归手段不需要真实 GPS 数据也能跑完整套逻辑。第二盯着 EDT 占用查询一律在 SwingWorker 的 doInBackground 里做界面更新在 done 里做遵守这条界面假死问题基本绝缘。第三用连接池日志观察连接使用情况确认 DAO 的 try-with-resources 没有连接泄漏。做完整个系统后我的习惯是把数据刷新频率刻意调高让系统在高频更新下硬跑一段时间暴露渲染和线程问题。这套系统的质量其实全靠这种贴近真实压力的验证方式打磨出来而不是靠写完代码看一眼觉得没问题就算结束。希望帮到你。本文还有配套的精品资源点击获取