news 2026/9/10 11:05:24

SpringBoot物联网数据采集系统:生产级设备接入与协议解析实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot物联网数据采集系统:生产级设备接入与协议解析实践

简介:本资源是一套基于SpringBoot框架开发的物联网数据采集系统服务器端完整源码,面向Java后端开发者及物联网平台学习者,解决多设备接入、高并发数据写入、分布式会话管理与缓存优化等典型IoT后端工程问题。压缩包共94个文件,含48个核心Java业务与配置类(涵盖Gateway、Sensor、Data采集及Redis集成模块)、25个Thymeleaf模板HTML页面、8个XML配置与Mapper定义、5个前端交互JS脚本,以及application.yml、建库SQL和README说明文档,整体仅644KB,轻量易部署。已有463人学习下载,代码结构清晰,采用SpringBoot+MyBatis+Redis技术栈,内置Tomcat集群支持、Nginx反向代理测试API、异步数据落库线程池及分布式Session方案,可直接用于课程设计、毕业项目或中小规模IoT平台原型开发。

1. 这不是个“Hello World”项目,而是一套能扛住产线压力的真实数据管道

我搭过太多SpringBoot项目,从后台管理到电商秒杀,但真正让我在凌晨三点被电话叫醒、盯着监控面板反复刷新的,永远是物联网数据采集系统。它不像Web服务那样有明确的用户点击路径,也不像定时任务那样节奏可控——它的数据流是持续的、不可预测的、带着工业现场真实毛刺的。标题里那个“基于SpringBoot框架搭建的物联网数据采集系统服务器端(源码)”,听起来平平无奇,可拆开来看,每个词都踩在工程落地的刀刃上:SpringBoot不是拿来写demo的,是选型时权衡了启动速度、生态成熟度和运维友好性后的结果;物联网意味着你要直面设备协议五花八门、网络环境千奇百怪、数据格式杂乱无章;数据采集不是简单地接收HTTP POST,而是要处理心跳保活、断线重连、数据校验、时序对齐、批量缓冲;服务器端三个字背后是Linux系统调优、JVM参数打磨、连接数压测、日志分级归档;最后那个括号里的源码,不是GitHub上clone下来就能跑通的玩具,而是包含了设备接入层抽象、协议解析器插件化设计、采集任务动态调度、异常数据熔断降级等一整套生产级逻辑的完整实现。

这个系统的核心价值,不在于它用了多少高大上的技术名词,而在于它把“让成百上千台分散在工厂车间、农田大棚、城市管网里的设备,把原始数据稳稳当当地送进数据库,并且在数据丢了、设备哑了、网络抖了的时候,你还能第一时间知道问题在哪、影响多大、怎么恢复”。它适合三类人:一是刚从学校出来、手握Java基础但没碰过真实设备对接的新人,想看看工业场景下SpringBoot怎么“脱掉西装穿上工装”;二是正在做MES/SCADA系统集成的工程师,需要一套可嵌入、可扩展的采集底座;三是技术负责人,需要评估一个轻量级、可维护、不绑架业务逻辑的数据接入方案。它不承诺“一键部署全自动”,但保证每一行关键代码都有其存在理由,每一个配置项背后都有一次产线故障的教训。

2. 整体架构设计:为什么不用Netty直接写?为什么非得用SpringBoot?

2.1 拒绝“裸写Netty”的底层诱惑,选择SpringBoot的务实逻辑

