使用ASM字節碼框架實現動態代理

想了解JDK動態代理和CGLIB的實現原理和細節的同窗,看過來, 本文將向大家展現如何從零開始構建構建一個動態代理對象。html

ASM字節碼操縱框架,能夠直接以二進制的形式來來修改已經存在的類或者建立新的類。ASM封裝了操做字節碼的大部分細節,並提供了很是方便的接口來對字節碼進行操做。ASM框架是全功能的,使用ASM字節碼框架,能夠方便地對類增長成員,修改方法,建立新的類等。關於ASM的學習,能夠參考:Learn ASM CoreApi。做爲學習ASM框架的第一篇總結,本文的主要內容是使用ASM框架實現一個簡單的JDK動態代理和CGLIB代理。java

設定代理類和被代理類

被代理類

被代理類很是簡單。web

public interface CalculatorInterface {
    int add(int i, int j);
    int sub(int i, int j);
}

public class Calculator implements CalculatorInterface {
    public int add(int i, int j) {
        return i + j;
    }

    public int sub(int i, int j) {
        return i - j;
    }
}

代理類目標代碼原型

CGLIB版本的代理類以下,直接從Calculator繼承。若是是JDK版本的,則改成實現CalculatorInterface便可。
這裏簡化了,對要攔截的方法的個數寫死了(m1,m2),實際在生成字節碼的時候並無寫死。
這一份代碼,就是我要用字節碼方式生成的代碼。數組

public class CalculatorProxy extends Calculator {
    private InvocationHandler handler;
    private Object target
    private Method m1;
    private Method m2;

    public CalculatorProxy(Object o, InvocationHandler h, Method targetMethod1, Method targetMethod2) {
        super();
        target = o;
        handler = h;
        m1 = targetMethod1;
        m2 = targetMethod2;
    }

    @Override
    public int add(final int i, final int j) {
        try {
            return (int)handler.invoke(this, m1, new Object[] {i, j});
        } catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }
}

代理工廠

代理工廠相似JDK的Proxy對象,只須要向代理工廠對象提供被代理對象和一個InvocationHandler便可使用CGLIB的方式來生成代理類,若是要使用JDK的方式來生成代理對象,則須要再額外提供一下待實現接口。使用newProxy方法獲得代理對象。oracle

public class AopProxy {
    public AopProxy(Object target, InvocationHandler handler) {
        ...
    }
    public AopProxy(Object target, InvocationHandler handler, Class<?>[] interfaces) {
        ...
    }
    public Object newProxy() throws Exception{
    } 
}

生成代理類

基本指令

使用ASM生成一個類最複雜的地方在於方法體的生成,至關於直接寫字節碼。生成上文的目標類須要用到以下指令:
這個表格說的指令參數,並非真正的JVM指令的參數,而是使用ASM框架生成相應字節碼時須要傳遞的參數框架

指令名稱 指令參數說明 指令含義 操做數棧
ILOAD index: unsigned byte 從局部變量表中加載下標爲index的int到操做數棧 無參數出棧,結果入棧
ALOAD index: unsigned byte 將棧頂的引用寫入到局部變量表中下標爲index的引用 無參數出棧,結果入棧
ASTORE index: unsigned byte 從局部變量表中加載下標爲index的引用到操做數棧 objectRef 引用 出棧
INVOKESPECIAL owner: string 類名
name: string 方法名
desc:方法簽名
itf:boolean 是不是接口(false)
調用構造器,私有方法,或顯示調用父類的方法,靜態綁定 objectRef:實例對象 出棧
arg1:第一個參數 出棧
arg...
有返回的話入棧
INVOKEVIRTUAL owner: string 類名
name: string 方法名
desc:方法簽名
itf:boolean 是不是接口(false)
根據對象類型多態調用,動態綁定 objectRef:實例對象 出棧
arg1:第一個參數 出棧
arg...
有返回的話入棧
INVOKEINTERFACE owner: string 類名
name: string 方法名
desc:方法簽名
itf:boolean 是不是接口(true)
根據對象類型多態調用,動態綁定 objectRef:實例對象 出棧
arg1:第一個參數 出棧
arg...
有返回的話入棧
NEW name: string 構造一個類型爲name的對象,分配內存,並完成成員初始化,可是並不調用構造方法 無參數出棧,結果入棧
PUTFIELD owner: string 類名
name: string 成員名
desc: string 成員類型描述
設置一個field值 objectRef 實例對象 出棧
value 成員的值 出棧
GETFIELD owner: string 類名
name: string 成員名
desc: string 成員類型描述
讀取一個field值 objectRef 實例對象 出棧
value 成員的值 入棧
ANEWARRAY type: string 類型名稱 新建引用類型爲type的數組 count:int 數組長度 出棧
arrayref:數組引用 入棧
DUP 無參 複製棧頂的數據 value 棧頂的值 出棧
value 棧頂的值 入棧兩次
AASTORE 無參 將引用存入數組指定位置 arrayRef 數組引用 出棧
index 下標 出棧
value 引用 出棧

