asm操作位元組碼,刪除類的成員變數
https://blog.csdn.net/fyyyr/article/details/102816064
ASM基礎
ASM是一個Java位元組碼操作框架,可用於class檔案的修改。
其原理是將class檔案載入,然後構建成一棵樹。然後根據使用者自定義的修改類對該樹進行加工,加工完成後即可得到修改後的class檔案。
故而ASM中使用了visitor模式:class檔案的結構是固定的,根據其構造出的樹作為被訪問者,則其節點也是固定的。只需要對每個節點定義一個訪問者即可進行指定的修改。
由於修改class主要涉及欄位和方法,故最常用的visitor是FieldVisitor
和MethodVisitor
。
以FieldVisitor
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
的官方文件。