[轉]ASM——運行時/編譯時動態修改class源碼


簡述

最近在看阿里的ARouter的源碼,從git上clone下來之后,run起來發現項目運行的效果和源碼有明顯區別。打個比方,源碼是這樣

boolean b = true;
System.out.println(b);

但是當你跑起來之后去發現打印出來的false,打開編譯好的class文件卻發現編譯出來的class的代碼和源碼不一樣。經過翻看ARouter的工程源碼,發現其實ARouter是利用了Gradle的 Transform API和ASM共同完成的編譯時修改源碼的功能。

Transform API的功能是讓你在java文件編譯成class文件之后對這些class文件進行讀寫,發生在編譯時,是Android的gradle打包插件自帶的功能,這里不詳細展開。本片文章主要是講解ASM的基本使用方法。有機會會出一個Transform + ASM插件教程。

ASM簡介

ASM 是一個 Java 字節碼操控框架。它能夠以二進制形式修改已有類或者動態生成類。ASM 可以直接產生二進制 class 文件,也可以在類被加載入 Java 虛擬機之前動態改變類行為。ASM 從類文件中讀入信息后,能夠改變類行為,分析類信息,甚至能夠根據用戶要求生成新類。許多AOP框架以及動態修改字節碼的庫的底層都是由ASM實現的,例如Spring AOP,cglib等等

一句話概括:ASM可以動態的修改創建class文件,達到動態修改java代碼的效果。

ASM 核心API

public abstract class ClassVisitor {
  // 實現的ASM的API版本。該字段的值必須為如下幾個之一:Opcodes.ASM4,ASM5,ASM6,ASM7
  protected final int api;
  // 該類的方法可以委托給子類
  protected ClassVisitor cv;
  // 構造器 
  public ClassVisitor(final int api) {
    this(api, null);
  }
  // 構造器
  public ClassVisitor(final int api, final ClassVisitor classVisitor) {
    if (api != Opcodes.ASM6 && api != Opcodes.ASM5 && api != Opcodes.ASM4 && api != Opcodes.ASM7) {
      throw new IllegalArgumentException();
    }
    this.api = api;
    this.cv = classVisitor;
  }
  /**
  * 訪問類頭部信息
  *
  * @param version
  *            類版本
  * @param access
  *            類訪問標識符public等
  * @param name
  *            類名稱
  * @param signature
  *            類簽名(非泛型為NUll)
  * @param superName
  *            類的父類
  * @param interfaces
  *            類實現的接口
  */
  public void visit(
      final int version,final int access,
      final String name, final String signature,
      final String superName,final String[] interfaces) {
    if (cv != null) {
      cv.visit(version, access, name, signature, superName, interfaces);
    }
  }
  /**
  * 訪問類的源文件.
  *
  * @param source
  *            源文件名稱
  * @param debug
  *            附加的驗證信息,可以為空
  */
  public void visitSource(final String source, final String debug) {
    if (cv != null) {
      cv.visitSource(source, debug);
    }
  }

  /**
  * 訪問與類對應的模塊. ASM6之后才有的API
  *
  * @param name
  *            模塊名稱
  * @param access
  *            模式 ACC_MANDATED 等
  * @param version
  *            版本號
  */
  public ModuleVisitor visitModule(final String name, final int access, final String version) {
    if (api < Opcodes.ASM6) {
      throw new UnsupportedOperationException("This feature requires ASM6");
    }
    if (cv != null) {
      return cv.visitModule(name, access, version);
    }
    return null;
  }

