1. 程式人生 > >JDK動態代理(4)ProxyGenerator生成代理類的位元組碼檔案解析

JDK動態代理(4)ProxyGenerator生成代理類的位元組碼檔案解析

通過前面幾篇的分析,我們知道代理類是通過Proxy類的ProxyClassFactory工廠生成的,這個工廠類會去呼叫ProxyGenerator類的generateProxyClass()方法來生成代理類的位元組碼。ProxyGenerator這個類存放在sun.misc包下,我們可以通過OpenJDK原始碼來找到這個類,該類的generateProxyClass()靜態方法的核心內容就是去呼叫generateClassFile()例項方法來生成Class檔案。我們直接來看generateClassFile()這個方法內部做了些什麼。

private byte[] generateClassFile
() { //第一步, 將所有的方法組裝成ProxyMethod物件 //首先為代理類生成toString, hashCode, equals等代理方法 addProxyMethod(hashCodeMethod, Object.class); addProxyMethod(equalsMethod, Object.class); addProxyMethod(toStringMethod, Object.class); //遍歷每一個介面的每一個方法, 並且為其生成ProxyMethod物件 for (int i = 0; i < interfaces.
length; i++) { Method[] methods = interfaces[i].getMethods(); for (int j = 0; j < methods.length; j++) { addProxyMethod(methods[j], interfaces[i]); } } //對於具有相同簽名的代理方法, 檢驗方法的返回值是否相容 for (List<ProxyMethod> sigmethods : proxyMethods.values()) { checkReturnTypes
(sigmethods); } //第二步, 組裝要生成的class檔案的所有的欄位資訊和方法資訊 try { //新增構造器方法 methods.add(generateConstructor()); //遍歷快取中的代理方法 for (List<ProxyMethod> sigmethods : proxyMethods.values()) { for (ProxyMethod pm : sigmethods) { //新增代理類的靜態欄位, 例如:private static Method m1; fields.add(new FieldInfo(pm.methodFieldName, "Ljava/lang/reflect/Method;", ACC_PRIVATE | ACC_STATIC)); //新增代理類的代理方法 methods.add(pm.generateMethod()); } } //新增代理類的靜態欄位初始化方法 methods.add(generateStaticInitializer()); } catch (IOException e) { throw new InternalError("unexpected I/O Exception"); } //驗證方法和欄位集合不能大於65535 if (methods.size() > 65535) { throw new IllegalArgumentException("method limit exceeded"); } if (fields.size() > 65535) { throw new IllegalArgumentException("field limit exceeded"); } //第三步, 寫入最終的class檔案 //驗證常量池中存在代理類的全限定名 cp.getClass(dotToSlash(className)); //驗證常量池中存在代理類父類的全限定名, 父類名為:"java/lang/reflect/Proxy" cp.getClass(superclassName); //驗證常量池存在代理類介面的全限定名 for (int i = 0; i < interfaces.length; i++) { cp.getClass(dotToSlash(interfaces[i].getName())); } //接下來要開始寫入檔案了,設定常量池只讀 cp.setReadOnly(); ByteArrayOutputStream bout = new ByteArrayOutputStream(); DataOutputStream dout = new DataOutputStream(bout); try { //1.寫入魔數 dout.writeInt(0xCAFEBABE); //2.寫入次版本號 dout.writeShort(CLASSFILE_MINOR_VERSION); //3.寫入主版本號 dout.writeShort(CLASSFILE_MAJOR_VERSION); //4.寫入常量池 cp.write(dout); //5.寫入訪問修飾符 dout.writeShort(ACC_PUBLIC | ACC_FINAL | ACC_SUPER); //6.寫入類索引 dout.writeShort(cp.getClass(dotToSlash(className))); //7.寫入父類索引, 生成的代理類都繼承自Proxy dout.writeShort(cp.getClass(superclassName)); //8.寫入介面計數值 dout.writeShort(interfaces.length); //9.寫入介面集合 for (int i = 0; i < interfaces.length; i++) { dout.writeShort(cp.getClass(dotToSlash(interfaces[i].getName()))); } //10.寫入欄位計數值 dout.writeShort(fields.size()); //11.寫入欄位集合 for (FieldInfo f : fields) { f.write(dout); } //12.寫入方法計數值 dout.writeShort(methods.size()); //13.寫入方法集合 for (MethodInfo m : methods) { m.write(dout); } //14.寫入屬性計數值, 代理類class檔案沒有屬性所以為0 dout.writeShort(0); } catch (IOException e) { throw new InternalError("unexpected I/O Exception"); } //轉換成二進位制陣列輸出 return bout.toByteArray(); }

