news 2026/9/19 6:04:00

手写 Arthas 外部命令(External Command)插件:基于 arthas-demo-external-command 从零落地一个可热插拔的 `demo-external` 命令

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手写 Arthas 外部命令(External Command)插件:基于 arthas-demo-external-command 从零落地一个可热插拔的 `demo-external` 命令

手写 Arthas 外部命令(External Command)插件:基于 arthas-demo-external-command 从零落地一个可热插拔的demo-external命令

【免费下载链接】arthasAlibaba Java Diagnostic Tool Arthas/Alibaba Java诊断利器Arthas项目地址: https://gitcode.com/gh_mirrors/ar/arthas

Arthas 除了内置的dashboardthreadtrace等命令之外,还支持通过 Java SPI 机制在服务端启动时加载外部 command jar,把团队内部常用的诊断动作封装成新的 Arthas 命令。本文以仓库中的最小示例模块 arthas-demo-external-command 为骨架,完整讲解外部命令的编写、构建、加载方式与底层实现原理,读完你可以独立为 Arthas 扩展一条自己的命令,并像内置命令一样通过help查看和执行。

外部命令机制是什么,为什么需要它

Arthas 的命令体系建立在 CommandResolver 之上:这是一个"命令解析器"接口,只有一个方法List<Command> commands(),由它把命令对象暴露给 Shell,Shell 才能识别并执行。内置命令(如dashboardtrace)由 BuiltinCommandPack 统一注册。

外部命令机制的意义在于:在不改动 Arthas 核心源码的前提下,把团队沉淀下来的诊断动作(比如一键 dump 特定线程、按业务标识查上下文等)封装成标准 Arthas 命令,随团队工具链一起分发。命令加载后与内置命令体验一致:支持参数解析、可以出现在help列表中、可在 telnet/Web Console 中直接执行。

仓库中的 arthas-demo-external-command 正是为了演示这条链路而提供的最小可运行示例,它定义了一条名为demo-external的命令,接受一个可选参数并回显输出。

示例模块的构成:三个关键部分

外部命令 jar 通过 Java SPI 暴露CommandResolver。以demo-external为例,一个最小外部命令包含三个组成部分。

1. 命令实现类:继承 AnnotatedCommand

命令类 DemoExternalCommand.java 继承com.taobao.arthas.core.shell.command.AnnotatedCommand,用com.alibaba.middleware.cli注解声明命令元数据与参数:

package demo.command; import com.taobao.arthas.core.shell.command.AnnotatedCommand; import com.taobao.arthas.core.shell.command.CommandProcess; import com.taobao.middleware.cli.annotations.Argument; import com.taobao.middleware.cli.annotations.Description; import com.taobao.middleware.cli.annotations.Name; import com.taobao.middleware.cli.annotations.Summary; @Name("demo-external") @Summary("Demo external command loaded from arthas.home/commands") @Description("Examples:\n" + " demo-external\n" + " demo-external Codex\n") public class DemoExternalCommand extends AnnotatedCommand { private String message; @Argument(index = 0, argName = "message", required = false) @Description("message printed by the demo external command") public void setMessage(String message) { this.message = message; } @Override public void process(CommandProcess process) { String value = message; if (value == null || value.trim().isEmpty()) { value = "hello"; } process.write("demo external command loaded: " + value + "\n"); process.end(); } }

几个值得注意的细节:

  • @Name声明命令名demo-external,即用户在 Arthas 交互界面输入的命令字;
  • @Summary是命令在help列表中的一句话说明,@Description展示命令的详细用法示例;
  • @Argument(index = 0, argName = "message", required = false)声明第一个位置参数message,且可省略——省略时process中回退为默认值hello
  • process(CommandProcess process)是命令的执行入口:通过process.write(...)向会话输出结果,最后必须调用process.end()结束命令执行。

2. CommandResolver 实现:返回命令列表

解析器 DemoExternalCommandResolver.java 实现CommandResolver,通过Command.create(...)把注解驱动的命令类转换成Command实例:

package demo.command; import java.util.Collections; import java.util.List; import com.taobao.arthas.core.shell.command.Command; import com.taobao.arthas.core.shell.command.CommandResolver; public class DemoExternalCommandResolver implements CommandResolver { @Override public List<Command> commands() { return Collections.singletonList(Command.create(DemoExternalCommand.class)); } }

一个 Resolver 可以返回多个命令(List<Command>),团队可以把一组相关的诊断命令放在同一个 jar、同一个 Resolver 中统一注册。

3. SPI 注册文件:让 ServiceLoader 发现 Resolver

src/main/resources/META-INF/services/下创建以接口全限定名为文件名的 SPI 文件:

META-INF/services/com.taobao.arthas.core.shell.command.CommandResolver

文件内容写入 Resolver 实现类的全限定名:

demo.command.DemoExternalCommandResolver

这一文件是外部命令能否被发现的关键。Arthas 服务端启动时通过ServiceLoader.load(CommandResolver.class, ...)扫描该文件并实例化 Resolver,具体见下文源码分析。

Maven 依赖:把 Arthas 接口打成 provided

外部命令必须依赖 Arthas 的命令接口与 CLI 注解,但建议声明为provided作用域,避免把 Arthas 自身类重复打包进外部命令 jar,从而与 Arthas 服务端自身的 ClassLoader 冲突。示例模块 pom.xml 的依赖如下:

<dependencies> <dependency> <groupId>com.taobao.arthas</groupId> <artifactId>arthas-core</artifactId> <version>${project.version}</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.alibaba.middleware</groupId> <artifactId>cli</artifactId> <scope>provided</scope> </dependency> </dependencies>

其中arthas-core提供AnnotatedCommandCommandProcessCommandCommandResolver等核心接口,cli提供@Name@Summary@Description@Argument等参数解析注解。运行时这些类由 Arthas 服务端提供,因此provided是最安全的做法。

构建与部署:三步让命令跑起来

第一步:构建命令 jar

在仓库根目录执行(复用仓库自带的 Maven Wrapper,不需要预先安装 Maven):

./mvnw -pl arthas-demo-external-command -DskipTests package

构建产物位于arthas-demo-external-command/target/目录下,形如arthas-demo-external-command-<version>.jar

第二步:把 jar 放入加载目录

把产物 jar 放到${arthas.home}/commands/目录下(${arthas.home}是 Arthas 安装目录,即as.sharthas-boot.jar所在位置):

mkdir -p ${arthas.home}/commands cp arthas-demo-external-command/target/arthas-demo-external-command-*.jar ${arthas.home}/commands/

注意:${arthas.home}/commands目录本身需要存在才会被扫描。如果目录不存在,直接跳过该默认加载位置。

第三步:启动 Arthas 并执行

启动 Arthas 并 attach 到目标 Java 进程:

java -jar arthas-boot.jar <pid>

进入 Arthas 交互界面后,执行:

demo-external demo-external Codex

预期输出:

demo external command loaded: hello demo external command loaded: Codex

不带参数时回显默认值hello,带参数时回显传入的Codex。也可以在help中看到这条命令:

help demo-external

外部命令的三种加载方式

外部命令只在 Arthas 服务端启动时加载。Arthas 已经 attach 到目标 JVM 后,再把 jar 放进目录不会自动生效,需要重启 Arthas 服务端。

方式一:启动参数--command-locations

arthas-boot.jar与完整包中的as.sh均支持--command-locations参数,值可以是 jar 文件路径,也可以是目录路径,多个路径用英文逗号分隔:

java -jar arthas-boot.jar --command-locations '/opt/arthas/ext-command.jar,/opt/arthas/ext-commands' <pid>
./as.sh --command-locations '/opt/arthas/ext-command.jar,/opt/arthas/ext-commands' <pid>

方式二:配置文件arthas.properties

在 Arthas 的 arthas.properties 中配置:

arthas.commandLocations=/opt/arthas/ext-command.jar,/opt/arthas/ext-commands