  public void visitNestHost(final String nestHost) {
    if (api < Opcodes.ASM7) {
      throw new UnsupportedOperationException("This feature requires ASM7");
    }
    if (cv != null) {
      cv.visitNestHost(nestHost);
    }
  }
  /**
  * 這個其實並不是訪問外部類的回調,而是訪問方法體中含有匿名內部類的方法
  *
  * @param owner 為創建匿名類的類,當然其也是一個enclosing class類型的類
  * @param name 創建匿名類的方法。
  * @param desc 創建匿名類的方法描述信息。
  * @return 返回一個注解值訪問器
  */
  public void visitOuterClass(final String owner, final String name, final String descriptor) {
    if (cv != null) {
      cv.visitOuterClass(owner, name, descriptor);
    }
  }
  /**
  * 訪問類的注解
  *
  * @param desc
  *            注解類的類描述
  * @param visible
  *            runtime時期注解是否可以被訪問
  * @return 返回一個注解值訪問器
  */
  public AnnotationVisitor visitAnnotation(final String descriptor, final boolean visible) {
    if (cv != null) {
      return cv.visitAnnotation(descriptor, visible);
    }
    return null;
  }
  /**
  * 訪問標注在類型上的注解
  *
  * @param typeRef
  * @param typePath
  * @param desc
  * @param visible
  * @return
  */
  public AnnotationVisitor visitTypeAnnotation(
      final int typeRef, final TypePath typePath, final String descriptor, final boolean visible) {
    if (api < Opcodes.ASM5) {
      throw new UnsupportedOperationException("This feature requires ASM5");
    }
    if (cv != null) {
      return cv.visitTypeAnnotation(typeRef, typePath, descriptor, visible);
    }
    return null;
  }
  /**
  * 訪問一個類的屬性
  *
  * @param attribute
  *            類的屬性
  */
  public void visitAttribute(final Attribute attribute) {
    if (cv != null) {
      cv.visitAttribute(attribute);
    }
  }

  public void visitNestMember(final String nestMember) {
    if (api < Opcodes.ASM7) {
      throw new UnsupportedOperationException("This feature requires ASM7");
    }
    if (cv != null) {
      cv.visitNestMember(nestMember);
    }
  }
  /**
  * 訪問內部類信息
  * @param name
  * @param outerName
  * @param innerName
  * @param access
  */
  public void visitInnerClass(
      final String name, final String outerName, final String innerName, final int access) {
    if (cv != null) {
      cv.visitInnerClass(name, outerName, innerName, access);
    }
  }
  /**
  * 訪問類的字段
  * @param access
  * @param name
  * @param desc
  * @param signature
  * @param value
  * @return
  */
  public FieldVisitor visitField(
      final int access,
      final String name,
      final String descriptor,
      final String signature,
      final Object value) {
    if (cv != null) {
      return cv.visitField(access, name, descriptor, signature, value);
    }
    return null;
  }
  /**
  * 訪問類的方法
  * @param access
  * @param name
  * @param desc
  * @param signature
  * @param exceptions
  * @return
  */
  public MethodVisitor visitMethod(
      final int access,
      final String name,
      final String descriptor,
      final String signature,
      final String[] exceptions) {
    if (cv != null) {
      return cv.visitMethod(access, name, descriptor, signature, exceptions);
    }
    return null;
  }

