在日常开发中,经常会使用到arthas排查线上问题,觉得arthas的功能非常强大,所以打算花了点时间了解一下其实现原理。并试着回答一下使用Arthas时存在的一些疑问。
Arthas主要基于是Instrumentation + JavaAgent + Attach API + ASM + 反射 + OGNL等技术实现的。在不停止应用服务的情况下,将Arthas的jar包代码动态加载到应用的JVM中,再配合Instrumentation类,动态修改应用JVM中运营的字节码,实现对目标应用增强,如获取某方法的参数、返回值、耗时等信息、调用JVM相关类获取JVM运行时信息,最后再通过OGNL过滤、存取对象属性。
1. 如何attach到应用?
1.1 如何debug?
Arthas的启动很简单,从github上把Arthas的代码clone到本地,然后直接运行/bin目录下的as.sh脚本便能启动。为了弄明白Arthas attach到应用的过程,可以加上–debug-attach参数,同时为了查看脚本的详细执行流程,bash加上-x选项。
bash -x ./as.sh --debug-attachbash -x ./as.sh --debug-attachbash -x ./as.sh --debug-attach
根据打印的执行流程,开始主要是进行一些配置检查、目录创建和运行参数的构造。前置工作准备好后,会调用jps命令列出系统当前所有运行的JVM。
选择我们需要attach的JVM,会判断本地目录$HOME/.arthas/
中是否存在Arthas对应版本的jar包,如果不存在,则下载并解压到指定目$HOME/.arthas/
。
最后通过java命令运行Arthas client,尝试连接Arthas server(127.0.0.1:3568)
java -agentlib:jdwp=transport=dt_socket,address=8888,server=y,suspend=y -Djava.awt.headless=true -jar /Users/banzhe/.arthas/lib/3.6.6/arthas/arthas-client.jar 127.0.0.1 3658 -c session --execution-timeout 2000java -agentlib:jdwp=transport=dt_socket,address=8888,server=y,suspend=y -Djava.awt.headless=true -jar /Users/banzhe/.arthas/lib/3.6.6/arthas/arthas-client.jar 127.0.0.1 3658 -c session --execution-timeout 2000java -agentlib:jdwp=transport=dt_socket,address=8888,server=y,suspend=y -Djava.awt.headless=true -jar /Users/banzhe/.arthas/lib/3.6.6/arthas/arthas-client.jar 127.0.0.1 3658 -c session --execution-timeout 2000
从启动的命令可以看到,主要是运行了arthas-client.jar包,同时开启了远程debug,远程debug端口号8888,因为设置了suspend=y,启动流程被阻塞,等待debugger attach。打开IDEA,配置远程debug,然后点击debug,流程即可继续。
因为当前Arthas server还没有启动(attach到应用JVM),所以抛了一个ConnectException异常
接着启动Arthas server,将其attach到指定的应用JVM
java -Xbootclasspath/a:/Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/lib/tools.jar -agentlib:jdwp=transport=dt_socket,address=8888,server=y,suspend=y -Djava.awt.headless=true -jar /Users/banzhe/.arthas/lib/3.6.6/arthas/arthas-core.jar -pid 57840 -core /Users/banzhe/.arthas/lib/3.6.6/arthas/arthas-core.jar -agent /Users/banzhe/.arthas/lib/3.6.6/arthas/arthas-agent.jarjava -Xbootclasspath/a:/Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/lib/tools.jar -agentlib:jdwp=transport=dt_socket,address=8888,server=y,suspend=y -Djava.awt.headless=true -jar /Users/banzhe/.arthas/lib/3.6.6/arthas/arthas-core.jar -pid 57840 -core /Users/banzhe/.arthas/lib/3.6.6/arthas/arthas-core.jar -agent /Users/banzhe/.arthas/lib/3.6.6/arthas/arthas-agent.jarjava -Xbootclasspath/a:/Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/lib/tools.jar -agentlib:jdwp=transport=dt_socket,address=8888,server=y,suspend=y -Djava.awt.headless=true -jar /Users/banzhe/.arthas/lib/3.6.6/arthas/arthas-core.jar -pid 57840 -core /Users/banzhe/.arthas/lib/3.6.6/arthas/arthas-core.jar -agent /Users/banzhe/.arthas/lib/3.6.6/arthas/arthas-agent.jar
从启动的命令可以看到,主要是运行了arthas-core.jar和arthas-agent.jar两个jar包,同时开启了远程debug,远程debug端口号8888。查看arthas-core和arthas-agent两个模块下的pom文件,可以发现main方法在arthas-core模块下的com.taobao.arthas.core.Arthas类中,所以在arthas-core模块的main方法设置断点,然后点击debug,即可开始attach过程的debug。
远程debug连接成功之后,attach流程就很容易弄明白了。attach的过程主要是在attachAgent中完成的。
1.2 Arthas attach到应用JVM
attach是使用sun提供JVM Attach API完成的。核心代码如下:
VirtualMachineDescriptor virtualMachineDescriptor = null;// 1. 列出当前系统运行的所有JVM,和jps类似for (VirtualMachineDescriptor descriptor : VirtualMachine.list()) {String pid = descriptor.id();// 找到指定PID对应的JVMif (pid.equals(Long.toString(configure.getJavaPid()))) {virtualMachineDescriptor = descriptor;break;}}// attach到指定JVMVirtualMachine virtualMachine = VirtualMachine.attach(virtualMachineDescriptor);// 指定agent jar包和相关配置virtualMachine.loadAgent(arthasAgentPath, configure.getArthasCore() + ";" + configure.toString());VirtualMachineDescriptor virtualMachineDescriptor = null; // 1. 列出当前系统运行的所有JVM,和jps类似 for (VirtualMachineDescriptor descriptor : VirtualMachine.list()) { String pid = descriptor.id(); // 找到指定PID对应的JVM if (pid.equals(Long.toString(configure.getJavaPid()))) { virtualMachineDescriptor = descriptor; break; } } // attach到指定JVM VirtualMachine virtualMachine = VirtualMachine.attach(virtualMachineDescriptor); // 指定agent jar包和相关配置 virtualMachine.loadAgent(arthasAgentPath, configure.getArthasCore() + ";" + configure.toString());VirtualMachineDescriptor virtualMachineDescriptor = null; // 1. 列出当前系统运行的所有JVM,和jps类似 for (VirtualMachineDescriptor descriptor : VirtualMachine.list()) { String pid = descriptor.id(); // 找到指定PID对应的JVM if (pid.equals(Long.toString(configure.getJavaPid()))) { virtualMachineDescriptor = descriptor; break; } } // attach到指定JVM VirtualMachine virtualMachine = VirtualMachine.attach(virtualMachineDescriptor); // 指定agent jar包和相关配置 virtualMachine.loadAgent(arthasAgentPath, configure.getArthasCore() + ";" + configure.toString());
至此,Arthas完成attach到目标应用JVM的过程。
2. Arthas与attach应用隔离
Arthas的代码attach到应用对应的JVM后,将由应用JVM加载运行,为了避免Arthas代码对应用的影响,Arthas进行了代码隔离。在介绍代码隔离的具体实现之前,先看一下如何进行debug。因为attach完成之后,Arthas的代码是由应用JVM进行加载和运行的,所以需要应用代码中进行debug。但是应用中并没有引入Arthas的jar包,无法直接进行debug。可以参考attach过程的debug,在启动应用的时候开启远程debug,然后在Arthas源码中进行debug。
2.1 在应用JVM中debug Arthas
直接借助官网的例子,假设应用代码如下:
package com.banzhe.loader.demo;import java.util.concurrent.TimeUnit;import java.util.concurrent.atomic.AtomicInteger;public class Demo {static class Counter {private static final AtomicInteger count = new AtomicInteger(0);public static void increment() {count.incrementAndGet();}public static int value() {return count.get();}}public static void main(String[] args) {while (true) {Counter.increment();try {System.out.println("counter: " + Counter.value());TimeUnit.SECONDS.sleep(1);} catch (InterruptedException ignored) {break;}}}}package com.banzhe.loader.demo; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; public class Demo { static class Counter { private static final AtomicInteger count = new AtomicInteger(0); public static void increment() { count.incrementAndGet(); } public static int value() { return count.get(); } } public static void main(String[] args) { while (true) { Counter.increment(); try { System.out.println("counter: " + Counter.value()); TimeUnit.SECONDS.sleep(1); } catch (InterruptedException ignored) { break; } } } }package com.banzhe.loader.demo; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; public class Demo { static class Counter { private static final AtomicInteger count = new AtomicInteger(0); public static void increment() { count.incrementAndGet(); } public static int value() { return count.get(); } } public static void main(String[] args) { while (true) { Counter.increment(); try { System.out.println("counter: " + Counter.value()); TimeUnit.SECONDS.sleep(1); } catch (InterruptedException ignored) { break; } } } }
以支持远程debug的方式启动,debug端口为:8000
# 编译javac com/banzhe/loader/demo/*.java# 运行java -agentlib:jdwp=transport=dt_socket,address=8000,server=y,suspend=n com.banzhe.loader.demo.Demo# 编译 javac com/banzhe/loader/demo/*.java # 运行 java -agentlib:jdwp=transport=dt_socket,address=8000,server=y,suspend=n com.banzhe.loader.demo.Demo# 编译 javac com/banzhe/loader/demo/*.java # 运行 java -agentlib:jdwp=transport=dt_socket,address=8000,server=y,suspend=n com.banzhe.loader.demo.Demo
因为Arthas是在应用运行时attach到应用JVM的,attach完成之后,应用JVM会以Agent-Class的agentmain方法作为入口方法执行。所以在agentmain方法中打断点,然后运行bash ./as.sh
启动Arthas即可。从arthas-agent模块的pom文件中可知道Agent-Class为com.taobao.arthas.agent334.AgentBootstrap
。
2.2 类隔离
为了避免Arthas代码对应用的影响,Arthas进行了代码隔离。在JVM中一个类型实例是通过类加载器+全类名确定的。也就是说为了避免不同模块代码间相互影响(两个jar中可能会存在全类名相同,但是逻辑完全不同的类),可以通过使用不同的ClassLoader进行加载来实现隔离。如pandora boot、tomcat等都是基于ClassLoader实现代码隔离的,Arthas也是通过定义了自己的ClassLoader——ArthasClassLoader来实现与应用代码隔离的。
public class ArthasClassloader extends URLClassLoader {public ArthasClassloader(URL[] urls) {super(urls, ClassLoader.getSystemClassLoader().getParent());}@Overrideprotected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {final Class<?> loadedClass = findLoadedClass(name);if (loadedClass != null) {return loadedClass;}// 优先从parent(SystemClassLoader)里加载系统类,避免抛出ClassNotFoundExceptionif (name != null && (name.startsWith("sun.") || name.startsWith("java."))) {return super.loadClass(name, resolve);}try {Class<?> aClass = findClass(name);if (resolve) {resolveClass(aClass);}return aClass;} catch (Exception e) {// ignore}return super.loadClass(name, resolve);}}public class ArthasClassloader extends URLClassLoader { public ArthasClassloader(URL[] urls) { super(urls, ClassLoader.getSystemClassLoader().getParent()); } @Override protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { final Class<?> loadedClass = findLoadedClass(name); if (loadedClass != null) { return loadedClass; } // 优先从parent(SystemClassLoader)里加载系统类,避免抛出ClassNotFoundException if (name != null && (name.startsWith("sun.") || name.startsWith("java."))) { return super.loadClass(name, resolve); } try { Class<?> aClass = findClass(name); if (resolve) { resolveClass(aClass); } return aClass; } catch (Exception e) { // ignore } return super.loadClass(name, resolve); } }public class ArthasClassloader extends URLClassLoader { public ArthasClassloader(URL[] urls) { super(urls, ClassLoader.getSystemClassLoader().getParent()); } @Override protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { final Class<?> loadedClass = findLoadedClass(name); if (loadedClass != null) { return loadedClass; } // 优先从parent(SystemClassLoader)里加载系统类,避免抛出ClassNotFoundException if (name != null && (name.startsWith("sun.") || name.startsWith("java."))) { return super.loadClass(name, resolve); } try { Class<?> aClass = findClass(name); if (resolve) { resolveClass(aClass); } return aClass; } catch (Exception e) { // ignore } return super.loadClass(name, resolve); } }
可以发现arthas-core.jar中的类都是由ArthasClassLoader加载的。
由于类加载有一个原则:
加载当前类的加载器,也会用于加载其所依赖的类(当然不一定是它加载,也可能遵循双亲委派原则,由双亲加载器加载)
因为Agent-Class——com.taobao.arthas.agent334.AgentBootstrap的agentmain是agent的入口方法,attach完成之后由应用JVM加载,一般是ApplicationClassLoader加载器加载。按照上述类加载器的原则,Arthas的代码也都会由应用的ApplicationClassLoader加载,无法实现代码隔离。所以Arthas通过反射打破了这个规则,实现Arthas代码与应用代码的隔离。
// 使用ArthasClassLoader加载arthas-core的初始化类Class<?> bootstrapClass = agentLoader.loadClass(""com.taobao.arthas.core.server.ArthasBootstrap"");// 通过反射调用初始化函数Object bootstrap = bootstrapClass.getMethod("getMethod", Instrumentation.class, String.class).invoke(null, inst, args);// 使用ArthasClassLoader加载arthas-core的初始化类 Class<?> bootstrapClass = agentLoader.loadClass(""com.taobao.arthas.core.server.ArthasBootstrap""); // 通过反射调用初始化函数 Object bootstrap = bootstrapClass.getMethod("getMethod", Instrumentation.class, String.class).invoke(null, inst, args);// 使用ArthasClassLoader加载arthas-core的初始化类 Class<?> bootstrapClass = agentLoader.loadClass(""com.taobao.arthas.core.server.ArthasBootstrap""); // 通过反射调用初始化函数 Object bootstrap = bootstrapClass.getMethod("getMethod", Instrumentation.class, String.class).invoke(null, inst, args);
- 首先通过ArthasClassLoader加载arthas-core的初始化类
ArthasBootstrap
- 通过反射调用的方式调用
getInstance
实现Arthas server端的初始化
因为ArthasBootstrap
是由ArthasClassLoader加载器加载的,ArthasBootstrap
负责初始化arthas-core,按照加载当前类的加载器,也会用于加载其所依赖的类原则,ArthasBootstrap依赖的类也会由ArthasClassLoader
加载,所以就是实现了Arthas与应用的隔离。
注意:不能将反射调用结果强制转换成ArthasBootstrap
,不然会抛ClassCastException
。因为左边的ArthasBootstrap
的class实例是由应用的ApplicationClassLoader
加载的,而右边的ArthasBootstrap
的class实例是由ArthasClassLoader
加载的,class实例不同不能进行转换。
ArthasBootstrap bootstrap = (ArthasBootstrap) bootstrapClass.getMethod("getMethod", Instrumentation.class, String.class).invoke(null, inst, args);ArthasBootstrap bootstrap = (ArthasBootstrap) bootstrapClass.getMethod("getMethod", Instrumentation.class, String.class).invoke(null, inst, args);ArthasBootstrap bootstrap = (ArthasBootstrap) bootstrapClass.getMethod("getMethod", Instrumentation.class, String.class).invoke(null, inst, args);
2.3 应用代码调用arthas代码
根据上一节我们可以知道,Arthas的代码和应用的代码是通过类加载器隔离的,其类加载器结构如下图。
根据类加载器的双亲委派原则,父类加载器加载的类对子类加载器是可见的,而子类加载器加载的类对父类加载器是不可见的,兄弟类加载器加载的类互相是不可见的。也就是说Arthas和应用JVM之间共享了JMX等底层API(由BootstrapClassLoader和ExtClassLoader加载的类),所以Arthas可以通过调用JDK的一些API获取应用JVM相应的运行时数据,比如dashboard/thread/mbean
等命令。但是对于增强型命令如watch/trace/tt
,Arthas会对应用代码注入一些代码,当被增强的应用代码执行时,会执行到Arthas注入的代码,从而实现功能的增强。但是由于ApplicationClassLoader
和ArthasClassLoader
加载器的类之间是不可见的,也就是说应用代码是不能直接调用Arthas代码的,会有ClassNotFoundException或者ClassCastException。
Arthas采用了一种很巧的方案,引入了一个arthas-spy模块,相当于在应用和Arthas之间架起了一座桥梁。arthas-spy模块中只有一个SpyAPI
类文件。ApyAPI
类由BoostrapClassLoader
加载,所以SpyAPI
对于ApplicationClassLoader
和ArthasClassLoader
都是可见的,应用通过SpyAPI
实现对Arthas的调用,从而实现功能的增强。
下面我们通过代码来看一下具体是如何实现的:
private void initSpy() throws Throwable {// TODO init SpyImpl ?// 将Spy添加到BootstrapClassLoaderClassLoader parent = ClassLoader.getSystemClassLoader().getParent();Class<?> spyClass = null;if (parent != null) {try {spyClass =parent.loadClass("java.arthas.SpyAPI");} catch (Throwable e) {// ignore}}if (spyClass == null) {CodeSource codeSource = ArthasBootstrap.class.getProtectionDomain().getCodeSource();if (codeSource != null) {File arthasCoreJarFile = new File(codeSource.getLocation().toURI().getSchemeSpecificPart());File spyJarFile = new File(arthasCoreJarFile.getParentFile(), "arthas-spy.jar");instrumentation.appendToBootstrapClassLoaderSearch(new JarFile(spyJarFile));} else {throw new IllegalStateException("can not find " + "arthas-spy.jar");}}}private void initSpy() throws Throwable { // TODO init SpyImpl ? // 将Spy添加到BootstrapClassLoader ClassLoader parent = ClassLoader.getSystemClassLoader().getParent(); Class<?> spyClass = null; if (parent != null) { try { spyClass =parent.loadClass("java.arthas.SpyAPI"); } catch (Throwable e) { // ignore } } if (spyClass == null) { CodeSource codeSource = ArthasBootstrap.class.getProtectionDomain().getCodeSource(); if (codeSource != null) { File arthasCoreJarFile = new File(codeSource.getLocation().toURI().getSchemeSpecificPart()); File spyJarFile = new File(arthasCoreJarFile.getParentFile(), "arthas-spy.jar"); instrumentation.appendToBootstrapClassLoaderSearch(new JarFile(spyJarFile)); } else { throw new IllegalStateException("can not find " + "arthas-spy.jar"); } } }private void initSpy() throws Throwable { // TODO init SpyImpl ? // 将Spy添加到BootstrapClassLoader ClassLoader parent = ClassLoader.getSystemClassLoader().getParent(); Class<?> spyClass = null; if (parent != null) { try { spyClass =parent.loadClass("java.arthas.SpyAPI"); } catch (Throwable e) { // ignore } } if (spyClass == null) { CodeSource codeSource = ArthasBootstrap.class.getProtectionDomain().getCodeSource(); if (codeSource != null) { File arthasCoreJarFile = new File(codeSource.getLocation().toURI().getSchemeSpecificPart()); File spyJarFile = new File(arthasCoreJarFile.getParentFile(), "arthas-spy.jar"); instrumentation.appendToBootstrapClassLoaderSearch(new JarFile(spyJarFile)); } else { throw new IllegalStateException("can not find " + "arthas-spy.jar"); } } }
- 首先通过
BootstrapClassLoader
直接加载SpyAPI
类 - 如果加载失败(可能是CLASSPATH中找不SpyAPI),将arthas-spy.jar添加到应用的
BootstrapClassLoader
的搜索路径中 - 按照类加载的双亲委派原则,加载SpyAPI类时,会优先委托给
BootstrapClassLoader
,所以SpyAPI
会被根类加载器加载,而不是ArthasClassLoader
加载
SpyAPI
中定义了不同时机的静态增强处理函数,具体的处理逻辑由抽象类AbstractSpy
的子类SpyImpl
实现。
public class SpyAPI {private static volatile AbstractSpy spyInstance = NOPSPY;public static void setSpy(AbstractSpy spy) {spyInstance = spy;}public static void atEnter(Class<?> clazz, String methodInfo, Object target, Object[] args) {spyInstance.atEnter(clazz, methodInfo, target, args);}public static void atExit(Class<?> clazz, String methodInfo, Object target, Object[] args,Object returnObject) {spyInstance.atExit(clazz, methodInfo, target, args, returnObject);}public static void atExceptionExit(Class<?> clazz, String methodInfo, Object target,Object[] args, Throwable throwable) {spyInstance.atExceptionExit(clazz, methodInfo, target, args, throwable);}public static void atBeforeInvoke(Class<?> clazz, String invokeInfo, Object target) {spyInstance.atBeforeInvoke(clazz, invokeInfo, target);}public static void atAfterInvoke(Class<?> clazz, String invokeInfo, Object target) {spyInstance.atAfterInvoke(clazz, invokeInfo, target);}public static void atInvokeException(Class<?> clazz, String invokeInfo, Object target, Throwable throwable) {spyInstance.atInvokeException(clazz, invokeInfo, target, throwable);}public static abstract class AbstractSpy {public abstract void atEnter(Class<?> clazz, String methodInfo, Object target,Object[] args);public abstract void atExit(Class<?> clazz, String methodInfo, Object target, Object[] args,Object returnObject);public abstract void atExceptionExit(Class<?> clazz, String methodInfo, Object target,Object[] args, Throwable throwable);public abstract void atBeforeInvoke(Class<?> clazz, String invokeInfo, Object target);public abstract void atAfterInvoke(Class<?> clazz, String invokeInfo, Object target);public abstract void atInvokeException(Class<?> clazz, String invokeInfo, Object target, Throwable throwable);}}public class SpyAPI { private static volatile AbstractSpy spyInstance = NOPSPY; public static void setSpy(AbstractSpy spy) { spyInstance = spy; } public static void atEnter(Class<?> clazz, String methodInfo, Object target, Object[] args) { spyInstance.atEnter(clazz, methodInfo, target, args); } public static void atExit(Class<?> clazz, String methodInfo, Object target, Object[] args, Object returnObject) { spyInstance.atExit(clazz, methodInfo, target, args, returnObject); } public static void atExceptionExit(Class<?> clazz, String methodInfo, Object target, Object[] args, Throwable throwable) { spyInstance.atExceptionExit(clazz, methodInfo, target, args, throwable); } public static void atBeforeInvoke(Class<?> clazz, String invokeInfo, Object target) { spyInstance.atBeforeInvoke(clazz, invokeInfo, target); } public static void atAfterInvoke(Class<?> clazz, String invokeInfo, Object target) { spyInstance.atAfterInvoke(clazz, invokeInfo, target); } public static void atInvokeException(Class<?> clazz, String invokeInfo, Object target, Throwable throwable) { spyInstance.atInvokeException(clazz, invokeInfo, target, throwable); } public static abstract class AbstractSpy { public abstract void atEnter(Class<?> clazz, String methodInfo, Object target, Object[] args); public abstract void atExit(Class<?> clazz, String methodInfo, Object target, Object[] args, Object returnObject); public abstract void atExceptionExit(Class<?> clazz, String methodInfo, Object target, Object[] args, Throwable throwable); public abstract void atBeforeInvoke(Class<?> clazz, String invokeInfo, Object target); public abstract void atAfterInvoke(Class<?> clazz, String invokeInfo, Object target); public abstract void atInvokeException(Class<?> clazz, String invokeInfo, Object target, Throwable throwable); } }public class SpyAPI { private static volatile AbstractSpy spyInstance = NOPSPY; public static void setSpy(AbstractSpy spy) { spyInstance = spy; } public static void atEnter(Class<?> clazz, String methodInfo, Object target, Object[] args) { spyInstance.atEnter(clazz, methodInfo, target, args); } public static void atExit(Class<?> clazz, String methodInfo, Object target, Object[] args, Object returnObject) { spyInstance.atExit(clazz, methodInfo, target, args, returnObject); } public static void atExceptionExit(Class<?> clazz, String methodInfo, Object target, Object[] args, Throwable throwable) { spyInstance.atExceptionExit(clazz, methodInfo, target, args, throwable); } public static void atBeforeInvoke(Class<?> clazz, String invokeInfo, Object target) { spyInstance.atBeforeInvoke(clazz, invokeInfo, target); } public static void atAfterInvoke(Class<?> clazz, String invokeInfo, Object target) { spyInstance.atAfterInvoke(clazz, invokeInfo, target); } public static void atInvokeException(Class<?> clazz, String invokeInfo, Object target, Throwable throwable) { spyInstance.atInvokeException(clazz, invokeInfo, target, throwable); } public static abstract class AbstractSpy { public abstract void atEnter(Class<?> clazz, String methodInfo, Object target, Object[] args); public abstract void atExit(Class<?> clazz, String methodInfo, Object target, Object[] args, Object returnObject); public abstract void atExceptionExit(Class<?> clazz, String methodInfo, Object target, Object[] args, Throwable throwable); public abstract void atBeforeInvoke(Class<?> clazz, String invokeInfo, Object target); public abstract void atAfterInvoke(Class<?> clazz, String invokeInfo, Object target); public abstract void atInvokeException(Class<?> clazz, String invokeInfo, Object target, Throwable throwable); } }
因为AbstractSpy
是SpyAPI
的内部静态类,并且在SpyAPI
中定义了一个静态属性,所以AbstractApy
也会由BoostrapClassLoader
加载。而SpyImpl
在arthas-core模块中实现,所以会被ArthasClassLoader
加载。
所以Arthas可以通过调用SpyAPI
的setSpy
方法设置增强代码的具体执行逻辑。因为AbstractApy
由BoostrapClassLoader
加载,SpyImpl
由ArthasClassLoader
加载,所以SpyImpl
实例可以向上类型转换成AbstractApy
实例,完成赋值操作。
应用代码在调用Arthas的增强代码时,是通过调用SpyAPI
的静态方法,然后调用AbstractSpy
实例实现方法增强。
3. 如何支持OGNL?
Arthas是支持OGNL表达式的,所以Arthas的时候可以非常灵活,例如下面查看第一个参数大于10的命令。
watch com.cm4j.loader.demo.Demo random '{params[0]}' 'params[0] > 10'watch com.cm4j.loader.demo.Demo random '{params[0]}' 'params[0] > 10'watch com.cm4j.loader.demo.Demo random '{params[0]}' 'params[0] > 10'
那它具体是怎么支持的呢?通过断点我们很容易可以定位到OGNL的处理逻辑:
Arthas会将增强执行的结果全部放在Advice实例中,主要包括增强的方法名、参数、执行结果、耗时等数据,在返回是先判断是否有OGNL表达式,如果有OGNL表达式,会执行OGNL表达式,针对OGNL设置的条件进行过滤或者数值筛选。主要依赖了ognl对应的jar包。
4. Arthas命令分类及原理
下面主要介绍一下watch/trace/tt等增强命令的实现的原理。Arthas的字节码增强是基于bytekit实现,bytekit是对ASM的封装,基于提供更高层的字节码处理能力,面向诊断/APM领域的字节码库,同时提供一套简洁的API,让开发人员可以很轻松的完成字节码增强。例如下面对Sample类的hello方法进行增强,打印hello方法的耗时:
public class ByteKitDemo {public static class Sample {public void hello(String name) {System.out.println("Hello " + name + "!");}}public static class SampleInterceptor {private static long start;@AtEnter(inline = true)public static void atEnter() {start = System.currentTimeMillis();}@AtExit(inline = true)public static void atEit() {System.out.println(System.currentTimeMillis() - start);}}public static void main(String[] args) throws Exception {// 解析定义的 Interceptor类 和相关的注解DefaultInterceptorClassParser interceptorClassParser = new DefaultInterceptorClassParser();List<InterceptorProcessor> processors = interceptorClassParser.parse(SampleInterceptor.class);// 加载字节码ClassNode classNode = AsmUtils.loadClass(Sample.class);// 对加载到的字节码做增强处理for (MethodNode methodNode : classNode.methods) {MethodProcessor methodProcessor = new MethodProcessor(classNode, methodNode);for (InterceptorProcessor interceptor : processors) {interceptor.process(methodProcessor);}}// 获取增强后的字节码byte[] bytes = AsmUtils.toBytes(classNode);// 查看反编译结果System.out.println(Decompiler.decompile(bytes));}}public class ByteKitDemo { public static class Sample { public void hello(String name) { System.out.println("Hello " + name + "!"); } } public static class SampleInterceptor { private static long start; @AtEnter(inline = true) public static void atEnter() { start = System.currentTimeMillis(); } @AtExit(inline = true) public static void atEit() { System.out.println(System.currentTimeMillis() - start); } } public static void main(String[] args) throws Exception { // 解析定义的 Interceptor类 和相关的注解 DefaultInterceptorClassParser interceptorClassParser = new DefaultInterceptorClassParser(); List<InterceptorProcessor> processors = interceptorClassParser.parse(SampleInterceptor.class); // 加载字节码 ClassNode classNode = AsmUtils.loadClass(Sample.class); // 对加载到的字节码做增强处理 for (MethodNode methodNode : classNode.methods) { MethodProcessor methodProcessor = new MethodProcessor(classNode, methodNode); for (InterceptorProcessor interceptor : processors) { interceptor.process(methodProcessor); } } // 获取增强后的字节码 byte[] bytes = AsmUtils.toBytes(classNode); // 查看反编译结果 System.out.println(Decompiler.decompile(bytes)); } }public class ByteKitDemo { public static class Sample { public void hello(String name) { System.out.println("Hello " + name + "!"); } } public static class SampleInterceptor { private static long start; @AtEnter(inline = true) public static void atEnter() { start = System.currentTimeMillis(); } @AtExit(inline = true) public static void atEit() { System.out.println(System.currentTimeMillis() - start); } } public static void main(String[] args) throws Exception { // 解析定义的 Interceptor类 和相关的注解 DefaultInterceptorClassParser interceptorClassParser = new DefaultInterceptorClassParser(); List<InterceptorProcessor> processors = interceptorClassParser.parse(SampleInterceptor.class); // 加载字节码 ClassNode classNode = AsmUtils.loadClass(Sample.class); // 对加载到的字节码做增强处理 for (MethodNode methodNode : classNode.methods) { MethodProcessor methodProcessor = new MethodProcessor(classNode, methodNode); for (InterceptorProcessor interceptor : processors) { interceptor.process(methodProcessor); } } // 获取增强后的字节码 byte[] bytes = AsmUtils.toBytes(classNode); // 查看反编译结果 System.out.println(Decompiler.decompile(bytes)); } }
执行结果:
public static class ByteKitDemo.Sample {public ByteKitDemo.Sample() {ByteKitDemo.SampleInterceptor.start = System.currentTimeMillis();System.out.println(System.currentTimeMillis() - ByteKitDemo.SampleInterceptor.start);}/** WARNING - void declaration*/public void hello(String string) {void name;ByteKitDemo.SampleInterceptor.start = System.currentTimeMillis();System.out.println("Hello " + (String)name + "!");System.out.println(System.currentTimeMillis() - ByteKitDemo.SampleInterceptor.start);}}public static class ByteKitDemo.Sample { public ByteKitDemo.Sample() { ByteKitDemo.SampleInterceptor.start = System.currentTimeMillis(); System.out.println(System.currentTimeMillis() - ByteKitDemo.SampleInterceptor.start); } /* * WARNING - void declaration */ public void hello(String string) { void name; ByteKitDemo.SampleInterceptor.start = System.currentTimeMillis(); System.out.println("Hello " + (String)name + "!"); System.out.println(System.currentTimeMillis() - ByteKitDemo.SampleInterceptor.start); } }public static class ByteKitDemo.Sample { public ByteKitDemo.Sample() { ByteKitDemo.SampleInterceptor.start = System.currentTimeMillis(); System.out.println(System.currentTimeMillis() - ByteKitDemo.SampleInterceptor.start); } /* * WARNING - void declaration */ public void hello(String string) { void name; ByteKitDemo.SampleInterceptor.start = System.currentTimeMillis(); System.out.println("Hello " + (String)name + "!"); System.out.println(System.currentTimeMillis() - ByteKitDemo.SampleInterceptor.start); } }
可以发现整个增强实现的代码可读性是非常好的。
Arthas的watch命令增强核心代码如下:
// SpyInterceptor1对应函数调用之前增强interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyInterceptor1.class));// SpyInterceptor2对应函数返回之后增强interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyInterceptor2.class));// SpyInterceptor3对应函数异常之后增强interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyInterceptor3.class));// SpyInterceptor1对应函数调用之前增强 interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyInterceptor1.class)); // SpyInterceptor2对应函数返回之后增强 interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyInterceptor2.class)); // SpyInterceptor3对应函数异常之后增强 interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyInterceptor3.class));// SpyInterceptor1对应函数调用之前增强 interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyInterceptor1.class)); // SpyInterceptor2对应函数返回之后增强 interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyInterceptor2.class)); // SpyInterceptor3对应函数异常之后增强 interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyInterceptor3.class));
所有增强的实现都在SpyImpl中实现,Arthas会将方法名、参数、返回值等信息统一保存到Advice中,然后在按照对应格式进行处理返回。
可以验证一下,使用watch命令时,arthas会对相关的方法进行字节码增强。首先打开一个arthas终端执行:
watch com.cm4j.loader.demo.Demo random -n 1000watch com.cm4j.loader.demo.Demo random -n 1000watch com.cm4j.loader.demo.Demo random -n 1000
然后打开另一个arthas终端,查看最新加载的Demo类
jad com.cm4j.loader.demo.Demojad com.cm4j.loader.demo.Demojad com.cm4j.loader.demo.Demo
可以发现,arthas会对相关方法进行3处增强:调用之前增强、函数返回之后增强、函数异常之后增强。