上一篇文章 中通过对一次线上事故的复盘,引出了福报厂的Arthas,一个建立在Java动态字节码技术之上的Java诊断工具;关于Arthas的使用方式就不赘述了,查看官方文档可以很快上手,玩法也特别多;上一篇中也仅仅只介绍了一种使用场景,即”debug线上JVM内部class信息、在线watch方法执行并查看方法输入输出、在线反编译class、重新编辑Java后直接热部署“的组合拳(手动狗头)…
上手一门技术最基本要做到 what-how-why,在知道了Arthas是什么(what),以及如何使用(how)之后,自然需要去了解它内如如何工作的,以及底层原理(why);而了解底层原理最直接的方式便是阅读源码,毕竟代码不会骗人的,但任何技术文章或书籍都会有”噪音“,那么接下来就从Arthas源码入手,聊聊Java Agent、Instrument、动态字节码技术;
一直都回避写源码方向的文章,源码读起来容易,但写解读就需要粘贴大量代码,会占据大量篇幅,并且很容易写成流水账,但像Arthas或者Dubbo这类优秀的国产项目还是值得的;
同时由于时间与精力有限,本篇着重介绍Java Agent,Instrument等动态字节码技术放到下一篇…
记得当初第一次拿到Arthas源码的时候,印象最深的就是那可爱的TODO.md
,哈哈,像是某位福娃老哥在做交接…
* 代码还是很乱,需要继续重构
* 依赖需要清理,几个问题:
* 所有 apache 的 common 库应当不需要
* json 库有好几份
* `jopt-simple` 看下能不能用 `cli` 取代
* `cli`, `termd` 的 artifactId, version 需要想下。是不是应该直接拿进来。他们的依赖也需要仔细看一下
* termd 依赖 netty,感觉有点重,而且第一次 attach 比较慢,不确定是 netty 的问题还是 attach 的问题
* 目前 web console 依赖 termd 中自带的 term.js 和 css,需要美化,需要想下如何集成到研发门户上
* 因为现在没有 Java 客户端了,所以 batch mode 也就没有了
* `com.taobao.arthas.core.shell.session.Session` 的能力需要和以前的 session 的实现对标。其中:
* 真的需要 textmode 吗?我觉得这个应该是 option 的事情
* 真的需要 encoding 吗?我觉得仍然应该在 option 中定义,就算是真的需要,因为我觉得就应该是 UTF-8
* duration 是应当展示的,session 的列表也许也应当展示
* 需要仔细看下 session 过期是否符合预期
* 多人协作的时候 session 原来是在多人之间共享的吗?
* 所有的命令现在实现的是 AnnotatedCommand,需要继续增强的是:
* Help 中的格式化输出被删除。需要为 `@Description` 定义一套统一的格式
* 命令的输入以及输出的日志 (record logger) 被删除,需要重新实现,因为现在是用 `CommandProcess` 来输出,所以,需要在 `CommandProcess` 的实现里打日志
* `com.taobao.arthas.core.GlobalOptions` 看上去好奇怪,感觉是 OptionCommand 应当做的事情
* `com.taobao.arthas.core.config.Configure` 需要清理,尤其是和 http 相关的
* 需要合并 develop 分支上后续的修复
* 代码中的 TODO/FIXME
回归主题,首先直奔 arthas-core
模块,因为这是整个arthas的入口,即执行java -jar arthas-core.jar
;主函数内最重要的任务就是通过java进程的pid来 attach JVM 与 load agent;
private void attachAgent(Configure configure) throws Exception {
VirtualMachineDescriptor virtualMachineDescriptor = null;
for (VirtualMachineDescriptor descriptor : VirtualMachine.list()) {
String pid = descriptor.id();
if (pid.equals(Long.toString(configure.getJavaPid()))) {
virtualMachineDescriptor = descriptor;
break;
}
}
VirtualMachine virtualMachine = null;
try {
if (null == virtualMachineDescriptor) { // 使用 attach(String pid) 这种方式
virtualMachine = VirtualMachine.attach("" + configure.getJavaPid());
} else {
virtualMachine = VirtualMachine.attach(virtualMachineDescriptor);
}
//略
virtualMachine.loadAgent(arthasAgentPath, configure.getArthasCore() + ";" + configure.toString());
} finally {
if (null != virtualMachine) {
virtualMachine.detach();
}
}
}
这里就用到了Java Agent技术
Java Agent可以说是JVM的一个后门,平时我们使用的 动态编译、热部署、APM监控工具、trace链路分析等,都是通过Java Agent,或者说都是建立在Java Instrument之上的;那么以Arthas为例看下Arthas是如何做的;
打开arthas-agent
模块,在pom中可以发现些端倪,以下四行是关键
<manifestEntries>
<Premain-Class>com.taobao.arthas.agent334.AgentBootstrapPremain-Class>
<Agent-Class>com.taobao.arthas.agent334.AgentBootstrapAgent-Class>
<Can-Redefine-Classes>trueCan-Redefine-Classes>
<Can-Retransform-Classes>trueCan-Retransform-Classes>
manifestEntries>
可以看到Manifest中都使用到了这个AgentBootstrap
,那么打开AgentBootstrap
可以看到它有一个private修饰的主函数,而真正调用它的是两个静态方法
public static void premain(String args, Instrumentation inst) {
main(args, inst);
}
public static void agentmain(String args, Instrumentation inst) {
main(args, inst);
}
private static synchronized void main(String args, final Instrumentation inst) {
//略
}
这两个方法,一个pre-main,一个agent-main,整好对应着Manifest中定义的两个入口;
而这两个,也正是Java Agent的两种实现,静态Agent与动态Agent;
可以看到,参数中除了args之外,还有一个Instrumentation
,这就是Java的字节码增强功能,Agent提供的任何功能底层都是通过对原字节码进行增强而动态织入的;这里先不深入Instrument,放到下一篇中去介绍;
继续看主函数,主函数中最重要的就是执行 bind
动作;
private static synchronized void main(String args, final Instrumentation inst) {
//略
final ClassLoader agentLoader = getClassLoader(inst, arthasCoreJarFile);
Thread bindingThread = new Thread() {
@Override
public void run() {
try {
bind(inst, agentLoader, agentArgs);
} catch (Throwable throwable) {
throwable.printStackTrace(ps);
}
}
};
//略
}
private static void bind(Instrumentation inst, ClassLoader agentLoader, String args) {
//ARTHAS_BOOTSTRAP => com.taobao.arthas.core.server.ArthasBootstrap
Class<?> bootstrapClass = agentLoader.loadClass(ARTHAS_BOOTSTRAP);
//GET_INSTANCE => getInstance()
Object bootstrap = bootstrapClass.getMethod(GET_INSTANCE, Instrumentation.class, String.class).invoke(null, inst, args);
//IS_BIND => boolean isBind()
boolean isBind = (Boolean) bootstrapClass.getMethod(IS_BIND).invoke(bootstrap);
if (!isBind) {
String errorMsg = "Arthas server port binding failed! Please check $HOME/logs/arthas/arthas.log for more details.";
ps.println(errorMsg);
throw new RuntimeException(errorMsg);
}
ps.println("Arthas server already bind.");
}
在这个 bind
中,通过自定的ClassLoader加载了 arthas-core
模块下的 server 包中的 ArthasBootstrap
这个关键类(代码结构确实比较乱…),同时通过反射,执行了两个方法,一个是 getInstance
另外一个是检测是否bind成功;
查看 ArthasBootstrap.getInstance
方法,可以看到这是个懒加载的单例模式,
public synchronized static ArthasBootstrap getInstance(Instrumentation instrumentation, String args){
if (arthasBootstrap != null) {
return arthasBootstrap;
}
Map<String, String> argsMap = FeatureCodec.DEFAULT_COMMANDLINE_CODEC.toMap(args);
//略
return getInstance(instrumentation, mapWithPrefix);
}
public synchronized static ArthasBootstrap getInstance(Instrumentation instrumentation, Map<String, String> args) throws Throwable {
if (arthasBootstrap == null) {
arthasBootstrap = new ArthasBootstrap(instrumentation, args);
}
return arthasBootstrap;
}
其中第5行是通过自定义的分隔符来解析配置参数;
终于来到了重头戏ArthasBootstrap
private ArthasBootstrap(Instrumentation instrumentation, Map<String, String> args){
initFastjson(); //略
initSpy(); // 1. initSpy()
initArthasEnvironment(args); //略
transformerManager = new TransformerManager(instrumentation); // 2. 增强
enhanceClassLoader(); // 2. 增强
initBeans(); //略
bind(configure); // 3. 启动server
shutdown = new Thread("as-shutdown-hooker") {
@Override
public void run() {
ArthasBootstrap.this.destroy();
}
};
Runtime.getRuntime().addShutdownHook(shutdown);
}
第一个值得说说的是 arthas.SpyAPI
, Arthas中的Spy就类似于AOP,可以在各个切入点进行方法织入,举个例子:
public class SpyAPI {
public static void atEnter(Class<?> clazz, String methodInfo, Object target, Object[] args) {
spyInstance.atEnter(clazz, methodInfo, target, args);
}
}
public class SpyImpl extends AbstractSpy {
@Override
public void atEnter(Class<?> clazz, String methodInfo, Object target, Object[] args) {
ClassLoader classLoader = clazz.getClassLoader();
String[] info = splitMethodInfo(methodInfo);
String methodName = info[0];
String methodDesc = info[1];
List<AdviceListener> listeners = AdviceListenerManager.queryAdviceListeners(classLoader, clazz.getName(),
methodName, methodDesc);
if (listeners != null) {
for (AdviceListener adviceListener : listeners) {
try {
if (skipAdviceListener(adviceListener)) {
continue;
}
adviceListener.before(clazz, methodName, methodDesc, target, args);
} catch (Throwable e) {
logger.error("class: {}, methodInfo: {}", clazz.getName(), methodInfo, e);
}
}
}
}
可以看出来这里也用到了设计模式观察者模式,首先获取到所有切面,然后对各个切面织入before方法!
接下来是 enhanceClassLoader()
这里是动态字节码技术中的 transformer
,也暂且不表,现在只需要知道它就是真正需要动态织入的逻辑即可;
最后是 bind
方法,底层是利用 Netty 搭建的server,最后的 isBind()
则是检测 server是否启动成功;server一旦启动成功,就可以通过客户端 CLI 命令行来发送指令了;
以上,不论是将arthas.jar 打包到项目中,还是先部署的项目后启动的arthas,都可以通过 pre-main 与 agent-main 将 arthas server 提供的功能织入到 原项目中;
那么,下一篇就深入介绍arthas是如何通过 instrument 来实现的字节码级别的功能织入…