ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Spring Boot与Android的智慧家居系统设计与实现

基于Spring Boot与Android的智慧家居系统设计与实现 1. 这套智慧家居系统到底做了什么先说说我做这个项目的动机。智能家居这个概念喊了好多年但市面上大多数方案要么依赖云端、要么绑定特定厂商的App本地设备一旦断网就成了摆设。我自己家里有几盏灯、一个空调、一个空气净化器一直想用一个统一的Android客户端把它们管起来同时又能保存完整的操作记录方便回溯。于是就用Java Spring Boot写了后端服务Android端做交互界面整套系统包含源码、文档、运行视频和讲解视频大概是毕设级项目里比较完整的一套了。这套系统的核心能力可以分成三块设备管理、场景联动和日志审计。设备管理解决的是“一个App控制多个设备”的问题每台设备有独立的开关状态、运行模式、定时任务场景联动解决的是“一键触发多个动作”的问题比如回家场景可以同时开灯、开空调、关净化器日志审计则是把每一次手动操作、自动触发、定时执行都记录下来方便分析使用习惯也方便排查设备异常。听起来不复杂但真要把这三块在Android端和Spring Boot后端之间稳稳地跑通里面有不少细节值得掰开揉碎讲一讲。适用对象很明确准备做毕业设计或者课程设计的计算机相关专业学生想快速搭一套前后端分离物联网项目骨架的开发者还有想了解Spring Boot如何为移动端提供稳定接口的Java后端工程师。如果你只是想要一个能跑的Demo那照着文档把环境配好就能起来如果你想深入理解整个系统的数据流和设计取舍这篇文章会把关键决策的来龙去脉交代清楚。2. 后端服务设计Spring Boot如何承接多设备并发请求2.1 工程结构与模块划分后端我用的是经典的Spring Boot分层架构但为了让设备控制这种实时性要求较高的业务不拖泥带水我把工程拆成了四个模块controller、service、mapper、entity。Controller层只负责参数校验和HTTP响应包装Service层处理业务逻辑Mapper层通过MyBatis操作MySQLEntity层映射数据库表结构。另外单独加了一个handler包用来做全局异常捕获和统一返回值封装。为什么这么拆因为设备控制接口的调用频率并不低而且Android端发出的请求往往带有多个设备ID和动作参数如果业务逻辑都堆在Controller里后期加一个“定时任务”功能就要动大量代码。分层之后新增功能只需要在Service层加方法Controller层保持轻薄测试起来也省心。来看看我的项目目录核心结构src/main/java/com/home/smart/ ├── controller │ ├── DeviceController.java │ ├── SceneController.java ├── service │ ├── DeviceService.java │ ├── SceneService.java │ └── impl ├── mapper │ ├── DeviceMapper.java │ ├── SceneMapper.java ├── entity │ ├── Device.java │ ├── Scene.java │ └── DeviceLog.java ├── handler │ └── GlobalExceptionHandler.java └── config └── WebConfig.java2.2 数据库表设计不要把设备状态硬编码到代码里设备表的设计是这套系统的地基。我见过不少同学把设备名称、状态直接写在Java枚举里这样确实省事但一旦要新增设备类型就得改代码重新编译很不灵活。我的做法是把设备元数据全部落到数据库里支持动态扩容。核心表就三张device存设备基础信息device_status存最新状态device_log存操作历史。device表字段包括id、name、type灯/空调/净化器等、room_id、create_timedevice_status表字段包括device_id、power0或1、mode、temperature、humidity等用updated_time字段标记最后一次状态变更device_log表记录device_id、action、param_json、operator、log_time。让我解释一下为什么状态和日志要分两张表。状态表存储的是当前值每次设备状态变化就更新一行记录日志表存储的是历史值每次设备状态变化就新增一行。这样可以快速查询“当前所有设备的状态”而不需要去日志表里做大量聚合计算。如果只有日志表查一个设备的最新状态就得ORDER BY log_time DESC LIMIT 1设备一多性能就撑不住。建表的DDL我放在项目文档里了这里给一个关键的示例片段展示device_status表如何和device表关联CREATE TABLE device_status ( id bigint NOT NULL AUTO_INCREMENT, device_id bigint NOT NULL COMMENT 设备ID, power tinyint DEFAULT 0 COMMENT 开关状态 0关 1开, mode varchar(32) DEFAULT NULL COMMENT 运行模式, temperature decimal(5,2) DEFAULT NULL COMMENT 温度设定, updated_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_device (device_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意那个UNIQUE KEY uk_device它保证了每个设备在状态表里只有一条记录避免因为并发更新出现重复行。这是我在实际调试中踩过的一个坑后文会详细说。2.3 Controller接口设计统一返回结构别让前端猜Android端和后端交互说白了就是HTTP请求接口设计的好坏直接影响联调效率。我定义了一个统一的响应体ResultT包含code、msg、data三个字段。所有接口返回这个对象Android端用Gson反序列化时只需要写一次泛型解析。举一个设备开关的例子RestController RequestMapping(/api/device) public class DeviceController { Autowired private DeviceService deviceService; PostMapping(/control) public ResultBoolean control(RequestBody ControlRequest req) { boolean success deviceService.controlDevice(req.getDeviceId(), req.getAction(), req.getParams()); return Result.success(success); } GetMapping(/list) public ResultListDeviceVO list() { return Result.success(deviceService.listDevices()); } }ControlRequest里有一个params字段类型是MapString, Object用来承载不同设备类型的特殊参数。比如空调需要设置温度灯只需要开或关。用Map接收的好处是后端不需要为每种设备单独写一个请求类扩展新设备类型时Controller层基本不用动。很多新手会在这里纠结既然要接收不同参数为什么不直接用JSON字符串存起来我的建议是尽量用结构化的Map因为Android端传过来的参数最终要落到device_log表的param_json字段Map序列化成JSON和字符串直接存本质上没有区别但Map在后面做参数校验和业务解析时要方便得多。2.4 定时任务与线程池防止高并发压垮设备控制智慧家居除了手动控制还有定时任务功能。比如每天早上八点打开卧室灯晚上十一点关闭所有设备。这个功能我用Spring Boot自带的Scheduled注解实现但没直接往任务方法里塞太多逻辑而是安排了一个TaskSchedulerConfig配置类专门管理调度线程池。关键的配置代码长这样Configuration EnableScheduling public class SchedulerConfig implements SchedulingConfigurer { Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.setScheduler(Executors.newScheduledThreadPool(10)); } }为什么要单独配置线程池因为Spring Boot默认的Scheduled是单线程执行的如果某个任务执行时间稍长就会阻塞后面所有定时任务。想象一下一个定时控制空调的任务因为设备响应慢卡了两秒结果下一个定时开灯的任务也跟着延迟用户的体验会非常糟糕。我把它改成10线程的调度池后基本就没再遇到过任务互相阻塞的情况。不过线程池也不是越大越好。调度线程池的任务类型是“短平快”的设备控制指令10个线程足够。如果你要执行长耗时任务比如生成统计报表应该另外建一个异步执行线程池否则调度池的线程全被长任务占满定时控制依然会被影响。3. Android客户端的实现从进度条到设备控制面板3.1 界面框架选择RecyclerView加自定义卡片Android端的设计目标是让用户在一屏内看到所有设备的状态并快速完成操作。界面主框架我用了底部三个Tab设备列表、场景模式、日志记录。设备列表用RecyclerView展示每一个设备是一张卡片卡片上有设备名称、状态图标、开关按钮点击卡片可以进入详情页进行更精细的设置。这里有一个容易被忽视的点设备状态是实时变化的所以RecyclerView的Adapter不能只是静态绑定数据还要提供局部更新方法。我自定义了一个DeviceAdapter对外暴露updateDevice(DeviceVO device)方法调用时通过对比设备ID找到对应ViewHolder只刷新变化的那一行数据而不是notifyDataSetChanged()全量刷新。这样既省电又流畅尤其在设备数量多的时候全量刷新经常会造成界面卡顿。关于大家搜索时很关心的“Android进度条”问题我在系统里也做了个隐藏的实用功能控制指令提交成功后按钮上会短暂显示一个ProgressBar然后恢复成原来的图标。这个进度条点击反馈本质上不是网络加载的进度而是“指令已提交等待设备回执”的过程提示。实现方式是在按钮的布局里同时放两个View一个ImageView一个ProgressBar根据状态切换可见性。请不要指望Android原生ProgressBar能真的展示网络层的百分比进度那是不现实的你要做的是交互层的等待提示。3.2 网络请求封装Retrofit加协程处理异步Android端的网络层我选了Retrofit OkHttp这是当前最成熟稳定的组合。核心配置在RetrofitClient单例里统一设置了BaseUrl、Gson转换器、超时时间。配合Kotlin协程如果你用的是Java也可以配合RxJava让网络请求不再回调地狱。不过我这里要提一个很实际的坑很多人在Android模拟器里访问本机后端localhost:8080死活连不上因为模拟器里的localhost指向的是模拟器自己不是电脑。你需要在后端配置里把地址改成10.0.2.2或者让手机和电脑处在同一局域网填电脑的局域网IP。我给出一个简化的Retrofit接口定义public interface ApiService { POST(api/device/control) CallResultBoolean controlDevice(Body ControlRequest request); GET(api/device/list) CallResultListDeviceVO getDeviceList(); }实际调用的地方我用了一个Callback统一处理网络错误、业务错误和成功数据避免每个页面重复写错误判断逻辑。3.3 Android端的状态同步机制轮询还是长连接这是很多做智能家居App都会纠结的问题设备状态变了App怎么立刻感知方案不外乎三种客户端轮询、服务端推送WebSocket/长连接、本地数据库缓存加分页拉取。我第一版用的是最简单粗暴的轮询每10秒调一次设备列表接口。后来发现两个问题一是流量和电量消耗大二是设备状态变化后最迟要等10秒才能看到体验迟钝。后来我改用WebSocket做实时通知后端在设备状态变更时推送一条消息到指定频道Android端收到消息后局部刷新。效果立竿见影几乎秒级同步。但WebSocket不是银弹。除了后端要额外管理连接Android端还得处理断线重连、心跳保活等逻辑。如果你的项目只是毕设或者Demo我建议先用轮询方案把功能跑通再用WebSocket做进阶加分项。我当时是先把轮询跑通后面用okhttp3.WebSocket做了重连机制文档里都写了具体代码。3.4 Android进度条、FileProvider 这些搜索热词跟我系统的关系写这一节是因为我发现你给的热搜词里出现了不少Android开发相关的东西比如“Android进度条”和“FileProvider”。进度条我在前面已经解释过了主要用在指令反馈。而关于content://com.baidu.searchbox.fileprovider/...这类长串实际上是Android应用间分享文件时使用的FileProvider授权路径。如果我的智慧家居系统要导出设备日志文件并分享给微信或者邮件就免不了要用到FileProvider。当时我确实配置过把fileprovider_paths.xml里的external-path指向了导出日志目录不然Android 7.0以上会直接抛FileUriExposedException。这里给一个配置例子供参考paths external-path namelogs pathAndroid/data/com.example.smarthome/logs/ / /paths同时在AndroidManifest.xml里声明provider android:nameandroidx.core.content.FileProvider android:authoritiescom.example.smarthome.fileprovider android:exportedfalse android:grantUriPermissionstrue meta-data android:nameandroid.support.FILE_PROVIDER_PATHS android:resourcexml/fileprovider_paths / /provider这一块不仔细做程序在真机上分享日志时会闪退别问我怎么知道的。4. 源码、文档、运行视频、讲解视频我这套资料包的结构与使用方式4.1 源码组织方式与导入技巧项目源码是标准的Maven工程后端是springboot-smarthome-backendAndroid端是AndroidStudio-smarthome-app。后端导入时直接用IntelliJ IDEA打开等Maven下载依赖JDK建议用1.8或11Spring Boot版本我用的2.7.xapplication.yml里配置了MySQL连接信息、端口号、日志级别。Android端用Android Studio打开Gradle版本和SDK版本在build.gradle里都有注明。需要注意一点Android Studio版本不能太老否则会提示Gradle版本不兼容。如果你用的是最新版Android Studio它可能会把Gradle升级到8.0以上运行时会出一些莫名其妙的兼容问题。我的建议是直接按照文档里锁定的版本配置不要随意升级。项目文档分三份一份是《系统设计说明书》把需求分析、数据库设计、接口设计都写了一份是《环境搭建与部署手册》从零开始教你怎么安装MySQL、JDK、Android Studio还有一份是《测试报告》记录了主要接口的测试用例和结果。运行视频是后端启动加Android模拟器操作的录屏讲解视频是PPT式的功能演示加代码讲解。4.2 常见启动报错与解决思路我收到过很多同学反馈的启动报错集中在下面几类第一类后端启动失败端口被占用。这是最简单的netstat -ano | findstr 8080看进程号杀掉或者改配置文件端口就行。第二类MySQL连接不上。排查顺序是MySQL服务有没有启动、用户名密码对不对、数据库有没有导入。注意application.yml里的spring.datasource.url要带useSSLfalseserverTimezoneAsia/Shanghai否则时区和SSL的报错会烦死你。第三类Android端编译时找不到依赖。绝大多数是Gradle配置问题。检查allprojects里的仓库是不是有google()和mavenCentral()如果网络不好切换阿里云镜像也能解决。第四类模拟器上安装APK后一打开就闪退。先用adb logcat看崩溃日志十有八九是网络权限没加或者后端地址写错。Android 9以上还要求明文HTTP请求必须显式配置AndroidManifest里要加android:usesCleartraffictrue或者在network_security_config.xml里配置域名白名单。4.3 讲解视频里不会细讲但你必须知道的三个扩展点这套系统虽然完整但如果你只是照抄一遍对能力的提升很有限。我建议你在看懂源码之后尝试做下面三个扩展一是给设备控制接口加上权限校验。现在的版本是任何请求都能控制设备这在实际场景里非常危险。你可以用Spring Security加上Token鉴权Android端登录后拿Token请求时放到Header里。二是把设备状态历史和前端可视化图表打通。device_log表里已经存了大量历史数据你可以用ECharts在网页里展示“过去一周温度变化曲线”这个功能面试时拿出来说很有分量。三是做设备分组和同步控制。目前的场景模式只能按预设顺序逐个控制设备你可以尝试用线程池并发控制同一组设备减少总耗时。我在带学生的过程中发现能做出一套完整系统的人不少但能把扩展点想清楚并动手实现的人很少。面试官问“你做的智能家居系统有什么亮点”你说“我会设计数据库表结构、会用Retrofit、会处理并发”远不如说“我能基于日志数据做使用习惯分析并通过WebSocket做状态实时推送”来得有说服力。5. 项目运行的全流程实测从零到跑通5.1 后端启动的完整步骤再赘述一遍实际启动流程确保你能复现。第一步安装并启动MySQL创建一个叫smart_home的数据库然后把项目根目录下的smart_home.sql导进去。命令行方式是这样mysql -u root -p CREATE DATABASE smart_home DEFAULT CHARACTER SET utf8mb4; USE smart_home; SOURCE /path/to/smart_home.sql;第二步用IDEA打开springboot-smarthome-backend等Maven依赖下载完成。检查application.yml里的MySQL账号密码是否和本地一致默认是root/root。启动类的入口是SmartHomeApplication.java右键运行。第三步启动成功后控制台会出现Spring Boot的Banner和项目启动日志最后看到Started SmartHomeApplication就说明成功了。然后用Postman测试一下接口GET http://localhost:8080/api/device/list返回{code:200,msg:success,data:[...]}即正常。5.2 Android端的连接与调试Android端注意把RetrofitClient里的BASE_URL改成本机IP。如果用的是模拟器填http://10.0.2.2:8080/如果用真机填电脑的局域网IP比如http://192.168.1.8:8080/。Android 9以上还要配置允许明文流量。然后编译安装到模拟器打开后设备列表应该是空的或者显示数据库里初始化的几条测试设备。如果是空的先检查后端device表里有没有数据没有就手动插入几条测试数据再下拉刷新设备列表。5.3 日志记录验证这是最容易被忽略但最能体现系统完整度的一个功能。你在App里开关几次设备然后查一下device_log表SELECT * FROM device_log ORDER BY log_time DESC LIMIT 10;正常情况下能看到每条操作的设备ID、动作、参数、时间。这里的param_json字段会根据设备类型记录不同内容比如开空调时写入{temperature:26,mode:cool}。这个数据在文档里也作为测试用例写进去了。我在第一次联调时遇到过一个问题开关灯后日志表里没有记录。后来查代码发现是Service层在控制设备成功后没有调用日志插入方法只更新了设备状态。这种低级错误排查起来最耗时间所以后来我干脆在controlDevice方法里把“更新状态”和“写日志”放在同一个事务里保证了数据一致性。5.4 运行视频和讲解视频的观看建议运行视频时长大概十几分钟从前端页面展示到后端接口调用都有截图和操作过程。讲解视频是按PPT顺序讲的适合先看把整体架构搞明白再对着源码看第二遍。我的建议是不要一次性把视频看完而是每看一段就去源码里找到对应的类和方法这样印象最深。6. 我在开发过程中踩过的坑并发更新与事务边界6.1 并发更新造成的脏数据问题前面提到device_status表用了UNIQUE KEY uk_device来防止重复记录但这个约束只解决了“多插一条”的问题没有解决“同时更新”的问题。假设Android端发了两个请求一个是开灯一个是关灯它们同时到达后端都读取了设备当前状态关然后各自更新成开和关最后的结果就很难预测。这就是典型的并发竞态问题。解决方式有几种。最简单的是在更新语句里直接加条件UPDATE device_status SET power #{power}, updated_time NOW() WHERE device_id #{deviceId}因为这里的更新是无条件的覆盖两个请求先后执行最后执行的那个会覆盖前一个实际上还是可能出现“先到后执”的逆序问题。更稳妥的方式是用数据库乐观锁在device_status表加一个version字段更新的时候带上版本号版本不匹配就更新失败UPDATE device_status SET power ?, version version 1 WHERE device_id ? AND version ?我最终采用了乐观锁同时把同一设备连续操作的请求通过设备ID加锁串行化。具体做法是在Service层创建一个ConcurrentHashMapString, Object作为锁对象池操作同一设备时先获取该设备的锁操作完成再释放。这部分逻辑在文档的“多设备并发控制”章节里有详细设计实际测试中并发冲突率几乎降为零。6.2 事务边界设备控制与日志写入必须保持一致设备控制接口有一个业务规则只有成功改变设备状态才允许写操作日志。如果设备状态更新成功日志写入失败用户会看到设备已经开关了但日志里查不到对后续的审计和分析都是麻烦事。我用的办法是在Service层方法上加Transactional注解。注意事务范围一定要覆盖“更新状态”和“插入日志”这两个操作。有些同学习惯把DAO层的每个方法都加上事务这样看起来安全实际上会让事务粒度过小根本管不住跨DAO的业务一致性。正确做法是Transactional(rollbackFor Exception.class) public boolean controlDevice(Long deviceId, String action, MapString, Object params) { boolean updated deviceStatusMapper.updatePower(deviceId, power); if (updated) { deviceLogMapper.insertLog(deviceId, action, JSONObject.toJSONString(params)); } return updated; }这里有一个注意点Transactional默认只回滚RuntimeException如果你写了一个自定义的检查异常必须显式设置rollbackFor否则异常时不会回滚事务形同虚设。我用的rollbackFor Exception.class就是让所有异常都触发回滚。6.3 时长统计与延迟坑接口耗时不能只看前端很多新手调试时只看App上的加载时间不看后端接口真实耗时。我在讲解视频里专门提过这个细节用Postman测接口的平均响应时间再用Android的日志打印每个请求的实际耗时对比一下你就会发现很多“App卡顿”其实是后端慢。后端慢最常见的三个原因一是SQL没有走索引device_log表数据量大之后按时间查询全表扫描二是MySQL连接池配置太小请求一多全部排队三是连接串里没有配置合理的超时时间。我的解决方案是给log_time字段加了普通索引连接池设置成initialSize5, maxActive20后端响应时间基本稳定在150ms以内。7. 这套系统还能怎么改从毕设到生产级的路还差多远做毕设或者学习项目这套系统已经足够完整但如果你想把它当成真正的产品去落地至少要再补三件事。第一是把单机部署改成“后端服务多实例 数据库读写分离”。现在所有请求打到同一个后端进程设备一多、请求量一大单机一定撑不住。引入Nginx做负载均衡部署两个后端实例MySQL做一主一从性能和可用性会好很多。第二是把设备接入方式改成MQTT协议。目前系统里的设备状态更新是模拟的用户点一下按钮后端直接改数据库。真实智能家居场景中控制指令要发给硬件硬件状态也要上报回云端MQTT是非常适合这种场景的轻量级消息协议。你可以把Spring Boot里的控制逻辑换成发布到MQTT主题硬件通过订阅主题接收指令非常简单。第三是引入分布式锁。前面提到的ConcurrentHashMap锁对象池只适用于单机进程如果部署了多个后端实例不同实例的锁互不可见乐观锁也只在单库场景下有效。生产环境可以用Redis做分布式锁保证跨实例的并发安全。我也理解很多同学的时间有限不一定要做到生产级但哪怕只把上面的第一点做掉写成“系统支持横向扩展”这样的简历亮点面试时的说服力都会上一个档次。希望这篇拆解能帮你把项目吃得更透做出来的东西既跑得起来也说得清楚。
返回列表