手写 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 除了内置的dashboard、thread、trace等命令之外,还支持通过 Java SPI 机制在服务端启动时加载外部 command jar,把团队内部常用的诊断动作封装成新的 Arthas 命令。本文以仓库中的最小示例模块 arthas-demo-external-command 为骨架,完整讲解外部命令的编写、构建、加载方式与底层实现原理,读完你可以独立为 Arthas 扩展一条自己的命令,并像内置命令一样通过help查看和执行。
外部命令机制是什么,为什么需要它
Arthas 的命令体系建立在 CommandResolver 之上:这是一个"命令解析器"接口,只有一个方法List<Command> commands(),由它把命令对象暴露给 Shell,Shell 才能识别并执行。内置命令(如dashboard、trace)由 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提供AnnotatedCommand、CommandProcess、Command、CommandResolver等核心接口,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.sh、arthas-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,链路如下:
- 收集加载位置:
collectCommandLocations把显式配置的arthas.commandLocations(按英文逗号切分)与默认${arthas.home}/commands目录合并为位置列表,默认目录标记为defaultLocation; - 解析为 URL:
resolveCommandLocationUrls逐一处理位置——目录则过滤出*.jar子文件,文件则校验.jar后缀,最终得到去重后的 URL 列表; - 加入 Arthas ClassLoader:
appendCommandUrls通过反射调用 Arthas 自定义 ClassLoader 的appendURL(URL)方法,把外部 jar 追加进类加载器,使外部命令的类对ServiceLoader可见; - SPI 发现 Resolver:
loadExternalCommandResolvers使用ServiceLoader.load(CommandResolver.class, classLoader)遍历 SPI 文件实例化所有外部 Resolver;单个 Resolver 加载失败(ServiceConfigurationError)只会记录 error 日志,不会中断其他 Resolver 的加载; - 注册命令并处理冲突:
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,完整模拟了真实使用场景:
- 先构建打包 Arthas 完整发行包(
packaging/target/arthas-bin)和 demo 命令 jar; - 把发行包复制到临时目录,在临时
${arthas.home}/commands/下放入 demo 命令 jar; - 启动目标 JVM(TargetJvmApp.java),用
as.sh --attach-only完成 attach; - 通过
arthas-client.jar以-c批处理模式执行help demo-external; demo-external Codex; - 断言输出包含
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-core、cli声明为provided | arthas-demo-external-command/pom.xml |
| 5. 构建 | ./mvnw -pl arthas-demo-external-command -DskipTests package | arthas-demo-external-command/target/*.jar |
| 6. 部署 | 放入${arthas.home}/commands/或通过--command-locations/arthas.properties指定 | ${arthas.home}/commands/ |
| 7. 验证 | 启动 Arthas 后执行help demo-external与demo-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),仅供参考