ASM的基礎用法


本文轉載自ASM的基礎用法

導語

新聞里使用的熱補丁修復方案是基於AspectJ,AspectJ是AOP的一種實現。

無意接觸到一種小巧輕便的Java字節碼操控框架ASM,它也能方便地生成和改造Java代碼。

本文主要分為幾個部分:

  1. 什么是ASM;
  2. 為什么要動態生成Java類;
  3. 為什么選擇ASM;
  4. ASM中的核心類和核心方法;
  5. ASM示例;

什么是ASM?

ASM是一個Java字節碼操控框架。它能被用來動態生成類或者增強既有類的功能。ASM可以直接產生二進制 class文件,也可以在類被加載入Java虛擬機之前動態改變類行為。

如果想了解Java虛擬機的工作過程可參考JVM原理淺析

為什么要動態生成Java類?

舉個例子,目前有一個既有的銀行管理系統,包括Bank、Customer、Account、Invoice等對象,現在要加入一個安全檢查模塊,對已有類的所有操作之前都必須進行一次安全檢查。

img

然而 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類的字節碼

img

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

使用方式:

  1. 生成對應的.class文件;
  2. Code-Show Bytecode Outline

我們寫一個代碼示例:

public class AccountASM {

    public void operation() {
        SecurityChecker.checkSecurity();
        System.out.println("operation...");
    }
}

通過上述方式查看ASM代碼

img

紅框的代碼是用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接口文檔說明

img

對接口中方法的調用必須遵守以下規則:

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

參考

  1. AOP 的利器:ASM 3.0 介紹
  2. 深入字節碼 – 使用 ASM 實現 AOP
  3. 深入字節碼 – ASM 關鍵接口 ClassVisitor
  4. 深入字節碼 – ASM 關鍵接口 MethodVisitor
  5. ASM 操作字節碼初探
  6. ASM3.0使用指南(中文)
  7. ASM4.0使用指南(英文)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM