預(yù)期目標
假如有一個HelloWorld類痪伦,代碼如下:
public class HelloWorld {
public void test() {
System.out.println("this is a test method.");
}
}
我們想實現(xiàn)的預(yù)期目標:對于test()方法,在“方法進入”時和“方法退出”時椭微,添加一條打印語句隐绵。
- 第一種情況之众,在“方法進入”時,預(yù)期目標如下所示:
public class HelloWorld {
public void test() {
System.out.println("Method Enter...");
System.out.println("this is a test method.");
}
}
- 第二種情況依许,在“方法退出”時棺禾,預(yù)期目標如下所示:
public class HelloWorld {
public void test() {
System.out.println("this is a test method.");
System.out.println("Method Exit...");
}
}
現(xiàn)在,我們有了明確的預(yù)期目標峭跳;接下來膘婶,就是將這個預(yù)期目標轉(zhuǎn)換成具體的ASM代碼。那么蛀醉,應(yīng)該怎么實現(xiàn)呢悬襟?從哪里著手呢?
實現(xiàn)思路
我們知道拯刁,現(xiàn)在的內(nèi)容是Class Transformation的操作脊岳,其中涉及到三個主要的類:ClassReader、ClassVisitor和ClassWriter垛玻。其中割捅,ClassReader負責讀取Class文件,ClassWriter負責生成Class文件帚桩,而具體的ClassVisitor負責進行Transformation的操作亿驾。換句話說,我們還是應(yīng)該從ClassVisitor類開始朗儒。
第一步颊乘,回顧一下ClassVisitor類當中主要的visitXxx()方法有哪些。在ClassVisitor類當中醉锄,有visit()乏悄、visitField()、visitMethod()和visitEnd()方法恳不;這些visitXxx()方法與.class文件里的不同部分之間是有對應(yīng)關(guān)系的檩小,如下圖:
根據(jù)我們的預(yù)期目標,現(xiàn)在想要修改的是“方法”的部分烟勋,那么就對應(yīng)著ClassVisitor類的visitMethod()方法规求。ClassVisitor.visitMethod()會返回一個MethodVisitor類的實例;而MethodVisitor類就是用來生成方法的“方法體”卵惦。
第二步阻肿,回顧一下MethodVisitor類當中定義了哪些visitXxx()方法。
在MethodVisitor類當中沮尿,定義的visitXxx()方法比較多丛塌,但是我們可以將這些visitXxx()方法進行分組:
- 第一組较解,visitCode()方法,標志著方法體(method body)的開始赴邻。
- 第二組印衔,visitXxxInsn()方法,對應(yīng)方法體(method body)本身姥敛,這里包含多個方法奸焙。
- 第三組,visitMaxs()方法彤敛,標志著方法體(method body)的結(jié)束与帆。
- 第四組,visitEnd()方法臊泌,是最后調(diào)用的方法鲤桥。
另外,我們也回顧一下渠概,在MethodVisitor類中茶凳,visitXxx()方法的調(diào)用順序:
- 第一步,調(diào)用visitCode()方法,調(diào)用一次。
- 第二步掷邦,調(diào)用visitXxxInsn()方法,可以調(diào)用多次箱沦。
- 第三步,調(diào)用visitMaxs()方法雇庙,調(diào)用一次谓形。
- 第四步,調(diào)用visitEnd()方法疆前,調(diào)用一次寒跳。
到了這一步,我們基本上就知道了:需要修改的內(nèi)容就位于visitCode()和visitMaxs()方法之間竹椒,這是一個大概的范圍童太。
第三步,精確定位胸完。也就是說书释,在MethodVisitor類當中,要確定出要在哪一個visitXxx()方法里進行修改赊窥。
方法進入
如果我們想在“方法進入”時爆惧,添加一些打印語句,那么我們有兩個位置可以添加打印語句:
- 第一個位置锨能,就是在visitCode()方法中检激。
- 第二個位置肴捉,就是在第1個visitXxxInsn()方法中。
在這兩個位置當中叔收,我們推薦使用visitCode()方法。因為visitCode()方法總是位于方法體(method body)的前面傲隶,而第1個visitXxxInsn()方法是不穩(wěn)定的饺律。
public void visitCode() {
// 首先,處理自己的代碼邏輯
// TODO: 添加“方法進入”時的代碼
// 其次跺株,調(diào)用父類的方法實現(xiàn)
super.visitCode();
}
方法退出
如果我們在“方法退出”時想添加的代碼复濒,是否可以添加到visitMaxs()方法內(nèi)呢?這樣做是不行的乒省。因為在執(zhí)行visitMaxs()方法之前巧颈,方法體(method body)已經(jīng)執(zhí)行過了:在方法體(method body)當中,里面會包含return語句袖扛;如果return語句一執(zhí)行砸泛,后面的任何語句都不會再執(zhí)行了;換句話說蛆封,如果在visitMaxs()方法內(nèi)添加的打印輸出語句唇礁,由于前面方法體(method body)中已經(jīng)執(zhí)行了return語句,后面的任何語句就執(zhí)行不到了惨篱。
那么盏筐,到底是應(yīng)該在哪里添加代碼呢?為了回答這個問題砸讳,我們需要知道“方法退出”有哪幾種情況琢融。方法的退出,有兩種情況簿寂,一種是正常退出(執(zhí)行return語句)漾抬,另一種是異常退出(執(zhí)行throw語句);接下來陶耍,就是將這兩種退出情況應(yīng)用到ASM的代碼層面奋蔚。
在MethodVisitor類當中,無論是執(zhí)行return語句烈钞,還是執(zhí)行throw語句泊碑,都是通過visitInsn(opcode)方法來實現(xiàn)的。所以毯欣,如果我們想在“方法退出”時馒过,添加一些語句,那么這些語句放到visitInsn(opcode)方法中就可以了酗钞。
public void visitInsn(int opcode) {
// 首先腹忽,處理自己的代碼邏輯
if (opcode == Opcodes.ATHROW || (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
// TODO: 添加“方法退出”時的代碼
}
// 其次来累,調(diào)用父類的方法實現(xiàn)
super.visitInsn(opcode);
}
推薦做法:在編寫ASM代碼的時候,如果寫了一個類窘奏,它繼承自ClassVisitor嘹锁,那么就命名成XxxVisitor;如果寫了一個類着裹,它繼承自MethodVisitor领猾,那么就命名成XxxAdapter。通過類的名字骇扇,我就可以區(qū)分出哪些類是繼承自ClassVisitor摔竿,哪些類是繼承自MethodVisitor。
示例一:方法進入
編碼實現(xiàn):
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public class MethodEnterVisitor extends ClassVisitor {
public MethodEnterVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
if (mv != null && !"<init>".equals(name)) {
mv = new MethodEnterAdapter(api, mv);
}
return mv;
}
private static class MethodEnterAdapter extends MethodVisitor {
public MethodEnterAdapter(int api, MethodVisitor methodVisitor) {
super(api, methodVisitor);
}
@Override
public void visitCode() {
// 首先少孝,處理自己的代碼邏輯
super.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
super.visitLdcInsn("Method Enter...");
super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
// 其次继低,調(diào)用父類的方法實現(xiàn)
super.visitCode();
}
}
}
在上面MethodEnterAdapter類的visitCode()方法中,主要是做兩件事情:
- 首先稍走,處理自己的代碼邏輯袁翁。
- 其次,調(diào)用父類的方法實現(xiàn)钱磅。
在處理自己的代碼邏輯中梦裂,有3行代碼。這3條語句的作用就是添加System.out.println("Method Enter...");語句:
super.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
super.visitLdcInsn("Method Enter...");
super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
注意盖淡,上面的代碼中使用了super關(guān)鍵字年柠。
事實上,在MethodVisitor類當中褪迟,定義了一個protected MethodVisitor mv;字段冗恨。我們也可以使用mv這個字段,代碼也可以這樣寫:
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Method Enter...");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
但是這樣寫味赃,可能會遇到mv為null的情況掀抹,這樣就會出現(xiàn)NullPointerException異常。
如果使用super心俗,就會避免NullPointerException異常的情況傲武。因為使用super的情況下,就是調(diào)用父類定義的方法城榛,在本例中其實就是調(diào)用MethodVisitor類里定義的方法揪利。在MethodVisitor類里的visitXxx()方法中,會先對mv進行是否為null的判斷狠持,所以就不會出現(xiàn)NullPointerException的情況疟位。
public abstract class MethodVisitor {
protected MethodVisitor mv;
public void visitCode() {
if (mv != null) {
mv.visitCode();
}
}
public void visitInsn(final int opcode) {
if (mv != null) {
mv.visitInsn(opcode);
}
}
public void visitIntInsn(final int opcode, final int operand) {
if (mv != null) {
mv.visitIntInsn(opcode, operand);
}
}
public void visitVarInsn(final int opcode, final int var) {
if (mv != null) {
mv.visitVarInsn(opcode, var);
}
}
public void visitFieldInsn(final int opcode, final String owner, final String name, final String descriptor) {
if (mv != null) {
mv.visitFieldInsn(opcode, owner, name, descriptor);
}
}
// ......
public void visitMaxs(final int maxStack, final int maxLocals) {
if (mv != null) {
mv.visitMaxs(maxStack, maxLocals);
}
}
public void visitEnd() {
if (mv != null) {
mv.visitEnd();
}
}
}
進行轉(zhuǎn)換:
import lsieun.utils.FileUtils;
import org.objectweb.asm.*;
public class HelloWorldTransformCore {
public static void main(String[] args) {
String relative_path = "sample/HelloWorld.class";
String filepath = FileUtils.getFilePath(relative_path);
byte[] bytes1 = FileUtils.readBytes(filepath);
//(1)構(gòu)建ClassReader
ClassReader cr = new ClassReader(bytes1);
//(2)構(gòu)建ClassWriter
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
//(3)串連ClassVisitor
int api = Opcodes.ASM9;
ClassVisitor cv = new MethodEnterVisitor(api, cw);
//(4)結(jié)合ClassReader和ClassVisitor
int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
cr.accept(cv, parsingOptions);
//(5)生成byte[]
byte[] bytes2 = cw.toByteArray();
FileUtils.writeBytes(filepath, bytes2);
}
}
結(jié)果驗證:
import java.lang.reflect.Method;
public class HelloWorldRun {
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("sample.HelloWorld");
Method m = clazz.getDeclaredMethod("test");
Object instance = clazz.newInstance();
m.invoke(instance);
}
}
特殊情況:<init>()方法
在.class文件中,<init>()方法喘垂,就表示類當中的構(gòu)造方法甜刻。
我們在“方法進入”時绍撞,有一個對于<init>的判斷:
if (mv != null && !"<init>".equals(name)) {
// ......
}
Java requires that if you call this() or super() in a constructor, it must be the first statement.
public class HelloWorld {
public HelloWorld() {
System.out.println("Method Enter...");
super(); // 報錯:Call to 'super()' must be first statement in constructor body
}
}
去掉對于<init>()方法的判斷,會發(fā)現(xiàn)它好像也是可以正常執(zhí)行的得院。
但是傻铣,如果我們換一下添加的語句,就會出錯了:
super.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
super.visitVarInsn(Opcodes.ALOAD, 0);
super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Object", "toString", "()Ljava/lang/String;", false);
super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
示例二:方法退出
代碼實現(xiàn):
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public class MethodExitVisitor extends ClassVisitor {
public MethodExitVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
if (mv != null && !"<init>".equals(name)) {
mv = new MethodExitAdapter(api, mv);
}
return mv;
}
private static class MethodExitAdapter extends MethodVisitor {
public MethodExitAdapter(int api, MethodVisitor methodVisitor) {
super(api, methodVisitor);
}
@Override
public void visitInsn(int opcode) {
// 首先尿招,處理自己的代碼邏輯
if (opcode == Opcodes.ATHROW || (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
super.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
super.visitLdcInsn("Method Exit...");
super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
// 其次矾柜,調(diào)用父類的方法實現(xiàn)
super.visitInsn(opcode);
}
}
}
進行轉(zhuǎn)換:
import lsieun.utils.FileUtils;
import org.objectweb.asm.*;
public class HelloWorldTransformCore {
public static void main(String[] args) {
String relative_path = "sample/HelloWorld.class";
String filepath = FileUtils.getFilePath(relative_path);
byte[] bytes1 = FileUtils.readBytes(filepath);
//(1)構(gòu)建ClassReader
ClassReader cr = new ClassReader(bytes1);
//(2)構(gòu)建ClassWriter
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
//(3)串連ClassVisitor
int api = Opcodes.ASM9;
ClassVisitor cv = new MethodExitVisitor(api, cw);
//(4)結(jié)合ClassReader和ClassVisitor
int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
cr.accept(cv, parsingOptions);
//(5)生成byte[]
byte[] bytes2 = cw.toByteArray();
FileUtils.writeBytes(filepath, bytes2);
}
}
結(jié)果驗證:
import java.lang.reflect.Method;
public class HelloWorldRun {
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("sample.HelloWorld");
Method m = clazz.getDeclaredMethod("test");
Object instance = clazz.newInstance();
m.invoke(instance);
}
}
輸出結(jié)果:
this is a test method.
Method Exit...
示例三:方法進入和方法退出
第一種方式
第一種方式,就是將多個ClassVisitor類串聯(lián)起來就谜。
import lsieun.utils.FileUtils;
import org.objectweb.asm.*;
public class HelloWorldTransformCore {
public static void main(String[] args) {
String relative_path = "sample/HelloWorld.class";
String filepath = FileUtils.getFilePath(relative_path);
byte[] bytes1 = FileUtils.readBytes(filepath);
//(1)構(gòu)建ClassReader
ClassReader cr = new ClassReader(bytes1);
//(2)構(gòu)建ClassWriter
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
//(3)串連ClassVisitor
int api = Opcodes.ASM9;
ClassVisitor cv1 = new MethodEnterVisitor(api, cw);
ClassVisitor cv2 = new MethodExitVisitor(api, cv1);
ClassVisitor cv = cv2;
//(4)結(jié)合ClassReader和ClassVisitor
int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
cr.accept(cv, parsingOptions);
//(5)生成byte[]
byte[] bytes2 = cw.toByteArray();
FileUtils.writeBytes(filepath, bytes2);
}
}
第二種方式
第二種方式,就是將所有的代碼都放到一個ClassVisitor類里面里覆。
編碼實現(xiàn):
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public class MethodAroundVisitor extends ClassVisitor {
public MethodAroundVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
if (mv != null && !"<init>".equals(name)) {
boolean isAbstractMethod = (access & Opcodes.ACC_ABSTRACT) == Opcodes.ACC_ABSTRACT;
boolean isNativeMethod = (access & Opcodes.ACC_NATIVE) == Opcodes.ACC_NATIVE;
if (!isAbstractMethod && !isNativeMethod) {
mv = new MethodAroundAdapter(api, mv);
}
}
return mv;
}
private static class MethodAroundAdapter extends MethodVisitor {
public MethodAroundAdapter(int api, MethodVisitor methodVisitor) {
super(api, methodVisitor);
}
@Override
public void visitCode() {
// 首先丧荐,處理自己的代碼邏輯
super.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
super.visitLdcInsn("Method Enter...");
super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
// 其次,調(diào)用父類的方法實現(xiàn)
super.visitCode();
}
@Override
public void visitInsn(int opcode) {
// 首先喧枷,處理自己的代碼邏輯
if (opcode == Opcodes.ATHROW || (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
super.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
super.visitLdcInsn("Method Exit...");
super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
// 其次虹统,調(diào)用父類的方法實現(xiàn)
super.visitInsn(opcode);
}
}
}
進行轉(zhuǎn)換:
import lsieun.utils.FileUtils;
import org.objectweb.asm.*;
public class HelloWorldTransformCore {
public static void main(String[] args) {
String relative_path = "sample/HelloWorld.class";
String filepath = FileUtils.getFilePath(relative_path);
byte[] bytes1 = FileUtils.readBytes(filepath);
//(1)構(gòu)建ClassReader
ClassReader cr = new ClassReader(bytes1);
//(2)構(gòu)建ClassWriter
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
//(3)串連ClassVisitor
int api = Opcodes.ASM9;
ClassVisitor cv = new MethodAroundVisitor(api, cw);
//(4)結(jié)合ClassReader和ClassVisitor
int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
cr.accept(cv, parsingOptions);
//(5)生成byte[]
byte[] bytes2 = cw.toByteArray();
FileUtils.writeBytes(filepath, bytes2);
}
}
總結(jié)
本文主要是對“方法進入”和“方法退出”添加代碼進行介紹,內(nèi)容總結(jié)如下:
- 第一點隧甚,在“方法進入”時和“方法退出”時添加代碼车荔,應(yīng)該如何實現(xiàn)?
- 在“方法進入”時添加代碼戚扳,是在visitCode()方法當中完成;
- 在“方法退出”添加代碼時珠增,是在visitInsn(opcode)方法中,判斷opcode為return或throw的情況下完成。
- 第二點梦皮,在“方法進入”時和“方法退出”時添加代碼,有一些特殊的情況退子,需要小心處理:
- 接口荐虐,是否需要處理福扬?接口當中的抽象方法沒有方法體,但也可能有帶有方法體的default方法汽烦。
- 帶有特殊修飾符的方法:
- 抽象方法,是否需要處理?不只是接口當中有抽象方法煮岁,抽象類里也可能有抽象方法。抽象方法,是沒有方法體的。
- native方法腌且,是否需要處理?native方法是沒有方法體的精续。
- 名字特殊的方法顷级,例如,構(gòu)造方法(<init>())和靜態(tài)初始化方法(<clinit>())翔冀,是否需要處理?
另外跌捆,在編寫代碼的時候姆钉,我們遵循一個“規(guī)則”:如果是ClassVisitor的子類,就取名為XxxVisitor類;如果是MethodVisitor的子類思恐,就取名為XxxAdapter類婚温。
本文的介紹方式側(cè)重于讓大家理解“工作原理”荆秦,而后續(xù)介紹的AdviceAdapter則側(cè)重于“應(yīng)用”,AdviceAdapter的實現(xiàn)也是基于visitCode()和visitInsn(opcode)方法實現(xiàn)的坪圾,在理解上有一個步步遞進的關(guān)系漾月。
本文也是基礎(chǔ)方式,可以應(yīng)對各種場景烛芬,大家需進行掌握。