java動態代理——jvm指令集基本概念和方法位元組碼結構的進一步探究及proxy原始碼分析四
前文地址
https://www.cnblogs.com/tera/p/13336627.html
本系列文章主要是博主在學習spring aop的過程中瞭解到其使用了java動態代理,本著究根問底的態度,於是對java動態代理的本質原理做了一些研究,於是便有了這個系列的文章
上一篇文章詳細分析了class位元組碼結構中的field_info和method_info,以及對應的Proxy的原始碼。本文將會更詳細的分析method_info中的方法執行體部分
因為方法的位元組碼涉及到了jvm的操作指令,因此我們先做一個基礎性的瞭解
原文地址:https://dzone.com/articles/introduction-to-java-bytecode
文中開始介紹的堆、棧、方法區等概念這裡就不詳細描述了,主要看它後面對一些簡單方法的位元組碼的解析
首先我們定義一個簡單的類
public class Test { public static void main(String[] args) { int a = 1; int b = 2; int c = a + b; } }
編譯生成Test.class
javac Test.java
檢視位元組碼結構
javap -v Test.class
我們關注其中的main方法部分
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=4, args_size=1 0: iconst_1 1: istore_1 2: iconst_2 3: istore_2 4: iload_15: iload_2 6: iadd 7: istore_3 8: return LineNumberTable: line 3: 0 line 4: 2 line 5: 4 line 6: 8
其中的Code正是方法的執行體,下面按照順序圖解具體操作
iconst_1:將常量1壓入操作棧
istore_1:彈出棧頂的運算元,存入棧的本地變數陣列的索引1,也就是變數a
iconst_2:將常量2壓入操作棧
istore_2:彈出棧頂的運算元,存入棧的本地變數陣列的索引2,也就是變數b
iload_1:從本地變數索引1種讀取值,並壓入操作棧
iload_2:從本地變數索引2種讀取值,並壓入操作棧
iadd:彈出棧頂的2個運算元,相加後將結果壓入操作棧
istore_3:彈出棧頂的運算元,存入棧的本地變數陣列的索引3,也就是變數c
return:從方法返回
如果我們在類中定義一個方法
public class Test { public static void main(String[] args) { int a = 1; int b = 2; int c = calc(a, b); } static int calc(int a, int b) { return (int) Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2)); } }
得到的位元組碼如下,這次我把部分Constant pool也展示在下面
Constant pool: #1 = Methodref #8.#19 // java/lang/Object."<init>":()V #2 = Methodref #7.#20 // Test.calc:(II)I #3 = Double 2.0d #5 = Methodref #21.#22 // java/lang/Math.pow:(DD)D #6 = Methodref #21.#23 // java/lang/Math.sqrt:(D)D public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=4, args_size=1 0: iconst_1 1: istore_1 2: iconst_2 3: istore_2 4: iload_1 5: iload_2 6: invokestatic #2 // Method calc:(II)I 9: istore_3 10: return LineNumberTable: line 3: 0 line 4: 2 line 5: 4 line 6: 10 static int calc(int, int); descriptor: (II)I flags: ACC_STATIC Code: stack=6, locals=2, args_size=2 0: iload_0 1: i2d 2: ldc2_w #3 // double 2.0d 5: invokestatic #5 // Method java/lang/Math.pow:(DD)D 8: iload_1 9: i2d 10: ldc2_w #3 // double 2.0d 13: invokestatic #5 // Method java/lang/Math.pow:(DD)D 16: dadd 17: invokestatic #6 // Method java/lang/Math.sqrt:(D)D 20: d2i 21: ireturn LineNumberTable: line 8: 0
這裡我們主要看一下一些新出現的操作指令
在main方法中,編號6
invokestatic #2:呼叫靜態方法,方法在Constant Pool中索引為2,表示Test.calc方法(這裡特別注意,呼叫的方法目標必須是常量池中的一個有效索引)
在cacl方法中
i2d:將int型別的轉換成double型別的
ldc2_w:將long型或者double型(思考一下為何是這2種類型放在同一個操作指令中)從靜態池中壓入棧
dadd:將double相加
d2i:將double型別轉換成int型別
ireturn:返回一個int
將上面的jvm指令結合java程式碼,就可以初步理解每一行java程式碼究竟是如何被jvm執行的了
接下去我們可以通過Proxy的程式碼結合實際來看看
方法還是generateClassFile()
在上一篇文章的第三部分位元組與方法位元組碼的寫入中,有提到
這裡的第一行,正是寫入構造器的位元組碼,這一部分因為涉及到jvm的執行指令,我們放到下篇文章再詳細看,所以這裡先跳過
this.methods.add(this.generateConstructor());
此時我們就可以詳細看下generateConstructor方法究竟幹了什麼
private ProxyGenerator.MethodInfo generateConstructor() throws IOException { ProxyGenerator.MethodInfo var1 = new ProxyGenerator.MethodInfo("<init>", "(Ljava/lang/reflect/InvocationHandler;)V", 1); DataOutputStream var2 = new DataOutputStream(var1.code); this.code_aload(0, var2); this.code_aload(1, var2); var2.writeByte(183); var2.writeShort(this.cp.getMethodRef("java/lang/reflect/Proxy", "<init>", "(Ljava/lang/reflect/InvocationHandler;)V")); var2.writeByte(177); var1.maxStack = 10; var1.maxLocals = 2; var1.declaredExceptions = new short[0]; return var1; }
接下一行一行分析
初始化MethodInfo物件,3個引數分別是,方法名、方法描述、access_flag,1表示public(參見Modifier.java)
因為是建構函式,所以方法名為<init>
方法的描述表示,該方法獲取一個java.lang.reflect.InvocationHandler型別的引數,返回值為V(表示void)
方法的access_flag為1,表示public
ProxyGenerator.MethodInfo var1 = new ProxyGenerator.MethodInfo("<init>", "(Ljava/lang/reflect/InvocationHandler;)V", 1);
寫入aload_0和aload_1操作指令
this.code_aload(0, var2); this.code_aload(1, var2);
寫入183號操作指令,查文件得:invokespecial
呼叫例項方法,特別用來處理父類的建構函式
var2.writeByte(183);
寫入需要呼叫的方法名和方法的引數
注意,這裡的方法是通過this.cp.getMethodRef方法得到的,也就是說,這裡寫入的最終資料,其實是一個符合該方法描述的常量池中的一個有效索引(這部分知識可以參看之前的3篇文章)
var2.writeShort(this.cp.getMethodRef("java/lang/reflect/Proxy", "<init>", "(Ljava/lang/reflect/InvocationHandler;)V"));
寫入177號指令,查文件得:return
返回void
var2.writeByte(177);
和上一篇文章中提到的一樣,最後還需要寫入棧深和本地變數數量,以及方法會丟擲的異常數量,因為建構函式不主動丟擲異常,所以異常數量直接為0
var1.maxStack = 10; var1.maxLocals = 2; var1.declaredExceptions = new short[0];
到此,一個建構函式的結構就完成了
此時我們總結一下該建構函式的結構
aload_0; aload_1; invokespecial #x //這裡x對應Constant pool中建構函式的編號 return;
驗證一下,我們建立一個類
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; public class Test extends Proxy { protected TestClass(InvocationHandler h) { super(h); } }
檢視其位元組碼
protected Test(java.lang.reflect.InvocationHandler); descriptor: (Ljava/lang/reflect/InvocationHandler;)V flags: ACC_PROTECTED Code: stack=2, locals=2, args_size=2 0: aload_0 1: aload_1 2: invokespecial #1 // Method java/lang/reflect/Proxy."<init>":(Ljava/lang/reflect/InvocationHandler;)V 5: return LineNumberTable: line 6: 0 line 7: 5
正和我們之前總結的一模一樣
結合之前的一些jvm指令的基本描述,我們就可以對method_info的正題結構有了更深入的瞭解
本文中我們初步瞭解了方法執行體Code的結構,jvm指令的基本概念,那麼在下一篇文章中,我們將會繼續探究Proxy的最核心的部分,代理方法的實際實現