很多人看到“物联网数据采集”,第一反应是“得用Netty啊,高性能、异步非阻塞、直接操作Socket”。我试过,也带团队用Netty从零撸过一套,结果呢?三个月后,当新同事接手维护时,光是理解那套自定义的连接池管理、心跳超时状态机、以及混合了Modbus TCP和MQTT的编解码器,就花了整整两周。而SpringBoot带来的核心价值,不是“写得快”,而是“看得懂、改得动、接得住”。

  • 生命周期管理:设备连接不是静态的。一台PLC可能上午在线,下午因断电离线,晚上又自动恢复。SpringBoot的@EventListener配合ApplicationRunner,能让你在应用启动时加载历史设备配置、在关闭时优雅释放所有TCP连接、在运行时监听ContextRefreshedEvent动态刷新采集策略。这种与Spring容器深度绑定的能力,Netty原生API根本没法比。

  • 配置驱动开发:产线环境千差万别。A车间用RS485转以太网模块,B车间直接走WiFi,C车间甚至还在用GPRS。如果硬编码协议类型、超时时间、重试次数,每次换车间就得改代码、打包、重启。SpringBoot的@ConfigurationProperties绑定YAML配置,让application.yml里一行protocol: modbus-tcp就能切换整个采集链路的行为,运维人员改个配置文件就能上线,这才是真正的DevOps友好。

  • 生态即生产力:一个采集系统,90%的代码不在“收数据”上,而在“收完之后做什么”。比如,收到温度传感器数据,要存进InfluxDB做时序分析;同时触发告警规则,发邮件给值班员;还要把原始数据压缩归档到MinIO;最后生成日报PDF报表。SpringBoot生态里,spring-boot-starter-data-influxdbspring-boot-starter-mailspring-boot-starter-webspring-boot-starter-quartz全都是开箱即用的轮子。你不用自己造连接池、自己写邮件发送器、自己实现定时任务调度器——这些组件经过千万次生产验证,稳定性和性能远超个人实现。

提示:这不是说Netty不好,而是说在物联网后端这个场景里,“快速交付、稳定运维、团队协作”的权重,远高于“理论峰值QPS”。SpringBoot不是性能瓶颈,真正的瓶颈永远在设备端协议解析耗时、数据库写入吞吐、网络带宽限制上。把精力花在优化这些地方,比纠结于是否用Netty省下那几毫秒更有意义。

2.2 分层解耦:设备接入层、协议解析层、业务处理层的边界在哪里?

这套源码最值得细看的,是它对“采集”这件事的分层抽象。很多项目把所有逻辑塞进一个DeviceController里,导致@PostMapping("/data")方法长达300行,里面混着JSON解析、CRC校验、数据库插入、告警判断……这根本没法测试,更别说扩展了。本系统强制划清三条线:

  • 设备接入层(Transport Layer):只负责“建立连接、维持心跳、收发原始字节流”。它不关心字节流里是什么协议,只提供统一的DeviceChannel接口,背后可以是TcpDeviceChannel(Modbus TCP)、MqttDeviceChannel(MQTT)、HttpDeviceChannel(HTTP REST)。这一层的职责极其单纯:确保数据包不丢、不乱序、有超时。所有网络异常(如IOExceptionSocketTimeoutException)都在这一层捕获并转换为标准的DeviceConnectionException,向上抛出。

  • 协议解析层(Protocol Layer):拿到原始字节流后,才开始“读懂设备语言”。这里采用SPI机制,每种协议对应一个ProtocolParser实现类。比如ModbusTcpParser负责解析功能码、寄存器地址、数据长度;CustomBinaryParser处理私有二进制协议;JsonOverHttpParser则专门对付那些用HTTP POST发JSON的智能传感器。关键点在于,解析器只做两件事:1)把字节流转成标准的DeviceDataPOJO(含设备ID、采集时间、原始值列表、校验结果);2)把DeviceData序列化成内部统一的消息格式(如采集事件),丢进消息队列。它绝不碰数据库、不发邮件、不调用业务服务。

  • 业务处理层(Business Layer):这才是真正“干活”的地方。它订阅消息队列里的采集事件,然后根据设备类型、数据点ID、业务规则,决定:该存进MySQL还是InfluxDB?是否触发阈值告警?要不要调用AI模型做异常检测?需不需要生成OPC UA数据点?这一层完全与设备协议解耦,新增一种设备,只需写一个新的ProtocolParser,业务逻辑代码一行都不用改。