如果通过arthas-spring-boot-starter启动(见 arthas-spring-boot-starter),Spring Boot 配置文件中推荐使用连字符写法:

arthas.command-locations=/opt/arthas/ext-command.jar,/opt/arthas/ext-commands

方式三:默认${arthas.home}/commands目录

如果${arthas.home}/commands目录存在,Arthas 启动时自动加载该目录下所有*.jar

mkdir -p ${arthas.home}/commands cp arthas-demo-external-command.jar ${arthas.home}/commands/

三种方式可叠加使用:显式配置的arthas.commandLocations先加载,随后再加载默认的${arthas.home}/commands目录。

目录扫描规则与打包建议

从 ArthasBootstrap.resolveCommandLocationUrls 的扫描实现可以归纳出以下规则:

  • 单个路径可以指向 jar 文件,也可以指向目录;
  • 目录只扫描当前目录下的*.jar,不会递归扫描子目录(使用File.listFiles过滤.jar后缀文件);
  • 目录内 jar 按文件名排序后逐个加入,保证加载顺序确定;
  • 同一个 jar 路径会按规范化后的绝对路径去重(内部用LinkedHashMap以规范路径为 key 去重,见 addCommandUrl);
  • 如果外部命令依赖第三方 jar,可以把依赖一起打包进命令 jar,或把依赖 jar 一起放到被扫描的目录中;
  • 不存在的路径会被跳过并记录 warn 日志(Skip arthas external command location because it does not exist)。

源码级原理:从启动到命令注册的完整调用链

外部命令加载的核心逻辑集中在 ArthasBootstrap.java,链路如下:

  1. 收集加载位置collectCommandLocations把显式配置的arthas.commandLocations(按英文逗号切分)与默认${arthas.home}/commands目录合并为位置列表,默认目录标记为defaultLocation
  2. 解析为 URLresolveCommandLocationUrls逐一处理位置——目录则过滤出*.jar子文件,文件则校验.jar后缀,最终得到去重后的 URL 列表;
  3. 加入 Arthas ClassLoaderappendCommandUrls通过反射调用 Arthas 自定义 ClassLoader 的appendURL(URL)方法,把外部 jar 追加进类加载器,使外部命令的类对ServiceLoader可见;
  4. SPI 发现 ResolverloadExternalCommandResolvers使用ServiceLoader.load(CommandResolver.class, classLoader)遍历 SPI 文件实例化所有外部 Resolver;单个 Resolver 加载失败(ServiceConfigurationError)只会记录 error 日志,不会中断其他 Resolver 的加载
  5. 注册命令并处理冲突createExternalCommandRegistry把外部命令逐个注册进CommandRegistry(其内部是一个ConcurrentHashMap<String, Command>,见 CommandRegistry.java),注册过程中做重名与保留名检查。

注册阶段的冲突处理规则

createExternalCommandRegistry中体现的注册规则(ArthasBootstrap.java):

  • 外部命令不能覆盖 Arthas 内置命令:内置命令名(来自 Shell 的命令管理器和BuiltinCommandPack)构成保留名集合,外部命令若与内置命令重名,会被跳过并记录the name is reserved的 warn 日志;
  • 多个外部命令重名时只保留第一个:内部用externalNames集合去重,后续重名命令被跳过并记录the name is duplicated的 warn 日志;
  • 命令名为空、command.name()抛异常等异常情况同样会被跳过并记录日志;
  • 如果没有发现任何有效外部命令,返回null,Arthas 按正常流程继续启动,不会报错中断。

从代码结构看,CommandResolver接口的设计非常精简(只有一个commands()方法),外部扩展点完全收敛在 SPI 上,这也是外部命令机制能保持内核稳定的原因。

集成测试:自动化验证"加载 → 注册 → 执行"全链路

