ASM是一個提供字節碼解析和操作的框架。Cglib框架就是基於ASM框架實現的,被廣泛應用的Hibernate,Spring就是基於Cglib 實現了AOP技術。
在說到AOP的Java實現,可能會優先想到java的Proxy api,通過invoke方法攔截處理相應的代碼邏輯,但是proxy 是面向接口的,被代理的class的所有方法調用都會通過反射調用invoke 方法,相對性能開銷大。另外的還有Java 5提供的Instrument,比較適用於監控檢查方面,但在處理靈活的代碼邏輯方面並不合適。
ASM 框架對用戶屏蔽了整個類字節碼的長度,偏移量,能夠更加靈活和方便得實現對字節碼的解析和操作。其主要提供了兩部分主要的API,Core Api 及Tree Api。本文先從Core Api的解析和生成字節碼開始介紹。
CoreApi 在解析和生成這裏,主要用的的類是ClassVisitor。ClassVisitor在2.0版本的時候是個接口,對於class文件操作都是訪問ClassAdepter。這裏主要基於4.0以後的API做介紹。
先看一下ClassVisitor代碼:
public abstract class ClassVisitor {
public ClassVisitor(int api);
public ClassVisitor(int api, ClassVisitor cv);
public void visit(int version, int access, String name,
String signature, String superName, String[] interfaces);
public void visitSource(String source, String debug);
public void visitOuterClass(String owner, String name, String desc);
AnnotationVisitor visitAnnotation(String desc, boolean visible);
public void visitAttribute(Attribute attr);
public void visitInnerClass(String name, String outerName,
String innerName, int access);
public FieldVisitor visitField(int access, String name, String desc,
String signature, Object value);
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions);
void visitEnd();
}
ClassVisitor 的調用必須是遵循下面的調用順序的:
visit visitSource? visitOuterClass? ( visitAnnotation | visitAttribute )*
( visitInnerClass | visitField | visitMethod )*
visitEnd
圍繞着ClassVisitor ,還有兩個核心類: 後續的例子代碼中可以看到,我們必須先調用visit方法,這就因爲class是字節流的二進制文件,而我們解析和生成也是要遵循一定的順序。ClassVisitor定義了我們需要操作的所有接口,並且ClassVisitor也可以接收一個ClassVisitor實例來構造,有點類似於一個事件的filter,可以套很多層的filter來一層層處理邏輯。
1、ClassReader 將class解析成byte 數組,然後會通過accept方法去按順序調用綁定對象(繼承了ClassVisitor的實例)的方法。可以視爲一個事件的生產者。
2、ClassWriter 是ClassVisitor 的子類。直接可以通過toByteArray()方法以返回的byte數組形式構建編譯後的class。可以視爲一個事件的消費者。
下面來看下具體這三個Api的應用。
一、解析
因爲Java編譯後的class文件就是由特定格式的字節流組成的二進制文件。這裏不展開說明class文件結構。那麼我們可以通過ASM中的ClassReader 類來解析類的方法,屬性,註解,父類和接口信息,內部類等等。下面這個例子主要示範下打印類的一些屬性、方法信息(javap 工具類似功能)。
Task 類是我們需要解析的類。
package asm.core;
/**
* Created by yunshen.ljy on 2015/6/8.
*/
public class Task {
private int isTask = 0;
private long tell = 0;
public void isTask(){
System.out.println("call isTask");
}
public void tellMe(){
System.out.println("call tellMe");
}
}
ClassPrintVisitor 類繼承自ClassVisitor類來打印解析類的類名,父類名以及“is”開頭的屬性和方法。
package asm.core;
import org.objectweb.asm.*;
/**
* Created by yunshen.ljy on 2015/6/8.
*/
public class ClassPrintVisitor extends ClassVisitor {
public ClassPrintVisitor() {
super(Opcodes.ASM4);
}
public void visit(int version, int access, String name,
String signature, String superName, String[] interfaces) {
System.out.println(name + " extends " + superName + " {");
}
public FieldVisitor visitField(int access, String name, String desc,
String signature, Object value) {
if (name.startsWith("is")) {
System.out.println(" field name: " + name + desc);
}
return null;
}
public MethodVisitor visitMethod(int access, String name,
String desc, String signature, String[] exceptions) {
if (name.startsWith("is")) {
System.out.println(" start with is method: " + name + desc);
}
return null;
}
public void visitEnd() {
System.out.println("}");
}
}
下面是測試類ClassesPrintTest 。將一個ClassPrintVisitor 對象傳給ClassReader。ClassReader作爲一個解析事件的producer 並且由ClassPrintVisitor去消費(處理打印邏輯)。accept()方法就將Task 字節碼進行解析,然後調用ClassPrintVisitor 的方法。
package asm.core;
import org.objectweb.asm.ClassReader;
import java.io.IOException;
/**
* Created by yunshen.ljy on 2015/6/8.
*/
public class ClassesPrintTest {
public static void main(String[] args) {
try {
ClassReader cr = new ClassReader("asm.core.Task");
ClassPrintVisitor cp = new ClassPrintVisitor();
cr.accept(cp, 0);
} catch (IOException e) {
e.printStackTrace();
}
}
}
調用結果:
asm/core/Task extends java/lang/Object {
field name: isTaskI
start with is method: isTask()V
}
這裏我們打印了is開頭的屬性名以及描述,方法名以及描述。這裏的desc其實是class文件屬性修飾或者方法參數、返回值的全限定名(fully qualified name)。這裏isTask後面的I 代表的是int類型的描述。IsTask()方法後面的V表示返回值是void。詳細的屬性、方法描述可以參考《The Java Virtual Machine Specification》
二、生成
生成字節碼,聽起來有點暴力。Java編譯後的class文件是以字節流的形式組成。所以也就是生成一個byte數組,就是之前說的以“特定格式”生成一個可被JVM加載、執行的字節流。下面這個例子是生成一個類ChildClass,繼承自ParentInter 接口,這個是個空接口(沒有具體變量和方法,只是爲了示範代碼)。我們用ASM 的ClassWriter 來實現。先看一下ChildClass 類的代碼:
package asm.core;
import asm.core.ParentInter;
public abstract class ChildClass implements ParentInter {
public static final int zero = 0;
public abstract int compareTo(Object var1);
}
我們通過調用ClassVisitor的方法就能實現生成上述代碼。
private static byte[] gen() {
ClassWriter cw = new ClassWriter(0);
cw.visit(Opcodes.V1_5, Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT ,
"asm/core/ChildClass", null, "java/lang/Object", new String[]{"asm/core/ParentInter"});
cw.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC, "zero", "I", null, new Integer(0))
.visitEnd();
cw.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT, "compareTo", "(Ljava/lang/Object;)I", null, null)
.visitEnd();
cw.visitEnd();
return cw.toByteArray();
}
生成一個能代表class文件的字節數組,我們需要先調用visit 方法生成一個class的頭部信息。這裏的幾個參數稍作解釋:第一個參數Opcode是ASM內部定義的操作數常量,這裏的V1_5代表Java1.5,第二個參數代表類的修飾符,這裏是一個抽象類。第三個是類名。第四個參數本例傳null ,因爲這裏我們沒有一個類型參數。後面兩個是父類和接口。visitField()和visitMethod()方法分別生成我們的屬性和方法,這裏看到都會再調用visitEnd方法,是因爲我們這裏的field後續沒有相應visitAnnotation
、visitAttribute等方法調用,method 後續也沒有其他調用。最後調用cw.visitEnd()結束整個創建過程,toByteArray()返回了我們需要的代表這個類的byte 數組。 這裏我們是new 一個ClassWriter。ClassWriter 繼承自ClassVisitor。前面已經介紹過ClassVisitor。這裏ClassWriter
的toByteArray() 返回的字節數組就能代表一個我們生成的class。
通過這個byte數組我們可以通過ClassLoader來加載我們的類,也可以用FileOutputStream來生成個class文件。