Java Swing智慧公交管理系统课程设计:GUI架构与JDBC实战
简介这是一套面向Java初学者与课程设计需求的智慧公交管理系统完整源码采用Java GUI结合MySQL开发适合作为数据库大作业、毕业设计或GUI编程练手项目。系统围绕公交公司日常运营场景提供车辆信息管理、员工信息管理、线路与站点信息管理、排班方案管理及密码修改等功能并区分管理员、调度员、员工三种角色登录权限与数据表相互独立。资源包共2个文件包含1个zip源码压缩包与1个busmanage.sql数据库脚本整体约10.72MB源码基于JDK1.8与mysql-connector-java-8.0.29可直接在IDEA中导入运行。数据库涵盖员工表、登录表、排班表、站点表、线路表、车辆表及线路-站点关联表结构完整便于理解多表关联与业务逻辑。目前已有1136人学习下载适合需要快速获取可运行项目、参考GUI界面搭建与数据库设计思路的读者。1. 从课程设计到可交付系统智慧公交管理系统的边界在哪很多 Java 课程设计题目里“智慧公交管理系统”看起来像是一个放大版的增删改查练习但真正动手做的时候问题往往不在业务逻辑而在 GUI 这一层。Swing 或 JavaFX 的组件模型、事件分发线程、表格与表单的数据绑定这些才是让项目从“能跑”变成“能演示”的关键。这个标题对应的场景很具体用 Java 的桌面 GUI 技术把线路、车辆、站点、调度和查询这几块业务串成一个可操作的窗口程序。它适合两类人一类是正在做课程设计、需要一套能讲清楚架构和实现路径的参考方案另一类是想借这个题目把 Java 基础、面向对象编程和事件驱动模型重新过一遍的开发者。核心难点不是数据库连不连得上而是 GUI 线程模型和业务逻辑怎么解耦否则界面卡死、数据错乱会接踵而来。2. 智慧公交管理系统的数据模型与 GUI 技术选型2.1 为什么用 Swing 而不是 JavaFX 做课程设计在 Java GUI 这个热搜词下面Swing 和 JavaFX 的争论一直没停过。对于智慧公交管理系统这种课程设计级别的项目我一般会选 Swing原因很实际JDK 自带不需要额外引入模块化依赖IDEA 里新建项目就能直接写网上关于 JTable、JTree、布局管理器的资料密度远高于 JavaFX答辩时老师机器上大概率只有 JDKSwing 的兼容性风险最低。JavaFX 在样式和动画上确实更现代但一旦涉及 FXML 加载和模块化配置对新手来说排错成本会陡增。选型不是比谁先进而是比谁能在有限时间里把线路管理、车辆调度、实时查询这几个模块稳定跑起来。2.2 公交业务的核心实体与表结构设计智慧公交管理系统的最小闭环通常围绕五张表展开线路表、站点表、车辆表、司机表和调度记录表。线路和站点是多对多关系需要一张线路站点关联表来维护顺序车辆和线路是多对一调度记录则把车辆、线路、司机和时间戳绑在一起。下面这段 SQL 是常见的建表方式字段命名尽量贴近业务避免用缩写让后期维护的人猜。-- 线路表一条公交线路的基本信息 CREATE TABLE bus_line ( line_id INT PRIMARY KEY AUTO_INCREMENT, line_name VARCHAR(50) NOT NULL, -- 如“快线1路” start_station VARCHAR(50), end_station VARCHAR(50), first_time TIME, -- 首班时间 last_time TIME, -- 末班时间 status TINYINT DEFAULT 1 -- 1运营 0停运 ); -- 车辆表车辆与线路的归属关系 CREATE TABLE bus_vehicle ( vehicle_id INT PRIMARY KEY AUTO_INCREMENT, plate_no VARCHAR(20) UNIQUE NOT NULL, line_id INT, capacity INT DEFAULT 40, driver_id INT, FOREIGN KEY (line_id) REFERENCES bus_line(line_id) ); -- 调度记录表每次发车或调整都留痕 CREATE TABLE dispatch_record ( record_id INT PRIMARY KEY AUTO_INCREMENT, vehicle_id INT, line_id INT, dispatch_time DATETIME, direction TINYINT, -- 0上行 1下行 remark VARCHAR(200) );这段 SQL 的逻辑说明bus_line是主表bus_vehicle通过line_id外键挂靠dispatch_record记录动态行为。参数上status用 TINYINT 而不是布尔是为了以后扩展“临时调整”“停运检修”等状态。direction用 0 和 1 表示上下行比存字符串更省空间也方便在 GUI 里用下拉框映射。注意外键约束在课程设计里可以加但如果用 H2 或 SQLite 做演示记得确认数据库是否默认开启外键检查。2.3 用 JDBC 封装一个可复用的 Dao 基类GUI 层不应该直接写 SQL否则改一个字段要翻遍所有按钮的事件代码。常见做法是抽一个BaseDao把连接获取、PreparedStatement 设置参数、结果集映射做成模板方法。下面是一个精简版用try-with-resources保证连接释放。public abstract class BaseDao { protected Connection getConn() throws SQLException { // 实际项目中建议用连接池课程设计用 DriverManager 即可 return DriverManager.getConnection( jdbc:mysql://localhost:3306/bus_system?useSSLfalseserverTimezoneUTC, root, 123456); } protected int update(String sql, Object... params) { try (Connection conn getConn(); PreparedStatement ps conn.prepareStatement(sql)) { for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); // 参数从1开始 } return ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); return -1; } } protected T ListT query(String sql, RowMapperT mapper, Object... params) { ListT list new ArrayList(); try (Connection conn getConn(); PreparedStatement ps conn.prepareStatement(sql)) { for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } try (ResultSet rs ps.executeQuery()) { while (rs.next()) { list.add(mapper.map(rs)); } } } catch (SQLException e) { e.printStackTrace(); } return list; } }逻辑说明update用于增删改返回受影响行数query接收一个RowMapper函数式接口把 ResultSet 的一行转成实体对象。参数说明params是可变参数顺序对应 SQL 里的问号占位符从 1 开始设置。注意serverTimezoneUTC这个参数在 MySQL 8 之后经常需要否则会报时区错误。如果换成 SQLite连接串和驱动类名要相应调整但模板方法的思路不变。3. 用 Swing 搭建主界面与线路管理模块3.1 主窗口布局BorderLayout 加 JTabbedPane 的分页策略智慧公交管理系统的功能模块多硬塞在一个面板里会显得很乱。我一般用JFrame做顶层容器BorderLayout放顶部工具栏和底部状态栏中间放JTabbedPane每个选项卡对应一个业务模块线路管理、车辆管理、调度记录、查询统计。这样切换模块时不需要重新创建窗口状态也容易保持。下面是最小可运行的主窗口骨架。public class MainFrame extends JFrame { public MainFrame() { setTitle(智慧公交管理系统); setSize(1000, 680); setDefaultCloseOperation(EXIT_ON_CLOSE); setLocationRelativeTo(null); // 居中显示 // 顶部工具栏 JToolBar toolBar new JToolBar(); toolBar.setFloatable(false); toolBar.add(new JButton(刷新)); toolBar.add(new JButton(导出)); // 中间选项卡 JTabbedPane tabs new JTabbedPane(); tabs.addTab(线路管理, new LinePanel()); tabs.addTab(车辆管理, new VehiclePanel()); tabs.addTab(调度记录, new DispatchPanel()); tabs.addTab(查询统计, new QueryPanel()); // 底部状态栏 JLabel status new JLabel(就绪); add(toolBar, BorderLayout.NORTH); add(tabs, BorderLayout.CENTER); add(status, BorderLayout.SOUTH); } public static void main(String[] args) { // 所有 Swing 组件必须在事件分发线程中创建 SwingUtilities.invokeLater(() - new MainFrame().setVisible(true)); } }逻辑说明SwingUtilities.invokeLater是必须的Swing 不是线程安全的所有界面更新都要放在 EDT 上执行。参数说明setLocationRelativeTo(null)让窗口在屏幕居中setFloatable(false)防止工具栏被拖拽出奇怪的位置。注意如果某个面板初始化很慢不要在构造函数里做数据库查询否则主窗口会迟迟不显示正确做法是在面板显示后再异步加载数据。3.2 线路管理面板JTable 与 TableModel 的数据绑定线路管理是使用频率最高的模块核心是一个JTable加几个操作按钮。直接用DefaultTableModel虽然快但字段一多就容易乱我一般会自定义一个LineTableModel继承AbstractTableModel把列名和实体列表管起来。public class LineTableModel extends AbstractTableModel { private final String[] columns {线路ID, 线路名称, 起点, 终点, 首班, 末班, 状态}; private ListBusLine data new ArrayList(); public void setData(ListBusLine data) { this.data data; fireTableDataChanged(); // 通知 JTable 刷新 } Override public int getRowCount() { return data.size(); } Override public int getColumnCount() { return columns.length; } Override public String getColumnName(int col) { return columns[col]; } Override public Object getValueAt(int row, int col) { BusLine line data.get(row); switch (col) { case 0: return line.getLineId(); case 1: return line.getLineName(); case 2: return line.getStartStation(); case 3: return line.getEndStation(); case 4: return line.getFirstTime(); case 5: return line.getLastTime(); case 6: return line.getStatus() 1 ? 运营 : 停运; default: return null; } } }逻辑说明fireTableDataChanged是通知视图刷新的关键漏掉它界面就不会更新。参数说明getValueAt里把状态码转成中文避免在界面上直接显示 0 和 1。注意如果数据量超过几千行JTable的渲染会变慢这时要考虑分页查询或者用RowSorter做排序过滤而不是一次性全查出来。3.3 增删改按钮的事件处理与 EDT 线程规则按钮的ActionListener里不要直接做耗时操作。比如“保存”按钮如果数据库响应慢界面会假死。常见做法是把数据库操作放到SwingWorker的doInBackground里完成后在done方法里更新表格。saveBtn.addActionListener(e - { BusLine line collectFormData(); // 从输入框收集数据 new SwingWorkerBoolean, Void() { Override protected Boolean doInBackground() { return lineDao.insert(line) 0; // 后台线程执行 } Override protected void done() { try { if (get()) { statusLabel.setText(保存成功); refreshTable(); // 回到 EDT 刷新界面 } } catch (Exception ex) { statusLabel.setText(保存失败 ex.getMessage()); } } }.execute(); });逻辑说明SwingWorker把耗时逻辑和界面更新分开doInBackground在后台线程跑done自动回到 EDT。参数说明get()会阻塞直到后台任务完成但因为它是在done里调用此时任务已经结束不会卡界面。注意不要在doInBackground里碰任何 Swing 组件否则会抛出难以排查的线程异常。4. 调度记录与查询统计的落地细节4.1 调度记录表的时间字段处理与 JSpinner 用法调度记录里最麻烦的是时间输入。用文本框让用户手敲yyyy-MM-dd HH:mm:ss很容易格式出错我一般用JSpinner配合SpinnerDateModel把格式固定下来。SpinnerDateModel dateModel new SpinnerDateModel(); JSpinner timeSpinner new JSpinner(dateModel); JSpinner.DateEditor editor new JSpinner.DateEditor(timeSpinner, yyyy-MM-dd HH:mm); timeSpinner.setEditor(editor); // 读取时转成 java.sql.Timestamp Date value (Date) timeSpinner.getValue(); Timestamp ts new Timestamp(value.getTime());逻辑说明SpinnerDateModel负责日期滚动逻辑DateEditor负责显示格式。参数说明格式串yyyy-MM-dd HH:mm可以根据业务精度调整如果只需要日期就去掉HH:mm。注意java.util.Date和java.sql.Timestamp之间的转换要用getTime()取毫秒数直接强转在某些 JDK 版本上会出问题。4.2 多条件查询的 SQL 拼接与防注入查询统计模块通常支持按线路、时间段、方向组合筛选。拼接 SQL 时不要用字符串直接拼用户输入否则会有注入风险。正确做法是用StringBuilder拼条件片段参数仍然走PreparedStatement的占位符。public ListDispatchRecord search(Integer lineId, Timestamp from, Timestamp to) { StringBuilder sql new StringBuilder( SELECT * FROM dispatch_record WHERE 11); ListObject params new ArrayList(); if (lineId ! null) { sql.append( AND line_id ?); params.add(lineId); } if (from ! null) { sql.append( AND dispatch_time ?); params.add(from); } if (to ! null) { sql.append( AND dispatch_time ?); params.add(to); } sql.append( ORDER BY dispatch_time DESC); return query(sql.toString(), rs - { DispatchRecord r new DispatchRecord(); r.setRecordId(rs.getInt(record_id)); r.setVehicleId(rs.getInt(vehicle_id)); r.setDispatchTime(rs.getTimestamp(dispatch_time)); return r; }, params.toArray()); }逻辑说明WHERE 11是为了让后续的AND拼接不用判断是否是第一个条件。参数说明params列表和 SQL 里的问号顺序一一对应最后转成数组传给query。注意ORDER BY放在条件之后不要拼在循环里。如果查询结果要导出 Excel可以在query返回后统一处理而不是在 SQL 里做格式化。4.3 用 JFreeChart 做线路客流统计图查询统计如果只有表格会显得单薄加一个柱状图或折线图能明显提升演示效果。JFreeChart 是 Swing 生态里最常用的图表库和ChartPanel配合可以直接嵌进选项卡。DefaultCategoryDataset dataset new DefaultCategoryDataset(); for (LineStat stat : statList) { dataset.addValue(stat.getPassengerCount(), 客流量, stat.getLineName()); } JFreeChart chart ChartFactory.createBarChart( 各线路客流量统计, 线路, 人次, dataset); ChartPanel chartPanel new ChartPanel(chart); statTab.add(chartPanel, BorderLayout.CENTER);逻辑说明DefaultCategoryDataset是类别数据集适合柱状图和折线图。参数说明addValue的三个参数分别是数值、系列名和类别名。注意 JFreeChart 需要额外引入 jar 包如果项目用 Maven在pom.xml里加对应依赖即可如果手动管理 jar记得把jcommon一起放进 classpath否则会报NoClassDefFoundError。5. 打包发布与几个容易翻车的细节5.1 用 Maven 打可执行 jar 并处理资源路径课程设计最后要交一个能双击运行的 jar用 Maven 的maven-jar-plugin配置Main-Class是最省事的做法。但要注意图片、图标、SQL 脚本这些资源文件不能再用new File(src/...)去读打包后路径会变。plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-jar-plugin/artifactId configuration archive manifest mainClasscom.bus.MainFrame/mainClass /manifest /archive /configuration /plugin逻辑说明这段配置告诉 Maven 在MANIFEST.MF里写入入口类。参数说明mainClass要写全限定名。资源读取要改成getClass().getResourceAsStream(/icons/bus.png)用流的方式加载这样无论从 IDE 还是从 jar 里运行都能找到文件。注意如果用了第三方依赖普通 jar 插件不会把依赖打进去需要maven-shade-plugin或maven-assembly-plugin做 fat jar。5.2 数据库连接失败时的排查顺序演示时最常见的翻车是数据库连不上。排查顺序我一般按这个来先确认 MySQL 服务是否启动用mysql -u root -p能登进去再看连接串里的端口、库名、时区参数是否和实际一致然后检查驱动 jar 是否在 classpath 里MySQL 8 的驱动类是com.mysql.cj.jdbc.Driver不是旧版的com.mysql.jdbc.Driver最后看账号权限和防火墙。如果报Public Key Retrieval is not allowed在连接串后面加allowPublicKeyRetrievaltrue。这些细节在答辩现场比业务逻辑更容易出问题提前把连接参数写进配置文件而不是硬编码在 Java 里改起来会快很多。5.3 界面卡顿与内存泄漏的两个检查点Swing 程序跑久了变卡通常有两个原因。一是事件监听器重复添加比如每次刷新表格都addActionListener导致一个按钮触发多次数据库操作。检查方法是把监听器注册放在构造函数里只执行一次。二是JTable的TableModel持有大量不再使用的对象fireTableDataChanged只是通知刷新旧数据如果还被别的集合引用就不会被回收。可以在setData里先把旧列表清空或者用弱引用包装。另外SwingWorker如果频繁创建而不复用线程池开销也会累积查询统计这种高频操作可以考虑用一个固定的后台线程池来跑任务。本文还有配套的精品资源点击获取