java動態代理——欄位和方法位元組碼的基礎結構及Proxy原始碼分析三

tera發表於2020-07-27

前文地址:https://www.cnblogs.com/tera/p/13280547.html

本系列文章主要是博主在學習spring aop的過程中瞭解到其使用了java動態代理,本著究根問底的態度,於是對java動態代理的本質原理做了一些研究,於是便有了這個系列的文章

 

接上文,我們對class位元組的結構有了一個整體的瞭解,並對Proxy的程式碼做了相應的解析,本文將繼續詳細看看欄位和方法的結構

我們還是回到方法的入口

ProxyGenerator var3 = new ProxyGenerator(var0, var1, var2);
final byte[] var4 = var3.generateClassFile();

進入generateClassFile()方法

第一部分,Object方法的預處理

 

this.addProxyMethod(hashCodeMethod, Object.class);
this.addProxyMethod(equalsMethod, Object.class);
this.addProxyMethod(toStringMethod, Object.class);

 

首先無論是什麼類,都是繼承自Object的,因此Object中的方法是一定需要的
注意,這裡addProxyMethod並非直接寫位元組碼了,而是做了一些預處理
我們先看下3個方法中的第一個引數是個啥
在靜態建構函式中,可以看到的確就是Object的3個方法

static {
    try {
        hashCodeMethod = Object.class.getMethod("hashCode");
        equalsMethod = Object.class.getMethod("equals", Object.class);
        toStringMethod = Object.class.getMethod("toString");
    } catch (NoSuchMethodException var1) {
        throw new NoSuchMethodError(var1.getMessage());
    }
}

我們進入addProxyMethod方法,這裡對變數名做了一個可讀性處理

String methodName = method.getName();
Class[] paramTypes = method.getParameterTypes();
Class returnType = method.getReturnType();
Class[] exceptionTypes = method.getExceptionTypes();
String cacheKey = methodName + getParameterDescriptors(paramTypes);
Object cache = (List)this.proxyMethods.get(cacheKey);
...
((List) cache).add(new ProxyGenerator.ProxyMethod(methodName, paramTypes, returnType, exceptionTypes, targetClass));

概括而言,就是根據方法的各個要素生成一個ProxyMethod物件,然後將其加入一個快取List中

接著我們進入ProxyMethod的建構函式檢視

private ProxyMethod(String var2, Class<?>[] var3, Class<?> var4, Class<?>[] var5, Class<?> var6) {
    this.methodName = var2;
    this.parameterTypes = var3;
    this.returnType = var4;
    this.exceptionTypes = var5;
    this.fromClass = var6;
    this.methodFieldName = "m" + ProxyGenerator.this.proxyMethodCount++;
}

值得注意的是,在ProxyMethod的建構函式中有2個欄位,在後面會有用到
一個是methodName,表示方法名
另外一個是以m+遞增數字的methodFieldName,表示該方法在最終生成的類中的Method型別的欄位的名稱

 

第二部分,介面方法的預處理

Class[] interfaces = this.interfaces;
int interfaceLength = interfaces.length;

int i;
Class clazz;
for(i = 0; i < interfaceLength; ++i) {
    clazz = interfaces[i];
    Method[] methods = clazz.getMethods();
    int methodLength = methods.length;

    for(int j = 0; j < methodLength; ++j) {
        Method m = methods[j];
        this.addProxyMethod(m, clazz);
    }
}

既然生成的類實現了傳入的介面,因此迴圈介面,將介面的方法要素新增到proxyMethods中,和之前處理Object的方法一樣

 

第三部分,欄位和方法的位元組碼寫入

Iterator iterator;
try {
    this.methods.add(this.generateConstructor());
    iterator = this.proxyMethods.values().iterator();
    while(iterator.hasNext()) {
        list = (List) iterator.next();
        listIterator = list.iterator();

        while(listIterator.hasNext()) {
            ProxyGenerator.ProxyMethod proxyMethod = (ProxyGenerator.ProxyMethod) listIterator.next();
            this.fields.add(new ProxyGenerator.FieldInfo(proxyMethod.methodFieldName, "Ljava/lang/reflect/Method;", 10));
            this.methods.add(proxyMethod.generateMethod());
        }
    }

    this.methods.add(this.generateStaticInitializer());
} catch (IOException var10) {
    throw new InternalError("unexpected I/O Exception", var10);
}

這裡的第一行,正是寫入構造器的位元組碼,這一部分因為涉及到jvm的執行指令,我們放到下篇文章再詳細看,所以這裡先跳過

this.methods.add(this.generateConstructor());

直接看後面的while迴圈,就是遍歷之前我們新增的Object和介面定義的方法,然後生成相應的欄位位元組碼和方法位元組碼