可以看到generateClassFile()方法是按照Class檔案結構進行動態拼接的。什麼是Class檔案呢?在這裡我們先要說明下,我們平時編寫的Java檔案是以.java結尾的,在編寫好了之後通過編譯器進行編譯會生成.class檔案,這個.class檔案就是Class檔案。Java程式的執行只依賴於Class檔案,和Java檔案是沒有關係的。這個Class檔案描述了一個類的資訊,當我們需要使用到一個類時,Java虛擬機器就會提前去載入這個類的Class檔案並進行初始化和相關的檢驗工作,Java虛擬機器能夠保證在你使用到這個類之前就會完成這些工作,我們只需要安心的去使用它就好了,而不必關心Java虛擬機器是怎樣載入它的。當然,Class檔案並不一定非得通過編譯Java檔案而來,你甚至可以直接通過文字編輯器來編寫Class檔案。在這裡,JDK動態代理就是通過程式來動態生成Class檔案的。我們再次回到上面的程式碼中,可以看到,生成Class檔案主要分為三步:

  1. 收集所有要生成的代理方法,將其包裝成ProxyMethod物件並註冊到Map集合中。

  2. 收集所有要為Class檔案生成的欄位資訊和方法資訊。

  3. 完成了上面的工作後,開始組裝Class檔案。

我們知道一個類的核心部分就是它的欄位和方法。我們重點聚焦第二步,看看它為代理類生成了哪些欄位和方法。在第二步中,按順序做了下面四件事。

  1. 為代理類生成一個帶參構造器,傳入InvocationHandler例項的引用並呼叫父類的帶參構造器。

  2. 遍歷代理方法Map集合,為每個代理方法生成對應的Method型別靜態域,並將其新增到fields集合中。

  3. 遍歷代理方法Map集合,為每個代理方法生成對應的MethodInfo物件,並將其新增到methods集合中。

  4. 為代理類生成靜態初始化方法,該靜態初始化方法主要是將每個代理方法的引用賦值給對應的靜態欄位。

通過以上分析,我們可以大致知道JDK動態代理最終會為我們生成如下結構的代理類:

public class Proxy0 extends Proxy implements UserDao {
 
    //第一步, 生成構造器
    protected Proxy0(InvocationHandler h) {
        super(h);
    }
 
    //第二步, 生成靜態域
    private static Method m1;   //hashCode方法
    private static Method m2;   //equals方法
    private static Method m3;   //toString方法
    private static Method m4;   //...
 
    //第三步, 生成代理方法
    @Override
    public int hashCode() {
        try {
            return (int) h.invoke(this, m1, null);
        } catch (Throwable e) {
            throw new UndeclaredThrowableException(e);
        }
    }
 
    @Override
    public boolean equals(Object obj) {
        try {
            Object[] args = new Object[] {obj};
            return (boolean) h.invoke(this, m2, args);
        } catch (Throwable e) {
            throw new UndeclaredThrowableException(e);
        }
    }
 
    @Override
    public String toString() {
        try {
            return (String) h.invoke(this, m3, null);
        } catch (Throwable e) {
            throw new UndeclaredThrowableException(e);
        }
    }
 
    @Override
    public void save(User user) {
        try {
            //構造引數陣列, 如果有多個引數往後面新增就行了
            Object[] args = new Object[] {user};
            h.invoke(this, m4, args);
        } catch (Throwable e) {
            throw new UndeclaredThrowableException(e);
        }
    }
 
    //第四步, 生成靜態初始化方法
    static {
        try {
            Class c1 = Class.forName(Object.class.getName());
            Class c2 = Class.forName(UserDao.class.getName());    
            m1 = c1.getMethod("hashCode", null);
            m2 = c1.getMethod("equals", new Class[]{Object.class});
            m3 = c1.getMethod("toString", null);
            m4 = c2.getMethod("save", new Class[]{User.class});
            //...
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
 
}

至此,經過層層分析,深入探究JDK原始碼,我們還原了動態生成的代理類的本來面目,之前心中存在的一些疑問也隨之得到了很好的解釋

  1. 代理類預設繼承Porxy類,因為Java中只支援單繼承,所以JDK動態代理只能去實現介面。

  2. 代理方法都會去呼叫InvocationHandler的invoke()方法,因此我們需要重寫InvocationHandler的invoke()方法。

  3. 呼叫invoke()方法時會傳入代理例項本身,目標方法和目標方法引數。解釋了invoke()方法的引數是怎樣來的。

在這裡插入圖片描述

使用剛剛構造出來的Proxy0作為代理類再次進行測試,可以看到最終的結果與使用JDK動態生成的代理類的效果是一樣的。再次驗證了我們的分析是可靠且準確的。至此,JDK動態代理系列文章宣告結束。通過本系列的分析,筆者解決了心中長久以來的疑惑,相信讀者們對JDK動態代理的理解也更深了一步。但是紙上得來終覺淺,想要更好的掌握JDK動態代理技術,讀者可參照本系列文章自行查閱JDK原始碼,也可與筆者交流學習心得,指出筆者分析不當的地方,共同學習,共同進步。