INVOKEVIRTUAL 和 INVOKEINTERFACE的區別見:連接。若是調用一個方法的時候,可以肯定這個方法在方法表中的位置,就調用INVOKEVIRTUAL,若是不能,就調用INVOKEINTERFACE。具體一點來講:若是一個變量的靜態類型是接口,就使用INVOKEINTERFACE,若是是類,就使用INVOKEVIRTUAL。jvm

方法調用基本知識

  • 調用一個方法以前,須要確保方法須要的參數都已經加載到操做數棧中。若是是實例方法,須要將隱含的this也加載到棧中。
  • 參數入棧的順序和參數聲明的順序一致。實際彈出參數時,和聲明順序相反。(後進先出)。

關鍵代碼生成

獲取要代理的方法

比較簡單,兼容了基於接口和基於類的代理兩種場景ide

private List<Method> getProxyMethods() {
    List<Method> methods = new LinkedList<>();
    List<Class<?>> superClassList = interfaceList;
    if (superClassList.isEmpty()) {  //若是沒有指定接口,就代理父類的全部公有方法
        superClassList = Collections.singletonList(targetClass);
    }
    for (final Class<?> aClass : superClassList) {
        methods.addAll(filterObjectMethods(aClass.getMethods())); //未來自Object的方法過濾掉
    }
    return methods;
}

寫入類描述

ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);  //COMPUTE_FRAMES可以幫咱們省去不少麻煩
cw.visit(Opcodes.V1_8,                  //字節碼版本
        Opcodes.ACC_PUBLIC,            //類是public的
        convertClassName(className),  //類名
        null,                          //類的簽名
        getSuperClassName(),          //父類
        getInterfaceNames());          //要實現的接口

寫入域

生成以下幾個域:target,handler,以及和要攔截的方法個數對應的Method域m1,m2...函數

public void writeFields(ClassWriter cw, List<Method> methods) {
    int i = 1;
    //爲每個要代理的方法,生成一個Method類型的成員,用於後面保存目標對象的方法
    for (final Method method : methods) {
        cw.visitField(Opcodes.ACC_PRIVATE, "m" + i++, "Ljava/lang/reflect/Method;", null, null).visitEnd();
    }
    //生成一個handler成員,保存回調接口
    cw.visitField(Opcodes.ACC_PRIVATE, "handler",
                  convertClassNameToDesc(InvocationHandler.class.getCanonicalName()),
                  null, null).visitEnd();

    //生成一個target成員,保存被代理對象
    cw.visitField(Opcodes.ACC_PRIVATE, "target",
                  convertClassNameToDesc(Object.class.getCanonicalName()),
                  null, null).visitEnd();
}

寫構造方法

1 生成方法簽名並調用父類構造方法工具

MethodVisitor mv =
    cw.visitMethod(Opcodes.ACC_PUBLIC,
                  "<init>",
                  "(Ljava/lang/Object;Ljava/lang/reflect/InvocationHandler;[Ljava/lang/reflect/Method;)V",
                  null, null);
mv.visitVarInsn(Opcodes.ALOAD, 0);  //調用構造方法須要傳入隱式參數this
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, convertClassName(superClass.getCanonicalName()), "<init>", "()V",
                  false);

2 初始化target和handler
從局部變量表中加載數據並賦值到各field種便可,構造函數的入參爲:target,handler,method[],所以target和handler在局部變量表中的位置是1和2. 使用javap -v -p -l classfile 能夠查看相關信息。

//初始化targe
mv.visitVarInsn(Opcodes.ALOAD, 0); //this
mv.visitVarInsn(Opcodes.ALOAD, 1); //局部變量target
mv.visitFieldInsn(Opcodes.PUTFIELD, convertClassName(className), "target",
                  convertClassNameToDesc(Object.class.getCanonicalName()));
//初始化handler
mv.visitVarInsn(Opcodes.ALOAD, 0);//this
mv.visitVarInsn(Opcodes.ALOAD, 2);//局部變量handler
mv.visitFieldInsn(Opcodes.PUTFIELD, convertClassName(className), "handler",
                  convertClassNameToDesc(InvocationHandler.class.getCanonicalName()));

3 初始化method域
構造函數的第三個參數時Method數組,須要將這些值依次保存到多個method域中。

