简介这是一套面向移动设备管理MDM方向的Java Web开源项目源码适合具备一定Java与Android开发基础、希望研究远程管控架构的开发者与学习者。项目由WEB管理端、设备控制服务和Android客户端三部分组成支持局域网与广域网模式可远程获取通讯录、通话记录、短信、应用与进程列表、SD文件及活动窗口等数据并实现通知栏消息实时获取、实地定位、锁屏、弹窗、关机、重启以及远程拨号、发短信、访问网址等管控操作。压缩包共约2000个文件大小152.88MB以1117个JavaScript、237个CSS、187个HTML构成前端界面138个Java源文件承载核心逻辑另有JSP、XML、JSON、JAR及SQL等配置与依赖文件。目前已有150人学习下载。代码全部开源、可重构性良好读者可借此理解Web端与Android端通信协议、指令下发与数据回传的完整链路并基于现有模块二次开发出实用的设备管控平台。1. 从一台跑在机房角落的测试机说起Java Web 平台怎么把 Android 设备管起来很多团队都遇到过这种场景测试机房里堆着十几台 Android 设备系统版本从 8 到 14 都有平时跑自动化、跑兼容性验证但设备散落在不同工位谁在用、装了什么包、电量还剩多少、有没有被误恢复出厂全靠人肉登记。更麻烦的是一旦某台设备被同事借走刷了机远程排查时只能干瞪眼。基于 Java 的 Web 平台 Android 设备远程管控要解决的就是这件事用一套跑在服务端的 Java Web 应用把分散的 Android 设备通过长连接纳管起来在浏览器里完成设备列表、实时状态、远程指令下发和结果回传。它适合做移动端测试平台、设备农场、企业内网终端管理的团队也适合想拿一个完整 Java Web Android 双端项目练手的开发者。源码层面的核心不是界面多花哨而是服务端如何维持设备在线、如何把指令可靠地送到端上、端上如何在不越狱不 root 的前提下拿到可用的管控能力。2. 先想清楚管控通道怎么建长连接选型与端上保活2.1 为什么不用 HTTP 轮询而用 WebSocket 长连接远程管控的第一需求是「指令要快、状态要准」。如果用 HTTP 短轮询服务端想下发一条「截屏」指令得等设备下一次来拉取延迟取决于轮询间隔间隔调小服务端连接数和电量又扛不住。常见做法是端上主动向 Java Web 服务端建立一条 WebSocket 长连接服务端维护deviceId - session的映射指令直接往对应 session 写。这样指令下行延迟基本等于网络 RTT状态上报也能做到秒级。选型上Java 侧我一般用 Spring Boot 起服务WebSocket 用原生ServerEndpoint或 Spring 的WebSocketHandler都行。前者更轻后者和 Spring 生态整合更顺。Android 侧用 OkHttp 的 WebSocket因为它自带心跳和重连回调比手写 Socket 省事。要注意的是WebSocket 只是通道真正决定可用性的是「断线后多久能重连上」和「重连后状态能不能对齐」。2.2 服务端会话注册与设备身份绑定设备连上来第一件事是报到带上设备唯一标识。Android 上不要用Settings.Secure.ANDROID_ID当唯一键它在部分机型恢复出厂后会变而且不同用户下可能不同。常见做法是首次启动生成一个 UUID 存到应用私有目录配合Build.SERIAL有权限限制时降级做辅助。服务端收到注册消息后把deviceId、session、lastSeen写进内存注册表同时落一份到数据库方便重启后恢复设备清单。// 服务端 WebSocket 端点设备注册与指令下发 ServerEndpoint(/ws/device) Component public class DeviceEndpoint { // deviceId - 当前活跃会话ConcurrentHashMap 保证并发安全 private static final MapString, Session ONLINE new ConcurrentHashMap(); OnOpen public void onOpen(Session session) { session.setMaxIdleTimeout(60_000); // 60 秒无数据判定空闲 } OnMessage public void onMessage(String raw, Session session) { JSONObject msg JSON.parseObject(raw); String type msg.getString(type); if (register.equals(type)) { String deviceId msg.getString(deviceId); ONLINE.put(deviceId, session); session.getUserProperties().put(deviceId, deviceId); // 回执端上据此确认注册成功 send(session, {\type\:\register_ack\}); } else if (heartbeat.equals(type)) { // 心跳只更新活跃时间不写库避免高频 IO session.getUserProperties().put(lastSeen, System.currentTimeMillis()); } } OnClose public void onClose(Session session) { String deviceId (String) session.getUserProperties().get(deviceId); if (deviceId ! null) { ONLINE.remove(deviceId, session); // 只移除自己那条防止误删新连接 } } private void send(Session session, String text) { session.getAsyncRemote().sendText(text); } }这段代码的关键点有三个。第一ONLINE用ConcurrentHashMap因为 WebSocket 回调是多线程的普通 HashMap 会出并发问题。第二onClose里用remove(key, value)而不是remove(key)避免设备快速重连时旧连接关闭把新连接踢掉这是血泪经验。第三心跳不落库只更新内存时间戳否则几百台设备每 30 秒一次写库数据库很快成瓶颈。2.3 Android 端保活与重连策略端上要解决的是「别被杀掉」。Android 8 以后后台限制越来越严纯后台 Service 很难长期存活。常见做法是前台服务加常驻通知把 WebSocket 放在前台 Service 里。重连用指数退避第一次 1 秒第二次 2 秒最多退到 30 秒避免网络刚断时疯狂重连把服务端打满。// Android 端 OkHttp WebSocket 重连简化 class WsClient(private val deviceId: String) { private var retry 0 private val client OkHttpClient.Builder() .pingInterval(20, TimeUnit.SECONDS) // 协议层心跳配合业务心跳双保险 .build() fun connect() { val req Request.Builder().url(ws://your-host/ws/device).build() client.newWebSocket(req, object : WebSocketListener() { override fun onOpen(ws: WebSocket, resp: Response) { retry 0 ws.send({type:register,deviceId:$deviceId}) } override fun onFailure(ws: WebSocket, t: Throwable, r: Response?) { scheduleReconnect() } override fun onClosed(ws: WebSocket, code: Int, reason: String) { scheduleReconnect() } }) } private fun scheduleReconnect() { val delay min(30, 1 shl retry).toLong() // 1,2,4,8,16,30 秒 retry Handler(Looper.getMainLooper()).postDelayed({ connect() }, delay * 1000) } }参数说明pingInterval是 OkHttp 的协议层 ping服务端要能响应 pong否则连接会被判定失效业务心跳是应用层的用来更新lastSeen和触发服务端离线判定。两者不冲突建议都留。1 shl retry是位移算 2 的 retry 次方封顶 30 秒。注意重连时不要重新生成 deviceId否则服务端会认为是新设备设备列表里出现重复条目。3. 指令下发与结果回传把「截屏、装包、日志」做成可复用的任务模型3.1 指令协议设计请求、回执、超时三件套远程管控最容易翻车的地方不是通道而是「指令发出去了不知道成没成」。我一般把每条指令设计成三部分服务端下发taskIdactionparams端上执行后回taskIdstatusresult服务端侧维护一个待确认任务表超时未回执就标记失败。这样前端点「截屏」按钮后能明确看到「下发中 / 执行中 / 成功 / 超时」而不是一直转圈。字段方向说明taskId下行/上行服务端生成的 UUID端上原样回传action下行指令类型如 screenshot、install_apk、pull_logparams下行指令参数如 APK 下载地址、日志路径status上行success / fail / timeoutresult上行结果数据截屏返回图片 URL日志返回文件地址3.2 服务端任务调度与超时补偿服务端下发指令时先把任务写进pending_task表状态SENT同时起一个延迟任务比如 30 秒后检查如果还是SENT就置为TIMEOUT并通知前端。延迟任务不要用Thread.sleep会占线程用ScheduledExecutorService或时间轮都行。// 指令下发与超时补偿 Service public class TaskService { Autowired private TaskMapper taskMapper; private final ScheduledExecutorService scheduler Executors.newScheduledThreadPool(2); public String dispatch(String deviceId, String action, JSONObject params) { String taskId UUID.randomUUID().toString(); Task t new Task(taskId, deviceId, action, params.toJSONString(), SENT); taskMapper.insert(t); Session session DeviceEndpoint.ONLINE.get(deviceId); if (session null) { taskMapper.updateStatus(taskId, OFFLINE); return taskId; } JSONObject cmd new JSONObject(); cmd.put(type, command); cmd.put(taskId, taskId); cmd.put(action, action); cmd.put(params, params); session.getAsyncRemote().sendText(cmd.toJSONString()); // 30 秒后检查是否仍为 SENT scheduler.schedule(() - { Task cur taskMapper.selectById(taskId); if (SENT.equals(cur.getStatus())) { taskMapper.updateStatus(taskId, TIMEOUT); } }, 30, TimeUnit.SECONDS); return taskId; } }逻辑说明先落库再下发保证即使服务端崩溃重启后也能从库里恢复任务状态。OFFLINE是设备不在线时的快速失败比等 30 秒超时体验好。超时时间 30 秒是经验值装 APK 这类耗时操作可以按 action 类型单独配置比如install_apk给 120 秒。3.3 端上执行器用无障碍与 MediaProjection 拿到管控能力不 root 的前提下Android 端能做的管控能力有限但常用几项够用截屏用MediaProjection需要用户授权一次应用安装用Intent拉起系统安装器或企业内网用 Device Owner 模式静默安装日志拉取读应用自身目录和logcat缓存。这里要注意MediaProjection每次截屏都要先拿到VirtualDisplay频繁截屏要复用否则会闪。// 端上指令分发简化 fun onCommand(action: String, params: JSONObject, taskId: String) { when (action) { screenshot - { val bmp ScreenCapture.capture() // 内部复用 VirtualDisplay val url Uploader.upload(bmp) // 上传到服务端文件接口 reply(taskId, success, {url:$url}) } install_apk - { val apkUrl params.getString(url) val file Downloader.download(apkUrl) Installer.install(file) // Device Owner 下可静默否则拉起安装界面 reply(taskId, success, {}) } else - reply(taskId, fail, {msg:unknown action}) } }参数说明ScreenCapture.capture()内部要判断VirtualDisplay是否已创建已创建就复用只更新ImageReaderUploader.upload建议走 HTTP 分片截屏图几百 KB 到几 MB走 WebSocket 会阻塞指令通道。Installer.install在普通模式下会弹安装界面需要用户点确认这是系统限制不是代码问题做设备农场时通常配合 Device Owner 或厂商定制 ROM 解决。4. 避坑与排查设备远程管控里最容易翻车的五件事4.1 设备显示在线指令却发不出去现象前端设备列表显示在线点截屏一直超时。原因ONLINE里存的 session 已经半死TCP 连接还在但对端进程已挂sendText不报错但数据进黑洞。解决服务端加应用层心跳超时判定比如 90 秒没收到 heartbeat 就主动session.close()并从ONLINE移除下发前先检查session.isOpen()。4.2 重连后设备列表出现两条记录现象设备断网重连后列表里同一台设备出现两条。原因端上重连时重新生成了 deviceId或服务端onClose用remove(key)把新连接误删后旧连接又注册回来。解决deviceId 持久化到端上私有目录首次生成后不再变onClose用remove(key, value)精确移除。4.3 截屏返回黑图现象MediaProjection截出来全黑。原因Android 14 以后对MediaProjection加了限制部分场景需要前台服务类型声明mediaProjection且用户授权后不能跨进程复用。解决在AndroidManifest里给前台服务加android:foregroundServiceTypemediaProjection授权回调里立刻创建VirtualDisplay不要延迟。4.4 大量设备同时上线把服务端打满现象机房批量上电几百台设备同时连服务端 CPU 飙高、部分设备连不上。原因注册消息里带了大量设备信息每条都写库或者心跳间隔太短。解决注册信息异步落库用队列削峰心跳间隔设 30 到 60 秒服务端只更新内存WebSocket 容器线程池按设备数调大Tomcat 默认 200 线程不够就改maxThreads。4.5 指令结果乱序现象先发的截屏任务后返回后发的日志任务先返回前端展示错乱。原因端上多线程执行指令回传顺序不保证。解决前端按 taskId 匹配结果不要按到达顺序服务端任务表用 taskId 做主键回执按 taskId 更新天然幂等。5. 把管控平台做得能长期跑状态对账与灰度验证的一个习惯平台能不能长期跑不取决于功能多而取决于「状态能不能对账」。我一般会在服务端加一个定时对账任务每 5 分钟跑一次把数据库里lastSeen超过 2 分钟的设备标记为离线和ONLINE内存表比对不一致就以内存表为准修正数据库。这样即使服务端重启、网络抖动设备列表也不会长期失真。验证方法上不要只测单台设备。我习惯用一台设备做功能验证再用脚本模拟 50 台设备并发连接做压力验证。模拟脚本可以用 Java 写也可以用 Python 的websockets库重点看三件事注册成功率、心跳丢失率、指令平均回执时间。这三个指标稳定了平台才算能上线。验证项合格线观察方式注册成功率99% 以上服务端注册日志计数心跳丢失率1% 以下90 秒无心跳设备数 / 总在线数指令平均回执3 秒以内任务表 SENT 到 SUCCESS 的时间差最后一个习惯任何新指令上线前先在灰度设备上跑 24 小时观察任务表和端上日志确认没有内存泄漏和电量异常再全量。远程管控平台最怕的不是功能少而是「看起来在线实际失控」。把状态对账和灰度验证做成固定动作比事后救火省心得多。希望帮到你。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
农作物病害数据集:4800张图37类样本,YOLOv8目标检测实战 简介:这份农作物病害数据集面向从事农业AI、目标检测与图像分类的开发者与科研人员,覆盖10种作物的健康样本及27类病害样本,其中24类附带病害程度分析,可用于病害识别、健康检测与监测类项目。资源包共2000个文件,以19… · 2026/9/26 16:42:14
K8S集群监控镜像清单与离线打包:一次备齐所有镜像 简介:这份资源面向Kubernetes运维工程师、SRE及云原生监控学习者,聚焦集群监控体系的镜像与配置落地,解决监控组件离线部署、镜像拉取困难及配置模板缺失等问题。压缩包共124个文件,约650.88MB,以57个yaml清单、16个gz… · 2026/9/26 16:42:14
统信UOS下QT5读写xls:解决中文路径问题的ExcelFormat修改版 简介:这份资源面向在 Linux 环境下从事 Qt5 开发、需要处理 Excel 文件的工程师,重点解决 xls 读写时中文路径与中文内容乱码的问题。其核心是经过修改的 ExcelFormat 第三方库源码,在统信 UOS 系统的 aarch64、mips64 架构上经 gcc 编译验证… · 2026/9/26 16:42:14
C#上位机集成RMBG-2.0:ONNX Runtime背景去除实践指南 简介:面向 C# 开发者的 RMBG-2.0 背景去除推理集成包,适用于在线抠图、照片编辑、视频通话、虚拟现实等需要实时人像分离的场景。该包基于 OnnxRuntime 运行时加载预训练模型,打通了模型加载、预处理、推理与后处理的完整链路,不必… · 2026/9/26 17:18:26
前端页面空白?TaoToken 统一 Key 通道下排查 HTML 未渲染的配置骨架 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 17:18:26
RAG+多智能体协同的心内科智能诊断系统落地实践 简介:面向医疗人工智能与心内科辅助诊断领域,这份资源适合医工交叉项目开发者、算法工程师以及相关课题学生,用于构建集成检索增强生成与多智能体协同的自动化诊断系统。项目围绕真实临床场景,覆盖心电图、超声心动图、生化指标等… · 2026/9/26 17:18:26
Spring AI实战:用Spring Boot快速构建RAG与工具调用AI应用 1. Spring AI 不是“另一个 LangChain”:它解决的是 Spring 生态里真实存在的缝合痛点你有没有在 Spring Boot 项目里写过这样的代码?——先用 RestTemplate 调一个大模型 API,再手动把返回的 JSON 解析成对象;为了加个 RAG 功能&… · 2026/9/26 17:18:13
JSP图书管理系统实战:Java Web课设部署与代码改造全攻略 简介:基于jspmysqlservlet的JSP图书馆图书管理系统源码包,面向Java Web初学者及需要完成课程设计或毕业设计的在校学生。系统包含管理员、游客、学生三类角色,覆盖管理员登录、用户/图书管理、罚款缴纳,学生借阅、归还、借阅记录查… · 2026/9/26 17:18:13
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46