 /**
  * 訪問類結束
  */
  public void visitEnd() {
    if (cv != null) {
      cv.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。可以視為一個事件的消費者。

但是需要注意的是雖然ClassReader和ClassWriter 看起來像是對稱類例如InputStream和OutputStream但其實類結構上並無關聯,ClassWriter 繼承於ClassVisitor,而ClassReader 直接繼承於Object,只是提供解析class,並依次調用ClassVisitor對象。也就是說ClassReader的api和ClassWriter 的api基本沒有相關性。

另外補充一下,ASM中常見參數desc直譯是描述,但是作用其實時限定方法的輸入參數和返回參數類型,比如"()V"是無輸入無輸出,"(I)Ljava/lang/String;"是輸入int,返回String。

無中生有 ——利用ASM動態創建一個類

由於是憑空創建,所以只需要ClassWriter 即可。

先上目標代碼,我們的目的是創造一個下面的類

public class Student{
    public int age = 11;

    public int getAge() {
        return age;
    }
}

一個特別簡單的javabean類。

簡單說一下創建流程:

  1. 創建一個類需要先調用visit創建類的頭部信息。
  2. 分別調用visitMethod或visitField生成需要的創建的方法或者字段。
  3. 調用visitEnd結束類的創建
  4. 調用ClassWriter 的toByteArray將動態生成的class轉為byte[]數組,可以用ClassLoader動態載入,或者寫出成.class文件
    完整代碼:

public byte[] createNewClass() {
        //創建ClassWriter ,構造參數的含義是是否自動計算棧幀,操作數棧及局部變量表的大小
        //0:完全手動計算 即手動調用visitFrame和visitMaxs完全生效
        //ClassWriter.COMPUTE_MAXS=1:需要自己計算棧幀大小,但本地變量與操作數已自動計算好,當然也可以調用visitMaxs方法,只不過不起作用,參數會被忽略;
        //ClassWriter.COMPUTE_FRAMES=2:棧幀本地變量和操作數棧都自動計算,不需要調用visitFrame和visitMaxs方法,即使調用也會被忽略。
        //這些選項非常方便,但會有一定的開銷,使用COMPUTE_MAXS會慢10%,使用COMPUTE_FRAMES會慢2倍。
        ClassWriter cw = new ClassWriter(0);
        //創建類頭部信息:jdk版本,修飾符,類全名,簽名信息,父類,接口集
        cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "asm/Student", null, "java/lang/Object", null);
        //創建字段age:修飾符,變量名,類型,簽名信息,初始值(不一定會起作用后面會說明)
        cw.visitField(Opcodes.ACC_PUBLIC , "age", "I", null, new Integer(11))
                .visitEnd();
        //創建方法:修飾符,方法名,類型,描述(輸入輸出類型),簽名信息,拋出異常集合
        // 方法的邏輯全部使用jvm指令來書寫的比較晦澀,門檻較高,后面會介紹簡單的方法
        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "getAge", "()I", null, null);
        // 創建方法第一步
        mv.visitCode();
        // 將索引為 #0 的本地變量列表加到操作數棧下。#0 索引的本地變量列表永遠是 this ,當前類實例的引用。
        mv.visitVarInsn(ALOAD, 0);
        // 獲取變量的值,
        mv.visitFieldInsn(GETFIELD, "asm/Student", "age", "I");
        // 返回age
        mv.visitInsn(IRETURN);
        // 設置操作數棧和本地變量表的大小
        mv.visitMaxs(1, 1);
        //結束方法生成
        mv.visitEnd();
        //結束類生成
        cw.visitEnd();
        //返回class的byte[]數組
        return cw.toByteArray();
    }

通過以上代碼可以看出其實類以及字段的創建還是比較簡單的,難點在於方法的創建上。如果對於jvm指令集不熟悉基本抓瞎。這里介紹一個方法,先手寫目標類,即Student的java文件,然后用javac編譯成class文件(或者用IDE編譯),找到編譯好的class文件,用javap -c Student打開class文件。輸出如下:

public class asm.Student {
  public int age;

  public asm.Student();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: bipush        10
       7: putfield      #2                  // Field age:I
      10: return

  public int getAge();
    Code:
       0: aload_0
       1: getfield      #2                  // Field age:I
       4: ireturn
}

可以看到jvm編譯時幫助Student補全了構造方法Student(),着重看getAge的指令代碼

       0: aload_0
       1: getfield      #2                  // Field age:I
       4: ireturn

一共三條正好和生成方法的代碼對應上

        mv.visitVarInsn(ALOAD, 0);
        mv.visitFieldInsn(GETFIELD, "asm/ASMDemo", "age", "I");
        mv.visitInsn(IRETURN);

當沒有思路時,可以用這參考這種辦法。
好了現在已經生成了新的class的byte[],剩下的就是加載,驗證了。
加載的代碼:

/**
  *用來加載byte[],由於defineClass不是public修飾的所以只能這樣寫。
  */
public class MyClassLoader extends ClassLoader {
  public Class getClassByBytes(byte[] bytes) {
        return defineClass(null, bytes, 0, bytes.length);
    }