//初始化mi
int i = 1;
for (final Method method : methods) {
    mv.visitVarInsn(Opcodes.ALOAD, 0);  //加載this,爲putfield指令作準備
    mv.visitVarInsn(Opcodes.ALOAD, 3); // 加載入參method數組
    mv.visitLdcInsn(i - 1); //將i-1做爲常量載入,這個常量和上面的method數組是AALoad質量的操做數,表示要加載數組中某個位置的值
    mv.visitInsn(Opcodes.AALOAD); //aaload指令消耗了上面的兩個操做數,並將結果放入到操做數棧頂,這個結果和this將被putfield使用
    mv.visitFieldInsn(Opcodes.PUTFIELD, convertClassName(className), "m" + i,
                      convertClassNameToDesc(Method.class.getCanonicalName()));
    i++;
}

4 完成構造函數

mv.visitInsn(Opcodes.RETURN);  //寫入return指令
mv.visitMaxs(1, 1); //計算棧和局部變量的大小,傳入的參數會被忽略,由於ClassWriter被設置了COMPUTE_FRAMES,操做數棧大小,局部變量表大小,還有StackMapFrame都會在此時被從新計算。
mv.visitEnd();

寫入要代理的方法

寫方法構造一個類最複雜的地方,須要很是當心地處理操做數棧,不然很容易出各類奇怪的問題。

private void writeMethods(ClassWriter cw, List<Method> methods) {
    int i = 1;
    for (Method x : methods) {
        writeMethod(cw, x, i++);  //寫入方法,i表示是第幾個方法,用來和method域對應
    }
}

private void writeMethod(ClassWriter cw, Method method, int i) {
大招都在這裏了
}

寫入方法描述

String name = method.getName();  //方法名稱
String desc = DescHelper.getDesc(method); //方法描述,包含形參列表和返回值類型
Class<?>[] exceptionTypes = method.getExceptionTypes();
List<String> exceptionDescList = new LinkedList<>();
for (final Class<?> exceptionType : exceptionTypes) {
    exceptionDescList.add(DescHelper.getDesc(exceptionType)); //異常聲明
}
MethodVisitor mv =
    cw.visitMethod(Opcodes.ACC_PUBLIC, name, desc, null,
                  exceptionDescList.toArray(new String[exceptionDescList.size()])); //寫入方法簽名
mv.visitCode();

寫入方法體

方法體核心代碼其實就一句話:

public int add(int i, int j) {
    return (Integer)this.handler.invoke(this, this.m1, new Object[]{i,j});
    ...
}

調用invoke方法,invoke方法須要的參數含handler本身的this引用在內,總共有四個:this,proxy,method,args。這四個參數須要先計算好並放依次入操做數棧以後,才能調用invoke方法。接下來就按照準備這幾個參數的順序來講明怎麼生成字節碼。

準備參數:this

域handler的引用,其實就是handler.invoke方法須要的this指針。

mv.visitVarInsn(Opcodes.ALOAD, 0);  //加載this參數
mv.visitFieldInsn(Opcodes.GETFIELD,
                  convertClassName(className), //指定field的owner
                  "handler", //指定field的名稱
                  convertClassNameToDesc(invocationHandlerClassName)); //指定field的描述

GETFIELD執行完以後,handler的引用會被放到操做數棧頂。此時,invoke方法第一個參數:this準備就緒。

準備參數:proxy

handler.invoke 須要的proxy參數,是指代理對象的引用。有於handler.invoke是在代理對象中執行的代碼,所以代理對象就是當前方法的this指針。

mv.visitVarInsn(Opcodes.ALOAD, 0);  //加載this指針。此時,invoke方法的第二個參數:proxy準備就緒。
準備參數:method

生成類的代碼是通用的,並不清楚當前是在生成哪個代理方法,所以須要使用哪一個method域取決於入參i。

mv.visitVarInsn(Opcodes.ALOAD, 0);  //加載this,爲GETFIELD指令準備操做數棧
mv.visitFieldInsn(Opcodes.GETFIELD,
                  convertClassName(className),  //域的owner
                  "m" + i,  //域的名稱
                  convertClassNameToDesc(Method.class.getCanonicalName())); //域的描述

GETFIELD指令執行完成以後,響應的method域就進入操做數棧。此時invoke方法的第三個參數:method準備就緒。

準備參數args

args參數是一個數組,當前上下文int add(i,j)中並不存在,須要手動構造出來。
代碼:new Object[]{i,j} 的生成步驟以下:
1 調用new指令

int parameterCount = method.getParameterCount();
mv.visitLdcInsn(parameterCount);  //將數組大小做爲常量加載的操做數棧中
mv.visitTypeInsn(Opcodes.ANEWARRAY, convertClassName(Object.class.getCanonicalName()));  //生成數組,棧頂的int參數出棧,數組引用入棧

執行完ANEWARRAY指令以後,數組的引用Arrayref位於棧頂。
2 填入數組值
因爲數組元素是Object,對於基本類型須要裝箱,裝箱以後再加入到數組中

int paramIndex = 0;
for (final Class<?> paramClass : method.getParameterTypes()) {
    mv.visitInsn(Opcodes.DUP); //備份一下棧頂的Arrayref,aastore指令會消耗掉這個參數,aastore指令的第一個操做數
    mv.visitLdcInsn(paramIndex); //aastore 的第二個操做數,數組下標
    mv.visitVarInsn(Opcodes.ILOAD, paramIndex + 1);  //載入局部變量 ,第一次載入i,第二次載入j
    mv.visitMethodInsn(Opcodes.INVOKESTATIC, convertClassName(Integer.class.getCanonicalName())
        , "valueOf", "(I)Ljava/lang/Integer;", false); //裝箱,valueOf會消耗到棧頂元素並返回一個Integer對象入棧
    mv.visitInsn(Opcodes.AASTORE);  //裝箱操做生成aastore指令的第三個參數 ,數組元素值,以後,就能夠進行指令調用了。這個指令會致使操做數棧出棧三次。
    paramIndex++;
}

AASTORE執行完以前,入棧三個操做數,執行完以後,三個操做數出棧,操做數棧不變,Arrayref依然位於棧頂。此時handler.invoke的方法就所有準備就緒了。

調用invoke方法
mv.visitMethodInsn(Opcodes.INVOKEINTERFACE,
                  invocationHandlerClassName,
                  INVOKE, INVOKE_DESC, true);

INVOKEINTERFACE按照invoke方法的參數列表,依次將須要的參數出棧。執行完以後,將結果入棧。根據invoke方法的簽名,結果Object類型,是一個引用。

unbox

add方法的返回值是int,invoke方法的返回值是Object,須要unbox才能返回。

mv.visitTypeInsn(Opcodes.CHECKCAST, "java/lang/Integer"); //類型檢查
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I", false); //調用intValue方法,object引用出棧,結果入棧。
mv.visitInsn(Opcodes.IRETURN);  //返回棧頂的整數
異常處理

因爲InvocationHandler的invoke方法拋出Throwable異常,須要捕獲,所以,實際生成的add方法,還須要捕獲異常。

private void writeMethod(ClassWriter cw, Method method, int i) {
    ...
    mv.visitCode();
    Label begin = new Label(); //方法入口加一個labe 
    mv.visitLabel(begin);
  ...
    Label end = new Label();  //方法出口加一個label
    mv.visitLabel(end);
//加入try catch,並指明捕獲的異常類型
    mv.visitTryCatchBlock(begin, end, end, convertClassName(Throwable.class.getCanonicalName()));
    mv.visitVarInsn(Opcodes.ASTORE, 3); //將異常存入到局部變量
//構造UndeclaredThrowableException對象
    mv.visitTypeInsn(Opcodes.NEW, convertClassName(UndeclaredThrowableException.class.getCanonicalName()));
//準備UndeclaredThrowableException構造方法的參數
    mv.visitInsn(Opcodes.DUP);  //準備UndeclaredThrowableException構造器的this參數
    mv.visitVarInsn(Opcodes.ALOAD, 3);//準備UndeclaredThrowableException構造器的undeclaredThrowable參數
    mv.visitMethodInsn(Opcodes.INVOKESPECIAL,
                      convertClassName(UndeclaredThrowableException.class.getCanonicalName()),
                      "<init>",
                      "(Ljava/lang/Throwable;)V", false);
    mv.visitInsn(Opcodes.ATHROW);  拋出UndeclaredThrowableException對象
    mv.visitMaxs(1, 1);
    mv.visitEnd();
}

至此,動態代理類就生成完了。生成的動態代理類以下:

小結

使用ASM字節碼框架生成代碼,能夠先本身用Java代碼寫出目標代碼,而後轉成字節碼來查看。也可使用ASMifer工具來生成ASM代碼。

java jdk.internal.org.objectweb.asm.util.ASMifier classfilename

ASMifier很好用,可是它生成的ASM代碼,是針對一個給定的類的硬編碼,不必定符合業務邏輯,可是很是值得參考。
另外,對於JVM字節碼指令有不清楚的地方,能夠參考文檔:JVM虛擬機指令集

相關文章
相關標籤/搜索