字節碼加強之ASM

1. 字節碼

1.1 什麼是字節碼

Java之因此能夠"一次編譯,處處運行",一是由於JVM針對各類平臺和操做系統都進行了定製,對開發者屏蔽了底層細節。二是由於不管在任何平臺都會編譯生成固定格式的字節碼(.class)文件供JVM使用,不一樣平臺上的JVM虛擬機均可以載入和執行同一種和平臺無關的字節碼。因而可知字節碼對於Java生態的重要性。之因此被稱爲字節碼,是由於字節碼文件由16進制值組成,JVM以字節爲單位進行讀取。在Java中通常使用javac命令編譯源代碼爲字節碼文件,一個.java文件從編譯到運行的示例以下圖所示。java

java運行示意圖

1.2 字節碼結構

.java文件經過javac編譯後將獲得.class文件,如編寫一個簡單的ByteCodeDemo類,以下圖的左側部分。編譯後生成ByteCodeDemo.class文件,打開後是一堆16進制數,按字節爲單位分割展現以下圖右側部分展現。框架

示例代碼及對應的字節碼

JVM規範要求每個字節碼文件都須要由10個部分按固定順序組成,總體結果以下圖所示。ide

字節碼文件結構

2. ASM

ASM是一個Java字節碼操控框架。它能夠被用來動態生成類或者加強既有類的功能。ASM能夠直接產生二進制的class文件,也能夠在類被加載到Java虛擬機以前改變類行爲。ASM的應用場景有AOP(cglib基於ASM)、熱部署、修改其它jar包中的類等。ASM修改字節碼流程以下圖所示。spa

ASM修改字節碼

2.1 ASM核心API

2.1.1 ClassReader

用於讀取已經編譯好的.class文件;操作系統

2.1.2 ClassWriter

用於從新構建編譯後的類,如修改類名、屬性及方法等;插件

2.1.3 ClassVistor

ASM內部採用訪問者模式根據字節碼從上到下依次處理,對於字節碼文件中不一樣的區域有不一樣的Visitor。code

  • 訪問類文件時,會回調ClassVistor的visit方法;
  • 訪問類註解時,會回調ClassVistor的visitAnnotation方法;
  • 訪問類成員時,會回調ClassVistor的visitField方法;
  • 訪問類方法時,會回調ClassVistor的visitMethod方法;
  • .......

當訪問不一樣區域時會回調相應方法,該方法會返回一個對應的字節碼操做對象。經過修改這個對象就能夠修改class文件相應結構對應的內容。最後將這個對象的字節碼內容覆蓋原先的.class文件就能夠實現字節碼的切入。cdn

2.2 ASM實現AOP示例

代碼示例以下,本來Base的process方法只輸出一行"process"。對象

目標:咱們指望經過字節碼加強後,在方法執行前輸出"start",在方法執行後輸出"end"。blog

public class Base {
    public void process() {
        System.out.println("process");
    }
}
複製代碼

要實如今process方法先後插入輸出代碼,須要有如下步驟:

  • (1)讀取Base.class文件,能夠經過ClassReader進行class文件的讀取;
  • (2)構造System.out.println(String)的字節碼,經過ClassVisitor將額外的字節碼嵌入合適地方;
  • (3)在ClassVisitor處理完成後,由ClassWriter將新的字節碼替換掉舊的字節碼;

基本實現代碼參考以下,SimpleGenerator類定義了ClassReader和ClassWriter對象。其中ClassReader負責讀取字節碼,而後交給ClassVisitor類處理,處理完成後由ClassWriter完成字節碼文件替換。

public class SimpleGenerator {

    public static void main(String[] args) throws Exception {
        //讀取類文件
        ClassReader classReader = new ClassReader("com.example.aop.asm.Base");
        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        //處理,經過classVisitor修改類
        ClassVisitor classVisitor = new SimpleClassVisitor(classWriter);
        classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);
        byte[] data = classWriter.toByteArray();
        //保存新的字節碼文件
        File file = new File("target/classes/com/example/aop/asm/Base.class");
        FileOutputStream outputStream = new FileOutputStream(file);
        outputStream.write(data);
        outputStream.close();
        System.out.println("generator new class file success.");
    }
}
複製代碼

SimpleClassVisitor類繼承ClassVisitor類。ClassVisitor中各方法按如下順序調用,這個順序在ClassVisitor的java doc中也有說明。

[ visitSource ] [ visitOuterClass ] ( visitAnnotation | visitTypeAnnotation | visitAttribute )* (visitInnerClass | visitField | visitMethod )* visitEnd.

其中visitMethod在訪問方法時調用,咱們能夠經過擴展MethodVisitor方法來實現加強邏輯。相似地,MethodVisitor也須要按順序調用其方法。

( visitParameter )* [ visitAnnotationDefault ] ( visitAnnotation | visitAnnotableParameterCount | visitParameterAnnotation visitTypeAnnotation | visitAttribute )* [ visitCode ( visitFrame | visitXInsn | visitLabel | visitInsnAnnotation | visitTryCatchBlock | visitTryCatchAnnotation | visitLocalVariable | visitLocalVariableAnnotation | visitLineNumber )* visitMaxs ] visitEnd.

public class SimpleClassVisitor extends ClassVisitor {

    public SimpleClassVisitor(ClassVisitor classVisitor) {
        super(Opcodes.ASM5, classVisitor);
    }

    // visitMethod在訪問類方法時回調,能夠獲知當前訪問到方法信息
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, desc, signature,
                exceptions);
        // 不須要加強Base類的構造方法
        if (mv != null && !name.equals("<init>")) {
            // 經過定製MethodVisitor,替換原來的MethodVisitor對象來實現方法改寫
            mv = new SimpleMethodVisitor(mv);
        }
        return mv;
    }


    /** * 定製方法visitor */
    class SimpleMethodVisitor extends MethodVisitor {

        public SimpleMethodVisitor(MethodVisitor methodVisitor) {
            super(Opcodes.ASM5, methodVisitor);
        }

        // visitCode方法在ASM開始訪問方法的Code區時回調
        @Override
        public void visitCode() {
            super.visitCode();
            // System.out.println("start")對應的字節碼,在visitCode訪問方法以後添加
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitLdcInsn("start");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        }

        // 每當ASM訪問到無參數指令時,都會調用visitInsn方法
        @Override
        public void visitInsn(int opcode) {
            if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
                // System.out.println("end")對應的字節碼,在visitInsn方法返回前添加
                mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                mv.visitLdcInsn("end");
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

            }
            super.visitInsn(opcode);
        }

    }
}
複製代碼

運行SimpleGenerator類的main方法便可完成對Base類的字節碼加強。加強後的class文件經過反編譯後結果以下圖所示。能夠看到對應的代碼已經改變,在其先後增長了"start"和"end"的輸出。

字節碼加強後的Base類

能夠經過ASM ByteCode Outline 插件很方便地實現源碼到字節碼的映射。

3. 參考資料

Java字節碼加強探祕


歡迎關注個人公衆號~

相關文章
相關標籤/搜索