想了解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
比較簡單,兼容了基於接口和基於類的代理兩種場景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方法。接下來就按照準備這幾個參數的順序來講明怎麼生成字節碼。
域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準備就緒。
handler.invoke 須要的proxy參數,是指代理對象的引用。有於handler.invoke是在代理對象中執行的代碼,所以代理對象就是當前方法的this指針。
mv.visitVarInsn(Opcodes.ALOAD, 0); //加載this指針。此時,invoke方法的第二個參數:proxy準備就緒。
生成類的代碼是通用的,並不清楚當前是在生成哪個代理方法,所以須要使用哪一個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參數是一個數組,當前上下文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的方法就所有準備就緒了。
mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, invocationHandlerClassName, INVOKE, INVOKE_DESC, true);
INVOKEINTERFACE按照invoke方法的參數列表,依次將須要的參數出棧。執行完以後,將結果入棧。根據invoke方法的簽名,結果Object類型,是一個引用。
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虛擬機指令集