AOP系列01:利用ASM動(dòng)態(tài)創(chuàng)建Class

背景

最近在調(diào)研在Android中運(yùn)用AOP,發(fā)現(xiàn)主要有這幾種技術(shù)方案:


  1. APT:可以在編譯期幫我們生成Java文件(需要手動(dòng)拼接代碼,或使用Javapoet),但無法修改已有Java文件,應(yīng)用案例:ButterKnife、Dragger2、EventBus3、DataBinding、AndroidAnnotation,主要就是一些DI框架
  2. AspectJ:可以修改Java文件,功能強(qiáng)大,最常見的AOP庫,但需要學(xué)習(xí)AspectJ語法,在Android端集成稍復(fù)雜,應(yīng)用案例:Hugo、AspectJx
  3. Javassist:可以修改Class字節(jié)碼,通過反射在編譯時(shí)加入邏輯,性能較低,應(yīng)用案例:HotFix
  4. ASM:可以修改Class字節(jié)碼,在編譯時(shí)插入邏輯,性能好,有ASM Btyecode Outline插件支持生成ASM代碼,應(yīng)用案例:各種JVM上的語言,比如Kotlin
  5. ASMDEX、DexMaker:也是靜態(tài)織入代碼,學(xué)習(xí)成本太高
  6. cglib:運(yùn)行時(shí)織入代碼,作用于class字節(jié)碼,常用的動(dòng)態(tài)代理庫,比JVM自帶的動(dòng)態(tài)代理更靈活,但不適用于Android,因?yàn)锳ndroid運(yùn)行時(shí)是dex文件,不是class文件
  7. xposed、dexposed、epic:運(yùn)行時(shí)hook,有兼容性問題,只適合調(diào)試時(shí)玩玩,不適合生產(chǎn)環(huán)境

也有一些基于上面這些技術(shù)方案的工具庫,比如Hunter、lancet、X-AOP,但集成到自己的項(xiàng)目中,還是有各種問題。

最開始考慮使用滬江的AspectJx,但是運(yùn)行時(shí)一直報(bào)找不到Application的錯(cuò)誤,無法運(yùn)行起來,只好放棄,其實(shí)這應(yīng)該是Android端最簡單的AOP方案了,雖然需要學(xué)一點(diǎn)AspectJ的語法,但是并不算太難,無奈集成失敗,只好放棄。

目前考慮利用ASM+自定義gradle插件,在編譯時(shí),通過gradle的Transformer修改Class文件,織入AOP的代碼,使用起來會(huì)比用AspectJ麻煩一點(diǎn),但好在有插件寫ASM Code,比起用Javassist手動(dòng)拼接Java代碼,還是好一點(diǎn)。

開始寫一個(gè)ASM的Demo

創(chuàng)建的類的原型

package com.ezbuy.asmdemo;

/**
 * author : yutianran
 * time   : 2019/01/15
 * desc   :
 * version: 1.0
 */
public class Person {

    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void say(String desc) {
        System.out.println(String.format("Hello,%s", desc));
    }

    public String getInfo() {
        return "name=" + name + ",age=" + age;
    }
}

根據(jù)原型,利用插件自動(dòng)生成的ASM代碼

先安裝ASM Btyecode Outline插件,重啟Android Studio后,右鍵原型文件,點(diǎn)擊show Bytecode outline,


然后等它編譯完成,就會(huì)出現(xiàn)我們想要的ASM的代碼


代碼如下:

package com.ezbuy.asmdemo;

import java.util.*;

import org.objectweb.asm.*;

public class PersonDump implements Opcodes {