  public static void main(String[] args) throws Exception {
        MyClassLoader myClassLoader = new MyClassLoader();
        Class classByBytes = myClassLoader.getClassByBytes(create());
        Object o = classByBytes.newInstance();
        Field field = classByBytes.getField("age");
        Object o1 = field.get(o);
        Method method = classByBytes.getMethod("getAge");
        Object o2 = method.invoke(o);
        System.out.println("Field age:  " + o1 );
        System.out.println("Method method :  " + o2);
    }
}

點擊運行,然后你就會發現——華麗麗的報錯了

Exception in thread "main" java.lang.InstantiationException: asm.CreateTest
    at java.lang.Class.newInstance(Class.java:427)
    at asm.ASMTest.main(ASMTest.java:20)
Caused by: java.lang.NoSuchMethodException: asm.CreateTest.<init>()
    at java.lang.Class.getConstructor0(Class.java:3082)
    at java.lang.Class.newInstance(Class.java:412)
    ... 1 more

asm.CreateTest.<init>()這個方法沒有找到,熟悉jvm的可能會知道其實<init>就是構造函數,構造函數在jvm中會被重新命名成<init>。但是我們手寫的java文件時也沒有寫構造函數,為什么就可以呢?翻到上面貼出的用javac編譯出的Student文件,可以看到編譯時編譯器自動幫我們加好了構造函數。然后再把咱們自己生成的class文件的byte[]通過輸出流寫成class文件,在通過javap -c 查看:

 public class asm.Student{
  public int zero;

  public int getZero();
    Code:
       0: aload_0
       1: getfield      #11                 // Field zero:I
       4: ireturn
}

果然利用ASM生成的class里的確沒有構造方法。ASM還是要比編譯器懶一些的,哈。既然沒有,咱們加上就行了。
先參考一下上面由java編譯成的class文件,其實構造函數的代碼:

  public asm.Student();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: bipush        10
       7: putfield      #2                  // Field age:I
      10: return

簡單翻譯一下這6條指令:

  1. this變量入棧
  2. 執行父類的<init>方法
  3. this再次入棧
  4. byte變量10入棧
  5. 給對象字段age賦值
  6. 方法結束

如此可以看到其實構造函數最核心的指令就會調用父類的<init>方法(暫時不考慮字段賦值的事情)。現在基本能夠確定,我們手寫的構造函數必須包含這三條指令

aload_0
invokespecial
return

然后和上面生成getAge方法類似的生成一個<init>方法即可,代碼如下:

        mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
        mv.visitCode();
        // aload_0
        mv.visitVarInsn(ALOAD, 0);
        // 獲取變量的值,
        mv.visitMethodInsn(INVOKESPECIAL,"java/lang/Object", "<init>", "()V", false);
        // 結束
        mv.visitInsn(IRETURN);
        // 設置操作數棧和本地變量表的大小
        mv.visitMaxs(1, 1);
        //結束方法生成
        mv.visitEnd();

然后再次運行,發現已經可以正常運行,輸出如下

Field age:  0
Method method :  0

說好的11呢???哈,其實通過查看java文件編譯后的class就能發現全局變量的默認值賦值其實是在構造函數中進行的,也就是說我們通過ASM創建字段時設置的默認值沒起效果,WTF!再次修改<init>方法(類似getAge,不在添加詳細注釋)

mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv.visitVarInsn(ALOAD, 0);
mv.visitIntInsn(BIPUSH, 10);
mv.visitFieldInsn(PUTFIELD, "asm/Student", "age", "I");
mv.visitInsn(RETURN);
mv.visitMaxs(2, 1);
mv.visitEnd();

再次運行

Field age:  10
Method method :  10

哈,完美運行。那么可能有同學會問了,那么設置字段默認值卵用沒有,為什么還有這個參數呢,其實也並不是一點用沒有,當生成的字段時static時,就會起作用。這里邊又會涉及到類的靜態變量加載時機,<cinit>函數等等,這里就不展開細講了,否則篇幅該hold不住了。總結起來一句話:ASM只是工具,掌握jvm知識才是硬道理。

偷梁換柱——ASM修改已有的class

其實除了動態生成class,還有一大部分需求是修改class,這里簡單介紹下最復雜的修改class的Method。其他的修改照葫蘆畫瓢就可以。
先上目標效果,首先原始類還用咱們的Student:

public class Student{
    public int age = 11;

    public int getAge() {
        return age;
    }
}

目標是在getAge里邊插入一句打印語句,即:

