淺談位元組碼增強技術系列1-位元組碼增強概覽

京東雲開發者發表於2022-12-12

作者:董子龍

前言

前段時間一直想參照lombok的實現原理寫一篇可以生成業務單據修改記錄外掛的專利,再查閱資料的過程中,偶然瞭解到了位元組碼增強工具-byteBuddy。但是由於當時時間緊促,所以沒有深入的對該元件進行了解。其實再我們的日常開發中,位元組碼增強元件的身影無處不在,例如spring-aop和mybatis。本著知其然也要知其所以然的精神,我決定沉下心來,對位元組碼增強技術做一個深入的學習和總結,本文作為該系列的開篇,主要是對位元組碼做一下簡單的介紹,為我們後面的深入學習打下一個好的基礎。

一、位元組碼簡述

位元組碼是一種中間狀態的二進位制檔案,是由原始碼編譯過來的,可讀性沒有原始碼的高。cpu並不能直接讀取位元組碼,在java中,位元組碼需要經過JVM轉譯成機械碼之後,cpu才能讀取並執行。

使用位元組碼的好處:一處編譯,到處執行。java就是典型的使用位元組碼作為中間語言,在一個地方編譯了原始碼,拿著.class檔案就可以在各種計算機執行。

二、位元組碼增強的使用場景

如果我們不想修改原始碼,但是又想加入新功能,讓程式按照我們的預期去執行,可以透過編譯過程和載入過程中去做相應的操作,簡單來講就是:將生成的.class檔案修改或者替換稱為我們需要的目標.class檔案。

由於位元組碼增強可以在完全不侵入業務程式碼的情況下植入程式碼邏輯,所以可以用它來做一些酷酷的事,比如下面的幾種常見場景:

1、動態代理

2、熱部署

3、呼叫鏈跟蹤埋點

4、動態插入log(效能監控)

5、測試程式碼覆蓋率跟蹤

...

三、位元組碼增強的實現方式

位元組碼工具 類建立 實現介面 方法呼叫 類擴充套件 父類方法呼叫 優點 缺點 常見使用 學習成本
java-proxy 支援 支援 支援 不支援 不支援 簡單動態代理首選 功能有限,不支援擴充套件 spring-aop,MyBatis 1星
asm 支援 支援 支援 支援 支援 任意位元組碼插入,幾乎不受限制 學習難度大,編寫程式碼多 cglib 5星
javaassit 支援 支援 支援 支援 支援 java原始語法,字串形式插入,寫入直觀 不支援jdk1.5以上的語法,如泛型,增強for Fastjson,MyBatis 2星
cglib 支援 支援 支援 支援 支援 與bytebuddy看起來差不多 正在被bytebuddy淘汰 EasyMock,jackson-databind 3星
bytebuddy 支援 支援 支援 支援 支援 支援任意維度的攔截,可以獲取原始類、方法,以及代理類和全部引數 不太直觀,學習理解有些成本,API非常多 SkyWalking,Mockito,Hibernate,powermock 3星

四、簡單示例

AOP是我們在日常開發中常用的架構設計思想,AOP的主要的實現有cglib,Aspectj,Javassist,java proxy等。接下來,我們就以我們日常開發中會遇到的在方法執行前後列印日誌為切入點,手動用位元組碼來實現一下AOP。

定義目標介面與實現

public class SayService{
   public void say(String str) {
      System.out.println("hello" + str); 
   }
 }

定義了類SayService,再執行say方法之前,我們會列印方法開始執行start,方法執行之後,我們會列印方法執行結束end

ASM實現AOP

4.1.1、引入jar包

<dependency>    
    <groupId>org.ow2.asm</groupId>    
    <artifactId>asm</artifactId>   
    <version>9.1</version>
</dependency>

4.1.2、AOP具體實現

public class ResourceClassVisitor extends ClassVisitor implements Opcodes {

    public ResourceClassVisitor(ClassVisitor cv) {
        super(Opcodes.ASM4, cv);
    }

    public ResourceClassVisitor(int i, ClassVisitor classVisitor) {
        super(i, classVisitor);
    }

    /**訪問類基本資訊*/
    @Override
    public void visit(int version, int access, String name,
                      String signature, String superName, String[] interfaces) {
        this.cv.visit(version, access, name, signature, superName, interfaces);
    }