这种分层不是为了炫技,而是为了应对物联网项目最头疼的“协议爆炸”。去年我们接入一家德国厂商的温控器,协议文档200页,全是德文,还带加密校验。如果业务逻辑和解析逻辑耦合,改一个校验算法就得把告警、存储、报表全测一遍。而分层后,我们只改了SiemensS7Parser的3个方法,其他模块完全不受影响,上线零故障。

2.3 为什么放弃传统单体架构,引入轻量级消息队列?

你可能会问:数据来了直接存库不就行了,为啥还要加一层RabbitMQ/Kafka?答案是:削峰填谷、解耦容错、异步扩展

想象一个场景:某化工厂有500台压力传感器,每5秒上报一次数据,峰值QPS=100。数据库写入能力是80 QPS。如果没有消息队列,第81个请求就会失败,要么丢数据,要么拖慢整个系统。而引入RabbitMQ后,采集服务只管往队列里“扔”,写库服务按自己节奏“取”。队列成了缓冲池,瞬间涌入的1000条数据,会被平滑消化,数据库永远在舒适区工作。

更重要的是容错。某天凌晨,MySQL主库因磁盘满挂了。没有队列的系统,所有新数据直接丢失。而有队列的系统,采集服务照常运行,数据全堆在RabbitMQ内存+磁盘里,等DB恢复后,写库服务自动从队列里重拉数据,全程无人工干预,数据零丢失。

本系统选用RabbitMQ而非Kafka,是基于成本与复杂度的权衡。Kafka确实吞吐更高,但运维成本也高——需要ZooKeeper、需要调优log.retention.hours、需要管理Topic分区。而RabbitMQ单节点部署,Docker一条命令搞定,docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:management,管理界面直观,对于中小规模物联网项目(<10万设备),它足够稳、足够简单。源码里application.yml的配置清晰展示了如何设置死信队列(DLX):当写库服务连续失败3次,消息自动路由到dead-letter-exchange,由专门的告警服务消费,避免错误数据无限重试拖垮系统。

3. 核心细节解析:从设备注册到数据落库,每一步都藏着坑

3.1 设备注册与元数据管理:为什么不用UUID而用业务编码?

设备ID是整个系统的基石。很多教程直接用UUID.randomUUID().toString()生成设备唯一标识,看似简单,实则埋雷。UUID是随机字符串,人类无法识别,运维查问题时,看到a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8,根本不知道这是3号车间的2号锅炉温度探头还是仓库的湿度传感器。

本系统强制要求设备注册时提交业务编码,格式为{厂区}-{车间}-{设备类型}-{编号},例如SH-ASM-TEMP-001(上海组装车间温度探头001号)。这个编码被存入MySQL的device_info表,并作为所有数据记录的外键。好处立竿见影:

  • 可读性:日志里打印[SH-ASM-TEMP-001] received data: [25.3, 25.4, 25.2],工程师一眼就知道问题设备位置;
  • 查询效率:按厂区、车间做统计分析,SELECT COUNT(*) FROM device_data WHERE device_id LIKE 'SH-ASM-%',索引能高效命中;
  • 权限控制:不同厂区管理员只能查看自己辖区设备,SQLWHERE device_id LIKE 'SH-%'天然支持。

注册接口POST /api/v1/devices的请求体长这样:

{ "deviceId": "SH-ASM-TEMP-001", "deviceName": "组装车间1号炉温探头", "protocol": "modbus-tcp", "ipAddress": "192.168.10.101", "port": 502, "registerAddress": 0, "dataLength": 2, "dataType": "float32" }

注意registerAddressdataLength字段——它们不是可选的,而是必须由设备厂商提供。很多新手以为“反正设备会发数据,我随便填个0就行”,结果调试时发现数据全是乱码。因为Modbus协议里,寄存器地址决定了从哪个内存单元读数据,dataLength决定了读几个字(16位)或字(32位)。源码里ModbusTcpParser会严格校验这两个值,如果设备实际返回的数据长度与配置不符,直接标记为DATA_FORMAT_ERROR并告警,而不是默默存入错误数据。

