class檔案的基本結構及proxy原始碼分析二

tera發表於2020-07-10

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

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

 

接上文,我們需要了解class位元組碼的結構,才能更好地理解後面的程式碼,這裡我直接引用jvm文件中的內容

jvm文件地址:https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html
下面對位元組碼的結構簡單地做了個說明,大部分都是顧名思義

ClassFile {
    u4             magic;//固定的開頭,值為0xCAFEBABE
    u2             minor_version;//版本號,用來標記class的版本
    u2             major_version;//版本號,用來標記class的版本
    u2             constant_pool_count;//靜態池大小,是靜態池物件數量+1
    cp_info        constant_pool[constant_pool_count-1];//靜態池物件,有效索引是1 ~ count-1
    u2             access_flags;//public、final等描述
    u2             this_class;//當前類的資訊
    u2             super_class;//父類的資訊
    u2             interfaces_count;//介面數量
    u2             interfaces[interfaces_count];//介面物件
    u2             fields_count;//欄位數量
    field_info     fields[fields_count];//欄位物件
    u2             methods_count;//方法數量
    method_info    methods[methods_count];//方法物件
    u2             attributes_count;//屬性數量
    attribute_info attributes[attributes_count];//屬性物件
}

為了不成為一篇枯燥的文件翻譯,並且儘快進入Proxy的原始碼,這裡並不會對每一個部分做特別詳細的說明,以把握整體為主

回想上篇文章最後,原始碼我們看到了這裡

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

接下去我們就可以進入generateClassFile()方法了

把握整體,我們先跳過一部分細節程式碼,先看下面這部分(這裡我做了一個可讀性的變數名修改)

注意對照著Class的位元組結構來看

最終輸出的位元組流

ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
DataOutputStream data = new DataOutputStream(byteStream);

寫入固定開頭magic,這裡-889275714就是對應0xCAFEBABE

data.writeInt(-889275714);

寫入版本號

data.writeShort(0);//minor_version
data.writeShort(49);//major_version

寫入常量池,這裡cp就是指constant pool

this.cp.write(data);

這裡我們需要進入cp的write方法看一下,也先不要糾結Entry的細節,我們還是先把握整體

public void write(OutputStream var1) throws IOException {
DataOutputStream var2 = new DataOutputStream(var1);
/**
* 這裡寫入cp的大小,注意size()+1,可以和之前Class結構中的constant_pool_count對應
*/
var2.writeShort(this.pool.size() + 1);
Iterator var3 = this.pool.iterator();
/**
* 遍歷cp中的物件,寫入詳細資訊,對應Class結構中的cp_info
*/
while(var3.hasNext()) {
ProxyGenerator.ConstantPool.Entry var4 = (ProxyGenerator.ConstantPool.Entry)var3.next();
var4.write(var2);
}
}

接著我們回到外層方法,繼續往下看

寫入access_flag

data.writeShort(this.accessFlags);

寫入當前類的資訊

data.writeShort.writeShort(this.cp.getClass(dotToSlash(this.className)));

寫入父類的資訊(回想類的屬性第一條,繼承了Proxy類)

data.writeShort.writeShort(this.cp.getClass("java/lang/reflect/Proxy"));

寫入介面數量

data.writeShort.writeShort(this.interfaces.length);

遍歷介面,寫入介面資訊

Class[] interfaces = this.interfaces;
int interfaceLength = interfaces.length;
for (int i = 0; i < interfaceLength; ++i) {
    Class intf = interfaces[i];
    data.writeShort(this.cp.getClass(dotToSlash(intf.getName())));
}

寫入欄位數量

data.writeShort(this.fields.size());

遍歷欄位,寫入欄位資訊

fieldInerator = this.fields.iterator();
while(fieldInerator.hasNext()) {
    ProxyGenerator.FieldInfo fieldInfo = (ProxyGenerator.FieldInfo) fieldInerator.next();
    fieldInfo.write(data);
}

寫入方法數量

data.writeShort(this.methods.size());

遍歷方法,寫入方法資訊

methodIterator = this.methods.iterator();
while(methodIterator.hasNext()) {
    ProxyGenerator.MethodInfo methodInfo = (ProxyGenerator.MethodInfo) methodIterator.next();
    methodInfo.write(data);
}

因為該類沒有特別的attribute,因此attribute數量直接寫0

data.writeShort(0);

正和之前的類結構完全一一對應,此時我們對proxy所做的事情就有了一個整體的把握

 


 

瞭解了整體之後,下面再深入介紹一下位元組碼中部分物件的具體格式,為後面進一步看Proxy的原始碼做一些準備
為了更好地理解下面的內容,我們先定義一個簡單的類Test.java

public class Test implements TestInt {
    private int field = 1;

public int add(int a, int b) {
return a + b;
}
}

interface TestInt {
}

生成.class檔案

javac Test.java

檢視.class檔案

javap -v Test.class

得到結果

Classfile /Users/tianjiyuan/Documents/jvm/Test.class
  Last modified 2020-7-3; size 292 bytes
  MD5 checksum 1afecf9ea44088238bc8aa9804b28208
  Compiled from "Test.java"