while(listIterator.hasNext()) {
    ProxyGenerator.ProxyMethod proxyMethod = (ProxyGenerator.ProxyMethod) listIterator.next();
    this.fields.add(new ProxyGenerator.FieldInfo(proxyMethod.methodFieldName, "Ljava/lang/reflect/Method;", 10));
    this.methods.add(proxyMethod.generateMethod());
}

下面先詳細看看欄位位元組碼的細節

 

第四部分,欄位位元組碼

this.fields.add(new ProxyGenerator.FieldInfo(proxyMethod.methodFieldName, "Ljava/lang/reflect/Method;", 10));

FieldInfo建構函式中
第一個引數proxyMethod.methodFieldName是我們在之前提到的m+遞增數字生成的methodFieldName
第二個引數是型別描述
第三個引數是accessFlag,10表示private static (Modifier.PRIVATE | Modifier.STATIC)

進入建構函式看一下

public FieldInfo(String var2, String var3, int var4) {
    this.name = var2;
    this.descriptor = var3;
    this.accessFlags = var4;
    ProxyGenerator.this.cp.getUtf8(var2);
    ProxyGenerator.this.cp.getUtf8(var3);
}

回想上一篇文章中的field_info型別(忽略attributes)

field_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
}

this.name、this.descriptor、this.accessFlags正好和field_info中的結構一一對應

同時,由於name_index和descriptor_index都是常量池中的一個索引,因此需要將其寫入常量池
這裡的cp就是指Constant pool,把methodFieldName和descriptor寫入到靜態池

ProxyGenerator.this.cp.getUtf8(var2);
ProxyGenerator.this.cp.getUtf8(var3);

之後我們可以直接看,FieldInfo中的write方法,這就是最後寫入的位元組的方法

public void write(DataOutputStream var1) throws IOException {
    var1.writeShort(this.accessFlags);
    var1.writeShort(ProxyGenerator.this.cp.getUtf8(this.name));
    var1.writeShort(ProxyGenerator.this.cp.getUtf8(this.descriptor));
    var1.writeShort(0);
}

對照之前的field_info
第一個寫入access_flags
接著寫入name_index和descriptor_index,值都是索引

最後因為attribute數量是0,因此直接寫0

此時一個完整的欄位結構就寫入完畢了 

 

接著我們回頭檢視ProxyGenerator.this.cp.getUtf8方法,看看索引是如何確定的

public short getUtf8(String var1) {
    if (var1 == null) {
        throw new NullPointerException();
    } else {
        return this.getValue(var1);
    }
}

接續檢視getValue方法

private short getValue(Object var1) {
    Short var2 = (Short)this.map.get(var1);
    if (var2 != null) {
        return var2;
    } else if (this.readOnly) {
        throw new InternalError("late constant pool addition: " + var1);
    } else {
        short var3 = this.addEntry(new ProxyGenerator.ConstantPool.ValueEntry(var1));
        this.map.put(var1, new Short(var3));
        return var3;
    }
}

這裡用map做了一個快取,key就是需要寫入的欄位,value就是索引值,如果命中了map,則直接返回value

如果沒有命中快取,則需要addEntry
檢視addEntry方法

private short addEntry(ProxyGenerator.ConstantPool.Entry var1) {
    this.pool.add(var1);
    if (this.pool.size() >= 65535) {
        throw new IllegalArgumentException("constant pool size limit exceeded");
    } else {
        return (short)this.pool.size();
    }
}

即將生成的entry新增入pool,並返回當前pool的大小,也就是該常量在池中的索引

回想一下cp的結構,其中cp數量是count+1,cp陣列有效索引是從1開始的,因此這裡直接返回pool的size,而不是size-1

因此
ProxyGenerator.this.cp.getUtf8()方法做了2件事情
1.將值寫入常量池
2.返回該值在常量池中的索引

到這裡,欄位的相關內容就結束了,接下去我們檢視方法的位元組碼

 

第五部分,方法位元組碼

先看之前while迴圈中的程式碼

this.methods.add(proxyMethod.generateMethod());

檢視generateMethod方法

因為方法的結構體其實包含兩個大部分,第一部分是和field_info一樣的基礎屬性,第二部分是方法的執行體,所以後面會單獨有文章介紹方法的執行體是怎麼寫入的,這裡我們先關注方法的基本結構

String var1 = ProxyGenerator.getMethodDescriptor(this.parameterTypes, this.returnType);
ProxyGenerator.MethodInfo var2 = ProxyGenerator.this.new MethodInfo(this.methodName, var1, 17);

這裡第一行是獲取方法的描述,類似於 ()V 描述方法的引數和返回引數,這裡()V表示獲取0個引數,返回為void的方法

第二行就生成一個MethodInfo物件,檢視其建構函式

public MethodInfo(String var2, String var3, int var4) {
    this.name = var2;
    this.descriptor = var3;
    this.accessFlags = var4;
    ProxyGenerator.this.cp.getUtf8(var2);
    ProxyGenerator.this.cp.getUtf8(var3);
    ProxyGenerator.this.cp.getUtf8("Code");
    ProxyGenerator.this.cp.getUtf8("Exceptions");
}