3.2 心跳与连接管理:TCP长连接不是“建了就完事”

物联网设备大多资源受限,不可能像手机App那样频繁重连。所以服务器必须维持TCP长连接,并通过心跳保活。但“心跳”二字背后,是大量容易被忽略的细节:

  • 心跳间隔不是越短越好:设备端心跳间隔设为30秒,服务器端检测超时必须大于30秒,否则会误判离线。源码里TcpDeviceChannelHEARTBEAT_TIMEOUT_MS = 60_000(60秒),HEARTBEAT_INTERVAL_MS = 30_000(30秒),留出充分的网络抖动余量。

  • 心跳包内容必须有意义:不能只发空包。本系统规定心跳包必须包含设备当前时间戳和电池电量(如果设备支持),服务器收到后更新last_heartbeat_timebattery_level字段。这样,运维看device_info表,就能知道哪台设备电量快耗尽了,提前更换电池,而不是等它彻底失联才报警。

  • 连接复用与隔离:同一IP可能有多个设备(如一个网关下挂10个传感器)。如果所有设备共用一个TCP连接,一台设备异常断开,会殃及池鱼。源码采用ConcurrentHashMap<String, DeviceChannel>deviceId隔离连接,SH-ASM-TEMP-001SH-ASM-HUMI-001即使在同一IP,也各自独立连接、独立心跳、独立重连。

注意:Linux系统默认的net.ipv4.tcp_fin_timeout是60秒,这意味着TIME_WAIT状态连接会占用端口60秒。如果设备频繁上下线,服务器端口可能被占满。源码启动脚本start.sh里预置了内核参数优化:

echo 'net.ipv4.tcp_fin_timeout = 30' >> /etc/sysctl.conf echo 'net.ipv4.tcp_tw_reuse = 1' >> /etc/sysctl.conf sysctl -p

这能让TIME_WAIT连接更快回收,支撑更高频的设备重连。

3.3 数据校验与清洗:原始数据从来不是“干净”的

设备发来的数据,99%都不是理想状态。源码里DataValidator类是数据质量的第一道闸门,它执行四层校验:

  1. 格式校验:检查JSON结构是否完整,deviceIdtimestampvalues字段是否存在。缺失timestamp?直接拒绝,因为时序数据库依赖精确时间戳。
  2. 范围校验:根据设备元数据里的min_valuemax_value(注册时录入),判断数值是否合理。比如温度探头min_value=0,max_value=100,若收到-200,标记为OUT_OF_RANGE
  3. 突变校验:计算当前值与上一值的差值率。若|current - last| / last > 0.5(50%突变),且未伴随设备重启事件,则触发VALUE_SPIKE告警。这能捕捉传感器漂移或线路干扰。
  4. 一致性校验:对多通道设备(如六轴振动传感器),检查各通道采样时间戳是否一致。若channel1_ts=1678886400000,channel2_ts=1678886400001,差1ms可接受;若差10s,则判定为TIMESTAMP_INCONSISTENT

校验结果不是简单丢弃,而是存入device_data_audit审计表,包含original_data(原始JSON)、audit_result(校验结果枚举)、audit_message(具体原因)。这样,当业务方质疑“为什么XX数据没进报表”,你能立刻查审计表,给出“因超出温度范围被过滤”的证据,而不是一句“可能设备坏了”。

3.4 批量写入与性能调优:单条INSERT是性能杀手