仓库提供了端到端集成测试 ExternalCommandLoadingIT.java,完整模拟了真实使用场景:

  1. 先构建打包 Arthas 完整发行包(packaging/target/arthas-bin)和 demo 命令 jar;
  2. 把发行包复制到临时目录,在临时${arthas.home}/commands/下放入 demo 命令 jar;
  3. 启动目标 JVM(TargetJvmApp.java),用as.sh --attach-only完成 attach;
  4. 通过arthas-client.jar-c批处理模式执行help demo-external; demo-external Codex
  5. 断言输出包含demo-external(说明已注册、可被help发现)和demo external command loaded: Codex(说明执行结果正确)。

这个测试是对整条外部命令链路的自动化背书:只要 jar 按规范放在加载位置,启动后就能被help发现并正常执行。注意测试中有Assumptions.assumeFalse(isWindows(), ...)的判断,即该集成测试依赖bash/as.sh,Windows 环境会跳过。

完整上手清单

步骤操作关键产物/路径
1. 写命令类继承AnnotatedCommand,用 CLI 注解声明元数据与参数demo.command.DemoExternalCommand
2. 写 Resolver实现CommandResolver.commands()返回命令列表demo.command.DemoExternalCommandResolver
3. 写 SPI 文件文件名为接口全限定名,内容为 Resolver 类名src/main/resources/META-INF/services/com.taobao.arthas.core.shell.command.CommandResolver
4. 配置依赖arthas-corecli声明为providedarthas-demo-external-command/pom.xml
5. 构建./mvnw -pl arthas-demo-external-command -DskipTests packagearthas-demo-external-command/target/*.jar
6. 部署放入${arthas.home}/commands/或通过--command-locations/arthas.properties指定${arthas.home}/commands/
7. 验证启动 Arthas 后执行help demo-externaldemo-external <msg>输出demo external command loaded: ...

更完整的机制说明(加载方式、扫描规则、冲突处理)可进一步阅读官方文档 site/docs/doc/external-command.md(英文版见 site/docs/en/doc/external-command.md)。掌握了这套最小链路后,把命令类中的process换成实际的诊断逻辑(如调用 Arthas 的增强 API、读取业务上下文),一条真正可复用的团队诊断命令就诞生了。

【免费下载链接】arthasAlibaba Java Diagnostic Tool Arthas/Alibaba Java诊断利器Arthas项目地址: https://gitcode.com/gh_mirrors/ar/arthas

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 6:01:02

Claude Code与MCP实战:从协议原理到自研Server的AI编程指南

最近小半年&#xff0c;程序员群里高频出现的词&#xff0c;一个是 Claude Code&#xff0c;一个是 MCP。我自己在主力项目里已经用上了一套组合&#xff1a;Claude Code 负责读代码、改文件、跑测试&#xff0c;MCP 负责把它跟本地数据、设计稿、数据库接起来。这套组合解决的…

作者头像 李华
网站建设 2026/9/19 5:59:50

企业毛利率计算全解析:从基础到实战

1. 毛利率计算的核心价值与常见误区毛利率作为企业经营的核心财务指标&#xff0c;直接反映产品的盈利能力和市场竞争力。但我在审计工作中发现&#xff0c;超过60%的中小企业主和创业者都存在计算错误或理解偏差。最常见的三类错误是&#xff1a;混淆成本口径&#xff08;把运…

作者头像 李华
网站建设 2026/9/19 5:59:20

全网信息深度挖掘:爆火项目的数据分析与技术实现

1. 项目背景与核心价值最近在技术圈里经常听到同行们讨论"全网信息深度挖掘"这个方向&#xff0c;特别是针对那些突然爆火的项目。作为一个常年和数据打交道的从业者&#xff0c;我深刻理解这类项目的商业价值和技术挑战。当某个项目突然走红时&#xff0c;快速获取全…

作者头像 李华
网站建设 2026/9/19 5:55:03

深入理解AST:从抽象语法树到自动化代码转换实战

1. 先弄懂AST到底是什么AST&#xff08;Abstract Syntax Tree&#xff0c;抽象语法树&#xff09;这个词&#xff0c;做前端、做编译器、做IDE插件的人基本天天见。但很多刚接触的同学第一次看到那棵“树”时是懵的&#xff1a;一堆对象、type、start、end、loc……它到底怎么来…

作者头像 李华