同樣回顧第二篇中的method_info

method_info {
    u2             access_flags;//access_flag
    u2             name_index;//常量池中的一個有效索引,必須是Utf8型別(表示方法或欄位的名字)
    u2             descriptor_index;//常量池中的一個有效索引,必須是Utf8型別(表示方法的描述)
    u2             attributes_count;//屬性數量
    attribute_info attributes[attributes_count];//屬性的具體內容
}

和field_info不同,除了基礎的access_flags、name_index、descriptor_index外,MethodInfo的建構函式還寫入了2個額外的常量池物件:Code和Exceptions,表示2種attributes

Code表示執行程式碼

Exceptions表示方法會丟擲的異常

同樣,我們接著就檢視MethodInfo中的write方法

寫入access_flags、name_index、descriptor_index

var1.writeShort(this.accessFlags);
var1.writeShort(ProxyGenerator.this.cp.getUtf8(this.name));
var1.writeShort(ProxyGenerator.this.cp.getUtf8(this.descriptor));

寫入屬性的數量

var1.writeShort(2);

此時我們就需要看下attributes的基礎結構了

attribute_info {
    u2 attribute_name_index;//名字在常量池的索引
    u4 attribute_length;//attribute的位元組長度
    u1 info[attribute_length];//attribute的實際資料
}

這裡我們就先了解2種具體的attribute,一個是Code,一個是Exception,正是之前在建構函式中看到的
Code的結構

Code_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 max_stack;
    u2 max_locals;
    u4 code_length;
    u1 code[code_length];
    u2 exception_table_length;
    {   u2 start_pc;
        u2 end_pc;
        u2 handler_pc;
        u2 catch_type;
    } exception_table[exception_table_length];
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

此時我們對應著程式碼來看

首先寫入attribute_name_index

var1.writeShort(ProxyGenerator.this.cp.getUtf8("Code"));

寫入資料長度attribute_length,這裡的12和8會在本文後面解釋

var1.writeInt(12 + this.code.size() + 8 * this.exceptionTable.size());

寫入棧深max_stack和max_locals本地變數數量,這2個值在下一篇文章的generateMethod()方法詳細介紹中涉及到,這裡就先不展開了

var1.writeShort(this.maxStack);
var1.writeShort(this.maxLocals);

寫入方法執行體位元組的長度code_length和方法執行體具體位元組code[code_length],這2部分也會在generateMethod()方法詳細介紹中涉及到,這裡就先不展開了

var1.writeInt(this.code.size());
this.code.writeTo(var1);

此時我們看到寫入max_stack、max_locals、code_length時,欄位的型別分別是short、short、integer,加起共8個位元組

寫入方法會丟擲的異常數量exception_table_length

var1.writeShort(this.exceptionTable.size());

這個時候exception_table_length是一個short型別,加上之前的8個位元組,一共是10個位元組

寫入異常的具體結構

Iterator var2 = this.exceptionTable.iterator();

while(var2.hasNext()) {
    ProxyGenerator.ExceptionTableEntry var3 = (ProxyGenerator.ExceptionTableEntry)var2.next();
    var1.writeShort(var3.startPc);
    var1.writeShort(var3.endPc);
    var1.writeShort(var3.handlerPc);
    var1.writeShort(var3.catchType);
}

每一個異常都有4個欄位,start_pc、end_pc、handler_pc、catch_type,都是short型別,因此一個Exception就會有8個位元組,這個8正對應了上面attribute_length中的8

最後寫入attributes自身的attributes_count,因為沒有,所以直接寫0

var1.writeShort(0);

這個數量是一個short型別,加上之前累積的10個位元組,一共12個位元組,對應了attribute_length中的12

 

接下去看Exception

Exception結構

Exceptions_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 number_of_exceptions;
    u2 exception_index_table[number_of_exceptions];
}

這個結構相對就簡單了很多,下面對應程式碼來看

先寫入常量池的索引attribute_name_index

var1.writeShort(ProxyGenerator.this.cp.getUtf8("Exceptions"));

寫入attribute長度attribute_length,這裡的2個2也在後面解釋,不過我想大家自己也能想到分別代表什麼了吧

var1.writeInt(2 + 2 * this.declaredExceptions.length);

寫入異常數量number_of_exceptions,型別是short,對應了第一個2

var1.writeShort(this.declaredExceptions.length);

寫入具體的異常在常量池中的索引,每一個資料都是一個short,對應了第二個2

var1.writeShort(this.declaredExceptions.length);
short[] var6 = this.declaredExceptions;
int var7 = var6.length;

for(int var4 = 0; var4 < var7; ++var4) {
    short var5 = var6[var4];
    var1.writeShort(var5);
}

以上,欄位和方法的寫入就基本解析就完成了
之後將探究generateMethod()方法最複雜的執行體內容

 

相關文章