简介这是一份面向Java初学者与毕业设计学生的环境监测系统完整源码涵盖空气、温度、湿度等多类环境数据的采集、展示与管理流程。项目采用Spring、Servlet/JSP与DAO分层架构覆盖业务处理、数据持久化与请求响应链路适合课程设计、毕业设计或Java Web入门练习。压缩包共46个文件核心为35个Java源文件按controller、service、dao、daoimpl分包另含数据库设计脚本、数据库更新脚本、监控管理方案及README说明整体约29KB。目前已有701人学习下载。通过阅读源码可快速理解Java Web项目从实体建模、持久层操作到控制器响应的完整链路掌握环境监测相关模块的接口写法与数据流转方式也能参考其中用户登录与权限管理、数据实时展示等模块进行二次开发。1. 环境监测软件选Java本质是选一条能落地的数据链路一套环境监测系统真正难的不是画曲线而是处理“数据从哪来、怎么存、怎么在页面上接近实时地出现”这条链路。以这套Java实现的环境监测系统源码为例核心流程是设备端按周期上报温湿度、PM2.5、VOC等指标服务端先做校验再写入MySQL前端页面通过WebSocket订阅增量数据独立的报警模块按预设规则判断是否触发告警。对正在做环境监测软件相关项目的Java开发者来说这套源码的价值在于把采集任务、数据模型、消息推送、规则判断拆成独立的模块既容易讲清楚设计思路也方便后续替换成生产级组件。下面把这条链路逐段拆开。2. 表结构与实体映射环境监测系统的数据底座2.1 设备、指标、规则、日志四张表的职责边界如果按业务的一时冲动建表很容易陷入字段冗余。覆盖主流程只需要四张核心表具体的表结构如下。表名作用关键字段索引建议device采集终端信息id、device_code、name、location、statusdevice_code 唯一索引env_record环境指标明细id、device_code、temperature、humidity、pm25、voc、collect_time(device_code, collect_time) 联合索引alarm_rule设备报警规则id、device_code、metric、operator、threshold、notify_typedevice_code 索引alarm_log报警触发记录id、rule_id、device_code、metric、real_value、alarm_time、handle_statushandle_status 索引我一般会强调一个容易被忽略的设计点在env_record里存逻辑外键device_code而不是device_id。原因有两个一是设备编号在排查问题时可以直接从日志里看出来不用再做一次ID映射二是InnoDB的外键约束在写入量大时会放大锁开销尤其是采集周期短、批量插入频繁的场景。既然本项目选择用Java直连MySQL就没必要增加这道约束。env_record表的联合索引(device_code, collect_time)是整个查询性能的关键。页面上最常见的请求是“查某个设备某段时间的温度曲线”没有这个联合索引时MySQL会走全表扫描数据量超过一万条就已经有可感知的延迟。反过来如果查询条件只有collect_time而没有device_code这个联合索引也无效所以接口层必须保证两个条件同时传入。2.2 实体类与MyBatis批量写入Java侧采用标准的Entity、Mapper、Service三层结构。环境指标实体类如下。public class EnvRecord { private Long id; private String deviceCode; private BigDecimal temperature; private BigDecimal humidity; private BigDecimal pm25; private BigDecimal voc; private LocalDateTime collectTime; public static EnvRecord of(String deviceCode, BigDecimal temperature, BigDecimal humidity, BigDecimal pm25, BigDecimal voc, LocalDateTime collectTime) { EnvRecord record new EnvRecord(); record.deviceCode deviceCode; record.temperature temperature; record.humidity humidity; record.pm25 pm25; record.voc voc; record.collectTime collectTime; return record; } // getter、setter 省略 }这里保留BigDecimal而不是double是有实际考量的。温湿度在后续计算体感温度、空气质量指数时需要做乘除运算double的累积误差会以小数点后第三位的形式暴露在报表里。MySQL侧对应字段用DECIMAL(5,2)、DECIMAL(5,2)、DECIMAL(7,1)、DECIMAL(6,2)分别对应温度、湿度、PM2.5和VOC。Mapper层逐条insert会有明显的网络往返成本单次采集周期如果有几百个设备这个开销是叠加放大的。推荐用批量插入insert idbatchInsert parameterTypelist INSERT INTO env_record (device_code, temperature, humidity, pm25, voc, collect_time) VALUES foreach collectionlist itemr separator, (#{r.deviceCode}, #{r.temperature}, #{r.humidity}, #{r.pm25}, #{r.voc}, #{r.collectTime}) /foreach /insertMyBatis的foreach批量拼接是最常见的做法。需要注意MySQL的max_allowed_packet默认是64MB单次batch超过上千条记录时由于每条记录都带字符串和时间戳预估包大小后按500条一批拆分更稳妥否则会出现类似“PacketTooBigException”的连接中断。采集频率越高这个参数越值得确认不能只在小数据量下跑通就认为没问题。2.3 Service层的事务边界Service层建议拆成三个入口采集入口、查询入口、报警入口。采集入口只负责写入查询入口只负责读取报警入口在采集完成后异步触发。这样无论环境监测软件的需求怎么演进核心读写路径都不会互相干扰。事务边界是一个容易踩坑的地方。env_record写入不应该和alarm_rule查询放在同一个REQUIRED事务里报警规则属于读多写少的配置数据每轮采集都重新开启大事务会放大锁等待时间。把报警判断放到事务提交之后执行既保证环境数据先落库又不会让规则查询拖累写入性能。3. 定时采集、数据校验与报警环境监测服务端的核心链路3.1 定时任务的防重入与失败补偿Java环境监测系统的采集任务通常用Spring的Scheduled驱动。下面的代码是一个带防重入保护的采集骨架。Component public class CollectTask { private final DeviceMapper deviceMapper; private final EnvRecordService recordService; private final AtomicBoolean running new AtomicBoolean(false); Scheduled(cron 0 */5 * * * ?) public void collect() { if (!running.compareAndSet(false, true)) { return; } try { ListDevice devices deviceMapper.selectOnlineDevices(); for (Device device : devices) { // 实际工程中通过 Modbus、MQTT 或 HTTP 上报获取指标 MapString, BigDecimal metrics readMetrics(device.getDeviceCode()); recordService.save(device.getDeviceCode(), metrics); } } finally { running.set(false); } } }防重入的核心是AtomicBoolean的compareAndSet它能保证同一时刻只有一个采集线程在执行。Spring Boot默认调度线程池大小是1但如果手动调整过线程池参数或者未来改成多实例部署任务就可能重叠执行。用running标志可以避免双写和重复报警。失败补偿的策略要分场景。设备离线导致采集异常时我会在readMetrics里把失败原因写入device_status_log表下一次定时任务再尝试连接连续三次失败就把device表的状态改为offline前端显示“设备掉线”而不是继续画一条假曲线。采集间隔建议用cron表达式控制它能精确到整点执行而fixedRate是任务结束后才开始计时任务耗时一旦不稳定采集节奏就会漂移。3.2 数据校验与异常指标标记从设备侧拿到的数据不能直接入库要先做范围校验。public class MetricValidator { private static final MapString, double[] RANGES new HashMap() {{ put(temperature, new double[]{-40, 60}); put(humidity, new double[]{0, 100}); put(pm25, new double[]{0, 2000}); put(voc, new double[]{0, 60000}); }}; public static boolean check(String metric, BigDecimal value) { double[] range RANGES.get(metric); if (range null) { return false; } return value.doubleValue() range[0] value.doubleValue() range[1]; } }参数说明temperature范围按户外设备定义室内场景可以收窄到050pm25上限取2000是考虑到沙尘等极端天气时实际值可能超过999。每个指标的范围要和传感器量程匹配这是环境监测软件开发里经常被忽略的环节。校验失败的数据写入alarm_log并标记quality_flag0而不是静默丢弃。这个设计后续在评估数据质量时很有价值因为它保留了原始异常记录而不只是丢了一行数据。3.3 规则判断与通知解耦报警模块不推荐把规则硬编码在if-else里。用alarm_rule表存储规则判断逻辑只依赖操作符和阈值即可。public boolean judge(String metric, BigDecimal value, AlarmRule rule) { int cmp value.compareTo(rule.getThreshold()); return switch (rule.getOperator()) { case - cmp 0; case - cmp 0; case - cmp 0; case - cmp 0; case EQ - cmp 0; default - false; }; }关键点是compareTo而不是equals。BigDecimal的compareTo忽略精度差异2.0和2.00在它看来相等equals反而会判不等。规则判断如果直接用doubleValue做比较在阈值是小数的场景容易出错。报警通知建议用Spring事件机制解耦采集完成后发布AlarmCheckEvent监听器里做规则判断命中后异步发送通知。这样采集主链路不会被通知耗时阻塞。通知方式平均耗时可靠性适用场景站内信入库小于10ms高默认必选邮件200ms~2s中值班通知短信/Webhook300ms~5s依赖第三方紧急告警4. 实时监测面板WebSocket推送与曲线可视化4.1 WebSocket推送增量数据环境监测软件最影响体验的地方是数据刷新方式。传统方案是前端轮询接口每秒请求一次页面看着还行但服务端会积累大量无效请求。更合适的方式是建立WebSocket长连接服务端在数据落库后主动推送。ServerEndpoint(/ws/env/{deviceCode}) public class EnvWebSocket { private static final MapString, Session SESSIONS new ConcurrentHashMap(); OnOpen public void onOpen(Session session, PathParam(deviceCode) String deviceCode) { session.getUserProperties().put(deviceCode, deviceCode); SESSIONS.put(session.getId(), session); } OnMessage public void onMessage(String message, Session session) { if (ping.equals(message)) { session.getAsyncRemote().sendText(pong); } } public static void sendToDevice(String deviceCode, String payload) { SESSIONS.values().forEach(s - { if (deviceCode.equals(s.getUserProperties().get(deviceCode))) { s.getAsyncRemote().sendText(payload); } }); } }这段代码有两个需要留意的点。第一用session.getId()做key可以避免同一个浏览器开多个标签页时相互误关第二sendToDevice遍历所有会话在设备数几十到上百的监测场景是够用的设备量到千级后就应该改成deviceCode映射到SetSession的结构。推送时机要与采集任务错开。采集任务在整点前后执行推送则在数据完成校验和入库之后触发。这里用Spring的ApplicationEvent做解耦比较合适采集完成后发布EnvRecordInsertedEventWebSocket监听器收到事件后把最新的指标推给对应设备的订阅端点。这样采集线程和推送线程各自独立互不阻塞。4.2 曲线查询与聚合SQL前端曲线图请求的数据通常不是原始明细而是聚合后的采样点。接口用一个参数控制聚合粒度。GetMapping(/api/env/trend) public ListEnvTrendVO trend(String deviceCode, RequestParam String start, RequestParam String end, RequestParam(defaultValue raw) String interval) { if (hour.equals(interval)) { return recordService.selectHourly(deviceCode, start, end); } return recordService.selectRaw(deviceCode, start, end); }interval的作用是控制前端拿到的数据点数。查询一天的数据时按小时聚合查询一周的数据时按天聚合否则前端要接收上千个原始点ECharts渲染时会明显卡顿。聚合SQL与明细查询走的是不同路径SELECT DATE_FORMAT(collect_time, %Y-%m-%d %H:00) AS bucket, ROUND(AVG(temperature), 2) AS avg_temp, ROUND(AVG(humidity), 2) AS avg_hum, MAX(pm25) AS max_pm25 FROM env_record WHERE device_code #{deviceCode} AND collect_time BETWEEN #{start} AND #{end} GROUP BY bucket ORDER BY bucket;注意聚合口径不能一刀切。温度可以算平均值PM2.5应该看日均值和峰值VOC则需要看超标频次这些指标的业务含义不同。聚合周期和指标维度要一起传给前端前端按照指标类型渲染不同形式的图表才能分辨哪些是持续性问题、哪些是瞬时超标。4.3 ECharts页面集成前端用原生HTML加ECharts足矣环境监测的曲线图没有用到特别复杂的图表类型。典型做法是页面加载后请求一次接口然后通过WebSocket增量更新最后几个点。const chart echarts.init(document.getElementById(trend)); fetch(/api/env/trend?deviceCodeDEV-001start2025-06-01 00:00end2025-06-01 23:59intervalhour) .then(res res.json()) .then(data { chart.setOption({ xAxis: { type: time }, yAxis: { type: value, name: 温度(℃) }, toolbox: { feature: { dataZoom: {}, saveAsImage: {} } }, series: [{ type: line, data: data.map(item [item.bucket, item.avgTemp]), smooth: true, symbol: none, areaStyle: { opacity: 0.15 } }] }); });接口字段与图表的映射放在VO层完成Controller只返回VO对象。VO对应EnvTrendVO和AlertVO两个结构化类型这样实体的字段变更不会直接波及前端契约。图表下方的指标卡片直接复用同一条接口数据展示当前值、超标时长和设备状态卡片信息与曲线数据保持一致避免出现前端各组件改一处漏一处的情况。5. 部署、性能优化与常见坑位5.1 设备掉线检测的实现技巧设备是否在线不能只看最后一条记录的时间。环境监测系统里传感器偶发断连是常态断电、网线松动、网关重启都会造成采集中断。我的做法是单独维护device.last_heartbeat_time字段每一条采集记录写入时同步更新心跳时间同时用一个清理任务把心跳超过三倍采集周期的设备标记为离线。UPDATE device SET status offline WHERE status online AND last_heartbeat_time DATE_SUB(NOW(), INTERVAL 15 MINUTE);这段SQL里的15分钟来自采集周期5分钟乘以3。这个倍率不是拍脑袋定的而是考虑到网络抖动、设备重启、服务端GC停顿等情况2倍周期可能误判3倍是一个相对合理的值。执行频率设为一分钟一次即可带来的开销可以忽略。状态变更后通过WebSocket推送一条设备离线事件前端把这个设备的所有指标卡片置灰避免用户看到一条断掉的时间线还以为是服务端出了问题。5.2 Linux部署环境检查清单Java环境监测系统部署到服务器时最先要确认的是JDK版本和编码格式。源码在Windows本地开发代码用了中文注释Linux服务器上如果不加JVM参数日志和数据库读写会出现乱码。在启动脚本中显式指定UTF-8nohup java -jar env-monitor.jar \ -Dfile.encodingUTF-8 \ -Xms512m -Xmx1024m \ -Dspring.datasource.urljdbc:mysql://localhost:3306/env_monitor?useUnicodetruecharacterEncodingutf8rewriteBatchedStatementstrue \ --spring.profiles.activeprod app.log 21 注意rewriteBatchedStatementstrue这个参数。JDBC如果没有开启它rewriteBatchedStatement不会把多个insert改写为多值insertMyBatis的批量插入会退化成逐条执行。很多环境监测软件源码在本地跑没问题一上服务器就慢原因往往就在数据库连接串少这个参数。排查时同时检查MySQL的max_allowed_packet确保两者匹配。5.3 排查报警延迟的路径报警延迟出现时不要先怀疑规则引擎按“采集任务是否准时执行 - 数据是否落库 - 事件是否发布 - 监听器是否消费”的链路逐段排查。第一步看定时任务日志里有没有超时记录第二步确认env_record的collect_time和当前时间是否吻合第三步看事件发布日志与消费日志的时间差。90%以上延迟问题出在采集任务被前一批数据拖住而不是推送或判断逻辑有问题。# 查看最近一次采集任务的时间与当前时间差 mysql -e SELECT MAX(collect_time), NOW() FROM env_monitor.env_record;这条SQL几毫秒就能确认数据链路是否在流转。环境监测系统的实时性不是靠某个单点组件保证的而是采集、入库、推送、展示四个环节的时间差总和任何一个环节出现漂移前端都会表现为数据陈旧。把这个排查路径固化下来比反复看代码更高效。本文还有配套的精品资源点击获取
