从“装不上 JVM“到“零权限诊断“:Arthas Diag开源软件适配鸿蒙 PC 适配实战全记录
从"装不上 JVM"到"零权限诊断":Arthas Diag开源软件适配鸿蒙 PC 适配实战全记录
欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/
欢迎在 PC 社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper
适配开源地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_arthas
本文记录一个 JVM 诊断工具在 HarmonyOS PC 上的完整适配历程:从第一版方案的彻底溃败,到架构级重构,再到真机联调中一个个错误码的排查,最后在鸿蒙 PC 的 openEuler 融合开发环境里跑通"本机 JVM + 原生 GUI"的闭环。所有命令、错误码、截图均来自真实操作记录,希望能给鸿蒙 PC 软件适配的同学一份可复用的参考。
一、缘起:为什么要做这个工具
Arthas 是阿里巴巴开源的 Java 诊断利器,日常排查线上 JVM 问题几乎离不开它。但它的官方交互形态是命令行(telnet/终端),在 PC 上做诊断时,工程师往往还想要一个图形化面板:线程、内存、JVM 参数一屏尽览,点开即用。

我们的目标是:在 HarmonyOS PC 上做一个原生的 Arthas GUI 诊断工具。名字很朴素,叫 Arthas Diag,功能定为五大只读面板——Dashboard、Threads、JVM、SysProp、SysEnv。


看起来是个"客户端壳子"的活儿,实际上这条适配之路走了三个阶段、推翻了一次架构、踩了至少六个大坑。先从最惨的部分讲起。
二、第一版方案的溃败:把 JVM 搬进鸿蒙的三条死路
适配组最初的思路非常直觉:Arthas 是 Java 写的,那就把 Java 运行时一起搬进鸿蒙 PC。具体做法是用 HNP(HarmonyOS Native Package)把完整 JDK + Arthas 打包,通过 HiShell 启动。
这个方案在纸面上成立,在真机上撞了三条硬约束,每一条都是系统级的、绕不过去的:
| 约束 | 具体表现 |
|---|---|
JIT 需要 execmem 权限 | normal 签名的应用拿不到,HotSpot JIT 一开就崩 |
| attach 其他 JVM 进程 | 鸿蒙沙箱隔离,跨进程 JVMTI attach 直接被拒 |
HNP 二进制 noexec | 能装进 hdc 域,但文件系统挂了 noexec,装了也跑不起来 |
三条约束分别封死了运行时(JVM 起不来)、诊断机制(attach 不了目标进程)和分发方式(二进制无法执行)。也就是说,在鸿蒙 PC 的应用沙箱里,"自带一个完整 JVM 去诊断别的 JVM"这条路线在物理上就不成立。
这里有个值得所有适配同学记住的教训:
在受限平台上,"把整个运行时打包进去"往往是下策。先问一句:目标平台原生提供了什么能力?我真正需要的是运行时本身,还是运行时产出的数据?
Arthas 的本质是"采集 JVM 内部数据 + 渲染"。我们需要的其实只是数据,而不是在鸿蒙沙箱里复刻一个 JVM。
三、架构重构:HAP 只做 HTTP 客户端
想通上面这一点,新架构就自然浮出来了:让目标 JVM 自己跑 Arthas agent(它本来就在任何标准 JVM 上能跑),鸿蒙端的 HAP 退化成一个纯粹的 HTTP 客户端。

