
简介这份资源是基于Java实现的公交车实时监控系统设计源码面向具备一定Java基础、希望学习智能交通或微服务架构的开发者与课程设计者可用于理解实时监控系统的后端实现思路。项目采用Spring Boot构建微服务通过RESTful API与笑园实时公交API对接获取车辆位置、运行状态与轨迹等数据并借助XML、YAML与SQL文件完成参数配置和数据持久化。压缩包共43个文件以37个Java源码为核心辅以2个XML配置、1个YAML、1个SQL及说明文件整体约61KB结构紧凑便于阅读。目前已有279人学习下载。读者可从中获取完整的后端模块划分、API数据接入方式、配置文件组织与数据库脚本适合作为毕业设计、课程作业或微服务入门项目的参考帮助快速搭建可扩展的公交实时监控后端框架。1. 公交车实时监控系统为什么 Java 技术栈是公交场景的稳妥选择早高峰的公交调度室里调度员盯着屏幕上一个个移动的车辆图标需要知道每辆车现在在哪、车上挤不挤、下一站还有几分钟到。这套「基于 Java 实现的公交车实时监控系统」本质上就是把车载终端上报的 GPS 坐标、客流数据、到离站事件经过服务端汇聚、计算、存储再实时推送到调度大屏和乘客端。它解决的是「车在哪、多久到、挤不挤」这三个最朴素也最要命的问题适合做课程设计的学生、做智慧交通原型的开发者以及需要快速搭一套可演示系统的 Java 工程师。选 Java 不是因为它新而是因为公交场景对稳定性、并发连接数、生态成熟度的要求恰好落在 Java 的舒适区里——一台车一个长连接几百上千台车同时在线Netty 加 Spring Boot 的组合能扛住出问题也查得到。2. 系统骨架怎么搭从车载上报到调度大屏的数据链路2.1 先想清楚数据从哪来、到哪去公交实时监控系统的数据链路其实不复杂但每一段都有坑。车载终端常见的是带 4G 模组的 GPS 盒子每隔几秒上报一次位置格式可能是 JT/T 808 国标协议也可能是厂商自定义的 JSON。服务端收到后要做三件事解析、落库、推送。解析是把原始报文变成结构化对象落库是把轨迹写进时序库或关系库推送是把最新状态广播给所有订阅了这条线路的客户端。我一般会把链路拆成四层接入层负责维持长连接和协议解析业务层负责计算到站预测和拥挤度存储层负责轨迹和状态推送层负责 WebSocket 广播。这样拆的好处是接入层可以独立扩容推送层挂了不影响数据落库排查问题时能快速定位是哪一层出的毛病。提示如果只是做课程设计或演示接入层可以直接用 Spring Boot 内嵌的 WebSocket 服务端不必一上来就上 Netty。等连接数超过两千再考虑换否则是给自己找麻烦。2.2 用 Spring Boot 搭最小可运行骨架下面这段代码是一个最小的车辆状态接收接口用 Spring Boot 的RestController接收车载终端上报的 JSON解析后放进内存缓存并广播。它不完整但能跑通「上报→接收→广播」这条最短路径。RestController RequestMapping(/api/vehicle) public class VehicleReportController { // 用 ConcurrentHashMap 暂存最新状态生产环境应换成 Redis private final MapString, VehicleStatus latestStatus new ConcurrentHashMap(); private final SimpMessagingTemplate messagingTemplate; public VehicleReportController(SimpMessagingTemplate messagingTemplate) { this.messagingTemplate messagingTemplate; } PostMapping(/report) public ResponseEntityString report(RequestBody VehicleReport report) { // 1. 基础校验车牌和坐标不能为空 if (report.getPlateNo() null || report.getLat() null) { return ResponseEntity.badRequest().body(missing plateNo or lat); } // 2. 组装状态对象 VehicleStatus status new VehicleStatus(); status.setPlateNo(report.getPlateNo()); status.setLat(report.getLat()); status.setLng(report.getLng()); status.setTimestamp(System.currentTimeMillis()); status.setLineId(report.getLineId()); // 3. 更新缓存 latestStatus.put(report.getPlateNo(), status); // 4. 广播到订阅了该线路的客户端 messagingTemplate.convertAndSend(/topic/line/ report.getLineId(), status); return ResponseEntity.ok(ok); } }逻辑说明report方法先做空值校验避免脏数据污染缓存然后用ConcurrentHashMap按车牌号覆盖最新状态这样查询某辆车时是 O(1)最后通过SimpMessagingTemplate把状态推到/topic/line/{lineId}这个 STOMP 目的地。参数方面plateNo是车辆唯一标识lineId决定广播频道lat/lng用 WGS84 坐标系如果终端给的是 GCJ02 需要先转换否则地图上会偏几百米。2.3 到站预测的简化算法与参数到站预测不需要上深度学习用「剩余距离 ÷ 近期平均速度」就能给出可接受的估计。关键参数有三个滑动窗口大小、速度下限、停站补偿。滑动窗口取最近 5 次上报的平均速度窗口太小会抖太大反应迟钝速度下限设 5 km/h避免堵车时算出无穷大的到站时间停站补偿是在车辆进站前 100 米内把预测时间固定为 0因为 GPS 在站台附近漂移严重。public long predictArrival(double remainDistance, DequeDouble speedWindow) { // 速度窗口为空时给一个保守值 if (speedWindow.isEmpty()) return -1; double avgSpeed speedWindow.stream() .mapToDouble(Double::doubleValue).average().orElse(0); // 低于 5 km/h 按 5 算避免除零和无穷大 avgSpeed Math.max(avgSpeed, 5.0); // remainDistance 单位米avgSpeed 单位 km/h换算成秒 return (long) (remainDistance / (avgSpeed * 1000 / 3600)); }这段代码里speedWindow由调用方维护每次上报后addLast新速度、removeFirst旧速度。remainDistance需要根据车辆当前位置和线路站点序列计算常见做法是把线路抽象成一条折线求车辆到下一站点的沿线路距离。注意这个算法在车辆掉头、绕行时会有偏差实际项目里会加一个「方向角校验」方向角突变超过 90 度就重置速度窗口。3. 并发连接与实时推送几百辆车同时在线怎么不崩3.1 长连接选型WebSocket 还是 MQTT公交场景里车载终端和服务端之间是长连接调度大屏和服务端之间也是长连接。常见做法是终端侧用 MQTT省流量、支持 QoS服务端内部用 WebSocket 推给浏览器。如果全用 WebSocket 也能跑但终端侧要自己实现心跳和重连工作量不小。我一般会这样分终端到接入层用 MQTT over TCP接入层到前端用 STOMP over WebSocket。这样终端侧可以用现成的 MQTT 客户端库前端侧直接用 SockJS 加 STOMP两边都省事。连接数估算一个中等城市 50 条线路、每条 20 辆车就是 1000 个终端连接。每个连接维持心跳的流量很小瓶颈不在带宽而在服务端的文件描述符和线程模型。用 Netty 的 NIO 模型1000 连接对单机来说很轻松用传统 BIO 就会卡在 1000 线程上。3.2 用 Redis 做状态共享和去重单机内存缓存扛不住多实例部署一旦接入层扩到两个节点A 节点收到的车辆状态 B 节点不知道调度大屏就连不上。解决办法是把最新状态放 Redis用 Hash 结构按线路存key 是vehicle:status:{lineId}field 是车牌号value 是序列化后的状态对象。// 写入最新状态过期时间 5 分钟避免僵尸车辆 public void updateStatus(VehicleStatus status) { String key vehicle:status: status.getLineId(); redisTemplate.opsForHash().put(key, status.getPlateNo(), status); redisTemplate.expire(key, 5, TimeUnit.MINUTES); } // 查询整条线路的车辆状态 public ListVehicleStatus getLineStatus(String lineId) { String key vehicle:status: lineId; return redisTemplate.opsForHash().values(key).stream() .map(o - (VehicleStatus) o) .collect(Collectors.toList()); }参数说明过期时间设 5 分钟是因为公交上报间隔通常 10 到 30 秒超过 5 分钟没上报基本可以判定终端离线或故障自动清理比手动踢更省心。Hash 的 field 用车牌号而不是设备 ID是因为调度员看的是车牌查询时不用再做映射。3.3 推送风暴的削峰处理早高峰时所有车辆都在动如果每辆车每次上报都触发一次广播前端会收到大量重复消息。我踩过的坑是1000 辆车每 10 秒上报一次就是每秒 100 条广播浏览器处理不过来会卡顿。解决办法是在推送层加一个「合并窗口」每 2 秒把同一线路的变更合并成一条批量消息再推。// 用 ScheduledExecutorService 每 2 秒批量推送一次 Scheduled(fixedRate 2000) public void flushBroadcast() { for (String lineId : dirtyLines) { ListVehicleStatus batch vehicleService.getLineStatus(lineId); messagingTemplate.convertAndSend(/topic/line/ lineId, batch); } dirtyLines.clear(); }dirtyLines是一个ConcurrentHashMap.newKeySet()车辆上报时往里加线路 ID。这样前端每 2 秒收到一次全量线路状态渲染压力小很多。代价是实时性从「秒级」降到「2 秒级」对公交场景完全够用乘客不会在意 2 秒的延迟。4. 避坑与排查公交监控系统上线后最容易翻车的五件事4.1 GPS 漂移导致车辆「瞬移」现象调度大屏上车辆图标突然跳到几百米外过几秒又跳回来。原因城市峡谷或高架桥下 GPS 信号反射定位精度骤降。解决在服务端加一个「速度合理性校验」如果两次上报之间算出的速度超过 120 km/h就丢弃这次坐标用上一次的位置加方向角推算一个临时位置。4.2 时间戳不一致导致轨迹乱序现象轨迹回放时线条来回折返明显不是正常行驶路线。原因车载终端本地时钟不准或者上报时用了设备时间而不是服务端接收时间。解决服务端统一用System.currentTimeMillis()打时间戳终端时间只作为参考字段存下来排序和回放一律用服务端时间。4.3 内存缓存无限增长现象服务跑两天后 OOM日志里全是 GC 超时。原因ConcurrentHashMap只增不减离线车辆的记录永远留在内存里。解决换成 Redis 加过期时间或者用Caffeine做本地缓存并设置expireAfterWrite。我一般直接上 Redis省得本地缓存和分布式状态打架。4.4 WebSocket 断连后前端不重连现象调度大屏开着开着就不更新了刷新页面又好了。原因前端没有实现自动重连网络抖动或服务端重启后连接就断了。解决用 SockJS 加 STOMP 时在stompClient上监听onWebSocketClose事件触发一个带退避的重连逻辑重连间隔从 1 秒逐步加到 30 秒。4.5 线路切换时收到旧线路消息现象调度员从 1 路切到 2 路屏幕上还偶尔冒出 1 路的车辆。原因前端订阅了新线路但没有取消旧线路的订阅或者服务端广播时没有按线路隔离。解决前端切换线路时先unsubscribe旧目的地再subscribe新目的地服务端广播前校验lineId确保只推给订阅了该线路的会话。5. 让系统更耐用的三个进阶技巧5.1 用历史轨迹做到站预测的校准前面那个「剩余距离 ÷ 平均速度」的算法在畅通路段误差能控制在 1 分钟内但遇到红灯或堵车就会偏。我后来加了一个校准把每条线路每个站间段的历史通行时间存下来预测时用「实时速度算出的时间」和「历史中位数时间」做加权平均权重按当前路况动态调。畅通时信实时拥堵时信历史。这个改动不大但到站预测的准头明显好了。路况实时权重历史权重说明畅通0.80.2实时速度可信缓行0.50.5两者折中拥堵0.20.8历史更稳5.2 用压力测试提前暴露连接瓶颈上线前我用 JMeter 模拟了 2000 个 MQTT 连接同时上报发现接入层在 1500 连接时开始丢心跳。排查下来是 Netty 的SO_BACKLOG默认值太小改成 1024 后稳定在 2000 连接无丢失。这个测试花了我半天但比上线后半夜被叫起来强。建议在本地用 Docker 起多个 MQTT 客户端容器逐步加压观察服务端的文件描述符和内存曲线。5.3 日志里一定要打车牌和线路最后说一个血泪经验早期日志只打了设备 ID排查问题时得先查数据库把设备 ID 翻译成车牌再查线路一来一回十分钟没了。后来我在日志的 MDC 里直接塞了plateNo和lineId出问题时grep一下车牌就能看到这辆车的完整上报、计算、推送链路。这个习惯让我在后来的每次故障排查里至少省了一半时间。系统能不能长期跑稳很多时候不取决于架构多漂亮而取决于出问题时你能不能五分钟内定位到那一辆车、那一条线路。希望帮到你。本文还有配套的精品资源点击获取