本文轉載自ASM的基礎用法
導語
新聞里使用的熱補丁修復方案是基於AspectJ,AspectJ是AOP的一種實現。
無意接觸到一種小巧輕便的Java字節碼操控框架ASM,它也能方便地生成和改造Java代碼。
本文主要分為幾個部分:
- 什么是ASM;
- 為什么要動態生成Java類;
- 為什么選擇ASM;
- ASM中的核心類和核心方法;
- ASM示例;
什么是ASM?
ASM是一個Java字節碼操控框架。它能被用來動態生成類或者增強既有類的功能。ASM可以直接產生二進制 class文件,也可以在類被加載入Java虛擬機之前動態改變類行為。
如果想了解Java虛擬機的工作過程可參考JVM原理淺析
為什么要動態生成Java類?
舉個例子,目前有一個既有的銀行管理系統,包括Bank、Customer、Account、Invoice等對象,現在要加入一個安全檢查模塊,對已有類的所有操作之前都必須進行一次安全檢查。
然而 Bank、Customer、Account、Invoice 是代表不同的事務,派生自不同的父類,很難在高層上加入關於Security Checker的共有功能。對於沒有多繼承的Java來說,更是如此。
傳統解決方案是使用裝飾器模式,裝飾器模式動態的將責任鏈附加到對象上,若要擴展功能,裝飾者提供了比繼承更加富有彈性的代替方案。
裝飾器模式可以在一定程度上改善耦合,而功能仍舊是分散的,每個需要Security Checker的類都必須派生一個Decorator,每個需要Security Checker的方法都要被包裝(wrap)。
下面我們以 Account類為例看一下Decorator:
首先,有一個SecurityChecker類,其靜態方法checkSecurity執行安全檢查功能:
public class SecurityChecker {
public static void checkSecurity() {
System.out.println("SecurityChecker.checkSecurity ...");
}
}
另一個是Account類:
public class Account {
public void operation() {
System.out.println("operation...");
}
}
若想對operation加入對SecurityCheck.checkSecurity()調用,標准的Decorator需要先定義一個 Account類的接口:
public interface IAccount {
void operation();
}
然后定義一個實現類:
public class AccountImpl implements IAccount {
@Override
public void operation() {
System.out.println("operation...");
}
}
定義一個AccountImpl類的Decorator,並包裝operation方法:
public class AccountWithSecurityCheck implements IAccount {
private IAccount account;
public AccountWithSecurityCheck(IAccount IAccount) {
this.account = IAccount;
}
public void operation() {
SecurityChecker.checkSecurity();
account.operation();
}
}
最后的調用方式為:
public class Test {
public static void main(String[] args) throws Exception {
// 1.使用包裝類
AccountWithSecurityCheck account = new AccountWithSecurityCheck(new AccountImpl());
account.operation();
}
}
在這個簡單的例子里,改造一個類的一個方法還好,如果是變動整個模塊,Decorator很快就會演化成另一個噩夢。動態改變Java類就是要解決AOP的問題,提供一種得到系統支持的可編程的方法,自動化地生成或者增強Java代碼。
為什么選擇ASM?
最直接的改造Java類的方法莫過於直接改寫class文件。Java 規范詳細說明了class文件的格式,直接編輯字節碼確實可以改變Java類的行為。
還有一種比較理想且流行的方式是是使用java.lang.reflect.Proxy。我們仍舊使用以上的例子,給Account類加上checkSecurity功能。
首先,Proxy編程是面向接口的,Proxy並不負責實例化對象,和Decorator模式一樣,要把Account定義成一個接口,然后在AccountImpl里實現Account接口,接着實現一個InvocationHandlerAccount方法被調用的時候,虛擬機都會實際調用這個InvocationHandler的invoke方法:
public class SecurityProxyInvocationHandler implements InvocationHandler {
private Object proxyedObject;
public SecurityProxyInvocationHandler(Object o) {
proxyedObject = o;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (proxy instanceof IAccount && "operation".equals(method.getName())) {
SecurityChecker.checkSecurity();
}
return method.invoke(proxyedObject, args);
}
}
最后,在應用程序中指定InvocationHandler生成代理對象:
public class Test {
public static void main(String[] args) throws Exception {
// 2.使用代理
IAccount account = (IAccount) Proxy.newProxyInstance(
IAccount.class.getClassLoader(),
new Class[]{IAccount.class},
new SecurityProxyInvocationHandler(new AccountImpl()));
account.operation();
}
}
其不足之處在於:
- Proxy是面向接口的,所有使用Proxy的對象都必須定義一個接口,而且用這些對象的代碼也必須是對接口編程的:Proxy生成的對象是接口一致的而不是對象一致的:例子中Proxy.newProxyInstance生成的是實現IAccount接口的對象而不是AccountImpl的子類。這對於軟件架構設計,尤其對於既有軟件系統是有一定掣肘的。
- Proxy畢竟是通過反射實現的,必須在效率上付出代價:有實驗數據表明,調用反射比一般的函數開銷至少要大10倍。而且,從程序實現上可以看出,對proxy class的所有方法調用都要通過使用反射的invoke方法。因此,對於性能關鍵的應用,使用proxy class是需要精心考慮的,以避免反射成為整個應用的瓶頸。
ASM能夠通過改造既有類,直接生成需要的代碼。增強的代碼是硬編碼在新生成的類文件內部的,沒有反射帶來性能上的付出。同時,ASM與Proxy編程不同,不需要為增強代碼而新定義一個接口,生成的代碼可以覆蓋原來的類,或者是原始類的子類。它是一個普通的Java類而不是Proxy類,甚至可以在應用程序的類框架中擁有自己的位置,派生自己的子類。
ASM使用
使用javap -c命令查看Account類的字節碼
5-9行表示的是一個的默認構造方法,是編譯器為我們自動添加的。參考來自深入字節碼 – 使用 ASM 實現 AOP
11-結束表示我們編寫的operation方法。
aload_0:這個指令是LOAD系列指令中的一個,它的意思表示裝載當前第0個元素到堆棧中。代碼上相當於“this”。
invokespecial:這個指令是調用系列指令中的一個。其目的是調用對象類的方法。后面需要給上父類的方法完整簽名。“#1”的意思是class文件常量表中第1個元素。值為:“java/lang/Object.””😦)V”。結合aload_0。這兩個指令可以翻譯為:“super()”。其含義是調用自己的父類構造方法。
getstatic:這個指令是GET系列指令中的一個,其作用是獲取靜態字段內容到堆棧中。
ldc:從常量表中裝載一個數據到堆棧中。
invokevirtual:也是一種調用指令,這個指令區別與invokespecial的是它是根據引用調用對象類的方法。
return:也是一系列指令中的一個,其目的是方法調用完畢返回:可用的其他指令有:IRETURN,DRETURN,ARETURN等,用於表示不同類型參數的返回。
invokespecial和invokevirtual的主要區別在於: invokespecial通常根據引用的類型選擇方法,而不是根據對象的類來選擇。
IntelliJ中ASM的插件
ASM Bytecode Outline
使用方式:
- 生成對應的.class文件;
- Code-Show Bytecode Outline
我們寫一個代碼示例:
public class AccountASM {
public void operation() {
SecurityChecker.checkSecurity();
System.out.println("operation...");
}
}
通過上述方式查看ASM代碼
紅框的代碼是用ASM輸出整個operation方法字節碼:
第36行:表示准備輸出一個公有方法“operation”,ACC_PUBLIC表示公有,相當於public修飾符;“()V”是方法的參數包括返回值簽名,“V”是void的縮寫,表示無返回值;后面兩個null分別是方法的異常拋出信息和屬性信息。
第37行:表示開始正式輸出方法的執行代碼。
第41行:表示調用靜態方法,這行代碼相當於“SecurityChecker.checkSecurity();”。
上面的38,39,40,42,43,44,48,49,50行看到的內容表示Java代碼的行號標記,可以刪除不用。
在方法的最后部分代碼52,53,54行表示向class文件中寫入方法本地變量表的名稱以及類型,可以刪除不用。
所以精簡的代碼如下:
{
mv = cw.visitMethod(ACC_PUBLIC, "operation", "()V", null, null);
mv.visitCode();
mv.visitMethodInsn(INVOKESTATIC, "com/xiongcen/asm/SecurityChecker", "checkSecurity", "()V", false);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("operation...");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(2, 1);
mv.visitEnd();
}
01行:相當於public void operation()方法聲明;
02行:正式開始方法內容的填充;
03行:調用靜態方法,相當於“SecurityChecker.checkSecurity();”;
04行:取得一個靜態字段將其放入堆棧,相當於“System.out”。“Ljava/io/PrintStream;”是字段類型的描述,翻譯過來相當於:“java.io.PrintStream”類型。在字節碼中凡是引用類型均由“L”開頭“;”結束表示,中間是類型的完整名稱;
05行:將字符串“operation…”放入堆棧,此時對戰中第一個元素是“System.out”,第二個元素是”operation…”
06行:調用PrintStream類型的“println”方法。簽名“(Ljava/lang/String;)V”表示方法需要一個字符串類型的參數,並且無返回值。
07行:是JVM在編譯時為方法自動加上的“return”指令。該指令必須在方法結束時執行不可缺少。
08行:表示在執行這個方法期間方法的堆棧空間最大給予多少。
09行:表示方法輸出結束。
ASM框架中的核心類
ClassVisitor接口:定義在讀取Class字節碼時會觸發的事件,如類頭解析完成、注解解析、字段解析、方法解析等。每當有事件發生時,調用注冊的ClassVisitor、AnnotationVisitor、FieldVisitor、MethodVisitor做相應的處理。
AnnotationVisitor接口:定義在解析注解時會觸發的事件,如解析到一個基本值類型的注解、enum值類型的注解、Array值類型的注解、注解值類型的注解等。
FieldVisitor接口:定義在解析字段時觸發的事件,如解析到字段上的注解、解析到字段相關的屬性等。
MethodVisitor接口:定義在解析方法時觸發的事件,如方法上的注解、屬性、代碼等。
ClassVisitor接口文檔說明
對接口中方法的調用必須遵守以下規則:
visit-visitSource方法(一次)-visitOuterClass(一次)-visitAnnotation|visitAttribute(任意)-visitInnerClass | visitField | visitMethod(任意)-visitEnd.
ClassVisitor的關鍵方法
參數含義請參考深入字節碼 – ASM 關鍵接口 ClassVisitor
1.visit(int version, int access, String name, String signature, String superName, String[] interfaces),該方法是當掃描類時第一個調用的方法,主要用於類的聲明。
visit(類版本,修飾符,類名,泛型信息,繼承的父類,實現的接口)
2.visitAnnotation(String desc, boolean visible),該方法是當掃描器掃描到類注解聲明時進行調用。
visitAnnotation(注解類型,注解是否可以在JVM中可見)
3.visitField(int access, String name, String desc, String signature, Object value),該方法是當掃描器掃描到類中字段時進行調用。
visitField(修飾符,字段名,字段類型,泛型描述,默認值)
4.visitMethod(int access, String name, String desc, String signature, String[] exceptions),該方法是當掃描器掃描到類的方法時進行調用。
visitMethod(修飾符,方法名,方法簽名,泛型信息,拋出的異常)。方法簽名的格式如下:“(參數列表)返回值類型”。參考簽名ASM 操作字節碼初探
5.visitEnd(),該方法是當掃描器完成類掃描時才會調用。
ASM的三大組件
ClassReader類:該類用來解析編譯過的class字節碼文件。
ClassWriter類:該類用來重新構建編譯后的類,比如說修改類名、屬性以及方法,甚至可以生成新的類的字節碼文件。
ClassAdapter類:實現了ClassVisitor接口所定義的所有函數,當新建一個ClassAdapter對象的時候,需要傳入一個實現了ClassVisitor接口的對象,作為職責鏈中的下一個訪問者(Visitor),這些函數的默認實現就是簡單的把調用委派給這個對象,然后依次傳遞下去形成職責鏈。
使用ASM增強既有類的功能
我們還是用上面的例子,給Account類加上Security check 的功能。與Proxy編程不同,ASM不需要將 Account聲明成接口,Account可以仍舊是一個實現類。ASM將直接在Account類上動手術,給Account類的operation方法首部加上對SecurityChecker.checkSecurity的調用。
首先,我們將從ClassAdapter繼承一個類。ClassAdapter是ASM框架提供的一個默認類,負責溝通 ClassReader和ClassWriter。如果想要改變ClassReader處讀入的類,然后從ClassWriter處輸出,可以重寫相應的ClassAdapter函數。這里,為了改變Account類的operation方法,我們將重寫visitMethdod方法。
public class AddSecurityCheckClassAdapter extends ClassAdapter {
public AddSecurityCheckClassAdapter(ClassVisitor cv) {
super(cv);
}
// 重寫 visitMethod,訪問到 "operation" 方法時,給出自定義 MethodVisitor,實際改寫方法內容
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
System.out.println("AddSecurityCheckClassAdapter;" + "name:" + name + ";desc:" + desc + ";signature:" + signature);
MethodVisitor wrappedMv = mv;
if (mv != null) {
if ("operation".equals(name)) {
// 使用自定義的MethodVisitor,實際改寫方法內容
wrappedMv = new AddSecurityCheckMethodAdapter(mv);
}
}
return wrappedMv;
}
}
下一步就是定義一個繼承自MethodAdapter的AddSecurityCheckMethodAdapter,在“operation”方法首部插入對SecurityChecker.checkSecurity()的調用。
public class AddSecurityCheckMethodAdapter extends MethodAdapter {
public AddSecurityCheckMethodAdapter(MethodVisitor mv) {
super(mv);
}
@Override
public void visitCode() {
// ClassReader讀到每個方法的首部時調用 visitCode(),在這個重寫方法里,
// 我們用 visitMethodInsn(Opcodes.INVOKESTATIC, "com/xiongcen/asm/SecurityChecker","checkSecurity", "()V");插入了安全檢查功能。
visitMethodInsn(Opcodes.INVOKESTATIC, "com/xiongcen/asm/SecurityChecker",
"checkSecurity", "()V");
}
}
最后,我們將集成上面定義的ClassAdapter,ClassReader和ClassWriter產生修改后的Account類文件 :
public static void main(String[] args) throws Exception {
// 3.使用ASM
// 使用 ClassReader 去讀取 Account 類的字節碼信息。
ClassReader cr = new ClassReader("com.xiongcen.asm.Account");
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassAdapter classAdapter = new AddSecurityCheckClassAdapter(cw);
// 通過accept方法掃描整個字節碼,SKIP_DEBUG選項的意義是在掃描過程中掠過所有有關行號方面的內容
cr.accept(classAdapter, ClassReader.SKIP_DEBUG);
byte[] data = cw.toByteArray();
File file = new File("/Users/xiongcen/Documents/IdeaProject/JniProject/out/production/JniProject/com/xiongcen/asm/Account.class");
FileOutputStream fout = new FileOutputStream(file);
fout.write(data);
fout.close();
Account account = new Account();
account.operation();
}
使用這個 Account,我們會得到下面的輸出:
SecurityChecker.checkSecurity ...
operation...
源碼可參考ASMDemo