    /**訪問方法基本資訊*/
    @Override
    public MethodVisitor visitMethod(int access, String name,
                                     String desc, String signature, String[] exceptions) {
        MethodVisitor mv = this.cv.visitMethod(access, name, desc,
                signature, exceptions);
        //假如不是構造方法,我們構建方法的訪問物件(MethodVisitor)
        if (!name.equals("<init>") && mv != null) {
            mv = new ResourceClassVisitor.MyMethodVisitor((MethodVisitor)mv);
        }

        return (MethodVisitor)mv;
    }

    /**自定義方法訪問物件*/
    class MyMethodVisitor extends MethodVisitor implements Opcodes {

        public MyMethodVisitor(MethodVisitor mv) {
            super(Opcodes.ASM4, mv);
        }
        /**此方法會在方法執行之前執行*/
        @Override
        public void visitCode() {
            super.visitCode();
            this.mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out",
                    "Ljava/io/PrintStream;");
            this.mv.visitLdcInsn("方法開始執行start");
            this.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream",
                    "println", "(Ljava/lang/String;)V", false);
        }
        /**對應方法體本身*/
        @Override
        public void visitInsn(int opcode) {
            //在方法return或異常之前,新增一個end輸出
            if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) {
                this.mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out",
                        "Ljava/io/PrintStream;");
                this.mv.visitLdcInsn("方法執行結束end");
                this.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream",
                        "println", "(Ljava/lang/String;)V", false);
            }
            this.mv.visitInsn(opcode);
        }
    }
}
public class AopTest {

    public static void main(String[] args) throws IOException {
        //第一步:構建ClassReader物件,讀取指定位置的class檔案(預設是類路徑-classpath)
        ClassReader classReader = new ClassReader("com/aop/SayService");
        //第二步:構建ClassWriter物件,基於此物件建立新的class檔案
        //ClassWriter.COMPUTE_FRAMES 表示ASM會自動計算max stacks、max locals和stack map frame的具體內容。
        //ClassWriter.COMPUTE_MAXS 表示ASM會自動計算max stacks和max locals,但不會自動計算stack map frames。
        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);//推薦使用COMPUTE_FRAMES
        //第三步:構建ClassVisitor物件,此物件用於接收ClassReader物件的資料,並將資料處理後傳給ClassWriter物件
        ClassVisitor classVisitor = new ResourceClassVisitor(classWriter);
        //第四步:基於ClassReader讀取class資訊,並將資料傳遞給ClassVisitor物件
        //這裡的引數ClassReader.SKIP_DEBUG表示跳過一些除錯資訊等,ASM程式碼看上去就會更簡潔
        //這裡的引數ClassReader.SKIP_FRAMES表示跳過一些方法中的部分棧幀資訊,棧幀手動計算非常複雜,所以交給系統去做吧
        //推薦用這兩個引數
        classReader.accept(classVisitor, ClassReader.SKIP_DEBUG|ClassReader.SKIP_FRAMES);
        //第五步:從ClassWriter拿到資料,並將資料寫出到一個class檔案中
        byte[] data = classWriter.toByteArray();
        //將位元組碼寫入到磁碟的class檔案
        File f = new File("target/classes/com/aop/SayService.class");
        FileOutputStream fout = new FileOutputStream(f);
        fout.write(data);
        fout.close();
        SayService rs = new SayService();
        rs.say("asm");//start,handle(),end
    }
}

4.1.3、測試類輸出結果

Javassist實現AOP

4.2.1、引入jar包

<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.28.0-GA</version>
</dependency>

4.2.2、AOP具體實現

public class AopTest {

    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.get("com.aop.SayService");
        CtMethod personFly = cc.getDeclaredMethod("say");
        personFly.insertBefore("System.out.println("方法開始執行start");");
        personFly.insertAfter("System.out.println("方法執行結束end");");
        cc.toClass();
        SayService sayService = new SayService();
        sayService.say("assist");
    }
}

4.2.3、測試類輸出結果

五、總結

作為位元組碼增強系列文章的開篇,只是簡單的介紹了一下位元組碼的定義、位元組碼的實現方式,最後透過具體示例向大家展示瞭如何對位元組碼進行增強。再後續的文章中,會對相關框架的原理及具體應用做一個細化的總結,歡迎各位大佬的批評與指正。

相關文章