每秒处理100条数据,如果用JdbcTemplate.update("INSERT INTO ...")逐条写,数据库很快崩溃。源码采用三级缓冲写入:

  • 内存缓冲DataBuffer类维护一个ConcurrentLinkedQueue<DeviceData>,采集服务将校验后的数据放入队列。
  • 定时聚合DataBatchWriter定时任务(每200ms触发)从队列中取出最多1000条数据,构造成List<DeviceData>
  • 批量JDBC:调用JdbcTemplate.batchUpdate,一条SQL插入多行:
    INSERT INTO device_data (device_id, timestamp, value, channel) VALUES (?, ?, ?, ?), (?, ?, ?, ?), ...;
    MySQL的rewriteBatchedStatements=true参数(在application.yml的JDBC URL里启用)会将多条INSERT重写为一条INSERT ... VALUES (...), (...), (...),性能提升3-5倍。

实测数据:单条INSERT平均耗时8ms,1000条批量INSERT平均耗时12ms。QPS从125飙升到8300。这个数字不是理论值,而是我们在阿里云ECS(4核8G)+ RDS MySQL(2核4G)环境下,用jmeter模拟500设备并发压测的真实结果。源码里application-prod.yml的JVM参数也针对此做了优化:

jvmOptions: "-Xms2g -Xmx2g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForPoolSize"

-Xms2g -Xmx2g避免GC时内存抖动;-XX:+UseG1GC选择低延迟垃圾收集器;-XX:MaxGCPauseMillis=200将GC停顿控制在200ms内,防止写库卡顿导致缓冲区溢出。

4. 实操过程:从零部署到产线运行,手把手带你避坑

4.1 环境准备:Linux服务器上的一键初始化脚本

别信“下载源码,mvn clean package,java -jar xxx.jar”就能跑。真实环境里,缺的不是代码,是配套的基础设施。源码根目录下的deploy/文件夹,提供了完整的生产环境初始化方案。

第一步,执行init-server.sh

#!/bin/bash # 安装必要软件 sudo apt update && sudo apt install -y openjdk-17-jdk docker.io docker-compose nginx # 创建专用用户,避免root运行 sudo useradd -m -s /bin/bash iot-server sudo usermod -aG docker iot-server # 配置Nginx反向代理,暴露80端口 sudo cp deploy/nginx.conf /etc/nginx/sites-available/iot-server sudo ln -sf /etc/nginx/sites-available/iot-server /etc/nginx/sites-enabled/iot-server sudo nginx -t && sudo systemctl reload nginx # 启动RabbitMQ sudo docker-compose -f deploy/docker-compose.yml up -d

这个脚本干了四件事:装JDK17(SpringBoot 3.x必需)、装Docker(跑RabbitMQ)、建普通用户(安全规范)、配Nginx(隐藏真实端口、加HTTPS)。其中docker-compose.yml定义了RabbitMQ和一个轻量级MySQL(用于存储设备元数据),所有配置都已预设好,无需手动修改。

第二步,切换到iot-server用户,克隆源码:

sudo su - iot-server git clone https://github.com/your-org/iot-collector.git cd iot-collector

注意:不要用root克隆!否则target/目录权限混乱,后续mvn package会失败。

第三步,修改application-prod.yml

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/iot_db?useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true username: iot_user password: your_secure_password rabbitmq: host: localhost port: 5672 username: admin password: admin_pass

密码必须改!源码里默认密码是admin_pass,但生产环境必须用强密码,且iot_user账号只授予iot_db数据库的SELECT, INSERT, UPDATE权限,禁用DROP

4.2 编译与启动:Maven参数里的魔鬼细节

mvn clean package -Pprod是标准命令,但-Pprod激活的prodProfile里,藏着关键配置:

  • maven-compiler-plugin指定sourcetarget17,确保Java17特性(如switch表达式)可用;
  • spring-boot-maven-pluginrepackage目标,会把所有依赖打成fat jar,target/iot-collector-1.0.0.jar可以直接运行;
  • maven-resources-pluginsrc/main/resources下,根据Profile复制对应的application.ymlprodProfile会覆盖application-dev.yml

启动命令不是简单的java -jar,而是:

nohup java -server -Xms2g -Xmx2g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 \ -Dspring.profiles.active=prod \ -Dlogging.config=classpath:logback-spring.xml \ -jar target/iot-collector-1.0.0.jar > logs/start.log 2>&1 &
  • -server:启用服务器模式JVM,优化长期运行;
  • -Dspring.profiles.active=prod:显式指定Profile,避免环境误判;
  • -Dlogging.config=classpath:logback-spring.xml:指向定制的日志配置,它将INFO以上日志按天滚动,ERROR日志单独存error.log,方便排查;
  • nohup&:后台运行,终端关闭不影响服务;
  • > logs/start.log 2>&1:将标准输出和错误输出重定向到日志文件,避免java -jar命令卡住。

启动后,检查日志logs/start.log,看到Started IotCollectorApplication in X.XXX seconds,再访问http://your-server-ip:80(Nginx代理),出现Swagger UI页面,说明服务已就绪。

4.3 设备接入实战:以西门子S7-1200 PLC为例

现在,我们接入一台真实的西门子S7-1200 PLC。它通过以太网连接到服务器,IP为192.168.10.200,使用S7协议(非Modbus)。

第一步,注册设备:

curl -X POST http://localhost/api/v1/devices \ -H "Content-Type: application/json" \ -d '{ "deviceId": "SH-ASM-PLC-001", "deviceName": "组装车间主控PLC", "protocol": "s7", "ipAddress": "192.168.10.200", "port": 102, "rack": 0, "slot": 1, "dataBlocks": [ {"dbNumber": 1, "startAddress": 0, "length": 4, "dataType": "int32"} ] }'

注意rackslot参数——这是S7协议特有,必须填对,否则连接失败。dataBlocks指定了要读取的数据块(DB1)、起始地址(0)、长度(4字节)、数据类型(int32)。

第二步,观察日志。如果连接成功,logs/app.log会出现:

INFO c.i.c.t.s.S7DeviceChannel - [SH-ASM-PLC-001] S7 connection established, rack=0, slot=1 INFO c.i.c.p.S7ProtocolParser - [SH-ASM-PLC-001] Read DB1 startAddress=0 length=4 success, values=[12345]

如果失败,常见原因:

  • Connection refused:PLC未开启PG/PC接口,或防火墙拦截了102端口;
  • S7 protocol error: 0x0005rackslot填错,S7协议规定CPU1200的rack=0, slot=1
  • Read timeout:PLC数据块未激活,或startAddress超出DB块范围。

第三步,验证数据入库。执行SQL:

SELECT * FROM device_data WHERE device_id = 'SH-ASM-PLC-001' ORDER BY timestamp DESC LIMIT 5;

应看到类似结果:

iddevice_idtimestampvaluechannel
1001SH-ASM-PLC-001167888640000012345db1_0

至此,PLC数据已稳定流入系统。后续只需在业务层订阅device_data表,就能做任何分析。

4.4 监控与告警:不只是看CPU,要看“设备在线率”

系统上线后,不能只靠top看CPU。真正的监控指标有三个:

  • 设备在线率SELECT COUNT(*)*100.0/(SELECT COUNT(*) FROM device_info) FROM device_info WHERE status='ONLINE'。低于95%就要排查网络或设备故障。
  • 数据延迟SELECT MAX(UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(timestamp)) FROM device_data。超过30秒说明采集链路有瓶颈。
  • 错误率SELECT COUNT(*)*100.0/(SELECT COUNT(*) FROM device_data_audit) FROM device_data_audit WHERE audit_result != 'PASS'。超过5%需检查校验规则或设备质量。

源码自带/actuator/prometheus端点,可直接被Prometheus抓取。prometheus.yml配置示例:

scrape_configs: - job_name: 'iot-collector' static_configs: - targets: ['your-server-ip:8080']

Grafana仪表盘模板已预置在deploy/grafana/目录,导入后即可看到实时设备地图、在线率曲线、错误类型分布饼图。