    public class Student{
        public int age = 11;

        public int getAge() {
            System.out.println("getAge");
            return age;
        }
    }

思路如下:

  1. 首先自定義一個ClassVisitor,重寫visitMethod,這樣就可以收到每個方法的回調
  2. 判斷方法名稱是不是getAge
  3. 如果是返回一個自定義的MethodVisitor
  4. 自定義的MethodVisitor重寫visitCode(訪問方法的第一個步驟)
  5. 添加相應的邏輯
  6. 通過重寫visitMaxs修改操作數棧和局部變量表的大小(添加了邏輯可能會導致操作數棧和局部變量表的最大值增大)

代碼如下

public class MyMethodVisitor extends MethodVisitor {
    public MyMethodVisitor(MethodVisitor mv) {
        super(ASM5, mv);
    }


    @Override
    public void visitCode() {
        mv.visitCode();
        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv.visitLdcInsn("getAge");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
    }

    @Override
    public void visitMaxs(int maxStack, int maxLocals) {
        super.visitMaxs(maxStack+1, maxLocals);
    }
}

public class MyClassVisitor extends ClassVisitor {
    public MyClassVisitor(ClassVisitor cv) {
        super(ASM5, cv);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions);
        if(name.equals("getAge")){
            return new MyMethodVisitor(methodVisitor);
        }else {
            return methodVisitor;
        }
    }
}

//修改測試代碼
public static void main(String[] args) throws Exception {
        ClassReader classReader = new ClassReader(createNewClass());
        ClassWriter classWriter = new ClassWriter(classReader, 0);
        ClassVisitor cv = new MyClassVisitor(classWriter);
        classReader.accept(cv,0);
        MyClassLoader myClassLoader = new MyClassLoader();
        Class classByBytes = myClassLoader.getClassByBytes(classWriter.toByteArray());
        Object o = classByBytes.newInstance();
        Field field = classByBytes.getField("age");
        Object o1 = field.get(o);
        Method method = classByBytes.getMethod("getAge");
        Object o2 = method.invoke(o);
        System.out.println("Field age:  " + o1);
        System.out.println("Method method :  " + o2);
    }

運行:

getAge
Field age:  10
Method method :  10

注入的邏輯完美運行!修改方法邏輯不僅僅可以在方法開始插入邏輯,包括方法結束時,甚至方法體中間都可以,可以利用這種思路很方便的寫出一個AOP框架。

ASMifier

ASM由於是基於jvm指令集的所以比較晦澀。官方可能是考慮到大家都是比較菜的,提供了很多的工具類,這里只介紹一種我認為最有用的:ASMifier。ASMifier最大的功能就是將一個java文件翻譯成ASM生成此文件的代碼。

ASMifier.main(new String[]{"asm.Student"});

運行后,就可以在控制台看見如何利用ASM生成Student類了,省了很大力氣。工具類很多就不一 一介紹了,推薦一個博客有興趣可以去看看:

https://blog.csdn.net/ljz2016/article/details/81363828

總結

ASM相對於一些其他的操作字節碼的框架偏底層了一些,只提供了一些低級api,要想熟練使用還是需要比較高的jvm知識的。但是作為其他操作字節碼的框架的底層實現,還是非常有必要了解一下的。真實項目中如果對性能要求不是特別高的話,結合項目需求完全可以用其他高級庫代替ASM,例如cglib javassist。
突然想起來前兩年做過的一個需求:拿到一個類序列化之后的文件,然后在本地沒有這個類的情況下反序列化它。
當時覺得這個需求真是扯淡,現在想想做反序列化時報出ClassNotFound這個錯誤之前,其實已經可以獲取類的包名,類名,簽名,以及字段詳情了。其實完全可以重寫反序列化方法,然后獲取到類的信息后動態生成class文件,然后再加載到內存中,之后再做正常的反序列化操作。兩年前的需求現在想出了解決方案,哈!

代碼地址:

https://github.com/fengao1004/ASM.git

參考:

https://blog.csdn.net/lijingyao8206/article/category/3276863

作者:FENGAO
鏈接:https://www.jianshu.com/p/a536e09181af
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

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



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