
简介智慧化养猪App完整工程包以Java技术栈实现定位在智慧农业中的猪场信息化管理适合Android开发学习者、Java编程人员以及农业物联网项目团队作为移动端项目参考。该工程还涉及猪舍环境数据采集、养殖档案维护、异常信息提醒等常见智慧养殖业务能够帮助开发者理解农业App从界面、逻辑到底层服务的完整链路。资源共179个文件、压缩后仅20.14MB工程结构完整清晰38个java文件承载核心业务与交互控制59个xml负责界面布局和数据配置34个png提供界面图形资源25个so内置百度LBS定位等底层能力再加上gradle构建脚本、jar依赖包以及log日志、properties配置基本还原了可直接编译运行的Android/Java应用骨架。目前已有314人学习下载。解压后可深入研究Java源码与xml布局的调用逻辑、gradle工程构建方式、第三方地图SDK接入方法以及资源文件组织规范对快速搭建同类养殖管理App、完成课程设计或梳理移动端项目流程都有直接参考价值。1. 智慧化养猪App要解决的不是“养”而是数据闭环一个三百头母猪的场每天最重的工作不是喂料而是巡栏、记录、翻报表。耳标看不清了发情没抓住产房温度高了半度没发现等看到时已经损失一窝。智慧化养猪App的本质是把猪舍里的传感器、饲喂器、称重秤和人工观察结果统一收进一套系统再由规则或模型给出预警和行动建议。这里“Java 100%”指的是一套可交付、可维护、不依赖多语言混编的实现方案采集网关、业务API、管理后台、Android端尽量落在同一套JVM技术栈上降低养殖场信息化项目的维护成本。适合Java后端、物联网开发者和农业信息化从业者参考本文按Spring Boot 3 JDK 21展开直接给出能落地的架构和代码。2. 智慧化养猪系统的Java全栈架构从REST到MQTT都交给JVM2.1 一条数据从猪舍传感器到App显示要经过几层智慧化养猪App的数据链路并不复杂但每一层都有容易出问题的地方。常见的做法是四层结构感知层是温湿度、氨气、风速、光照传感器通过RS485总线汇聚到现场采集器采集器再以MQTT协议上报到Broker传输层用EMQX或Mosquitto这类独立Broker承担并发接入平台层由Spring Boot服务负责订阅Topic、解析消息、落库并触发规则应用层App通过REST接口拉取历史曲线通过WebSocket或消息推送接收告警。在规模养殖场里一个场区可能有几十个采集器每个采集器每30秒上报一条环境数据。如果后端服务直接同步写库很快会被写入瓶颈卡住。所以平台层要拆分两个职责对上是稳定的数据接入对下是高效的存储与查询。端到端延迟的正常标准是传感器触发上报到App收到告警不超过3秒超过这个数字就要检查Broker配置或消费线程池。2.2 “Java 100%”到底指什么很多人在标题里看到“Java 100%”以为是指后端用Java、前端App也用Android原生Java写。实际上国内养猪信息化项目更常见的组合是后端Java提供API采集网关用Java或C写固件App端用Android原生或Flutter管理后台用Vue。所谓“100%”我理解成“核心业务链路全Java实现”更现实采集接入、业务逻辑、规则引擎、定时任务、AI模型调用都在JVM里完成不引入Python服务或Node中间层这样部署和排障都简单得多。选型上我建议在一个中型猪场项目里固定这套组合组件选型理由后端框架Spring Boot 3.2生态成熟社区答案多招人容易JDK21虚拟线程在IoT场景有明显收益ORMMyBatis-Plus 3.5.x单表操作快分页好用MQTT BrokerEMQX 4.x/5.x并发接入稳定规则引擎顺手消息消费Spring Integration MQTT与Spring Boot集成成本低缓存Redis 7告警去重、热点数据、分布式锁对象存储MinIO猪只图片、视频回放数据库MySQL 8.x中小规模够用报表也好做这套组合的核心优势是只学一套语言体系。Java开发者维护后端的同时也能看懂采集网关的状态日志遇到问题不需要跨技术栈找原因。2.3 项目骨架Spring Boot 3项目需要提前加好的依赖新建一个工程时除了常规的web和mybatis依赖下面这几个是智慧化养猪场景特有的少了后面都得补dependency groupIdorg.springframework.integration/groupId artifactIdspring-integration-mqtt/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.5/version /dependency dependency groupIdorg.redisson/groupId artifactIdredisson-spring-boot-starter/artifactId version3.27.2/version /dependency dependency groupIdio.minio/groupId artifactIdminio/artifactId version8.5.10/version /dependency dependency groupIdcom.microsoft.onnxruntime/groupId artifactIdonnxruntime/artifactId version1.17.1/version /dependency依赖里最容易忽略的是spring-integration-mqtt很多人自己封装MQTT客户端结果断线重连、订阅恢复都要手写而Spring Integration把这几件事都处理好了。Redisson用来做分布式锁和幂等判断比手动操作Redis客户端省事。ONNX Runtime是为后面的猪只识别和体重估算预留的没有识别需求可以去掉但建议先加上因为后续加功能不用改依赖结构。2.4 核心表结构猪只档案、环境记录、饲喂流水、健康事件数据模型决定了业务代码怎么写。智慧化养猪系统里最核心的四张表分别是猪只档案表、环境数据表、饲喂记录表和健康事件表。猪只档案表以耳标号为业务主键RFID和摄像头识别ID都挂在这个表上环境数据表记录温湿度、氨气浓度、风速等指标查询频率最高必须按时间做分区或复合索引饲喂记录表是流水型数据只增不改适合按月分表健康事件表则由规则引擎写入App的预警列表直接查这张表。我给出一个基本的猪只档案表设计其余三张表结构可以在此基础上扩展字段类型说明idbigint主键雪花算法ear_tagvarchar(32)耳标号唯一索引pen_idvarchar(32)猪舍/栏位编号rfid_codevarchar(64)RFID编码birth_datedate出生日期weight_latestdecimal(6,1)最近一次称重health_statustinyint0正常 1观察 2异常created_timedatetime建档时间环境数据表的索引设计要特别注意。按照(pen_id, collect_time)建联合索引然后按天分区查询“某个栏位最近24小时曲线”时MySQL可以在秒级返回结果。如果一张表存了十几个场区的数据还只建单列索引接口超时是必然的。3. 用Java打通“采集—入库—接口分发”的养猪数据链路3.1 MQTT接入网关配置用Spring Integration替代手写客户端采集器端最常见的协议是MQTT 3.1.1Payload是一段JSON字符串。后端接入的稳定做法是配置一个MqttInboundChannelAdapter把它桥接到业务处理的线程池上而不是在回调里直接写业务代码。Bean public MqttPahoClientFactory mqttClientFactory() { DefaultMqttPahoClientFactory factory new DefaultMqttPahoClientFactory(); MqttConnectOptions options new MqttConnectOptions(); options.setServerURIs(new String[]{tcp://emqx-host:1883}); options.setUserName(pigfarm); options.setPassword(pigfarm-2024.toCharArray()); options.setAutomaticReconnect(true); options.setCleanSession(false); factory.setConnectionOptions(options); return factory; } Bean public MessageProducer mqttInbound() { MqttPahoMessageDrivenChannelAdapter adapter new MqttPahoMessageDrivenChannelAdapter(pigfarm-service- UUID.randomUUID(), mqttClientFactory(), pen//env, pen//feed); adapter.setCompletionTimeout(5000); adapter.setQos(1); adapter.setConverter(new DefaultPahoMessageConverter()); return adapter; }这段配置里有三个参数必须说清楚。setCleanSession(false)是关闭干净会话Broker会替客户端保留离线期间的消息防止服务重启期间丢数据代价是Broker内存占用上升线上按需权衡。QoS设为1表示消息至少送达一次应用层必须自己做幂等。Topic用通配符pen//env和pen//feed把环境数据和饲喂数据分开订阅后面的匹配栏位编号。3.2 数据入库的幂等去重为什么QoS1也会产生重复MQTT的QoS1在Broker和客户端之间可能重发消息加上采集器自身有补传机制一条环境记录收到两三次是常态。如果直接INSERT统计报表会偏大告警判断也会被假数据带偏。常见的处理方式是用“采集器编号采集时间指标类型”做唯一键插入时用INSERT IGNORE或ON DUPLICATE KEY UPDATE。ALTER TABLE pigpen_env_record ADD UNIQUE KEY uk_collector_time (collector_id, collect_time, indicator);写入代码里配合MyBatis-Plus的批量插入可以在消费线程池里一次处理一批消息SneakyThrows public void batchSaveEnvRecords(ListPigpenEnvRecord records) { JdbcTemplate template new JdbcTemplate(dataSource); String sql INSERT INTO pigpen_env_record (id, pen_id, collector_id, indicator, value, collect_time, created_time) VALUES (?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE value VALUES(value); template.batchUpdate(sql, records, 500, (ps, record) - { ps.setObject(1, record.getId()); ps.setObject(2, record.getPenId()); ps.setObject(3, record.getCollectorId()); ps.setString(4, record.getIndicator()); ps.setBigDecimal(5, record.getValue()); ps.setObject(6, record.getCollectTime()); ps.setObject(7, LocalDateTime.now()); }); }这里没有用MyBatis-Plus自带批量接口而是直接上JdbcTemplate的batchUpdate因为环境数据表的写入频率高SQL语句固定手写JDBC批量写入比ORM的逐条insert要快一倍以上。批量大小设为500条是因为MySQL的max_allowed_packet默认64MB500条JSON解析后的记录加索引更新单批事务时间在200毫秒内不拖垮数据库。3.3 App端环境数据接口分页查询与聚合曲线分开写App首页一般展示当前各栏位的环境状态卡片点击进去才看历史曲线。这两类接口的负载特征完全不同不应该用一个SQL解决。GetMapping(/api/v1/pen/env/current) public ResultListEnvCurrentVO currentEnv(RequestParam String penId) { // 先从Redis读取查不到再回源数据库 String key env:current: penId; ListEnvCurrentVO list redisTemplate.opsForList() .range(key, 0, -1); if (list ! null !list.isEmpty()) { return Result.ok(list); } ListEnvCurrentVO dbList envRecordMapper.selectLatestByPen(penId); redisTemplate.opsForList().rightPushAll(key, dbList); redisTemplate.expire(key, Duration.ofSeconds(30)); return Result.ok(dbList); }这里有个小经验实时环境数据接口的缓存过期时间设定在30秒而不是永久。因为采集器每30秒上报一次App端拉到“新鲜缺失30秒”的数据在参数上是可接受的而且30秒的过期时间能保证每次测得异常让系统自动拖底。如果缓存永久有效采集器故障恢复后App拿到的还是旧数据。App端聚合曲线的接口按DATE_FORMAT(collect_time, %Y-%m-%d %H:00:00)做小时分桶查询最近24小时就group by 24个桶数据量不大时执行效率很高。3.4 WebSocket推送告警从规则命中到App响铃环境告警的推送通道一般不用轮询而是服务端主动走WebSocket。Spring Boot里用原生WebSocket加一个拦截器保存在线会话比STOMP协议少一层封装排查连接问题也更直观。Component public class AlertWebSocketHandler extends TextWebSocketHandler { private static final MapString, WebSocketSession SESSIONS new ConcurrentHashMap(); Override public void afterConnectionEstablished(WebSocketSession session) { String userId (String) session.getAttributes().get(userId); SESSIONS.put(userId, session); } public void sendAlert(String userId, String message) { WebSocketSession session SESSIONS.get(userId); if (session ! null session.isOpen()) { session.sendMessage(new TextMessage(message)); } } }WebSocket session的并发写是个容易踩的坑同一个session如果被多个线程同时调用sendMessage会抛The remote endpoint was in state [TEXT_FULL_WRITING]。上面的代码用ConcurrentHashMap保存session但并发写session仍可能冲突。稳妥的写法是为每个session配一个独立的发送线程或使用同步块。4. 智慧化养猪核心业务档案、饲喂、健康预警的Java实现4.1 猪只档案管理图片存储到MinIO耳标规则落地为代码生猪档案是后续所有业务的基础建档时最容易出错的是耳标号重复。耳标号一般是“场区码年份流水号”的固定格式例如A242001001A表示一分场24表示年份2001是栏位001是流水。服务端要做格式校验不能只依赖前端输入框的placeholder提示。private static final Pattern EAR_TAG_PATTERN Pattern.compile(^[A-Z]\\d{2}\\d{4}\\d{3}$); public void validateEarTag(String earTag) { if (!EAR_TAG_PATTERN.matcher(earTag).matches()) { throw new BizException(耳标格式不正确应为场区年份栏位流水例如A242001001); } Long count pigInfoMapper.selectCount( new LambdaQueryWrapperPigInfo().eq(PigInfo::getEarTag, earTag)); if (count 0) { throw new BizException(该耳标已存在请核对后重新输入); } }猪只照片上传走MinIO接口里用putObject上传存储路径按pig/{earTag}/{timestamp}.jpg组织。这里的路径设计是有意义的按耳标分目录方便排查单个个体的历史照片后续做个体识别训练时也方便按目录批量导出数据集。对5年以上Java开发者来说这个功能不难难点在于图片压缩和格式统一App端上传前先用libjpeg-turbo压缩到宽800像素以内服务端就不再二次处理。4.2 个体识别与体重估算用ONNX Runtime在Java里跑视觉模型猪只个体识别不是用RFID读卡器扫码而是靠摄像头抓拍后比对猪背花纹或面部特征。Java调用ONNX模型做推理不需要引入Python服务ONNX Runtime的Java API可以直接加载训练好的模型。public float[] predictWeight(File imageFile) throws OrtException { OrtEnvironment env OrtEnvironment.getEnvironment(); OrtSession session env.createSession(weight_model.onnx, new OrtSession.SessionOptions()); // 图片预处理缩放到模型要求的尺寸转CHW格式 FloatBuffer buffer imageToTensor(imageFile, 224, 224); OnnxTensor tensor OnnxTensor.createTensor(env, buffer, new long[]{1, 3, 224, 224}); OrtSession.Result result session.run(Collections.singletonMap(input, tensor)); // 输出层是体长、胸围、体重三个回归值 float[] outputs result.get(0).getFloatValue(); float weight outputs[2]; return new float[]{ /* 体长cm */ outputs[0], /* 胸围cm */ outputs[1], weight }; }这里必须说明Java做AI推理的几个边界。ONNX Runtime承担的是模型推理但训练阶段的数据采集、标注和训练本身还是需要Python做离线处理这是逃不掉的。Java端适合的是“加载训练好的模型做实时推理”人员行走路径、抓拍时机、照片质量筛选这些前置逻辑可以用摄像头SDK配合Java实现。实际项目中体重估算的误差一般在5%10%之间不足以作为精确饲喂的依据但足够做异常提醒某头猪体重连续两周负增长就自动生成健康事件让饲养员去查看。4.3 全链路事件溯源饲喂记录为什么用流水表而不是更新累计值饲喂系统每次投料都会上报一条记录包含猪只ID、饲料批次、计划量、实际采食量、投喂时间。很多初版设计会在猪只档案表上直接更新“今日采食量”字段这样做当天还好做日结的时候发现少了一次补喂数据就再也对不上了。流水表的设计是只追加、不修改、不删除。每次投喂新增一条记录报表统计时再按时间段聚合。这样做的收益是回溯断料问题、做饲料转化率分析、对账饲料批次时都能找到原始事件。public void recordFeed(FeedRecord record) { String idempotentKey record.getFeederId() : record.getFeedTime().toEpochSecond(ZoneOffset.UTC); Boolean first redisTemplate.opsForValue() .setIfAbsent(feed:dup: idempotentKey, 1, Duration.ofMinutes(5)); if (Boolean.FALSE.equals(first)) { log.warn(重复投喂上报忽略{}, idempotentKey); return; } feedRecordMapper.insert(record); // 异步更新当日累计供报表使用 asyncFeedSummaryService.updateDailySummary(record); }幂等键用的是“饲喂器编号投喂时间戳”。采集器在断电重传或网络抖动时可能会把同一条记录重发三四次这个键可以保证同一秒内来自同一台饲喂器的投喂事件只入库一次。4.4 健康预警规则引擎不引入Drools用组合模式也能跑健康预警是“智慧化”叫得最响、实际最容易做假的部分。常见做法是把规则写死在if-else里结果兽医要加一条规则就得找开发改代码发版。我建议用一套轻量规则引擎规则配置在数据库里Java端用模板方法模式执行。规则编码指标触发条件等级R001舍内温度夏季连续30分钟超过30℃警告R002氨气浓度超过25ppm持续15分钟严重R003猪只活动量较前一日同一时段下降50%以上观察R004采食量连续两餐低于设定的60%严重规则引擎的核心是“评估指标从哪里来”和“连续多久才算触发”。温度超过阈值30分钟才告警是为了避免白天开门通风造成的单次瞬时尖峰误报。实现上使用Redis的ZSET存储每分钟指标连续时间判断用滑动窗口超过阈值的时间点数量占窗口比例超过80%才触发。public void evaluateTemperature(String penId, double value, LocalDateTime now) { String key env:temp: penId; long score now.toEpochSecond(ZoneOffset.UTC); redisTemplate.opsForZSet().add(key, now.toString(), score); redisTemplate.expire(key, Duration.ofMinutes(40)); long start score - 1800; // 前30分钟窗口 Long count redisTemplate.opsForZSet().count(key, start, score); // 上报频率是每分钟一条30分钟窗口内最多30个点 if (count 24) { // 80%时间超标 healthEventService.createEvent(penId, R001, 舍内温度连续超标); redisTemplate.delete(key); // 触发后重置窗口防止重复告警 } }这套写法的好处是规则阈值在管理后台可配置改温度上限不用重新发版。没人规定智慧化养猪必须用复杂规则引擎把最常用的温度、氨气、活动量三条规则写好比盲目引入规则引擎更实用。5. Java并发与性能调优上报堆积、接口超时与内存抖动5.1 上报高峰时消费线程池打满怎么办规模场早中晚喂料时段是传感器上报的高峰几十个采集器同时上报MQTT消费者如果处理不过来消息会在Broker端堆积。排查现象是App端环境查看接口正常但告警推送延迟超过5分钟。这时先看消费者线程池的状态而不是急着加机器。Bean(mqttConsumerExecutor) public ThreadPoolTaskExecutor mqttConsumerExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(8); executor.setMaxPoolSize(16); executor.setQueueCapacity(2000); executor.setThreadNamePrefix(mqtt-consume-); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; }这里的核心参数是队列容量设为2000。队列太小高峰期直接触发拒绝策略太大则消息积压在内存里服务重启就全部丢失。配合CallerRunsPolicy队列满了以后不再接收新任务而是让MQTT回调线程自己处理消息形成天然背压。线上经验是8核心的容器堆内存4G这套配置能抗住每秒500条环境数据的写入。5.2 首屏加载慢App首页不只是查一次数据库App打开后的首屏通常要同时展示各栏位环境状态、今日异常数、最近告警三条数据。新手会写三个接口App端并行请求结果每个接口都查主库一个慢SQL就拖垮全部。我惯用的做法是做一个聚合接口第一次请求查库并缓存120秒后续请求直接走Redis。GetMapping(/api/v1/home/overview) public ResultHomeOverviewVO overview() { String cacheKey home:overview; HomeOverviewVO cached redisTemplate.opsForValue().get(cacheKey); if (cached ! null) { return Result.ok(cached); } HomeOverviewVO vo new HomeOverviewVO(); vo.setPenStatusList(penMapper.selectLatestStatus()); vo.setAbnormalCount(healthEventMapper.countTodayAbnormal()); vo.setRecentAlerts(healthEventMapper.selectTopN(10)); redisTemplate.opsForValue().set(cacheKey, vo, Duration.ofSeconds(120)); return Result.ok(vo); }App端拿到首页数据是秒开的因为后面两个查询走了缓存。缓存过期后第一个请求仍然会穿透所以在栏位状态查询上再加一个LIMIT 20防止极端情况并保证列表页分页必带penId条件走组合索引。5.3 Java八股文在排障里的真实映射面试里问的“动态代理原理”“线程池拒绝策略”“内存屏障”在这个项目里全都有对应场景。MyBatis-Plus的Mapper接口就是JDK动态代理的产物MapperScan扫描到的接口最终由MapperProxy创建代理对象排障时看到一个奇怪的$Proxy类名不要慌先确认是不是代理对象调用了目标方法。上面MQTT消费者线程池的队列满了走拒绝策略就是面试题“AbortPolicy和CallerRunsPolicy区别”在生产里的体现选错策略的代价是丢数据而不是扣分。内存抖动方面常见问题是每收到一条MQTT消息就new一个大数组做JSON解析高峰期频繁触发Young GC。处理办法是复用ObjectMapper实例Spring Boot默认就是单例的并避免在循环里使用new String(bytes, charset)重复分配缓冲区。观察方式是压测时用jstat -gcutil查看Eden区使用率Eden区回收后下降但Old区持续上涨就要检查是不是有对象意外逃逸到了老年代。6. 上线前用Java做一轮自检监控、压测与幂等验证6.1 本地起一套最小环境验证全部链路如果手头没有真实的采集器硬件可以用一个模拟程序代替。先在本地用Docker Compose起一套依赖环境再运行一个模拟上报程序就能把采集到展示的全链路打通。docker compose up -d mysql emqx redis minio # 等待依赖全部就绪后启动Spring Boot应用 mvn spring-boot:run # 模拟一条环境上报数据 mosquitto_pub -h localhost -p 1883 \ -u pigfarm -P pigfarm-2024 \ -t pen/A01/env \ -m {collector:C001,pen:A01,temp:29.8,humidity:71.2,nh3:8.5,ts:1700000000}用mosquitto_pub发完这条消息后观察后端日志里是否有handleEnvMessage相关输出再到/api/v1/pen/env/current?penIdA01接口拿数据能返回刚才的温度值就说明整条链路通了。这个验证过程能在五分钟内完成比在猪场现场用真实传感器联调快得多。6.2 压测指标以“每分钟告警并发”为基准更贴近业务压测智慧化养猪系统压测场景不是登录接口而是环境数据上报和告警推送。用JMeter建立两个线程组一个模拟500台采集器并发上报另一个模拟100个App在线接收WebSocket推送。上报线程组建议设置为500线程、Ramp-Up时间30秒、循环次数200次重点看服务端JVM线程数和EMQX的连接数。通过标准很简单上报场景下P99延迟低于500ms、无消息积压推送场景下App端收到告警延迟低于2秒。上报接口是全链路最容易出现瓶颈的地方JF结果里如果看到Connection reset优先检查MySQL的max_connections和连接池大小如果看到GC overhead limit exceeded说明上一章说的内存问题在真实流量下被放大了。6.3 App写操作统一带幂等号减少断网重试引发的脏数据移动网络不稳定App请求失败后自动重试是常见逻辑但同一个“猪只建档”请求被重发两次就可能生成两个耳标相同的档案。在App端的写接口统一要求携带Idempotency-Key请求头服务端用Redis实现幂等。public Long createPig(PigCreateRequest request, String idempotencyKey) { String redisKey idem:createPig: idempotencyKey; Boolean isNew redisTemplate.opsForValue() .setIfAbsent(redisKey, processing, Duration.ofHours(24)); if (Boolean.FALSE.equals(isNew)) { // 从Redis中读取已生成的主键返回给App return Long.parseLong(redisTemplate.opsForValue().get(redisKey)); } PigInfo pig buildPigEntity(request); pigInfoMapper.insert(pig); redisTemplate.opsForValue().set(redisKey, pig.getId().toString()); return pig.getId(); }把幂等号作为App端所有写接口的默认参数上线前扫一遍接口文档就能少一半由断网重发引起的脏数据。这里的重点是setIfAbsent操作是原子的并发重试时只有一个线程能拿到创建权其余线程拿到的是同一主键。本文还有配套的精品资源点击获取