Arthas 从早期版本就内置了 Http API:向 http://<host>:<port>/api 发一个 {"action":"exec","command":"dashboard -n 1"},返回标准 JSON。这个接口天然适合 GUI 化——命令行的一切能力(dashboard、thread、jvm、sysprop、sysenv)都能映射成一个面板。
新方案的收益是全方位的:
- 零特权:所有通信走
@kit.NetworkKit的http,声明INTERNET+GET_NETWORK_INFO两个 normal 权限即可,normal APL 签名直接过; - 零注入:不碰 JVMTI、不碰 execmem、不碰字节码增强,只读诊断;
- 跨平台:目标 JVM 可以是本机的,也可以是局域网里任何一台机器——鸿蒙 PC 由此变成了整个研发网的"移动诊断终端"。
工程结构也随之极简,核心就三个文件:
entry/src/main/ets/
├── common/
│ ├── ArthasClient.ets # HTTP 客户端(核心,约 200 行)
│ ├── DiagFormat.ets # 数据格式化层
│ └── Theme.ets # 暗色主题 Token
├── entryability/EntryAbility.ets
└── pages/Index.ets # 主页面(连接栏 + 5 面板)
四、ArkTS 实现的四个关键细节
架构简单,不等于实现没有坑。这四个细节是实战中反复打磨出来的。
4.1 诊断流量必须直连:usingProxy: false
这是最隐蔽的一个坑。鸿蒙 PC 上如果系统配置了代理(公司内网环境太常见了),http.request 默认可能走代理,而诊断目标往往是内网 IP(192.168.x.x),代理根本转发不进去,表现为超时或者莫名的连接失败。所以必须显式声明直连:
const resp = await request.request(url, {
method: http.RequestMethod.POST,
header: headers,
// 诊断目标是指定 JVM,必须直连,不能走系统代理
usingProxy: false,
extraData: JSON.stringify(payload),
connectTimeout: 8000,
readTimeout: timeoutMs + 5000,
expectDataType: http.HttpDataType.STRING
});
4.2 ArkTS 严格模式:禁止索引签名
Arthas 返回的 results 数组里,每条结果的字段随命令变化(dashboard 有 memory/threads,thread 有 threadInfo…)。习惯了 TS 的同学会顺手写 obj['type'] 这种索引访问——ArkTS 严格模式直接报错。解法是用 Record<string, Object> 显式承接动态字段,再封装一个安全取值函数:
export interface ArthasResult {
type: string;
statusCode: number;
/** 该条结果的完整原始对象(含所有动态字段) */
data: Record<string, Object>;
}
export function getField(obj: Record<string, Object> | undefined,
key: string): Object | undefined {
if (!obj) {
return undefined;
}
const v: Object | undefined = obj[key];
return v;
}
这套"显式接口 + Record + getField"的模式,后来成了项目里处理一切动态 JSON 的标准姿势。
4.3 Arthas 的鉴权语义:非 localhost 强制 Basic Auth
Arthas 有个容易被忽略的安全策略:localConnectionNonAuth 只对来自 127.0.0.1 的连接免鉴权;只要来源不是本机回环,就强制要求 Basic Auth。而当你用 --target-ip 0.0.0.0 把 Arthas 暴露到网卡上时,它会自动生成一个随机密码,写在自己的启动日志里(~/logs/arthas/arthas.log)。
所以客户端必须做两件事:支持 Basic Auth 头,以及对 401 给出人话提示:
if (resp.responseCode === 401) {
return this.fail('认证失败(401):该 Arthas 要求用户名密码,请在密码框填入其启动日志中的密码');
}
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (this.username.length > 0) {
headers['Authorization'] = `Basic ${this.basicAuth()}`;
}
4.4 把网络自检做进 UI
联调时最浪费时间的莫过于"连不上,但不知道是哪一层的锅"。我们把网络自检直接做成了连接栏的一个按钮,一键打印本机 IP、网关、DNS、目标地址:
async runDiag(): Promise<void> {
const lines: string[] = [];
const has = await connection.hasDefaultNet();
lines.push(`默认网络:${has ? '有' : '无'}`);
if (has) {
const net = await connection.getDefaultNet();
const props = await connection.getConnectionProperties(net);
// ... 输出本机IP / DNS
}
lines.push(`目标:${this.host}:${this.portText}`);
this.diagText = lines.join(' ');
}

这个 200 行不到的小功能,在后面的联调里立了大功——看到"本机IP:192.168.1.7 目标:192.168.1.9"却依然连不上,我们才能确定问题不在地址,而在网段或代理。
五、真机联调实录:每一个错误码背后都是一堂课
架构正确了,联调依然是一场硬仗。以下按时间顺序记录几个典型回合,错误码均为真机实录。
5.1 第一回合:局域网直连不通
最初的目标 JVM 跑在 Mac 上(192.168.1.9:8563),鸿蒙 PC 填这个地址,连接失败。网络自检显示本机是 192.168.1.7,同网段,却依然不通——最终定位是中间网络设备/代理策略的问题,而非应用配置。
解法:hdc 反向转发,绕开整个局域网。 用 USB 调试通道把 Mac 的 8563 映射到鸿蒙 PC 的回环口:
# Mac 侧执行
hdc rport tcp:8563 tcp:8563
之后鸿蒙 PC 上访问 127.0.0.1:8563,流量走 USB 线直达 Mac 的 Arthas,不经过任何局域网设备。这一招让"App 连不上开发机"的问题被结构性消灭,也让第一版 GUI 顺利截图验证。
复盘:跨设备联调时,USB 通道(hdc rport/fport)是比排查局域网策略可靠得多的兜底路径。
rport反向(设备端口→主机服务)、fport正向(主机端口→设备服务),配合使用几乎可以无视网络环境。
5.2 第二回合:错误码 2300007 与一个字符的战争