告警通过spring-boot-starter-mail实现。当device_info.status变为OFFLINE持续5分钟,或device_data_audit.audit_resultDATA_FORMAT_ERROR超过10次/小时,系统自动发邮件给运维组。邮件模板在src/main/resources/templates/alert-email.ftl,支持变量替换,如${device.deviceName}${alert.reason}

5. 常见问题与排查技巧实录:那些文档里不会写的真相

5.1 “设备明明在线,数据却收不到”——网络层排查清单

这是最高频问题。别急着查代码,先按顺序排查网络:

  1. 确认设备端口开放:在服务器上执行telnet 192.168.10.101 502。如果Connection refused,说明设备没开Modbus服务,或防火墙阻止了502端口。此时去设备端检查Modbus TCP服务是否启动。
  2. 检查服务器端口占用netstat -tuln | grep :8080,确认SpringBoot进程确实在监听8080。如果没输出,说明应用没起来,查logs/start.log
  3. 验证RabbitMQ连通性curl -i http://localhost:15672/api/vhosts/,输入admin/admin_pass,看是否返回JSON。如果Connection refused,说明RabbitMQ没启动,docker ps看容器状态。
  4. 抓包定位:在服务器上sudo tcpdump -i any port 502 -w modbus.pcap,然后让设备发一次数据。用Wireshark打开modbus.pcap,看是否有Modbus Request包发出,是否有Modbus Response包返回。如果只有Request没有Response,问题在设备端;如果Request都没有,问题在采集服务的连接逻辑。

实操心得:我曾遇到一个案例,设备IP是192.168.10.101,但服务器路由表里,192.168.10.0/24网段被错误指向了另一台交换机。ping通,telnet也通(因为ICMP和TCP握手成功),但Modbus数据包被转发到了错误设备,自然收不到响应。用tcpdump抓包后,发现Response包源IP是192.168.10.200(另一台设备),真相大白。

5.2 “数据入库后全是NULL”——协议解析器的隐形陷阱

现象:设备注册成功,日志显示received data,但device_data表里value字段全是NULL。

根源几乎总是ProtocolParser里的数据类型转换错误。以ModbusTcpParser为例,它把原始字节流转成short[],再根据dataType转成目标类型。常见陷阱:

  • dataType: int16:直接取short[0]
  • dataType: int32:需合并short[0]short[1],公式为(short[0] << 16) | (short[1] & 0xFFFF)
  • dataType: float32:需用ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).getFloat(),且字节顺序必须与设备一致(大端/小端)。

源码里ModbusTcpParserparseValue方法,对每种dataType都有独立分支,并加了LOG.debug打印转换前后的值。如果发现转换后是0.0-1,一定是字节顺序或合并逻辑错了。解决方案:用设备厂商提供的调试工具,抓取原始字节流(如01 00 00 00),在parseValue里打断点,一步步看ByteBuffer如何解析。

5.3 “服务启动后内存暴涨,然后OOM”——JVM参数的血泪教训

现象:java -jar启动后,topRES内存从500MB一路涨到2GB,然后java.lang.OutOfMemoryError: Java heap space

根本原因:DataBuffer的内存队列无界。默认ConcurrentLinkedQueue可以无限增长,当设备暴增或下游写库卡住,数据在内存里堆积,最终撑爆JVM。

解决方案:在application-prod.yml里,为缓冲队列加硬限制:

collector: buffer: max-size: 10000 # 最多缓存10000条数据 reject-policy: DISCARD_OLDEST # 超限时丢弃最老数据,而非阻塞

源码里DataBuffer构造时读取此配置,创建BlockingQueue时传入max-size。同时,DataBatchWriter@Scheduled(fixedDelay = 200)改为@Scheduled(fixedDelayString = "${collector.buffer.write-interval:200}"),支持动态调整写入频率。

踩过的坑:某次升级后,运维把max-size设成了1000000,以为“越大越好”。结果网络抖动导致写库暂停2分钟,内存瞬间吃满。后来我们加了@PostConstruct方法,在应用启动时打印Runtime.getRuntime().maxMemory()buffer.max-size,如果后者超过前者的10%,自动warn日志:“缓冲区设置过大,可能导致OOM”。

