简介:这是一套面向高校计算机专业学生与Java初学者、用于毕业设计或课程实践的环境监测系统完整源码,围绕空气质量、噪声、温湿度等环境数据的采集、处理、分析与可视化展开,帮助读者理解一个典型Java Web项目的分层架构与业务实现。压缩包共46个文件,约29KB,以35个Java源文件为核心,涵盖监控管理方案与用户管理两大模块的Controller、Service、DAO及DTO分层代码,另附数据库设计、数据库更新脚本、README说明与text文档,便于快速梳理表结构与接口调用关系。项目采用MVC模式组织代码,涉及Spring依赖注入、Servlet与JSP后端接口、JDBC数据库操作以及RESTful风格API设计等知识点,读者可据此掌握从数据持久化到前端展示的完整链路。目前已有702人学习下载,适合作为毕业设计参考模板,也可用于二次开发与课程答辩准备。
1. 环境监测系统源码选型:为什么 Java 技术栈是毕业设计的稳妥牌
做过环境监测系统毕业设计的人都有一个共识:硬件采集端用 STM32 跑传感器,数据回传之后,真正决定项目能不能顺利答辩、能不能在简历上写一笔的,是后端那套 Java 服务。标题里的「JAVA实现的环境监测系统源码」,本质上就是一套能跑通「传感器数据上报 → 入库 → 实时展示 → 阈值告警」闭环的 Spring Boot 工程。它解决的不是算法难题,而是把物联网数据链路和 Web 管理后台拼装成一个可演示、可扩展、可写论文的系统。
适合谁?计算机、物联网、电气类专业的毕业设计同学,以及刚入行想找一个完整 CRUD + 实时推送练手项目的 Java 初学者。热搜里反复出现「毕业设计从 github 抄来」「java 环境配置」「java 启动失败怎么解决」,说明大家卡的不是业务逻辑,而是环境搭建和工程结构理解。这篇笔记就按「先讲清架构选型,再给可复现的代码骨架,最后把踩过的坑摊开」的顺序走,目标是你照着能跑起来,而不是看完只知道有个东西叫环境监测系统。
2. 环境监测系统的技术架构与数据链路拆解
2.1 从传感器到浏览器:一条数据要经过几层
环境监测系统的数据链路可以拆成四段:采集层、传输层、服务层、展示层。采集层常见做法是 STM32 或 ESP32 接温湿度(DHT22/SHT30)、PM2.5(GP2Y1010)、CO2(MH-Z19)等传感器,通过串口或 ADC 读原始值。传输层在毕业设计里最省事的是 MQTT(EMQX 或 Mosquitto 做 broker),设备端用 PubSubClient 或 Paho 把 JSON 发到指定 topic。服务层就是 Java 后端订阅 MQTT,解析后写库,同时对外提供 REST 接口和 WebSocket 推送。展示层用 Vue 或 Thymeleaf 都行,图表用 ECharts。
为什么服务层选 Java 而不是 Python?热搜里「python与java的优缺点」被反复搜,落到这个项目上,Java 的优势是 Spring Boot 生态成熟、MyBatis-Plus 能根据实体类生成建表 SQL(热搜里正好有「mybatisplus根据java实体类生成创建表的sql语句」)、WebSocket 支持稳定,答辩时老师问「你用了什么框架」你能答得清楚。Python 写采集脚本快,但做后台管理和权限控制,Java 的工程化程度更高,论文里也好写分层架构。
2.2 数据库表设计:四张表撑起整个系统
环境监测系统的库表不用多,四张核心表足够:设备表、监测数据表、告警规则表、用户表。监测数据表是增长最快的,设计时要注意时间字段加索引,否则数据量上来查询会明显变慢。
-- 设备表:记录每个采集节点的信息 CREATE TABLE `device` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `device_code` VARCHAR(64) NOT NULL COMMENT '设备编号,与硬件端一致', `device_name` VARCHAR(128) DEFAULT NULL COMMENT '设备名称', `location` VARCHAR(255) DEFAULT NULL COMMENT '安装位置', `status` TINYINT DEFAULT 1 COMMENT '1在线 0离线', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_device_code` (`device_code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 监测数据表:核心业务表,按时间倒序查询 CREATE TABLE `monitor_data` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `device_code` VARCHAR(64) NOT NULL COMMENT '设备编号', `temperature` DECIMAL(5,2) DEFAULT NULL COMMENT '温度 ℃', `humidity` DECIMAL(5,2) DEFAULT NULL COMMENT '湿度 %', `pm25` DECIMAL(6,2) DEFAULT NULL COMMENT 'PM2.5 μg/m³', `co2` DECIMAL(6,2) DEFAULT NULL COMMENT 'CO2 ppm', `collect_time` DATETIME NOT NULL COMMENT '采集时间', PRIMARY KEY (`id`), KEY `idx_device_time` (`device_code`, `collect_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;字段类型选择有讲究:温湿度用 DECIMAL 而不是 FLOAT,避免浮点误差导致告警误判;collect_time 单独建联合索引,因为前端查历史曲线一定是「某设备某时间段」,联合索引能直接命中。device_code 做唯一索引,防止硬件重复注册。热搜里「mybatisplus根据java实体类生成创建表的sql语句」提到的做法,是在实体类上加 @TableName 和 @TableField 注解,配合 MyBatis-Plus 的代码生成器反向产出,但生产环境我一般手写 DDL,因为生成器对索引和注释的处理不够细。
2.3 MQTT 接入与数据解析:Java 端怎么接住硬件上报
Java 端接 MQTT 用 Eclipse Paho 客户端最轻量,Spring Integration MQTT 也可以但配置偏重。下面是一个最小可用的订阅与入库逻辑。
@Component public class MqttSubscriber { // MQTT broker 地址,毕业设计本地跑 EMQX 默认 1883 private static final String BROKER = "tcp://127.0.0.1:1883"; private static final String TOPIC = "env/monitor/+/data"; // + 通配单个设备编号 @Autowired private MonitorDataService monitorDataService; @PostConstruct public void subscribe() throws MqttException { MqttClient client = new MqttClient(BROKER, MqttClient.generateClientId()); MqttConnectOptions options = new MqttConnectOptions(); options.setCleanSession(true); options.setAutomaticReconnect(true); // 断线自动重连,答辩演示时很关键 client.connect(options); client.setCallback(new MqttCallback() { @Override public void messageArrived(String topic, MqttMessage message) { // 硬件上报格式:{"temp":25.3,"hum":60.1,"pm25":35,"co2":800} String payload = new String(message.getPayload()); String deviceCode = topic.split("/")[2]; // 从 topic 提取设备编号 MonitorData data = JSON.parseObject(payload, MonitorData.class); data.setDeviceCode(deviceCode); data.setCollectTime(new Date()); monitorDataService.save(data); // 入库 monitorDataService.checkThreshold(data); // 触发告警判断 } @Override public void connectionLost(Throwable cause) { /* 记录日志 */ } @Override public void deliveryComplete(IMqttDeliveryToken token) { } }); client.subscribe(TOPIC, 1); // QoS 1 保证至少送达一次 } }这段代码的关键点有三个。第一,topic 用通配符+匹配设备编号,新增设备不用改代码。第二,setAutomaticReconnect(true)必须开,否则网络抖动后订阅就断了,答辩现场演示时数据不刷新,翻车概率极高。第三,QoS 设 1 而不是 0,环境数据丢一条可能影响曲线连续性,QoS 1 的重复消息在入库时用 collect_time + device_code 做幂等去重即可。
2.4 实时推送与前端展示:WebSocket 比轮询省多少事
前端要实时看数据变化,轮询是最容易想到的方案,但 3 秒一次轮询在毕业设计演示时会有明显延迟感。用 Spring Boot 的 WebSocket 推送,数据入库后主动推给前端,体验完全不同。
@ServerEndpoint("/ws/monitor") @Component public class MonitorWebSocket { // 用线程安全集合保存所有在线会话 private static final CopyOnWriteArraySet<Session> SESSIONS = new CopyOnWriteArraySet<>(); @OnOpen public void onOpen(Session session) { SESSIONS.add(session); } @OnClose public void onClose(Session session) { SESSIONS.remove(session); } // 由数据入库服务调用,广播最新数据 public static void broadcast(String json) { for (Session s : SESSIONS) { if (s.isOpen()) { try { s.getBasicRemote().sendText(json); } catch (IOException e) { /* 单个会话异常不影响其他 */ } } } } }参数说明:CopyOnWriteArraySet适合读多写少的会话集合,避免并发修改异常。广播时逐个 try-catch,一个前端断开不会影响其他人。前端用原生 WebSocket 或 SockJS 连接/ws/monitor,收到消息后更新 ECharts 的 series 数据即可。这套组合比轮询节省大量无效请求,论文里也能作为「实时性优化」的论据。
3. 从零跑通环境监测系统的最小可执行步骤
3.1 环境准备:JDK、Maven、MySQL、EMQX 四件套
先把四个基础组件装好,版本不用追新,稳定优先。JDK 用 8 或 11(Spring Boot 2.7 对 8 支持最好),Maven 3.6+,MySQL 5.7 或 8.0,EMQX 用 4.x 社区版。热搜里「java 环境配置」是高频问题,核心就三件事:JAVA_HOME 指向 JDK 根目录、PATH 里加 %JAVA_HOME%\bin、Maven 的 settings.xml 配好国内镜像。
# 验证环境是否就绪,四条命令都要有正常输出 java -version # 应显示 1.8.x 或 11.x mvn -v # 应显示 Maven 版本和 JDK 版本 mysql -u root -p # 能进 MySQL 命令行 # EMQX 启动后访问 http://localhost:18083 默认 admin/public如果java -version报「不是内部或外部命令」,说明 JAVA_HOME 没配或 Path 没引用。如果 Maven 编译时卡在下载依赖,检查 settings.xml 的 mirror 是否指向国内源。这两个问题占了「java 启动失败怎么解决」搜索量的一大半。
3.2 Spring Boot 工程骨架:pom 依赖与配置文件
新建 Spring Boot 工程,pom.xml 里引入 Web、MyBatis-Plus、MySQL 驱动、Paho MQTT、WebSocket 五个核心依赖。
<dependencies> <!-- Web 基础 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis-Plus:简化 CRUD,支持实体类映射 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <!-- MQTT 客户端 --> <dependency> <groupId>org.eclipse.paho</groupId> <artifactId>org.eclipse.paho.client.mqttv3</artifactId> <version>1.2.5</version> </dependency> <!-- WebSocket --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> </dependencies>application.yml 里配数据源和 MyBatis-Plus 的映射规则。
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/env_monitor?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true # 数据库下划线转 Java 驼峰 global-config: db-config: id-type: auto # 主键自增serverTimezone=Asia/Shanghai不加会报时区错误,这是 MySQL 8 的经典坑。map-underscore-to-camel-case让 collect_time 自动映射到 collectTime,省去手写 resultMap。
3.3 实体类与 Mapper:MyBatis-Plus 怎么少写一半代码
实体类用注解映射表名和字段,Mapper 接口继承 BaseMapper 就自带增删改查。
@Data @TableName("monitor_data") public class MonitorData { @TableId(type = IdType.AUTO) private Long id; private String deviceCode; private BigDecimal temperature; private BigDecimal humidity; private BigDecimal pm25; private BigDecimal co2; private Date collectTime; } @Mapper public interface MonitorDataMapper extends BaseMapper<MonitorData> { // 自定义查询:查某设备最近 N 条数据 @Select("SELECT * FROM monitor_data WHERE device_code = #{code} " + "ORDER BY collect_time DESC LIMIT #{limit}") List<MonitorData> selectLatest(@Param("code") String code, @Param("limit") int limit); }BaseMapper 提供了 insert、selectById、updateById、deleteById 等基础方法,业务里 80% 的 CRUD 不用写 SQL。自定义的 selectLatest 用注解写 SQL,适合简单查询;复杂联表再上 XML。热搜里「mybatis源码」被搜得多,但毕业设计阶段先把 MyBatis-Plus 用熟,理解 Mapper 代理和 SQL 注入原理即可,不必深挖源码。
3.4 告警判断与阈值配置:别把阈值写死在代码里
告警逻辑最容易犯的错是把阈值硬编码在 if 里,改一个值要重新编译。正确做法是存到告警规则表,启动时加载到内存,判断时查内存。
@Service public class AlertService { // 规则缓存:key 为 deviceCode + 指标名 private Map<String, BigDecimal> thresholdCache = new ConcurrentHashMap<>(); @PostConstruct public void loadRules() { // 从 alert_rule 表加载,格式:deviceCode:metric -> threshold thresholdCache.put("D001:temperature", new BigDecimal("35")); thresholdCache.put("D001:pm25", new BigDecimal("75")); thresholdCache.put("D001:co2", new BigDecimal("1000")); } public void check(MonitorData data) { checkOne(data, "temperature", data.getTemperature()); checkOne(data, "pm25", data.getPm25()); checkOne(data, "co2", data.getCo2()); } private void checkOne(MonitorData data, String metric, BigDecimal value) { if (value == null) return; String key = data.getDeviceCode() + ":" + metric; BigDecimal threshold = thresholdCache.get(key); if (threshold != null && value.compareTo(threshold) > 0) { // 触发告警:写告警记录 + WebSocket 推送 alertRecordService.save(data, metric, value, threshold); MonitorWebSocket.broadcast(buildAlertJson(data, metric, value)); } } }用 BigDecimal 的 compareTo 而不是 equals 比较,因为 equals 会比较精度(35 和 35.00 不相等)。阈值缓存用 ConcurrentHashMap,避免多线程读写问题。告警记录单独存表,方便前端做告警历史列表,论文里也能作为「异常处理机制」的章节素材。
4. 环境监测系统源码落地避坑:五个血泪教训
4.1 现象:前端图表数据不更新,刷新页面才有新数据
原因:WebSocket 的@ServerEndpoint注解类没有被 Spring 管理,@Autowired注入的服务是 null,广播时静默失败。这是 Spring Boot 整合 WebSocket 最经典的坑。
解决:加一个ServerEndpointExporter的 Bean,并且不要在@ServerEndpoint类里直接@Autowired,改用静态 ApplicationContext 获取 Bean,或者把广播逻辑抽到独立的 Spring Service 里,WebSocket 类只负责会话管理。
4.2 现象:MQTT 订阅一段时间后收不到消息,重启才好
原因:MqttClient的 clientId 每次启动随机生成,broker 认为旧会话还在,新连接被踢或消息路由异常。另外没开自动重连,网络抖动后连接断了不恢复。
解决:clientId 用固定前缀 + 设备编号,保证同一设备重连时复用会话;setAutomaticReconnect(true)和setCleanSession(false)配合使用,QoS 至少设 1。
4.3 现象:MySQL 插入中文乱码,或者时间差 8 小时
原因:连接 URL 没加characterEncoding=utf8,或者serverTimezone没设成Asia/Shanghai。MySQL 8 默认时区是 UTC,Java 的 Date 按本地时区写入就会差 8 小时。
解决:URL 里同时加useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai,建库时指定DEFAULT CHARSET=utf8mb4。
4.4 现象:数据量到几万条后,历史曲线查询明显变慢
原因:monitor_data 表只建了主键索引,按 device_code + collect_time 查询走全表扫描。
解决:建联合索引idx_device_time (device_code, collect_time),查询时 WHERE 条件按索引顺序写。如果数据量继续增长,按月份分表或加 Redis 缓存最近数据。
4.5 现象:答辩演示时硬件端没电或传感器坏了,系统一片空白
原因:过度依赖真实硬件,没有模拟数据兜底。
解决:写一个定时任务,当检测到某设备超过 5 分钟没有新数据时,自动生成模拟数据插入,保证演示时图表始终有内容。这个「数据模拟器」在论文里可以写成「系统容错设计」,反而是加分项。
5. 让环境监测系统源码经得起追问的进阶技巧
5.1 用数据模拟器兜底演示,同时验证告警链路
模拟器不只是为了演示好看,它还能帮你验证告警逻辑是否正确。写一个@Scheduled任务,每 10 秒为每个在线设备生成一条随机数据,其中 10% 的概率生成超过阈值的值,这样告警记录和 WebSocket 推送都能被触发。
@Component public class DataSimulator { @Autowired private MonitorDataService dataService; private Random random = new Random(); // 每 10 秒执行一次,fixedRate 表示上次开始后计时 @Scheduled(fixedRate = 10000) public void simulate() { for (String code : Arrays.asList("D001", "D002")) { MonitorData d = new MonitorData(); d.setDeviceCode(code); // 正常范围 20-30℃,10% 概率生成 36-40℃ 触发告警 d.setTemperature(random.nextDouble() < 0.1 ? BigDecimal.valueOf(36 + random.nextDouble() * 4) : BigDecimal.valueOf(20 + random.nextDouble() * 10)); d.setHumidity(BigDecimal.valueOf(40 + random.nextDouble() * 30)); d.setPm25(BigDecimal.valueOf(20 + random.nextDouble() * 40)); d.setCo2(BigDecimal.valueOf(400 + random.nextDouble() * 400)); d.setCollectTime(new Date()); dataService.save(d); dataService.checkThreshold(d); } } }fixedRate和fixedDelay的区别要清楚:fixedRate 按固定频率执行,任务耗时长时会堆积;fixedDelay 是上次执行完再等固定时间。模拟器任务轻,用 fixedRate 没问题。这个类在答辩时可以直接展示「系统在无硬件情况下也能完整运行」,同时告警链路的正确性也顺带验证了。
5.2 用接口文档和日志把项目变成可讲的故事
毕业设计答辩最怕老师问「你这个接口怎么调的」「出错了怎么排查」。提前用 Swagger 或 Knife4j 生成接口文档,每个 Controller 方法加@ApiOperation注解,前端同学和答辩老师都能直接看懂。日志用 Logback 按天滚动,关键节点(MQTT 连接、数据入库、告警触发)都打 INFO 日志,异常打 ERROR 并带堆栈。
@ApiOperation("查询设备最新监测数据") @GetMapping("/latest/{deviceCode}") public Result<List<MonitorData>> latest(@PathVariable String deviceCode, @RequestParam(defaultValue = "20") int limit) { log.info("查询设备 {} 最新 {} 条数据", deviceCode, limit); return Result.ok(monitorDataMapper.selectLatest(deviceCode, limit)); }Result是统一返回体,包含 code、msg、data 三个字段,前端处理起来不用判断 HTTP 状态码。日志里带上设备编号和条数,排查问题时一眼能定位。这套组合拳打下来,你的环境监测系统源码就不只是一个能跑的程序,而是一个有文档、有日志、有容错、有告警的完整工程,论文的「系统实现」和「测试」两章素材全都有了。
我自己的习惯是:每次改完代码,先跑一遍模拟器看告警有没有触发,再看日志有没有 ERROR,最后刷新前端确认图表在动。这三步走完,基本不会在答辩现场翻车。希望帮到你。
本文还有配套的精品资源,点击获取