服务端 netstat 明明显示 127.0.0.1:8563 LISTEN,应用却报 2300007(Failed to connect),反复重试无果。排查了近十个回合,最后发现地址栏里填的是 127.0.1——不是 127.0.0.1,少了一个 0。五段变四段,输入框里肉眼几乎不可辨,服务端一切正常,唯独客户端目标地址是错的。
这个回合的教训促成了两个产品决策:
- 对 IPv4 地址做格式校验:四段、每段 0-255,不合格直接拦下并提示,而不是放行后吐一个裸错误码;
- 错误提示要带上下文:把"目标地址:端口"回显在错误信息里,让"填错地址"这类问题一眼可见。
人眼校验是最低效的调试手段。能放进代码里的校验,就不要留给肉眼。
5.3 第三回合:用户名 arthas、密码留空,还是 401
连上本机回环后,又卡在鉴权上。这里恰好是 Arthas 鉴权语义(见 4.3)的实践现场:
- 连
127.0.0.1时,来源是本机回环,免鉴权,用户名密码留空即通; - 一旦换成
--target-ip 0.0.0.0暴露到网卡、从别的地址连,必须带 Basic Auth,且密码是 Arthas 自动生成、写在目标机~/logs/arthas/arthas.log里的随机串。
于是客户端的连接表单最终定型为:地址、端口、用户名(默认 arthas)、密码(可空)四项,并对 401 单独给出"去启动日志里找密码"的提示。
5.4 第四回合:在 openEuler 容器里点亮本机 JVM
最初的联调目标是 Mac 上的 JVM,但产品愿景毕竟是"鸿蒙 PC 自成一体"。于是有了最后一章探索:鸿蒙 PC 自己能不能跑 JVM?
鸿蒙 PC 的原生系统里确实没有 java。但 HarmonyOS PC 提供了融合开发引擎——一个 openEuler 容器环境。在里面实测:

$ java -version
openjdk version "17.0.15" 2025-04-15
OpenJDK Runtime Environment BiSheng (build 17.0.15+6)
OpenJDK 64-Bit Server VM BiSheng (build 17.0.15+6, mixed mode, sharing)

毕昇 JDK 17,完整可用,javac 也在。于是三步走:

第一步,写一个有活动负载的 Demo 进程(后台线程做计算、主线程周期性分配内存,这样 Dashboard 上才有东西可看):
cd ~/arthas-demo && cat > Demo.java <<'EOF'
// business-worker 线程每秒 busyWork;
// 主线程每 2s 分配 512KB,到 50 块清空,制造 GC 波形
EOF
javac Demo.java && nohup java Demo > demo.log 2>&1 &


第二步,挂 Arthas(容器内在线下载一路畅通):
curl -sL https://arthas.aliyun.com/arthas-boot.jar -o arthas-boot.jar
java -jar arthas-boot.jar 397 \
--telnet-port 8563 --http-port 3658 \
--target-ip 0.0.0.0 \
--username arthas --password arthas
Arthas 4.3.5 自动下载、attach 成功,熟悉的 ASCII 大字 logo 跳出来:
[INFO] Attach process 397 success.
[INFO] arthas-client connect 127.0.0.1 8563
wiki https://arthas.aliyun.com/doc
version 4.3.5
main_class Demo
pid 397
[arthas@397]$

顺带一个反向提醒:[arthas@397]$ 这个提示符不是 shell。把 netstat、cd 甚至误触的 v 敲进去,只会得到 command not found——它只认 dashboard、thread、watch 这些 Arthas 自家命令。实战中在这里空转过几个回合,值得记一笔。

第三步,验证端口与连通:
$ netstat -an | grep -E '8563|3658'
tcp6 0 0 127.0.0.1:8563 :::* LISTEN
tcp6 0 0 127.0.0.1:3658 :::* LISTEN

到这里出现了一个非常有价值的认知修正:容器内 netstat 显示监听 127.0.0.1,但鸿蒙原生应用连这个 127.0.0.1 是连不到的——openEuler 容器与鸿蒙原生系统是隔离的两套网络栈,容器里的回环地址只在容器内可见。这和第二部分"沙箱里装不下 JVM"是同一类结构问题:边界之内的一切,对边界之外都是透明的反面——不可见。
正确姿势是让容器内服务监听 0.0.0.0(如上命令),再从容器分到的网卡 IP 去连;跨环境的 127.0.0.1 永远只指"自己所在的这一层"。