    public static byte[] dump() throws Exception {

        ClassWriter cw = new ClassWriter(0);
        FieldVisitor fv;
        MethodVisitor mv;
        AnnotationVisitor av0;

        cw.visit(V1_7, ACC_PUBLIC + ACC_SUPER, "com/ezbuy/asmdemo/Person", null, "java/lang/Object", null);

        cw.visitSource("Person.java", null);

        {
            fv = cw.visitField(ACC_PRIVATE, "name", "Ljava/lang/String;", null, null);
            fv.visitEnd();
        }
        {
            fv = cw.visitField(ACC_PRIVATE, "age", "I", null, null);
            fv.visitEnd();
        }
        {
            mv = cw.visitMethod(ACC_PUBLIC, "<init>", "(Ljava/lang/String;I)V", null, null);
            mv.visitCode();
            Label l0 = new Label();
            mv.visitLabel(l0);
            mv.visitLineNumber(14, l0);
            mv.visitVarInsn(ALOAD, 0);
            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
            Label l1 = new Label();
            mv.visitLabel(l1);
            mv.visitLineNumber(15, l1);
            mv.visitVarInsn(ALOAD, 0);
            mv.visitVarInsn(ALOAD, 1);
            mv.visitFieldInsn(PUTFIELD, "com/ezbuy/asmdemo/Person", "name", "Ljava/lang/String;");
            Label l2 = new Label();
            mv.visitLabel(l2);
            mv.visitLineNumber(16, l2);
            mv.visitVarInsn(ALOAD, 0);
            mv.visitVarInsn(ILOAD, 2);
            mv.visitFieldInsn(PUTFIELD, "com/ezbuy/asmdemo/Person", "age", "I");
            Label l3 = new Label();
            mv.visitLabel(l3);
            mv.visitLineNumber(17, l3);
            mv.visitInsn(RETURN);
            Label l4 = new Label();
            mv.visitLabel(l4);
            mv.visitLocalVariable("this", "Lcom/ezbuy/asmdemo/Person;", null, l0, l4, 0);
            mv.visitLocalVariable("name", "Ljava/lang/String;", null, l0, l4, 1);
            mv.visitLocalVariable("age", "I", null, l0, l4, 2);
            mv.visitMaxs(2, 3);
            mv.visitEnd();
        }
        {
            mv = cw.visitMethod(ACC_PUBLIC, "say", "(Ljava/lang/String;)V", null, null);
            mv.visitCode();
            Label l0 = new Label();
            mv.visitLabel(l0);
            mv.visitLineNumber(20, l0);
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitLdcInsn("Hello,%s");
            mv.visitInsn(ICONST_1);
            mv.visitTypeInsn(ANEWARRAY, "java/lang/Object");
            mv.visitInsn(DUP);
            mv.visitInsn(ICONST_0);
            mv.visitVarInsn(ALOAD, 1);
            mv.visitInsn(AASTORE);
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/String", "format", "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;", false);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            Label l1 = new Label();
            mv.visitLabel(l1);
            mv.visitLineNumber(21, l1);
            mv.visitInsn(RETURN);
            Label l2 = new Label();
            mv.visitLabel(l2);
            mv.visitLocalVariable("this", "Lcom/ezbuy/asmdemo/Person;", null, l0, l2, 0);
            mv.visitLocalVariable("desc", "Ljava/lang/String;", null, l0, l2, 1);
            mv.visitMaxs(6, 2);
            mv.visitEnd();
        }
        {
            mv = cw.visitMethod(ACC_PUBLIC, "getInfo", "()Ljava/lang/String;", null, null);
            mv.visitCode();
            Label l0 = new Label();
            mv.visitLabel(l0);
            mv.visitLineNumber(24, l0);
            mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
            mv.visitInsn(DUP);
            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
            mv.visitLdcInsn("name=");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitVarInsn(ALOAD, 0);
            mv.visitFieldInsn(GETFIELD, "com/ezbuy/asmdemo/Person", "name", "Ljava/lang/String;");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitLdcInsn(",age=");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitVarInsn(ALOAD, 0);
            mv.visitFieldInsn(GETFIELD, "com/ezbuy/asmdemo/Person", "age", "I");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(I)Ljava/lang/StringBuilder;", false);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
            mv.visitInsn(ARETURN);
            Label l1 = new Label();
            mv.visitLabel(l1);
            mv.visitLocalVariable("this", "Lcom/ezbuy/asmdemo/Person;", null, l0, l1, 0);
            mv.visitMaxs(2, 1);
            mv.visitEnd();
        }
        cw.visitEnd();

        return cw.toByteArray();
    }
}

測(cè)試類,比較兩種方式

好了,原型類和Class生成類我們都有了,現(xiàn)在我們來比較下兩種方式創(chuàng)建對(duì)象和調(diào)用方法的區(qū)別

這里為了防止Class命名沖突,我將原型文件重命名為Person2了

package com.ezbuy.asmdemo;

import com.shanhy.demo.asm.hello.MyClassLoader;

import org.junit.Test;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class TestClient {

    @Test
    public void testFile() throws Exception {
        Person2 libai = new Person2("libai", 24);
        String info = libai.getInfo();
        libai.say(info);
    }

    @Test
    public void testGen() throws Exception {
        //ASM創(chuàng)建Class
        byte[] data = PersonDump.dump();
        MyClassLoader myClassLoader = new MyClassLoader();
        Class<?> personClass = myClassLoader.defineClass("com.ezbuy.asmdemo.Person", data);
        //反射調(diào)用構(gòu)造方法
        Constructor<?> constructor = personClass.getConstructor(String.class, int.class);
        Object libai = constructor.newInstance("libai", 24);
        //反射調(diào)用普通方法
        Method getInfo = personClass.getMethod("getInfo", null);
        Object info = getInfo.invoke(libai, null);
        Method say = personClass.getMethod("say", String.class);
        say.invoke(libai, info);
    }


}

跑一下test方法,發(fā)現(xiàn)兩個(gè)方法輸出結(jié)果是一致的


最后,放一下我這個(gè)demo用的依賴

implementation 'junit:junit:4.12'
//ASM相關(guān)
implementation 'org.ow2.asm:asm:5.1'
implementation 'org.ow2.asm:asm-util:5.1'
implementation 'org.ow2.asm:asm-commons:5.1'

代碼已上傳到碼云:ASMDemo

總結(jié)

可能你會(huì)說,我明明可以自己手寫一個(gè)Java文件啊,干嘛還得這么麻煩去ASM去動(dòng)態(tài)創(chuàng)建Class呢。但是,你想一下,如果你能操控Class文件,可以動(dòng)態(tài)的添加Class、方法、字段,那你可以做多少黑科技的事情啊。在編譯的時(shí)候,通過gradle提供的Transformer,在這里做做手腳,給原有的方法加加料,比如加上日志統(tǒng)計(jì)、性能監(jiān)控、埋點(diǎn)、權(quán)限控制、事務(wù)控制、防抖,不是很容易么。只要你定義好在什么時(shí)候,加上什么代碼,就可以實(shí)現(xiàn)批量修改多個(gè)Class,而且不侵入原有的Java文件,業(yè)務(wù)邏輯和通用的切面邏輯解耦,多爽。假如現(xiàn)在要你在你的包下面的每個(gè)方法里面都加一個(gè)統(tǒng)計(jì)該方法的耗時(shí),你原來的方式得修改你的每個(gè)方法,但現(xiàn)在,你只需要在編譯時(shí)加一點(diǎn)料就可以了。

相關(guān)參考

  1. 一文讀懂 AOP | 你想要的最全面 AOP 方法探討
  2. eleme/lancet
  3. ASM Bytecode Framework探索與使用
  4. 使用ASM操作Java字節(jié)碼,實(shí)現(xiàn)AOP原理
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容