Java物联网环境监测系统源码解析:从串口采集到Spring Boot展示
2026/9/23 17:02:15 网站建设 项目流程

简介:这份基于Java语言的物联网环境监测系统设计源码,面向具备Java基础、希望实践物联网项目开发的学生与开发者,可用于课程设计、毕业设计或环境监测类应用的原型搭建。资源包共43个文件,约3.51MB,其中15个Java源文件承载数据采集、处理、通信与界面等核心业务逻辑,14个XML配置文件负责参数与工程配置,另有3个JAR包提供dom4j、log4j等库支持,2个properties文件保存关键运行参数,整体采用模块化设计,结构清晰、便于二次扩展。项目可连接温度、湿度、空气质量等传感器,实现环境数据的实时采集、传输与图形化展示,并支持历史数据查询与监测参数调整。目前已有337人学习下载,适合作为物联网入门到进阶的实战参考,帮助读者理解传感器接入、数据通信与模块协作的完整实现思路。

1. 从一份 Java 物联网环境监测源码说起

很多做 Java 课程设计或毕业设计的同学,拿到「基于 Java 语言的物联网环境监测系统设计源码」这类资源时,第一反应是直接跑起来看效果,结果卡在串口读不到数据、数据库连不上、前端页面空白。这套源码的价值不在于界面多漂亮,而在于它把「传感器采集 → 网关汇聚 → 服务端存储 → Web 展示」这条链路用 Java 技术栈完整串了一遍。它适合两类人:一是需要交课程设计、毕业设计,想快速理解物联网项目骨架的学生;二是做 Java 后端、想补一补设备接入层经验的开发者。源码里通常包含数据采集模块、Spring Boot 服务端、MyBatis 持久层和定时任务,理解它的分层结构比改几个参数更重要。下面按数据怎么进来、怎么存、怎么查、怎么排错这条线拆开讲。

2. 环境监测数据采集层:串口、Modbus 与模拟数据源

2.1 采集层在 Java 侧到底做什么

物联网环境监测系统的第一公里是设备数据进入 Java 进程。常见做法有两种:一种是真实硬件通过串口(RS485/RS232)或 Modbus TCP 上报,Java 侧用串口库或 Modbus 库读取;另一种是课程设计里没有硬件,用模拟数据源定时生成温湿度、PM2.5、CO2 等数值。这套源码一般把采集逻辑抽象成一个接口,真实设备和模拟器各实现一份,方便切换。理解这一点,后面看 Service 层就不会迷路。

采集层要解决三个问题:数据格式解析、采集频率控制、异常断连重试。环境监测场景里,温度湿度变化慢,采集频率通常 5 到 30 秒一次;PM2.5 和 CO2 可以稍快。频率设太高会压垮串口和数据库,设太低曲线就不连续。源码里一般用ScheduledExecutorService或 Spring 的@Scheduled控制节奏。

2.2 串口读取与 Modbus 解析的可复现写法

真实设备接入时,Java 常用 jSerialComm 或 RXTX 读串口,Modbus 协议用 modbus4j 或 j2mod。下面是一段典型的串口读取加解析骨架,参数按实际设备手册改。

// 串口采集示例:读取一帧环境数据并解析 SerialPort port = SerialPort.getCommPort("COM3"); // Linux 下为 /dev/ttyUSB0 port.setComPortParameters(9600, 8, 1, 0); // 波特率9600,8数据位,1停止位,无校验 port.setComPortTimeouts(SerialPort.TIMEOUT_READ_SEMI_BLOCKING, 2000, 0); if (!port.openPort()) { throw new IllegalStateException("串口打开失败,检查设备占用与权限"); } byte[] buffer = new byte[64]; int read = port.readBytes(buffer, buffer.length); // 阻塞读取,超时2秒 if (read > 0) { // 按设备协议解析:前2字节温度,后2字节湿度,需除以10 int tempRaw = ((buffer[0] & 0xFF) << 8) | (buffer[1] & 0xFF); int humiRaw = ((buffer[2] & 0xFF) << 8) | (buffer[3] & 0xFF); double temperature = tempRaw / 10.0; double humidity = humiRaw / 10.0; System.out.printf("温度=%.1f℃ 湿度=%.1f%%%n", temperature, humidity); } port.closePort();

这段代码的逻辑是:先按设备手册配置串口参数,再阻塞读取一帧字节流,最后按协议偏移量解析。参数说明上,波特率、数据位、停止位、校验位必须和设备一致,错一个就读出乱码;TIMEOUT_READ_SEMI_BLOCKING表示读满或超时返回,避免线程卡死。Linux 下串口权限问题很常见,需要把用户加入dialout组,否则openPort直接返回 false。

