零侵入式启动javaagent在stagemonitor中的实现
启动 Javaagent
以下摘自 Oracle Javase 8 Doc :
Command-Line Interface
An implementation is not required to provide a way to start agents from the command-line interface. On implementations that do provide a way to start agents from the command-line interface, an agent is started by adding this option to the command-line:
-javaagent:jarpath[=options]
通过命令行参数的方式启动 Javaagent 是最常用的一种方式,但却不是唯一的方式。正如文档中提到的, 启动 Javaagent 其实是有两个入口方法:
public static void premain(...); // [1]
public static void agentmain(...); // [2]
- 命令行参数的方式启动的是
premain
,而另一种 agentmain
则是通过 Attach API 启动的,如下:
String pid = ...
VirtualMachine vm = VirtualMachine.attach(pid);
vm.loadAgent("/path/to/agent");
vm.detach();
零侵入式随JVM运行而启动 Javaagent
所谓零侵入, 是指对目标JVM应用不做任何修改,有且仅将 Javaagent 置于特定的位置就能实现对其启动呢?
只是基于前文的知识,几乎是不可能做到的。幸运的是,我曾和一位资深专家聊到过这个话题,他启发我从 stagemonitor 中找到了答案。
其实,要想启动 Javaagent 就不可能存在有别于前文的第三种途径,毕竟 JVM 的实现只提供了这两种。
二选一,问题比原先想象的要简单。我们可以排除命令行参数的方式,因为它必须要改变 JVM 应用原有的启动命令。那么剩下 Attach API 的方式是唯一的选择, 接下来要搞明白的事情是 由谁来执行预先准备好的 Attach 过程。
看似就剩 “最后一公里”,实在不然。揣摩之前,必须破除两个思维的限制:
- 对 Attach API 的调用只能是在另个进程中执行;
- 对 Instrumentation API 的调用只能在
agentmain
方法中执行。
stagemonitor 恰恰是因为没有上述两堵思维的墙,因而实现得非常巧妙。
ServiceLoader
Tomcat 作为 Servlet 容器的标准实现,借助 ServiceLoader 回调所有能够 找到 的ServletContainerInitializer
接口的实现者。
stagemonitor 中 WebPlugin 实现了 ServletContainerInitializer
接口, 并在类初始化阶段完成 Attach 的过程。
public class WebPlugin extends StagemonitorPlugin
implements ServletContainerInitializer {
static {
Stagemonitor.init();
}
...
}
这里打破了第一堵墙,在同一进程内执行 Attach。
ByteBuddy
看过源码之后, 你可能会疑惑,stagemonitor 并没有直接使用 Attach API, 而是在AgentAttacher 中采用 ByteBuddy 提供的 ByteBuddyAgent.install
来获得 Instrumentation 的实例,进而完成修改字节码的逻辑。
这里打破了第二堵墙,
ByteBuddyAgent.install
在运行时将 Installer 变成 Javaagent 交由 JVM 进行回调agentmain
, 仅仅只为获得 Instrumentation 的实例引用。至于如何使用 Instrumentation 完全可以不在agentmain
中执行。
零侵入的代价
看似用户体验友好的完美方案,其实是有“代价”的:
- 这是一个针对 Tomcat 应用场景的特定方案,并不适用于所有的 JVM 应用,这在 stagemonitor 的 wiki 中也有体现;
- Attach API 并非由标准库提供,这意味着没有安装 JDK (仅有 JRE)的环境是不支持的,这点在 stagemonitor 的 wiki 中也有声明;
- Attach API 启动 Javaagent (回调
agentmain
)的时机是在main
方法开始之后的,这意味着在启动之前已经完成装载的字节码,哪怕是属于修改范畴,在 Javaagent 启动后的进行修改也是不会生效,除非特别指定要Instrumentation.retransformClasses(Class<?>... classes)
。
零侵入带来的友好体验,相对于通过修改 命令行参数 而言并不见得有很大的优势。 同样是 Tomcat 的场景, 实现 命令行参数 的方式:
CATALINA_OPTS="$CATALINA_OPTS -javaagent:/path/to/agent"
后记
stagemonitor 的零侵入思路是可以借鉴并举一反三的,但其代码的实现方式未必是最简洁优雅的,至少我在看的时候有种吃翔的感觉 😂 。