5.5 联调方法论沉淀
四个回合下来,沉淀出一套鸿蒙 PC 网络问题的分层排查顺序,后来凡遇连接问题按此走,基本不再空转:
- 客户端校验层:地址格式、端口范围——把
127.0.1这类问题拦在发起请求之前; - 系统网络层:网络自检(本机 IP/DNS/默认网络),确认不是无网、不是网段错;
- 代理层:
usingProxy: false了吗?环境代理是否吞掉了内网请求; - 服务端层:目标机上
netstat/ss确认监听地址与端口——注意监听在127.0.0.1还是0.0.0.0,跨环境时这是生死线; - 鉴权层:401 → 查目标机
~/logs/arthas/arthas.log里的自动生成密码; - 兜底:USB 通道
hdc rport/fport,绕开一切网络策略。
六、五大面板的实现映射
连通之后,面板就是纯粹的"命令 → JSON → UI"映射,这也是 HTTP 客户端方案的优雅之处——Arthas 命令行的能力边界就是 GUI 的能力边界:
| 面板 | Arthas 命令 | 说明 |
|---|---|---|
| Dashboard | dashboard -n 1 | 线程统计、内存、运行时、CPU Top 线程 |
| Threads | thread | 全部线程列表,含状态/优先级/CPU 占比 |
| JVM | jvm | JVM 参数分组 KV 展示 |
| SysProp | sysprop | 系统属性,支持实时过滤 |
| SysEnv | sysenv | 环境变量,支持实时过滤 |

值得强调的是只读纪律:客户端只下发查询类命令,不封装 watch/trace/retransform 这类有侵入性的命令,保证工具本身对目标 JVM 零风险——这也是它能通过 normal APL 审核、被放心使用的底气。
七、总结:鸿蒙 PC 适配的六条军规
整条适配之路,可以浓缩成六条经验:
- 先问"该不该跑",再问"能不能跑"。三条系统级硬约束(execmem/attach/noexec)证明:在受限平台上,搬运完整运行时常常是死路;重新切分职责(数据采集留在目标 JVM,渲染留在 HAP)才是活路。
- 善用平台已有的容器能力。鸿蒙 PC 的 openEuler 融合开发引擎自带毕昇 JDK 17,“鸿蒙 PC 上没有 Java"是个伪命题——准确说是"原生侧没有,容器里有”。
- 容器边界意识。容器内外的
127.0.0.1不是同一个地址;跨环境连接,要么监听0.0.0.0走真实 IP,要么用 hdc 端口转发打洞。 - 诊断流量直连。
usingProxy: false应该是所有运维/诊断类工具的默认选择。 - 把校验和自检做进产品。地址格式校验、一键网络自检、带上下文的错误提示(包括把错误码翻译成人话,比如 2300007 → 连接失败),这些"小事"在联调时省下的时间是指数级的。
- 错误码是最好的文档。每一个 2300007、每一次 401,背后都对应一层明确的原因;把排查顺序固化成分层清单,问题就变成了查表。
从"多少个回合都连不上"的深夜,到 openEuler 容器里 [arthas@397]$ 的提示符亮起,再到鸿蒙 PC 原生界面上 Dashboard 数据开始滚动——这条路上真正的产出,不只是一个 200 行核心客户端的 HAP,更是一套"在受限平台上重新思考软件形态"的方法论。
鸿蒙 PC 的生态适配,拼的从来不是蛮力移植,而是对平台边界的精确理解,以及在边界之内做对的架构选择。
八、常见问题 FAQ
Q1:鸿蒙 PC 上能跑 Java 吗?
原生侧不能,但融合开发引擎的 openEuler 容器里自带毕昇 JDK 17(含 javac),java -version 直接可用。“鸿蒙 PC 没有 Java"是个伪命题——准确说是"原生侧没有,容器里有”。
Q2:连接报 2300007(Failed to connect)怎么排查?
按分层清单走:先核对地址格式(实战中曾把 127.0.0.1 误填成 127.0.1,肉眼几乎不可辨)→ 应用内网络自检确认本机 IP/网段 → 确认目标机 netstat 端口在监听 → 检查是否被系统代理拦截(客户端需 usingProxy: false)。
Q3:Arthas 的密码是什么?401 怎么破?
Arthas 只对来自 127.0.0.1 的连接免鉴权。一旦用 --target-ip 0.0.0.0 监听到网卡,它会自动生成随机密码,写在目标机 ~/logs/arthas/arthas.log 里,用户名默认 arthas,填进连接表单即可。
Q4:容器里明明监听了 127.0.0.1:8563,原生 App 为什么连不上?
openEuler 容器与鸿蒙原生系统是隔离的两套网络栈,容器内的回环地址只在容器内可见。解法:服务监听 0.0.0.0,App 改连容器分到的网卡 IP;或者用 hdc rport/fport 做 USB 通道端口转发打洞。
Q5:normal APL 签名能通过审核吗?
能。本方案只声明 INTERNET + GET_NETWORK_INFO 两个标准权限,全程标准 HTTP + JSON,不碰 JVMTI/execmem/字节码注入,普通 normal 签名即可运行。
Q6:在 [arthas@397]$ 提示符里敲 netstat、cd 报 command not found?
这不是 shell,是 Arthas 的交互控制台,只认 dashboard、thread、watch 等自家命令。要跑系统命令,请另开一个终端。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐




所有评论(0)