1. 前言
之前一直使用greys及其内部升级二次开发版来排查问题。最近周末刚好事情不多,作为一名程序员本能地想要弄懂这么神奇的greys到底是怎么实现的?周末从github上拉了代码仔细读了读,其基本技术框架是JVM attach + Instrumentation + asm实现的。关于JVM attach和Instrumentation的功能,下次再写文章介绍,本文着重于greys中非常神奇的一个类AdviceWeaver,该类使用asm代码实现了简单的aop功能,本文的实现方式基本参考该类,具体的代码放在了scrat-profiler模块中。下文将结合asm的使用方法讲解如何实现简单的aop功能。
2. ASM简介
什么是asm?ASM是一个java字节码操纵框架,它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为(摘自网友翻译)。asm的文档请参考asm文档,文档写的比较全。主要几个重要的类为ClassReader、ClassWriter、ClassVisitor、MethodVisitor等。
2.1 ClassVisitor、MethodVisitor与AdviceAdapter
在使用ASM操作字节码之前,我们需要稍微了解下ClassVisitor,ClassVisitor用来generating and transforming compiled classes,ClassVisitor中的每个方法都对应了class file的相关部分,ClassVisitor里方法的执行顺序应该如下:
visit visitSource? visitOuterClass? ( visitAnnotation | visitAttribute )*
( visitInnerClass | visitField | visitMethod )*
visitEnd
由于本文主要讲的是enhance class,所以着重讨论下ClassVisitor Transforming classes。Transforming classes通常需要借助于两个类ClassReader与ClassWriter,ClassWriter是ClassVisitor子类,与ClassReader或者其他ClassVisitor子类一起使用来generate a modified class from one or more existing Java classes。典型的用法如下:
byte[] b1 = ...;
ClassWriter cw = new ClassWriter(0);
ClassReader cr = new ClassReader(b1);
cr.accept(cw, 0);
byte[] b2 = cw.toByteArray();
与其他ClassVisitor一起使用的典型用法如下:
byte[] b1 = ...;
ClassWriter cw = new ClassWriter(0);
// cv forwards all events to cw
ClassVisitor cv = new ClassVisitor(ASM4, cw) { };
ClassReader cr = new ClassReader(b1);
cr.accept(cv, 0);
byte[] b2 = cw.toByteArray();
由于我想实现的是方法级别的AOP增强,所以我更加关注与ClassVisitor的visitMethod方法:
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
if (cv != null) {
return cv.visitMethod(access, name, desc, signature, exceptions);
}
return null;
}
可以看到的是该方法返回的是MethodVisitor,当我们继承ClassVisitor并且复写visitMethod返回自定义的MethodVisitor时,我们可以实现对method的字节码进行替换增强。首先我们来研究下MethodVisitor如何使用。
MethodVisitor接口中的方法调用必须按照以下的顺序:
visitAnnotationDefault?
( visitAnnotation | visitParameterAnnotation | visitAttribute )*
( visitCode
( visitTryCatchBlock | visitLabel | visitFrame | visitXxxInsn |
visitLocalVariable | visitLineNumber )*
visitMaxs )?
visitEnd
摘取ASM user guide中比较重要的几句话:
This means that annotations and attributes, if any, must be visited first, followed
by the method’s bytecode, for non abstract methods. For these methods
the code must be visited in sequential order, between exactly one call to
visitCode and exactly one call to visitMaxs.
The visitCode and visitMaxs methods can therefore be used to detect the
start and end of the method’s bytecode in a sequence of events. Like for
classes, the visitEnd method must be called last, and is used to detect the end of a method in a sequence of events
从中可知,visitCode和visitMaxs成对出现在method字节码前后(如果存在字节码的话),visitEnd出现在最后。
在user guide中还给出了一个非常重要的工具类AdviceAdapter,按照user guide里面的说法:
This method adapter is an abstract class that can be used to insert code at
the beginning of a method and just before any RETURN or ATHROW
instruction.
Its main advantage is that it also works for constructors, where code must not
be inserted just at the beginning of the constructor, but after the call to the
super constructor
看起来非常适合我们用来实现AOP(在method字节码前后插入我们的代码)
2.2 字节码与指令
由于ASM操作比较底层,所以我们进行字节码增强的时候需要了解字节码与相关指令。Java bytecode instruction listings 里面非常详尽的介绍了字节码指令以及字节码指令对栈帧的影响(这个特别重要)!在此说明一下,目前网络上很多博客写的字节码指令的中文解释非常不全面而且存在非常明显的错误(比如对于dup2_x1的解释网上中文博客基本找不到正确的),所以大家最好还是以该英文维基百科为准。
3. 具体实现
以下实现代码均在scrat-profiler中,首先声明,该代码基本全部参考的greys的相关代码,且为玩票性质,不能用于生产环境。
3.1 AOP通知方法定义
本文不纠结与AOP的专业定义,例如通知、切片等。只求以通俗易理解的语言说明。AOP无非是要在方法前后运行自定义的增强代码。此‘增强代码’可定义为如下接口。
public interface AdviceListener {
/**
* 方法前置通知
*
* @param classLoader target类加载器
* @param className 类名
* @param methodName 增强的方法名
* @param methodDesc 增强的方法描述
* @param target 目标类实例对象,如果目标为静态方法,则为null
* @param args 增强的方法参数
* @throws Throwable 通知执行过程中的异常
*/
void before(ClassLoader classLoader, String className, String methodName, String methodDesc, Object target,
Object[] args) throws Throwable;
/**
* 方法正常返回后的通知
*
* @param classLoader target类加载器
* @param className 类名
* @param methodName 方法名
* @param methodDesc 方法描述
* @param target 目标类实例对象,如果目标为静态方法,则为null
* @param args 增强的方法参数
* @param returnObj 目标方法返回值
* @throws Throwable 通知执行过程中的异常
*/
void afterReturning(ClassLoader classLoader, String className, String methodName, String methodDesc, Object target,
Object[] args, Object returnObj) throws Throwable;
/**
* 方法抛出异常后的通知
*
* @param loader target类加载器
* @param className 类名
* @param methodName 方法名
* @param methodDesc 方法描述
* @param target 目标类实例对象,如果目标为静态方法,则为null
* @param args 增强的方法参数
* @param throwable 目标方法返回值
* @throws Throwable 通知执行过程中的异常
*/
void afterThrowing(ClassLoader loader, String className, String methodName, String methodDesc, Object target,
Object[] args, Throwable throwable) throws Throwable;
}
该方法基本copy于greys,定义了target方法运行前的增强代码、方法正常返回后的增强代码以及方法运行过程中抛出异常的增强代码。该接口基本满足了我们日常的AOP增强需求。
3.2 使用AdviceAdapter植入增强代码
如前所述,ASM提供了AdviceAdapter工具类用于在method字节码中插入增强代码,onMethodEnter、onMethodExit、visitMaxs成为比较好的切入点。
3.2.1 字节码操作
让我们首先学习下常用的字节码操作。
- invokestatic
invokestatic b8 1011 1000 2: indexbyte1, indexbyte2 [arg1, arg2, ...] → result invoke a static method and puts the result on the stack (might be void); the method is identified by method reference index in constant pool (indexbyte1 << 8 + indexbyte2)
也就是说在使用invokestatic之前,需要再栈顶依次push该static方法的入参,在调用完成后会将方法的返回结果留在栈顶。例如如果我们想调用Class.forName(String className),我们首先需要将className push到执行栈中,使用
ldc "some.Person"
此时的栈顶上就是"some.Person"的这个类的Class类实例。
- invokevirtual
接下来我们看看如何调用某类的实例方法,最典型的就是调用System.out.println(String str),让我们试着用字节码完成这个操作。
让我们先看看invokevirtual的字节码说明:
invokevirtual b6 1011 0110 2: indexbyte1, indexbyte2 objectref, [arg1, arg2, ...] → result invoke virtual method on object objectref and puts the result on the stack (might be void); the method is identified by method reference index in constant pool (indexbyte1 << 8 + indexbyte2)
如上所示,在使用invokevirtual之前,我们需要先将method所在的Object ref压入堆栈,然后将方法的参数一次压入堆栈,然后使用invokevirtual,然后该方法的返回值会被存在栈顶。下面演示下如何调用System.out.println(String str)
首先需要将Object ref压入堆栈(使用ASM取得Ojbect ref通常不是一件容易的事情),我们使用getstatic将System的out filed压入栈顶。
visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
然后将入参string压入栈顶
ldc string
然后使用invokevirtual调用
visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
3.2.2 织入增强代码
如前所述,基本使用方法:
ClassReader cr = new ClassReader(targetClass.getName());
// 字节码增强
final ClassWriter cw = new ClassWriter(cr, COMPUTE_FRAMES | COMPUTE_MAXS);
cr.accept(new AdviceWeaver(adviceId, adviceListener, genTransferClassName(), targetClass, cw),
ClassReader.EXPAND_FRAMES);
byte[] enhanced = cw.toByteArray();
AdviceWeaver继承ClassVisitor,并且复写了
public MethodVisitor visitMethod(int access, final String name, final String desc, String signature,String[] exceptions)
返回了一个AdviceAdapter的子类,定制了onMethodEnter、onMethodExit、visitMaxs方法。
3.2.3 前置增强onMethodEnter
在onMethodEnter中,主要增加了调用AdviceWeaver#methodOnBegin的逻辑,methodOnBegin的方法体如下:
public static void methodOnBegin(int adviceId, ClassLoader loader, String className,
String methodName, String methodDesc, Object target, Object[] args) {
if (selfCalled.get()) {
return;
} else {
selfCalled.set(true);
}
try {
AdviceListener listener = LISTENER_MAP.get(adviceId);
if (listener == null) {
throw new RuntimeException("no listener for:" + adviceId);
}
//为了方便returning/throwing方法,先保护现场入栈
Stack<Object> beginMethodFrame = new Stack<>();
beginMethodFrame.push(listener);
beginMethodFrame.push(loader);
beginMethodFrame.push(className);
beginMethodFrame.push(methodName);
beginMethodFrame.push(methodDesc);
beginMethodFrame.push(target);
beginMethodFrame.push(args);
threadFrameStack.get().push(beginMethodFrame);
//执行before listener
doBefore(listener, loader, className, methodName, methodDesc, target, args);
} finally {
selfCalled.set(false);
}
}
基本逻辑很简单,就是从map中取出对应的listener,然后调用listener的前置通知方法。而onMethodEnter方法如下:
@Override
protected void onMethodEnter() {
//前置增強
//push adviceId
push(adviceId);
box(Type.getType(Integer.class));
//push classloader
loadClassLoader();
//push className
push(className);
//push methodName
push(name);
//push methodDesc
push(desc);
//push target
loadThisOrNullIfStatic();
//push method args
loadArgArray();
//调用methodOnBegin方法
invokeStatic(ADVICE_WEAVER_TYPE, AdviceWeaver_methodOnBegin);
//标记method begin,用于throwing的try-catch-finally block
mark(beginLabel);
}
基本上也就是将methodOnBegin方法的入参依次压入堆栈,然后invokeStatic。这其实有几个比较有意思的点,第一个就是为啥要压入adviceId而不是压入adviceListener呢,主要是在AdviceAdapter的上下文中,使用ASM很难获取到adviceListener的实例变量。转而使用adviceId进行标识然后从静态Map中获取。
3.2.4 后置增强onMethodExit
在本例中,后置增强只处理正常返回值,代码如下
protected void onMethodExit(int opcode) {
//判断不是以异常结束
if (ATHROW != opcode) {
//加载正常的返回值
loadReturn(opcode);
//只有一个参数就是返回值
invokeStatic(ADVICE_WEAVER_TYPE, AdviceWeaver_methodOnReturning);
}
}
private void loadReturn(int opcode) {
switch (opcode) {
case RETURN: {
push((Type)null);
break;
}
case ARETURN: {
dup();
break;
}
case LRETURN:
case DRETURN: {
dup2();
box(Type.getReturnType(methodDesc));
break;
}
default: {
dup();
box(Type.getReturnType(methodDesc));
break;
}
}
}
dup相当于复制栈顶元素然后入栈该元素,为啥要进行一次dup呢,因为其中一个栈顶元素(当前返回值)需要作为AdviceWeaver_methodOnReturning的方法参数被消耗掉,为了不侵入所以先复制一份。
3.2.5 异常返回增强visitMaxs
前文提到,
The visitCode and visitMaxs methods can therefore be used to detect the
start and end of the method’s bytecode in a sequence of events. Like for
classes, the visitEnd method must be called last, and is used to detect the end of a method in a sequence of events
可知,visitMaxs可以detect end of the method’s bytecode。
我们看下visitMaxs如何将method’s bytecode包括在try-catch block中。
public void visitMaxs(int maxStack, int maxLocals) {
//每个方法最后调用一次,在visitEnd之前
mark(endLabel);
//在beginLabel和endLabel之间使用try-catch block,在这之后需要紧跟exception的处理逻辑code
catchException(beginLabel, endLabel, THROWABLE_TYPE);
//从栈顶加载异常(复制一份给onThrowing当参数用)
dup();
invokeStatic(ADVICE_WEAVER_TYPE, AdviceWeaver_methodOnThrowing);
//将原有的异常抛出(不破坏原有异常逻辑)
throwException();
super.visitMaxs(maxStack, maxLocals);
}
需要注意的是,在onMethodEnter的最后我们进行了mark(beginLabel),也就是标记了method bytecode的开始,在visitMaxs开始的时候mark(endLabel),然后我们使用了catchException(beginLabel, endLabel, THROWABLE_TYPE)这个语句相当于说,在beginLabel和endLabel之间使用try catch block,并且catch的类型为THROWABLE_TYPE。由于java没有异常处理语句,字节码执行过程中异常的跳转完全靠异常表完成,那么这句话的意思也可以理解为向异常表中添加一种异常 handler,该handler的起始部分为beginLabel ~ endLabel。catchException语句后面跟着的部分就是handler语句,即此处的:
//从栈顶加载异常(复制一份给onThrowing当参数用)
dup();
invokeStatic(ADVICE_WEAVER_TYPE, AdviceWeaver_methodOnThrowing);
//将原有的异常抛出(不破坏原有异常逻辑)
throwException();
结合注释会比较好理解。
4. 总结
以上基本介绍了比较关键的点,大家可以参考github里给出的代码。后续可能会需要再讲解下关于JVM Agent的知识。本文的代码基本参考与greys,感谢!