
简介基于Java的智慧农业物联网平台设计源码面向农业信息化开发者、物联网专业学生及智慧农业项目从业人员旨在解决农业生产环境的智能感知与精准管控问题。平台整合气象站、水肥一体机、墒情监控和视频监控四大模块覆盖环境数据采集、智能预警、水肥决策与可视化远程管理为传统农业数据化转型提供了可运行的参考实现。资源包共21个文件以12个Java源文件为核心辅以Markdown说明文档、Maven构建配置pom.xml、mvnw、properties配置及Git忽略文件等整体压缩后仅99KB目录结构清晰源码按主程序、测试与资源模块分层便于导入IDE后快速定位环境数据解析、设备控制、报警联动等典型业务流程。已有495人浏览学习。开发者可获得完整的平台雏形理解多模块交互逻辑并能结合不同作物或区域需求扩展传感器接入、灌溉策略和远程监控界面适合用于课程设计、毕业设计或企业原型验证。1. 一套基于Java的智慧农业物联网平台源码到底在解决谁的麻烦这套基于Java的智慧农业物联网平台设计源码一句话说清楚它把气象站、水肥一体机、墒情传感器和视频摄像头四类硬件统一接进同一套Java后端让农户和运维人员在一张Web大屏上同时看到空气温度、土壤湿度、设备启停和现场画面并在条件越限时自动联动灌溉、施肥。它解决的是传统农业里四套系统互不相通、数据靠人抄表的问题也是目前物联网毕业设计、农业信息化项目里最常被选中的工程骨架。适合想快速搭出可演示系统的人也适合要做真实农业项目落地的Java工程师——前者看它能交差后者看它怎么扛住Modbus轮询、RTSP拉流和联动规则这些脏活。等真正把气象站、水肥机和摄像头接进同一套Java服务之后才会发现麻烦不在写接口而在把几十种传感器协议揉成一整套可维护的数据模型。下面从选型开始拆。2. 技术选型和工程骨架为什么是 Spring Boot Netty EMQ 这套组合2.1 硬件侧先理清网关和传感器不是一回事很多第一次接触农业物联网的人把“传感器”和“网关”混在一起拿到源码后对着配置文档一头雾水。实际上设备端通常分两层最底层是土壤温湿度、空气温湿度、风速风向这类传感器它们通过RS485总线或模拟量输出往上一层是物联网网关负责给传感器供电、定时轮询、把RS485上的Modbus RTU报文转换成TCP/IP数据包再主动连到Java平台的接入端口。常见做法是用STM32跑FreeRTOS做成农业网关或者直接用市面上的4G DTU加串口服务器。做这套Java平台时你根本不需要关心传感器怎么采集只需要面向“网关”这个唯一设备建模。网关和传感器的关系必须先画清楚否则后面所有点位映射都会翻车。一张简表能说明分工层级典型硬件负责的事Java平台需要处理的传感层土壤墒情探头、气象百叶箱、雨量筒感知物理量RS485/模拟量输出无不直接接触网关层STM32网关、DTU、串口服务器轮询传感器、Modbus RTU转TCP、维护长连接设备登录、心跳、报文解析平台层Java后端 MySQL Redis数据入库、阈值判断、设备联动业务主战场2.2 Java侧为什么不是单纯 Spring Boot MyBatis如果只是做管理后台Spring Boot MyBatis 确实够了。但农业物联网平台面前横着两个硬需求一是成千上万个网关要同时保持TCP长连接二是气象站和水肥机之间要做秒级联动。Spring Boot自带的Tomcat容器适合短连接HTTP请求硬用来做长连接接入层线程数和内存很快被拖垮。所以这套源码的核心思路是分两层Netty负责设备接入层Spring Boot负责Web API和业务编排中间通过EMQ消息队列解耦。这种结构在行业里很常见类似thinglinks这类开源物联网平台也几乎都采用“Netty做连接、消息队列做缓冲、业务服务做处理”的路线。Java工程师拿到这套源码后改得最频繁的通常不是Netty部分而是用Spring Boot写的联动规则服务。工程里的关键依赖大致是这个形态dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdio.netty/groupId artifactIdnetty-all/artifactId version4.1.100.Final/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdorg.eclipse.paho/groupId artifactIdorg.eclipse.paho.client.mqttv3/artifactId version1.2.2/version /dependencyNetty版本建议锁定4.1.x不要轻易上5.x后者改动较大且社区维护路线不同。MyBatis-Plus在这里的价值是快速做单表CRUD点位配置、告警记录、设备台账这些表结构都很规整用Plus能省掉大量重复的XML。MQTT客户端是给EMQ用的网关采集到的数据先发到EMQ业务服务从EMQ订阅后再落库这样即使MySQL短暂抖动设备上报数据也不会丢。如果你只做演示跳过EMQ直接把Netty收到的报文写进MySQL也能跑但并发一上来就会看到数据库连接池被打满。2.3 工程目录长什么样先从哪几个包读起拿到源码第一件事不是看全部代码而是找包结构里跟“设备接入”相关的模块。一般这套源码目录结构会是iot-agriculture-platform/ ├── iot-common # 公共工具、统一返回、常量 ├── iot-netty-server # 设备接入层Netty长连接服务 ├── iot-admin # Web管理端接口Spring Boot ├── iot-quartz # 定时任务设备状态巡检、统计 └── iot-agri-core # 业务核心气象/墒情/水肥/视频阅读顺序建议先从iot-agri-core里的device.product和device.point两个实体看起把设备模型搞清楚再回iot-netty-server看报文接入最后才看iot-admin里的Controller。很多人在源码里迷路是因为一上来就盯Controller结果连一个点位数据从哪来的都找不到。设备模型是整个平台的骨架气象站、水肥一体机、墒情探头看起来是三类设备但在数据模型里它们统一被抽象为“产品 网关 点位”三元组产品决定点位列表网关决定通信通道点位决定每个数值的存储位置。2.4 启动配置里最容易被忽略的三个参数Spring Boot的application.yml有几十个配置但农业物联网场景下最关键的三个是Netty端口、心跳超时和设备上报的异步线程池。iot: netty: port: 8300 boss-threads: 1 worker-threads: 8 heartbeat-timeout-seconds: 180 max-frame-length: 4096 mqtt: broker: 192.168.1.100:1883 topic-prefix: agri/device/up thread-pool: core: 8 max: 32 queue-capacity: 2048heartbeat-timeout-seconds设成180秒是因为很多农业网关的保活周期是60秒或120秒太短会产生大量误下线告警太长则设备死了半天才发现。max-frame-length设4096是因为一条Modbus批量读寄存器的报文经常超过1024字节尤其气象站一次读取十几个点位时保守一点用4096比较稳。异步线程池的队列容量不要小于2048否则设备集中上报时队列塞满后面进来的数据直接被丢弃表现为大屏曲线突然缺了一段。3. 气象站与水肥一体机接入Modbus报文解析、点位模型与阈值联动3.1 气象站的Modbus报文到底怎么读气象站的数据采集一般是网关主动发起轮询。以最常见的Modbus RTU为例网关周期性地向气象站主控发送读保持寄存器的请求帧比如功能码0x03、起始地址0x0000、寄存器数量0x000A然后气象站返回一大串字节。Java平台拿到的不是温湿度文本而是一段需要自己拼协议的字节流。网关通常已经做了协议转换把RTU帧包装成TCP报文发给Java侧平台拿到Payload后第一个要做的就是把高字节和低字节拼成数值。一套可靠的解析代码会这样写/** * 解析气象站Modbus返回帧 * 帧格式: 地址(1B) 功能码(1B) 字节数(1B) 数据(NB) CRC(2BRTU时存在) * 数据段每2字节为一个寄存器值高位在前 */ public static ListDouble parseModbusFrame(byte[] frame, int startRegister, int registerCount) { ListDouble values new ArrayList(); // 跳过地址和功能码直接移到数据段 int dataIndex 2; // 有些网关会带CRC有些已剥离这里按数据段长度折算 for (int i 0; i registerCount; i) { int high frame[dataIndex i * 2] 0xFF; int low frame[dataIndex i * 2 1] 0xFF; int raw (high 8) | low; // 遇到负数需要做符号扩展 if ((raw 0x8000) ! 0) { raw raw - 0x10000; } values.add((double) raw); } return values; }逻辑说明 0xFF这一步必须加否则Java的byte类型带符号0xFB会被当成-5拼出来的16位整数永远不对。寄存器数量要从设备文档里对清楚不是每个气象站都按连续地址排列有的温度和湿度中间夹着保留地址那种情况不能批量读要按点位表一个个读。返回值统一用Double因为后续联动规则和前端图表都不希望被类型体系绑死。参数上要注意起始寄存器例如风速是0x0000、风向是0x0001、雨量是0x0002但不同厂商定义不同我这里说的是最常见的排列。3.2 点位表把“第几号寄存器”翻译成人话设备报文解析出来后必须落到一张“点位表”才能变成业务数据。点位表是这套源码里最核心的表之一它决定了Modbus里的某个寄存器地址对应UI上的“空气温度”还是“土壤湿度”也决定了联动规则能读到哪些值。建表SQL大概是这个形态CREATE TABLE device_point ( id bigint NOT NULL AUTO_INCREMENT, device_id bigint NOT NULL COMMENT 关联网关设备ID, point_code varchar(64) NOT NULL COMMENT 点位编码如 air_temperature, point_name varchar(128) NOT NULL COMMENT 点位名称, register_addr int NOT NULL COMMENT Modbus寄存器地址, register_type tinyint NOT NULL COMMENT 0-保持寄存器 1-输入寄存器, data_type tinyint NOT NULL COMMENT 0-int16 1-uint16 2-float32, scale decimal(10,4) DEFAULT 1.0000 COMMENT 缩放系数, unit varchar(16) DEFAULT NULL COMMENT 单位如 ℃、%RH、m/s, report_interval int DEFAULT 30 COMMENT 上报周期秒, is_alarm tinyint DEFAULT 0 COMMENT 是否参与告警, alarm_min decimal(10,2) DEFAULT NULL, alarm_max decimal(10,2) DEFAULT NULL, PRIMARY KEY (id), KEY idx_device_addr (device_id, register_addr) ) COMMENT设备点位配置表;运行逻辑说明设备上报原始报文字节后接入层每解析出一个寄存器值就拿着device_id和register_addr去点位表匹配匹配上再用scale做一次换算比如有些土壤湿度传感器输出的原始值是0~1023需要乘以0.1才是实际百分比。report_interval的作用不是让平台控制传感器而是给看板模块做降采样聚合用的——大屏显示最近一小时曲线时不需要每一条原始记录都画上去按点位配置的周期做Aggregation反而更顺滑。这张表在源码里既是静态配置也是运行时热更新的对象改一个报警阈值不必重启服务。3.3 水肥一体机联动不能只判断一次要有“持续N秒”的防抖水肥一体机在最简单的实现里就是一个可控的灌溉泵和施肥泵组合平台通过继电器或者Modbus写线圈来控制启停。判断逻辑本身很朴素墒情过低就开泵浇水温度过高就开风机。但直接这么写会在现场被坑死因为土壤湿度传感器受探头接触影响会产生瞬时毛刺比如刚翻地后探头悬空读数瞬间冲到0系统如果立刻触发浇水地还没干就先涝了。因此联动规则必须加防抖条件——同一个越限状态要连续稳定保持一段时间才算成立。下面这段是联动服务的核心判断Override public void evaluateRule(DeviceMetric metric) { // 墒情值连续5次低于阈值才触发灌溉 String pointCode metric.getPointCode(); if (soil_humidity.equals(pointCode)) { int count redisTemplate.opsForValue() .increment(tmp:soil_low: metric.getDeviceId()); if (metric.getValue() 30.0) { // 数值恢复后清零计数 redisTemplate.delete(tmp:soil_low: metric.getDeviceId()); return; } redisTemplate.expire(tmp:soil_low: metric.getDeviceId(), 10, TimeUnit.MINUTES); if (count 5) { sendCommand(metric.getDeviceId(), water_pump, true); redisTemplate.delete(tmp:soil_low: metric.getDeviceId()); saveRuleLog(metric, soil_humidity_low, 自动开启水肥机灌溉); } } }参数说明这里用Redis的INCR做连续计数而没有用数据库查询状态在内存中判断10秒内连续5次低于阈值才会执行联动。expire设置10分钟是为了防止设备长时间失联时Redis里塞满无用的计数键。30.0这个阈值是示例真实项目要根据不同作物的根系深度和土壤类型去标定不要照抄。联动触发后要立即复位计数否则下次标记会以更高的初始值起步。所有联动行为需要写入rule_log表这是一条血泪经验——现场农技人员最怕“系统偷偷开泵”有完整日志才能追溯每一次执行的原因。3.4 联动规则的参数设置记住三个原则联动规则不是配置得越激进越好。做农业项目几年后我一般建议遵循三个原则第一所有联动必须可手动旁路现场检修水肥机时如果平台还在自动开泵会出安全事故第二做联动判断尽量用“削峰”后的数值比如一分钟均值而不是瞬时值避免传感器信号跳变导致设备频繁启停第三报警阈值要比联动阈值宽松先把异常告诉人再让系统介入设备操作顺序反了会变成机器和人抢控制权。这三条是农业物联网现场和纯工业SCADA最大的区别后者追求自动前者必须留人。4. 墒情监控与视频监控时序数据落库、一张大屏两种流4.1 墒情数据落库为什么推荐按天分表而不是堆一张大表墒情传感器每10~30分钟上报一次单台设备一天约产生48~144条记录30台设备一个月就有13万条左右。如果所有设备共享一张大表随着点位数量增长查询最近一周曲线时MySQL会越来越吃力对于这种按时序写入、按时间范围读取的数据最直接的做法是分表。轻量方案是按“点位日期”分表中等规模用MySQL分区表规模再往上才需要迁移到时序数据库。这套Java源码里比较实用的做法是每天凌晨由定时任务检查并创建当天的分区表写入层按日期路由。CREATE TABLE soil_humidity_20250412 ( id bigint NOT NULL AUTO_INCREMENT, device_id bigint NOT NULL, point_code varchar(64) NOT NULL, value decimal(10,3) NOT NULL, collect_time datetime NOT NULL, PRIMARY KEY (id), KEY idx_device_time (device_id, collect_time) ) ENGINEInnoDB COMMENT墒情时序表按日期切分;建表时要有心理准备如果业务代码里到处写死了表名分表后所有查询都要跟着改。见过最惨的翻车是把路由逻辑只写在Service里定时任务和报表模块直接拼了原始表名导致数据写进新表而报表还在查旧表大屏曲线突然静止。建议在项目一开始就做一个TableRouter工具类所有访问时序数据的入口都走这个类来解析表名避免后面补丁式修改。4.2 视频监控Java后端该碰多少不该碰多少视频监控是这套平台里最容易产生误解的模块。很多人以为Java源码里应该包含海康SDK、RTSP解码头结果一打开发现只有一个流媒体网关配置就误以为功能是假的。实际上Java后端在视频链路里最适合扮演的角色是“取流入口 鉴权 播放地址管理”而不是把每一路视频码流都拉进JVM进程里做转发。Rtsp拉流、转封装、分发这些重活在真实项目里通常交给专门的流媒体服务器来做Java侧只需要告诉他“哪路摄像头应该从哪里拉流播放凭证是什么”。一个典型的Controller只做地址鉴权和拼接GetMapping(/video/playUrl) public ResultString getPlayUrl(RequestParam String cameraCode, RequestParam String token) { // token校验避免摄像头地址被到处乱发 if (!checkToken(token)) { return Result.fail(401, 非法访问); } CameraInfo info cameraService.getByCode(cameraCode); // 流媒体服务器对外统一提供 http-flv 预览地址 String url String.format( http://%s:%s/live/%s.flv?sign%s, streamConfig.getHost(), streamConfig.getPort(), info.getStreamName(), buildSign(info) ); return Result.ok(url); }逻辑说明streamName通常是由摄像头信息映射出来的唯一流名比如cam_0001_01上级流媒体服务器已经在后台通过RTSP拉取摄像头画面并把该路流转成FLV或HLS。buildSign生成带过期时间的签名避免大屏上的播放地址被别人拿走直接观看。直播技术栈上Web端用HTTP-FLV延迟在1~3秒配合视频监控足够如果用HLS能看到5~10秒以上的延迟对“看现场”可以接受但想配合云台控制就很难受了。这里把真正做视频接入的活留在流媒体服务器Java平台保持轻量性能上不会因为同时拉几路流而耗尽内存。4.3 大屏看板WebSocket推送比前端轮询更合适墒情数据和气象数据如果全部靠前端定时轮询30秒一次虽然压力不大但4G网络下每次轮询的响应时间不稳定画面上一卡一卡的体验很差。用WebSocket推送更适合看板场景后端每收到一条新数据就实时推给前端图表更新延迟几乎为零。Netty接入层解析完成后消息先走EMQ大屏服务订阅EMQ主题再通过WebSocket广播到前端。下面这段是后端主题订阅与推送的核心写法RabbitListener 或 MqttSubscriber public void onDeviceReport(String topic, String payload) { // topic 形如 agri/device/up/{deviceId} DeviceReport report JSON.parseObject(payload, DeviceReport.class); String cacheKey realtime:device: report.getDeviceId(); redisTemplate.opsForValue().set(cacheKey, payload, 30, TimeUnit.SECONDS); // WebSocket按设备维度广播给登录的大屏页面 wsSessionManager.sendToSubscribers(report.getDeviceId(), payload); }cacheKey的过期时间设30秒是为了配合设备心跳——超过30秒没收到前端就该显示灰色离线态了。wsSessionManager需要在设备级别维护订阅关系而不能粗暴地把所有消息广播给所有人否则十几个设备上报时每个屏幕都收到全量数据物联网场景下行流量会无谓地增大。5. 排查避坑设备离线、传感器IP错位这些高频坑现象、原因与解决5.1 离线问题设备明明在跑平台却判定“掉线”现象现场网关绿灯常亮传感器也有读数但Java平台设备列表里显示离线或者数据曲线停在上一次上报的时间点后不再更新。原因最常见的是心跳超时参数不匹配。我之前调试过一个项目平台配置的心跳超时是120秒但现场网关的心跳周期被厂家设置成180秒网关自认为还在上报平台却因为超过120秒没收到数据包就把连接标记为失效。还有一种是网关主动断开连接后平台侧没有正确处理TCP的半包关闭事件连接已经断了但Netty管道里还挂着旧Channel。解决先把心跳超时和网关实际发送周期对齐一般把平台的超时窗口设置为设备上报周期的3倍。再看Netty的channelInactive方法里有没有主动清理Redis中的在线状态很多离线不消失的bug都出在这里。另外排查一种隐蔽情况现场有两台网关被配置成同一IP交换机ARP表反复横跳报文到了但是对不上会话——这是“物联网网关与传感器的IP关系”没理清的典型表现解决方法是让每台网关用独立的IP或者靠设备ID做连接鉴权而不要依赖IP识别设备。5.2 联动误触发水肥机半夜自动开泵这个坑最吓人现象夜间没有人在现场水肥一体机突然启动等第二天才发现某个区域的土壤已经被灌透。原因大部分情况是传感器数值毛刺加联动规则太灵敏。墒情探头和土壤接触不良时读数会瞬间掉到很低可能只持续几秒就恢复正常但联动判断只认“当前值低于阈值”这一条瞬间触发。另外联动规则没有考虑设备“禁止自动时间段”深夜执行了本应只在白天执行的指令。解决联动逻辑必须加连续次数消抖参考第三章的做法连续N次才触发同时给每个联动规则加时间窗比如晚上22点到凌晨6点不自动执行只发送告警。还有一点容易忽略水肥机本身如果带有手动/自动切换旋钮平台侧要在联动前读一次设备状态寄存器确认设备在“自动”档才能下发命令否则会出现平台开了泵但现场没反应或者现场开了泵却被平台强行关掉的矛盾。5.3 Netty内存异常增长盯着GC没用问题在堆外内存现象服务运行几天后内存占用曲线一路走高Metaspace和堆内存看起来都很正常但操作系统层面Java进程占用的内存已经接近机器物理内存最终被OOM Kill。原因Netty默认使用直接内存做数据缓冲解码后的ByteBuf如果没有被正确release就会产生堆外内存泄漏。最常见的写法是在channelRead里取出ByteBuf后只toString转成字符串却没有调用ReferenceCountUtil.release(msg)或者忘记对解码器产出的对象做释放。农业物联网报文虽然短但连接数一多泄漏速度会非常可观。解决凡是SimpleChannelInboundHandler中处理完的消息Netty会自动释放而如果用普通ChannelInboundHandler必须手动ReferenceCountUtil.release(msg)。建议在解码器上优先继承ByteToMessageDecoder它内部会自己管理引用计数不容易出问题。排查阶段可以打开Netty的-Dio.netty.leakDetection.levelparanoid把可疑泄漏点直接打到日志里定位效率比人肉翻代码高很多。5.4 视频流在Web端打不开大屏盒子装了个黑匣子现象摄像头在线流媒体服务器日志显示RTSP拉流正常但大屏页面上的播放器一直黑屏转圈。用浏览器直接访问发布的FLV地址也能出画面一旦嵌到大屏页面就失败。原因这种问题十有八九是跨域限制或鉴权参数丢失。Web播放器通过fetch或WebSocket访问流媒体服务器时如果流媒体服务端没有设置允许跨域响应头浏览器会拦截请求。另一个常见原因是播放地址里带sign参数但前端把参数解析丢了或者是播放器初始化时URL里带了空格和特殊字符没有encode。解决给流媒体服务器加上跨域配置播放URL统一用encodeURIComponent处理。排查步骤是先打开浏览器开发者工具的Network面板看请求状态如果看到CORS错误就是跨域问题如果看到403/401就是鉴权参数不对网络正常的黑屏则要看流格式是否被浏览器支持老版本Safari对FLV支持很差需要改成HLS源切换逻辑。农业现场经常用老旧平板看大屏建议前端保留“FLV优先HLS兜底”的双线路。6. 验证闭环一台模拟传感器把气象到水肥联动的整条链路跑通源码到手后最廉价的验收方式不是买一堆真硬件而是写一个模拟器假装自己是网关按真实设备的上报周期往平台推数据。先准备一个Java命令行工程启动后主动连接Netty端口每5秒上报一次模拟的“空气温度”和“土壤湿度”报文。模拟器代码里温度固定输出25度湿度每10次上报随机下降一次从50降到15这样就能在10秒内触发一次“土壤湿度连续5次低于阈值”的联动执行——如果水肥机也带着模拟状态就能在控制日志里看到开泵记录。验收时按下面这张表逐项核对比人肉盯大屏靠谱验证项操作预期结果判定标准气象数据入库模拟器上报温度数据库出现新记录且数值正确误差不超过1位小数墒情曲线模拟器连续上报30分钟曲线平滑且无断点时间连续无30秒以上缺口联动触发模拟器把湿度降到155次内触发水肥机开泵日志出现联动记录越限告警模拟器上报温度超过报警上限告警列表出现新纪录告警类型和设备正确视频入口配置一路测试流大屏播放器能出画面3秒内完成缓冲这个验收闭环我每次做农业物联网项目都会先跑一遍再上真设备。真硬件到了现场再排错成本和时间消耗都太大模拟器能提前暴露协议字段错位、联动规则条件不成立这些后端问题。需要提醒的是模拟器的报文必须严格对照点位表字段尤其注意高低字节序和缩放系数否则“假数据”会把验收结果带偏。回头看我踩过的这些坑最值得记住的教训就是——联动规则一定要有防抖视频拉流一定要和流媒体服务器分开部署Netty的ByteBuf一定要养成随手释放的习惯。希望这条验证路径能帮到你。本文还有配套的精品资源点击获取