public class Test implements TestInt
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#16         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#17         // Test.field:I
   #3 = Class              #18            // Test
   #4 = Class              #19            // java/lang/Object
   #5 = Class              #20            // TestInt
   #6 = Utf8               field
   #7 = Utf8               I
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               add
  #13 = Utf8               (II)I
  #14 = Utf8               SourceFile
  #15 = Utf8               Test.java
  #16 = NameAndType        #8:#9          // "<init>":()V
  #17 = NameAndType        #6:#7          // field:I
  #18 = Utf8               Test
  #19 = Utf8               java/lang/Object
  #20 = Utf8               TestInt
{
  public Test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_1
         6: putfield      #2                  // Field field:I
         9: return
      LineNumberTable:
        line 1: 0
        line 2: 4

  public int add(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3
         0: iload_1
         1: iload_2
         2: iadd
         3: ireturn
      LineNumberTable:
        line 5: 0
}
SourceFile: "Test.java"

我們先看下面這3個部分正對應minor_version,major_version,access_flags

minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER

接著看Constant Pool

Constant pool:
   #1 = Methodref          #4.#16         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#17         // Test.field:I
   #3 = Class              #18            // Test
   ...
   #6 = Utf8               field
...
#16 = NameAndType #8:#9 // "<init>":()V

其中有如下幾種型別

Methodref :方法的引用
Fieldref:欄位的引用
Class :類的引用
Utf8 :字串的引用
NameAndType 型別的描述

 

下面一個一個介紹

Class結構

CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

表示一個類的引用
tag:表示自身的編號
name_index:必須是常量池中的有效索引,用來表示類的名字
例如

#3 = Class              #18            // Test

tag = 3,表示自身索引為3

name_index = 18,表示名字的索引是18

此時我們檢視#18,即這個類的名字是Test

#18 = Utf8               Test

 

Field、Method、Interface結構

文件中這3者是放在一起的

CONSTANT_Fieldref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

CONSTANT_Methodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

CONSTANT_InterfaceMethodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

表示一個欄位、方法、介面方法的引用

tag:表示自身編號
class_index:表示常量池中的一個有效索引
  如果是Methodref_info必須是Class型別的
  如果是InterfaceMethodref_info則必須是一個Interface
  如果是Fieldref_info則可以是Class或者是Interface
name_and_type_index:表示常量池中的一個有效索引(表示方法的名字、返回型別、引數)
  如果是Fieldref_info,則必須是一個對欄位的描述,否則必須是一個對方法的描述

例如

#1 = Methodref          #4.#16         // java/lang/Object."<init>":()V

tag = 1,表示自身索引為1
class_index = 4,表示型別是索引為4的類
name_and_type_index = 16,表示方法的描述為索引16

檢視4和16

   #4 = Class              #19            // java/lang/Object
  #16 = NameAndType        #8:#9          // "<init>":()V

即表示這個方法是Object類中的建構函式

 

NameAndType結構

CONSTANT_NameAndType_info {
    u1 tag;
    u2 name_index;
    u2 descriptor_index;
}

用來表示一個方法或者欄位,其中不包括該欄位或方法所屬的類

tag:表示自身編號
name_index:常量池中的一個有效索引,必須是Utf8型別(表示方法或欄位的名字)
descriptor_index:常量池中的一個有效索引,必須是Utf8型別(表示方法的返回型別和引數)

例如

#16 = NameAndType        #8:#9          // "<init>":()V

tag = 16
name_index = 8
descriptor_index = 9

檢視索引8和9

   #8 = Utf8               <init>
   #9 = Utf8               ()V

方法名為<init>表示建構函式,引數0個,返回值為void

 

UTF-8結構

CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

表示一個字串常量

tag:表示自身編號
length:表示byte陣列的長度
bytes[length]:表示具體資料內容
這個部分其實還有很多細節,不過這裡就不展開了,有興趣的可以自行檢視jvm文件,後面會有文章詳細分析

 

常量池的內容就介紹到這裡,接下去我們還需要看下類結構的其他成員

this_class,必須是一個有效的常量池索引,需要是CONSTANT_Class_info型別的
super_class,必須是一個有效的常量池索引,需要是CONSTANT_Class_info型別的或者為0,表示沒有父類
interfaces_count,介面數量,一個int值
interfaces[],介面陣列,陣列中的值必須是一個常量池的有效索引,需要是CONSTANT_Class_info型別
fields_count,欄位數量

fields[],欄位陣列,陣列中的值都是field_info結構

field_info {
    u2             access_flags;//access_flag
    u2             name_index;//常量池中的一個有效索引,必須是Utf8型別(表示方法或欄位的名字)
    u2             descriptor_index;//常量池中的一個有效索引,必須是Utf8型別(表示欄位的描述)
    u2             attributes_count;//跳過,本文不涉及
    attribute_info attributes[attributes_count];//跳過,本文不涉及
}

methods_count,方法數量
methods[],方法陣列,結構如下

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

class檔案的一些基本結構就介紹到這裡,下一篇文章中會繼續結合Proxy的原始碼,進一步深入瞭解class的各種結構究竟是怎麼被構造的

 

相關文章