资讯动态

基于Java的物联网环境监测系统:从设备接入到告警的完整实现

发布时间:2026/10/9 8:09:47 来源:尧图企业网站定制
简介这份源码面向Java开发者与物联网初学者提供一套数据中心环境监测系统的完整实现方案可用于课程设计、毕业设计或二次开发参考。项目共49个文件以37个Java源文件为核心涵盖环境数据实体、采集接口与具体实现等模块另有9个XML配置文件负责数据库连接、网络与消息队列等运行参数2个properties文件管理日志级别与监测频率等全局项压缩包约98KB结构清晰、便于按模块阅读。系统围绕温度、湿度、烟雾等传感器数据的采集、存储与预警通知展开通过接口抽象与实现分离体现面向对象设计并借助XML配置提升可维护性。目前已有97人学习下载适合希望理解Java在物联网领域落地方式、掌握模块化环境监测系统搭建思路的读者参考借鉴。1. 从一堆传感器到一块看板Java 物联网环境监测系统到底在做什么机房、温室大棚、档案库房、小型实验室这些场景有个共同点温湿度、烟雾、光照一旦失控损失往往不可逆。很多团队第一反应是买现成网关加云平台但真到落地时会发现数据要留在自己服务器、告警逻辑要按业务改、历史曲线要能导出给审计——这时候一套自己可控的物联网环境监测系统就成了刚需。标题里的「基于 Java 语言开发」不是随便选的Java 生态里 Spring Boot 做后端接口、MyBatis-Plus 管数据、Netty 或 MQTT 客户端接设备这套组合在物联网平台开发里已经被验证过很多轮招人也好招。这套系统要解决的核心链路其实就四段设备侧采集、网络传输、服务端存储与告警、前端可视化。它适合两类人一类是物联网毕业设计或课程设计阶段的学生需要一套能跑通、能讲清架构的完整源码另一类是小团队的后端Java 工程师被派去做一个内部环境监控工具不想从零造轮子。后面几章我会按「数据怎么进来 → 怎么存 → 怎么告警 → 怎么排错」的顺序把每个环节的选型理由、关键参数和踩过的坑讲清楚代码能抄就抄参数能调就调。2. 设备接入与协议选型MQTT、Modbus 还是 HTTP 轮询2.1 三种接入方式的实际差别环境监测设备五花八门常见的有带 4G 模块的温湿度变送器、RS485 输出的烟感、走 WiFi 的 ESP32 节点。它们上报数据的方式直接决定了服务端怎么写。我一般把接入方式分成三类来看接入方式典型设备实时性服务端复杂度适用规模MQTTESP32、4G DTU秒级中需 Broker几十到上万节点Modbus TCP/RTURS485 传感器网关秒级高需协议解析中小规模、工业现场HTTP 轮询简易 WiFi 模块分钟级低直接写接口几个到几十个点选型逻辑很简单设备数量少、上报频率低HTTP 轮询最省事一个PostMapping就收完了设备多、要下行控制、要断线重连就上 MQTT。无源物联网这类靠反向散射通信的方案目前还偏研究工程落地里基本见不到别被概念带偏。至于「物联网的交换机与路由器连接」这种网络层问题本质是设备所在网段能不能路由到 Broker 所在服务器配好静态路由和端口放行即可跟应用层代码无关。2.2 用 Spring Boot 搭一个 MQTT 接入层下面这段是接入层的核心用 Eclipse Paho 客户端订阅设备主题收到消息后解析成统一的数据对象再入库。Broker 我一般用 EMQX 或 Mosquitto本地测试 Mosquitto 足够。Component public class MqttSubscriber { private static final String BROKER tcp://127.0.0.1:1883; // 主题格式env/{deviceId}/data用通配符订阅所有设备 private static final String TOPIC env//data; Autowired private EnvDataService envDataService; PostConstruct public void subscribe() throws MqttException { MqttClient client new MqttClient(BROKER, server-sub- UUID.randomUUID()); MqttConnectOptions options new MqttConnectOptions(); options.setCleanSession(true); options.setAutomaticReconnect(true); // 断线自动重连生产必开 options.setConnectionTimeout(10); // 连接超时 10 秒 options.setKeepAliveInterval(30); // 心跳 30 秒小于 Broker 的 1.5 倍 client.connect(options); client.subscribe(TOPIC, (topic, message) - { // topic 形如 env/DEV001/data从中截出设备编号 String deviceId topic.split(/)[1]; String payload new String(message.getPayload(), StandardCharsets.UTF_8); EnvData data JSON.parseObject(payload, EnvData.class); data.setDeviceId(deviceId); data.setCollectTime(LocalDateTime.now()); envDataService.save(data); }); } }逻辑说明cleanSessiontrue表示不保留会话适合数据可丢的场景如果要求断线期间的消息补发改成false并给客户端固定 clientId。setAutomaticReconnect(true)是血泪经验不加的话网络抖动一次就得重启服务。参数上keepAliveInterval必须小于 Broker 配置的keepalive上限否则会被踢下线。设备侧上报的 JSON 建议固定字段名比如temp、humi、smoke别一会儿temperature一会儿temp解析层会疯。2.3 设备编号与主题设计主题设计是接入层最容易翻车的地方。我见过有人用env/data一个主题收所有设备结果设备一多根本分不清谁是谁。正确做法是把设备编号编进主题层级env/{deviceId}/data和env/{deviceId}/cmd分开上行和下行。设备编号建议用 MAC 后六位或出厂序列号别用自增 ID否则设备换服务器后编号全乱。数据库里device_id字段加唯一索引重复上报直接覆盖或按时间戳去重。3. 数据存储与 MyBatis-Plus 建表从实体类到 SQL 的落地3.1 时序数据到底存哪里环境监测的数据是典型时序数据每条记录带时间戳写入多、查询按时间段。选型上有三条路MySQL 单表、MySQL 分表、专用时序库InfluxDB、TDengine。我的建议是——中小规模直接 MySQL单表加时间索引几百万行毫无压力数据量上到千万级再考虑按月分表或迁 TDengine。别一上来就上时序库运维成本会劝退小团队。表结构设计上一条环境数据记录包含设备编号、温度、湿度、烟雾浓度、光照、采集时间、入库时间。温度和湿度用DECIMAL(5,2)别用FLOAT浮点误差在告警阈值判断时会坑你。采集时间加索引因为查询几乎都是「查某设备某时间段」。3.2 用 MyBatis-Plus 从实体类生成建表 SQL热搜里「mybatisplus 根据 java 实体类生成创建表的 sql 语句」是个高频需求这里给一个能直接用的思路用 MyBatis-Plus 的TableInfoHelper拿到实体元信息再拼 DDL。下面是一个简化版工具类。public class DdlGenerator { public static String generate(Class? entityClass) { TableInfo tableInfo TableInfoHelper.getTableInfo(entityClass); StringBuilder sql new StringBuilder(); sql.append(CREATE TABLE IF NOT EXISTS ) .append(tableInfo.getTableName()).append( (\n); for (TableFieldInfo field : tableInfo.getFieldList()) { sql.append( ).append(field.getColumn()).append( ) .append(mapType(field.getPropertyType())).append( ); // 主键、非空、注释按需拼接 if (field.isKeyInsertStrategy()) { sql.append(NOT NULL ); } sql.append(COMMENT ).append(field.getColumn()).append(,\n); } sql.append( PRIMARY KEY (id)\n) ENGINEInnoDB DEFAULT CHARSETutf8mb4;); return sql.toString(); } private static String mapType(Class? type) { if (type String.class) return VARCHAR(255); if (type Integer.class) return INT; if (type Long.class) return BIGINT; if (type BigDecimal.class) return DECIMAL(10,2); if (type LocalDateTime.class) return DATETIME; return VARCHAR(255); } }逻辑说明TableInfoHelper.getTableInfo会读取实体上的TableName、TableField注解拿到表名和字段映射。mapType做 Java 类型到 MySQL 类型的映射实际项目里建议用完整的映射表覆盖Boolean、Date、Double等。参数上注意DECIMAL(10,2)的精度要按业务定温度范围 -50 到 100 用DECIMAL(5,2)就够。这个工具适合开发期快速建表生产环境还是老老实实写 Flyway 或 Liquibase 迁移脚本别让程序自动改表结构。3.3 批量写入与索引的取舍设备一多逐条insert会把数据库打满。常见做法是攒一批再批量写MyBatis-Plus 的saveBatch底层就是 JDBC 批处理。批大小我一般设 500太大内存吃紧太小没效果。索引方面device_id和collect_time建联合索引查询「某设备某时间段」能直接走索引。但索引不是越多越好每个索引都会拖慢写入环境监测这种写多读少的场景索引控制在三个以内。提示批量写入时如果开了事务批与批之间记得提交否则长事务会锁表。用Transactional时把批处理放在独立方法里别和业务查询混在一个事务。4. 阈值告警与规则引擎别把判断逻辑写死在代码里4.1 告警规则为什么不能硬编码新手最容易犯的错是把「温度大于 30 就告警」直接写在 Service 里。等业务方说「夏天阈值调到 35」「湿度低于 20 也要告警」「烟雾超过 50 连续三次才告警」你就得改代码重新发版。正确做法是把规则抽成配置存数据库或配置中心运行时动态加载。规则的核心要素设备范围、监测指标、比较符、阈值、持续次数、告警级别。4.2 一个轻量规则引擎的实现下面这段用策略模式加配置表实现阈值判断规则从数据库加载支持动态调整。Service public class AlarmRuleEngine { Autowired private AlarmRuleMapper ruleMapper; public void check(EnvData data) { // 每次查询该设备启用的规则生产环境建议加缓存 ListAlarmRule rules ruleMapper.selectByDevice(data.getDeviceId()); for (AlarmRule rule : rules) { BigDecimal value extract(data, rule.getMetric()); if (value null) continue; boolean hit compare(value, rule.getOperator(), rule.getThreshold()); if (hit) { // 连续次数判断用 Redis 计数器记录连续命中次数 String key alarm:cnt: data.getDeviceId() : rule.getId(); Long cnt redisTemplate.opsForValue().increment(key); redisTemplate.expire(key, 5, TimeUnit.MINUTES); if (cnt rule.getContinuousTimes()) { alarmService.raise(data, rule); redisTemplate.delete(key); // 触发后清零避免重复告警 } } else { redisTemplate.delete(alarm:cnt: data.getDeviceId() : rule.getId()); } } } private BigDecimal extract(EnvData d, String metric) { switch (metric) { case temp: return d.getTemp(); case humi: return d.getHumi(); case smoke: return d.getSmoke(); default: return null; } } private boolean compare(BigDecimal v, String op, BigDecimal t) { int c v.compareTo(t); switch (op) { case : return c 0; case : return c 0; case : return c 0; case : return c 0; default: return false; } } }逻辑说明extract按指标名取值compare做比较。连续次数用 Redis 计数器实现命中就加一没命中就清零达到阈值触发告警后删除计数器。参数上expire设 5 分钟意思是「5 分钟内的连续命中才算数」避免设备偶尔抖一下就告警。BigDecimal比较必须用compareTo用equals会因为精度不同返回 false这是经典翻车点。4.3 告警去重与恢复通知告警最烦的是重复轰炸。同一个设备同一个规则触发一次后应该进入「告警中」状态直到数值恢复正常才发恢复通知。实现上给告警记录加status字段0 正常、1 告警中、2 已恢复触发时先查有没有未恢复的同规则告警有就跳过。恢复判断就是反向比较数值回到阈值内就更新状态并发恢复消息。通知渠道常见的是邮件、短信、企业微信机器人用策略模式封装加渠道不用改核心逻辑。5. 避坑与排查那些让系统半夜挂掉的细节5.1 设备时间戳与服务端时间不一致现象历史曲线出现未来时间的数据点或者同一秒涌入大量数据。原因设备侧 RTC 没校准或者设备用本地时间上报而服务端按 UTC 存。解决统一约定上报时间戳用 UTC 毫秒数服务端收到后校验偏差超过 5 分钟的数据打标记或丢弃。别信设备的时间服务端入库时间才是准的。5.2 MQTT 消息重复导致数据翻倍现象数据库里同一设备同一秒有两条一模一样的记录。原因QoS 设为 1 时 Broker 会重发客户端没做幂等。解决给数据表加device_id collect_time唯一索引插入用INSERT IGNORE或ON DUPLICATE KEY UPDATE。或者用 Redis 做去重key 是设备编号加时间戳setnx 成功才入库。5.3 连接池耗尽导致接口全挂现象服务跑一段时间后所有接口超时日志里全是Connection is not available。原因MQTT 回调里直接调用了数据库回调线程池和 HTTP 线程池抢连接或者慢查询占着连接不放。解决MQTT 回调只做解析把数据丢进内存队列Disruptor 或 BlockingQueue另起线程消费入库。连接池大小按CPU 核数 * 2 磁盘数估算别拍脑袋设 100。5.4 阈值判断用了 float 导致边界误判现象温度设 30.0 告警设备上报 30.0 却不告警。原因float 存储 30.0 实际是 29.999999比较时小于阈值。解决数据库用DECIMALJava 用BigDecimal比较用compareTo。这个坑我在两个项目里都踩过现在看到 float 存传感器数据就条件反射。5.5 前端轮询把后端打垮现象看板页面开着后端 QPS 飙升。原因前端用setInterval每秒请求一次全量数据。解决改成 WebSocket 推送或者轮询间隔拉到 10 秒以上接口做缓存。数据变化没那么快环境监测秒级刷新已经足够别为了「实时」把服务器拖死。6. 进阶技巧用 Netty 自定义协议接非标设备标准 MQTT 设备好接但现场经常遇到只支持 TCP 私有协议的采集器报文是十六进制字节流。这时候 Spring Boot 那套 HTTP 接口用不上得用 Netty 写 TCP 服务端。核心是自定义ByteToMessageDecoder按协议头长度拆包再解析成业务对象。public class EnvFrameDecoder extends ByteToMessageDecoder { Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, ListObject out) { // 协议2 字节魔数 0xAA55 1 字节长度 N 字节数据 1 字节校验 if (in.readableBytes() 4) return; in.markReaderIndex(); short magic in.readShort(); if (magic ! (short) 0xAA55) { ctx.close(); // 魔数不对直接断开 return; } int len in.readByte(); if (in.readableBytes() len 1) { in.resetReaderIndex(); // 数据不够等下一批 return; } byte[] body new byte[len]; in.readBytes(body); byte checksum in.readByte(); if (checksum ! calc(body)) { return; // 校验失败丢弃 } out.add(parse(body)); } private byte calc(byte[] data) { byte sum 0; for (byte b : data) sum ^ b; return sum; } private EnvData parse(byte[] body) { // 按协议文档解析温度、湿度等字段 EnvData d new EnvData(); d.setTemp(BigDecimal.valueOf(((body[0] 0xFF) 8 | (body[1] 0xFF)) / 10.0)); return d; } }逻辑说明markReaderIndex和resetReaderIndex配合实现「数据不够就等」这是 Netty 拆包的固定套路。魔数校验防止乱连校验和用异或最简单实际项目按设备文档来。解析时注意字节序大端小端搞反了温度会变成离谱的值。Netty 的ByteBuf读取后要释放用SimpleChannelInboundHandler会自动释放别手动release两次。验证方法上我习惯用netcat或写个 Python 脚本模拟设备发十六进制报文先确认拆包正确再对接真实设备。真实设备往往有各种非标行为比如心跳包、登录包协议文档一定要拿到手没有文档就抓包分析别猜。这套系统从接入到告警再到非标协议扩展核心链路就这些。我自己的习惯是每接一类新设备先写一个最小可用的解析器跑通数据入库再补告警和前端别一上来就搭大框架。环境监测这行数据准不准比界面炫不炫重要得多。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