2.3 没有硬件时用模拟数据源兜底

课程设计多数没有真实传感器,源码里通常带一个模拟采集器。它按固定间隔生成带随机波动的数据,写入和真实采集相同的队列或 Service。这样上层逻辑不用改,答辩时也能演示完整链路。

// 模拟数据源:每5秒生成一条环境数据 @Scheduled(fixedRate = 5000) public void mockCollect() { double temp = 20 + ThreadLocalRandom.current().nextDouble(-3, 3); double humi = 50 + ThreadLocalRandom.current().nextDouble(-10, 10); double pm25 = 30 + ThreadLocalRandom.current().nextDouble(0, 40); EnvData data = new EnvData(deviceId, temp, humi, pm25, LocalDateTime.now()); dataQueue.offer(data); // 放入队列,由消费线程批量入库 }

fixedRate = 5000表示每 5 秒触发一次,ThreadLocalRandomRandom在多线程下更稳。数据先进队列再批量入库,是为了避免每条数据都开一次数据库连接,这一点在数据量大时差别很明显。

采集方式依赖适用场景常见坑
串口 RS485jSerialComm有线传感器权限、波特率不匹配
Modbus TCPmodbus4j网口仪表寄存器地址偏移
模拟数据源课程设计演示数据太规律被看出假

提示:串口读到的字节序可能是大端也可能是小端,解析前先拿一帧已知数据验证,别直接套模板。

3. Spring Boot 服务端与 MyBatis 持久化落地

3.1 分层结构与数据表设计

采集层拿到数据后,交给 Spring Boot 服务端处理。典型分层是 Controller 接收查询请求,Service 处理业务和定时入库,Mapper 负责 SQL。环境监测的数据表一般分两张:设备表device记录设备编号、位置、状态;数据表env_data记录设备编号、温度、湿度、PM2.5、采集时间。数据表要按时间建索引,否则查历史曲线会全表扫描。