5.4 “Swagger UI打不开,报404”——SpringBoot 3.x的路径变更

现象:SpringBoot 2.x项目里,Swagger在/swagger-ui.html,但升级到3.x后,访问/swagger-ui.html返回404。

原因:SpringBoot 3.x默认移除了springfox-swagger,改用springdoc-openapi,且路径变了。源码里pom.xml已引入:

<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-api</artifactId> <version>2.2.0</version> </dependency>

正确访问路径是/swagger-ui/index.html。如果还是404,检查application.yml是否启用了springdoc.api-docs.enabled=true(默认true),以及springdoc.swagger-ui.path=/swagger-ui(默认值)。

更隐蔽的问题:Nginx反向代理配置。如果Nginx里写了location /swagger-ui { proxy_pass http://localhost:8080/swagger-ui; },而SpringBoot的springdoc.swagger-ui.path/swagger-ui,会导致路径重复。正确配置是:

location /swagger-ui/ { proxy_pass http://localhost:8080/swagger-ui/; }

注意末尾的/,它确保路径正确映射。

6. 源码结构与扩展指南:如何添加新协议、新存储、新告警

6.1 添加新设备协议:三步完成Modbus RTU支持

现有源码只支持Modbus TCP和MQTT。现在要接入串口设备(如RS485温湿度传感器),需支持Modbus RTU。步骤如下:

  1. 新增协议解析器:在com.iot.collector.protocol包下,新建ModbusRtuParser.java,实现ProtocolParser接口。重点实现parse(byte[] rawBytes)方法,用j2mod库解析RTU帧(含CRC校验)。
  2. 新增接入通道:在com.iot.collector.transport包下,新建SerialDeviceChannel.java,继承AbstractDeviceChannel。用jserialcomm库打开串口(COM3/dev/ttyUSB0),设置波特率、数据位、停止位。
  3. **

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/5 10:44:57

烤面筋烤面经:技术面试准备全流程指南

傍晚路过夜市&#xff0c;烤面筋的摊子冒着烟&#xff0c;刷酱、翻面、撒孜然&#xff0c;一串下来焦香四溢。我站在摊前突然想&#xff0c;这玩意儿跟写面经太像了——都得把零散原料串起来&#xff0c;掌握好火候&#xff0c;才能端得上台面。最近正好在整理技术面试的复盘&a…

作者头像 李华
网站建设 2026/9/5 15:41:56

腾讯后台开发练习卷:网络、系统、算法与数据库核心解析

1. 先给这份练习卷定个位如果你跟我一样&#xff0c;是从大学实验室、或者从第一次找实习的慌乱里走过来的人&#xff0c;那“腾讯2015春招后台开发练习卷”这几个字应该不陌生。后台开发这个岗位&#xff0c;听名字像是“写接口、调服务”&#xff0c;但实际一练卷子你才发现&…

作者头像 李华
网站建设 2026/9/5 14:02:33

前端面试题追根溯源:从Vue3原理到微前端实战,告别背题陷阱

在面试候场区等我前面几个人出来的时候&#xff0c;我其实挺有把握的。简历上的项目经验写得满满当当&#xff0c;Vue3、React、微前端、组件库开发全都有。结果一面第一个问题就把我砸懵了&#xff1a;“你说你做过组件库&#xff0c;那你知道el-table的虚拟滚动为什么在大数据…

作者头像 李华
网站建设 2026/9/3 17:23:04

一条命令跑通自然语言编程:Open Interpreter 上手指南

一条命令跑通自然语言编程&#xff1a;Open Interpreter 上手指南 【免费下载链接】openinterpreter A coding agent for open models like Kimi K3 项目地址: https://gitcode.com/GitHub_Trending/op/openinterpreter Open Interpreter 是一个面向 Kimi K3 这类低成本…

作者头像 李华