https://blog.csdn.net/fyyyr/article/details/102816064
ASM基礎
ASM是一個Java字節碼操作框架,可用於class文件的修改。
其原理是將class文件載入,然后構建成一棵樹。然后根據用戶自定義的修改類對該樹進行加工,加工完成后即可得到修改后的class文件。
故而ASM中使用了visitor模式:class文件的結構是固定的,根據其構造出的樹作為被訪問者,則其節點也是固定的。只需要對每個節點定義一個訪問者即可進行指定的修改。
由於修改class主要涉及字段和方法,故最常用的visitor是FieldVisitor
和MethodVisitor
。
以FieldVisitor
為例,當ASM使用FieldVisitor
來處理一個class的樹時,則該class的每個方法都會被傳入定義的FieldVisitor
。於是只需要在FieldVisitor
對特定的方法進行過濾處理即可。
ASM的官方文檔地址為:
https://asm.ow2.io/javadoc/overview-summary.html
打開可以看到其類庫的內容:
其第一個包org.objectweb.asm
為核心core包,包含了主要的功能接口和對象。
環境搭建
ASM使用的是com.sun.xml.internal.ws.org.objectweb.asm
,因此不需要額外引入庫文件。
創建一個JBoss工程,載入class並修改,然后將修改后的class文件進行保存:
import com.sun.xml.internal.ws.org.objectweb.asm.ClassAdapter; import com.sun.xml.internal.ws.org.objectweb.asm.ClassReader; import com.sun.xml.internal.ws.org.objectweb.asm.ClassWriter; import java.io.*; public class Main { public static void main(String[] args) throws Exception { // 載入class文件 FileInputStream fis = new FileInputStream("D:\\Test\\Hello.class"); // 修改 ClassReader cr = new ClassReader(fis); ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); ClassAdapter classAdapter = new ASMTest(cw); cr.accept(classAdapter, ClassReader.SKIP_DEBUG); // 保存class文件 FileOutputStream fos = new FileOutputStream("D:\\Test\\Change\\Hello.class"); fos.write(cw.toByteArray()); fos.close(); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
這就是總體的框架。至於修改,由ASMTest
類負責。
新建一個Java類ASMTest
:
import com.sun.xml.internal.ws.org.objectweb.asm.*; public class ASMTest extends ClassAdapter { public ASMTest(ClassVisitor cv) { super(cv); } // 字段處理 public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { return this.cv.visitField(access, name, desc, signature, value); } // 方法處理 public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { return this.cv.visitMethod(access, name, desc, signature, exceptions); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
上面定義了負責進行修改的類ASMTest
。直接運行main()
,會在目標路徑下生成一個class。由於ASMTest
沒有進行任何修改,故而生成的class與原始class內容相同。
解析
ClassReader
ClassReader
能夠處理class文件的字節碼數據,構建出一棵該類的抽象樹。然后執行傳入的參數對象所包含的操作,從而對抽象樹進行加工。
ClassAdapter
ClassAdapter
繼承自ClassVisitor
,負責對class樹進行修改,開發者需要對其繼承並重載對應的修改方法。
也就是說,所有的visitor都集成在了ClassAdapter
的方法中,只需要順序調用ClassAdapter
的所有方法,即可實現所有visitor順序訪問class樹。ClassAdapter
的定義為:
public class ClassAdapter implements ClassVisitor { protected ClassVisitor cv; public ClassAdapter(ClassVisitor cv) { this.cv = cv; } public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { this.cv.visit(version, access, name, signature, superName, interfaces); } public void visitSource(String source, String debug) { this.cv.visitSource(source, debug); } public void visitOuterClass(String owner, String name, String desc) { this.cv.visitOuterClass(owner, name, desc); } public AnnotationVisitor visitAnnotation(String desc, boolean visible) { return this.cv.visitAnnotation(desc, visible); } public void visitAttribute(Attribute attr) { this.cv.visitAttribute(attr); } public void visitInnerClass(String name, String outerName, String innerName, int access) { this.cv.visitInnerClass(name, outerName, innerName, access); } public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { return this.cv.visitField(access, name, desc, signature, value); } public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { return this.cv.visitMethod(access, name, desc, signature, exceptions); } public void visitEnd() { this.cv.visitEnd(); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
其所有方法都可被重載。前面的例子中,ASMTest
重載了visitField()
和visitMethod()
,從而對字段和方法進行修改。ClassAdapter
的所有方法是按已安排好的順序來調用的,也就是ClassAdapter
所有方法的定義順序。這一點通常不需要開發者關心。
關於每個方法的具體說明,可參考其父類ClassVisitor
的文檔:
https://asm.ow2.io/javadoc/org/objectweb/asm/ClassVisitor.html
修改
以前面ASMTest
為例。
設Hello.class的實現為:
public class Hello { String words = "Hello world!"; int value = 1; public void say() { System.out.println(words); } public void think() { words = "new thought"; } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
變量
變量有個descriptor
屬性,即描述符,是個字符串,每種類型都對應一個字符串:
java類型 | descriptor |
---|---|
boolean | Z |
char | C |
byte | B |
short | S |
int | I |
float | F |
long | J |
double | D |
Object | Ljava/lang/Object; |
int[] | [I |
Object[][] | [[Ljava/lang/Object; |
刪除成員變量
刪除Hello.class中的變量value
。
public class ASMTest extends ClassAdapter { public ASMTest(ClassVisitor cv) { super(cv); } public FieldVisitor visitField(final int access, final String name,final String desc, final String signature, final Object value) { if (name.equals("value")) { return null; } return cv.visitField(access, name, desc, signature, value); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
Hello.class的所有成員變量會依次傳入visitField()
。當傳入的變量名為value
時,直接return null;
,會將該變量從class樹中刪除。
修改成員變量權限
修改Hello.class中的變量value
權限為public
。
public class ASMTest extends ClassAdapter { public ASMTest(ClassVisitor cv) { super(cv); } public FieldVisitor visitField(final int access, final String name,final String desc, final String signature, final Object value) { if (name.equals("value")) { return cv.visitField(Opcodes.ACC_PUBLIC, name, desc, signature, value); } return cv.visitField(access, name, desc, signature, value); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
添加成員變量
為Hello.class添加int變量tInt
,其初始值為3。
public class ASMTest extends ClassAdapter { public ASMTest(ClassVisitor cv) { super(cv); } public void visitEnd() { FieldVisitor fv = this.cv.visitField(Opcodes.ACC_PRIVATE, "temp", "I", null, 3); if (fv!=null){ fv.visitEnd(); } super.visitEnd(); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
要點:
- 在所有字段都訪問完成后,會調用
ClassVisitor.visitEnd()
作為結束,此時即可進行成員變量的添加。 - 調用
visitField()
來訪問成員變量。若要訪問的成員變量不存在,則創建。
visitField()
的定義為:
public FieldVisitor visitField(int access, java.lang.String name, java.lang.String descriptor, java.lang.String signature, java.lang.Object value)
- 1
- 2
- 3
- 4
- 5
其中:
- descriptor: 類型描述符,即該成員變量的類型。
- signature: 簽名。默認null即可。
- value: 初始值。
方法
方法的描述符descriptor
是個字符串,包含:
java類型 | descriptor |
---|---|
void m(int i, float) | (IF)V |
int m(Object o) | (Ljava/lang/Object;)I |
int[] m(int i, String s) | (ILjava/lang/String;)[I |
Object m(int[] i) | ([I)Ljava/lang/Object; |
包含兩部分:
()
內的是參數類型,多個參數直接拼接即可。例如(IF)
,指有2個參數,第一個是int
,第二個是float
。()
后的是返回值類型。
刪除方法
刪除Hello.class中的方法think
。
public class ASMTest extends ClassAdapter { public ASMTest(ClassVisitor cv) { super(cv); } public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { if (name.equals("think")) { return null; } return this.cv.visitMethod(access, name, desc, signature, exceptions); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
Hello.class的所有方法會依次傳入visitMethod()
。當傳入的方法名為think
時,直接return null;
,會將該方法從class樹中刪除。
上面這種寫法是使用方法名來進行判斷。然而,有些類有多個同名方法,使用上面的寫法會將所有同名方法都刪除。若要只刪除某個方法,則需要同時對descriptor
進行判斷:
if (name.equals("think") && desc.equals("I")) { return null; }
- 1
- 2
- 3
修改/添加
方法的修改/添加較為復雜。對於修改,常規做法是攔截到目標方法后,返回一個新的MethodVisitor
:
public class ASMTest extends ClassAdapter { public ASMTest(ClassVisitor cv) { super(cv); } public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { if (name.equals("think")) { MethodVisitor mv = cv.visitMethod(access, name, desc, signature,exceptions); return new NewMethodAdapter(mv); } return this.cv.visitMethod(access, name, desc, signature, exceptions); } } class NewMethodAdapter extends MethodAdapter { public NewMethodAdapter(MethodVisitor mv) { super(mv); } public void visitCode() {} }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
MethodAdapter
繼承自MethodVisitor
。通過重載MethodAdapter
的各個方法來實現對類方法的修改。可參考MethodVisitor
的官方文檔。