CREATE TABLE env_data ( id BIGINT PRIMARY KEY AUTO_INCREMENT, device_id VARCHAR(32) NOT NULL COMMENT '设备编号', temperature DECIMAL(5,1) COMMENT '温度', humidity DECIMAL(5,1) COMMENT '湿度', pm25 DECIMAL(6,1) COMMENT 'PM2.5', collect_time DATETIME NOT NULL COMMENT '采集时间', INDEX idx_device_time (device_id, collect_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

idx_device_time是联合索引,因为查询几乎都是「某设备某时间段」,联合索引能直接命中。字段用DECIMAL而不是FLOAT,避免浮点误差在报表里累积。collect_time单独建索引意义不大,和device_id组合才有用。

3.2 批量入库与 MyBatis 写法

高频采集下逐条 insert 会成为瓶颈。常见做法是攒一批再批量插入,MyBatis 用<foreach>拼批量 SQL。

<insert id="batchInsert" parameterType="java.util.List"> INSERT INTO env_data (device_id, temperature, humidity, pm25, collect_time) VALUES <foreach collection="list" item="item" separator=","> (#{item.deviceId}, #{item.temperature}, #{item.humidity}, #{item.pm25}, #{item.collectTime}) </foreach> </insert>

collection="list"对应 Mapper 方法传入的 List,separator=","保证多条 VALUES 之间用逗号连接。批量大小一般控制在 500 到 1000 条,太大可能超过max_allowed_packet。Service 层用队列攒够一批或超时触发,兼顾实时性和吞吐。

3.3 定时任务与数据清理

环境数据是持续增长的,源码里通常带一个清理任务,删除超过保留期的数据,比如只留 30 天。

@Scheduled(cron = "0 0 3 * * ?") // 每天凌晨3点执行 public void cleanExpiredData() { LocalDateTime deadline = LocalDateTime.now().minusDays(30); int deleted = envDataMapper.deleteBefore(deadline); log.info("清理过期环境数据 {} 条", deleted); }

cron = "0 0 3 * * ?"是每天 3 点,避开白天查询高峰。删除时按collect_time走索引,别用DELETE不带条件。如果数据量特别大,直接删会锁表,常见做法是分批删或按分区表管理。

注意:批量插入和定时清理都要在事务边界内考虑,清理任务别和采集任务抢同一张表的锁。

4. 实时展示、历史查询与接口联调

4.1 实时数据推送的两种做法

前端要看到实时曲线,Java 侧常见两种方案:轮询和 WebSocket。轮询实现简单,前端每几秒调一次接口;WebSocket 由服务端主动推,延迟低但要多维护一个连接。课程设计里轮询够用,生产环境倾向 WebSocket 或 SSE。

// 轮询接口:返回某设备最新一条数据 @GetMapping("/latest/{deviceId}") public EnvData latest(@PathVariable String deviceId) { return envDataMapper.selectLatestByDevice(deviceId); // 按时间倒序取1条 }

selectLatestByDevice的 SQL 是ORDER BY collect_time DESC LIMIT 1,配合联合索引能快速返回。前端用setInterval每 5 秒调一次,注意页面切到后台时要清掉定时器,否则请求会堆积。

4.2 历史曲线查询与时间范围参数

历史查询接口要接收开始和结束时间,返回区间内的数据点。参数格式建议统一用 ISO 时间,避免时区歧义。

@GetMapping("/history") public List<EnvData> history(@RequestParam String deviceId, @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime start, @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime end) { return envDataMapper.selectByRange(deviceId, start, end); }

@DateTimeFormat把请求里的字符串转成LocalDateTime,前端传2024-06-01T00:00:00这种格式。查询区间别开太大,一次拉几个月的数据前端渲染会卡,常见做法是按区间聚合,比如每小时取平均值再返回。

4.3 接口联调常见报错对照

联调阶段报错集中在几类,提前知道能省很多时间。

现象可能原因排查方向
返回 400时间格式不对检查 ISO 格式和时区
返回空数组设备编号不匹配对比库里的 device_id
曲线断点采集有丢帧看采集日志和队列积压
接口超时区间太大无索引看执行计划是否走索引

提示:联调时先用 Postman 或 curl 单独打接口,确认服务端没问题再查前端,别两边一起猜。

5. 源码二次开发与部署排错技巧

5.1 改造成自己的设备协议

拿到源码后最常做的是接入自己的传感器。改动集中在采集层的解析方法:把字节偏移、量纲换算、寄存器地址换成自己设备手册里的值。改完先写一个单元测试,喂一帧已知数据,断言解析结果,别直接连设备试。

@Test public void testParseFrame() { byte[] frame = {(byte)0x00, (byte)0xFA, (byte)0x01, (byte)0x2C}; // 25.0℃ 30.0% EnvData data = parser.parse(frame); assertEquals(25.0, data.getTemperature(), 0.01); assertEquals(30.0, data.getHumidity(), 0.01); }

assertEquals第三个参数是误差容忍度,浮点比较必须给。这样改协议时先跑测试,通过了再上设备,能避免反复插拔调试。

5.2 部署时的环境变量与数据库连接

源码本地能跑、服务器跑不起来,多半是环境变量和数据库配置。Spring Boot 的application.yml里数据库地址、账号密码建议用环境变量注入,别硬编码。

export DB_HOST=127.0.0.1 export DB_PORT=3306 export DB_NAME=env_monitor export DB_USER=env_user export DB_PASS=your_password java -jar env-monitor.jar --spring.profiles.active=prod

--spring.profiles.active=prod指定生产配置,配合application-prod.yml读取环境变量。这样换环境不用改代码,也避免密码进版本库。启动后先看日志里有没有Started ... in x seconds,再看数据库连接池有没有报错。

5.3 用日志和指标定位采集丢数据

采集丢数据是最难查的问题之一。有效手段是在采集入口、入队、入库三个点各打一条计数日志,对比数量差在哪一段。

log.info("采集计数={} 入队计数={} 入库计数={}", collectCount.get(), queueCount.get(), saveCount.get());

三个计数用AtomicLong累加,定时打印。如果采集数大于入队数,说明解析或过滤环节丢了;入队大于入库,说明批量入库有失败或队列满被丢弃。定位到段再细查,比盲目看代码快得多。

5.4 一个容易被忽略的时区问题

环境监测数据带时间戳,服务器时区和数据库时区不一致时,曲线会整体偏移几小时。常见做法是数据库连接串里显式指定时区,Java 侧统一用LocalDateTime或带时区的Instant,别混用java.util.Date

spring: datasource: url: jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8

serverTimezone=Asia/Shanghai告诉驱动用哪个时区解释时间,characterEncoding=utf8避免中文乱码。这两个参数在部署到不同机器时经常被漏掉,导致数据看起来「对但时间不对」。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询