Java 代碼審計 — 3. Dynamic Proxies

參考:

https://mp.weixin.qq.com/s/HtLjYHLAQQz83aoOI7D0ig

https://zhishihezi.net/b/5d644b6f81cbc9e40460fe7eea3c7925

簡介

代理的概念

此處代理是一種設計思想,指的是對模塊進行封裝,向其添加額外的功能。也就是創建一個代理對象進行包裝,用該代理對象取代原對象,在後續操作中,任何對原對象的調用都要先通過代理對象。

相當於在不修改原模塊代碼的基礎上,對原模塊的功能進行增強。

代理主要使用場景:

  1. 統計方法執行所耗時間。
  2. 在方法執行前後添加日誌。
  3. 檢測方法的參數或返回值。
  4. 方法訪問權限控制。

代理實現有兩種類型,靜態代理和動態代理。動態代理是靜態代理的改進。它可以只使用一個類,一個方法,可以服務多個類的多個方法。

假想有兩個類,學生類,汽車類。在不修改原代碼的情況下,想添加所有方法的執行日誌。

  • 如果採用基於繼承方式的靜態代理實現。就是新創建兩個子類,分別繼承、重寫其父類的每個方法,爲其添加日誌輸出功能。相當於每個類的每個方法都要重寫。
  • 採用動態代理。那就只需要創建一個代理類,一個方法。可以用這個方法服務多個類的多個方法。

在實際中常用的是動態代理,動態代理也有許多實現,此處我們僅關注 jdk 提供的動態代理。

基於 JDK 的動態代理

相關接口和類

import java.lang.reflect.Proxy;
import java.lang.reflect.InvocationHandler;
package java.lang.reflect;

import java.lang.reflect.InvocationHandler;

public class Proxy implements java.io.Serializable {

    // 創建代理對象
    public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
    //ClassLoader loader:執行目標類對象的類加載器即可,用於加載目標類及其接口的代碼
    //Class<?>[] interfaces:指定目標類對象的所有接口的Class對象的數組,通常使用目標類的Class對象調用getInterfaces()即可得到
	//InvocationHandler h:這個參數類型是一個接口,主要關注它裏面唯一一個方法,invoke方法。它會在代理對象調用方法時執行。也就是說,在代理類對象中調用任何方法,都會執行到 invoke()方法。所以在該方法中完成對增強或擴展代碼邏輯

	
	// 獲取動態代理類
    public static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces) 
        
    
    // 返回代理對象綁定的調用處理程序
    public static InvocationHandler getInvocationHandler(Object proxy)
    
    //檢測某個類是否是動態代理類
    public static boolean isProxyClass(Class<?> cl);

    /**
     * 向指定的類加載器中定義一個類對象
     *
     * @param loader 類加載器
     * @param name   類名
     * @param b      類字節碼
     * @param off    截取開始位置
     * @param len    截取長度
     * @return JVM創建的類Class對象
     */
    private static native Class defineClass0(ClassLoader loader, String name, byte[] b, int off, int len);

}
package java.lang.reflect;

import java.lang.reflect.Method;

/**
 * 每個代理實例都具有一個關聯的調用處理程序。對代理實例調用方法時,將對方法調用進行編碼並
 * 將其指派到它的調用處理程序的 invoke 方法。
 */
public interface InvocationHandler {

    // 在代理實例上處理方法調用並返回結果。在與方法關聯的代理實例上調用方法時,將在調用處理程序上調用此方法。
	// proxy:就是代理類對象的一個引用,也就是 Proxy.newProxyInstrance的返回值,此引用幾乎不會用到。
    // method:對應的觸發invoke執行的方法的Method對象。
    // args   包含傳入代理實例上方法調用的參數值的對象數組,如果接口方法不使用參數,則爲 null。基本類型的參數被包裝在適當基本包裝器類(如 java.lang.Integer 或 java.lang.Boolean)的實例中。
	// 返回從代理實例的方法調用返回的值。
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}

代碼示例

接口

import java.io.Serializable;
public interface SayHelloInterface extends Serializable {
    public String sayHello(String name);
}

接口實現類

public class SayHelloImpl implements SayHelloInterface {

    @Override
    public String sayHello(String name) {
        String ret = "this is SayHelloImpl.sayHello method! Let`s say " + name;
        System.out.println(ret);
        return ret;
    }
}

代理對象類

package Proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class SayHelloInvocation implements InvocationHandler {
    private Object target;

    public SayHelloInvocation() {

    }

    public SayHelloInvocation(Object target) {
        this.target = target;
    }

    public Object bind(Object object) {
        this.target = object;
        return Proxy.newProxyInstance(
                object.getClass().getClassLoader(),
                object.getClass().getInterfaces(),
                this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 只對 sayHello 方法進行代理
        if (!("sayHello".equals(method.getName()))) {
            return method.invoke(target, args);
        }

        if(Proxy.getInvocationHandler(proxy) == this){
            System.out.println("proxy == this");
        }
        
        System.out.println("即將調用[ " + target.getClass().getName() + " ]類的[ " + method.getName() + " ]方法...");
        Object obj = method.invoke(target, args);
        System.out.println("已完成[ " + target.getClass().getName() + " ]類的[ " + method.getName() + " ]方法調用...");

        return obj;
    }
}

代碼中調用動態代理類。

package Proxy;

import org.junit.Test;
import java.lang.reflect.Proxy;

public class TestProxy {
    @Test
    public void test1(){
        {
            SayHelloInterface sayHelloProxyInstance = (SayHelloInterface) Proxy.newProxyInstance(
                    SayHelloImpl.class.getClassLoader(),    // 指定動態代理類的類加載器
                    SayHelloImpl.class.getInterfaces(), // new Class[]{SayHelloInterface.class},   // 定義動態代理生成的類實現的接口
                    new SayHelloInvocation(new SayHelloImpl())   // 動態代理處理類
            );
            sayHelloProxyInstance.sayHello("liuyun");
        }

//        {
//            SayHelloInvocation sayHelloInvocation = new SayHelloInvocation();
//            SayHelloInterface sayHelloProxyInstance = (SayHelloInterface) sayHelloInvocation.bind(new SayHelloImpl());
//            sayHelloProxyInstance.sayHello("liuyun");
//        }

//        Object string = ((SayHelloInterface) ((new SayHelloInvocation()).bind(new SayHelloImpl()))).sayHello("liuyun");
    }

}

值得注意的是:

SayHelloInvocation 和 SayHelloImpl 兩個類是並列關係。由於不能相互轉化,所以要使用雙方共同的接口來接收。

動態代理,實際上是在內存中生成了一個對象,該對象實現了指定的目標類對象擁有的接口。所以代理類對象和目標類對象是並列關係。不能相互轉換,在後續使用 Spring 框架時,如果配置 JDK 的動態代理,一定要使用接口類型來接收代理類。

創建代理類實例的兩種方法

我們可以使用 Proxy.newProxyInstance 方法直接創建動態代理類實例,也可以使用Proxy.getProxyClass() 獲取代理類對象再通過反射的方式來創建。

下面我們以 com.anbai.sec.proxy.FileSystem 接口爲例,演示如何創建其動態代理類實例。

// 創建UnixFileSystem類實例
FileSystem fileSystem = new UnixFileSystem();

// 使用JDK動態代理生成FileSystem動態代理類實例
FileSystem proxyInstance = (FileSystem) Proxy.newProxyInstance(
      FileSystem.class.getClassLoader(),// 指定動態代理類的類加載器
      new Class[]{FileSystem.class}, // 定義動態代理生成的類實現的接口
      new JDKInvocationHandler(fileSystem)// 動態代理處理類
);
// 創建UnixFileSystem類實例
FileSystem fileSystem = new UnixFileSystem();

// 創建動態代理處理類
InvocationHandler handler = new JDKInvocationHandler(fileSystem);

// 通過指定類加載器、類實現的接口數組生成一個動態代理類
Class proxyClass = Proxy.getProxyClass(
      FileSystem.class.getClassLoader(),// 指定動態代理類的類加載器
      new Class[]{FileSystem.class}// 定義動態代理生成的類實現的接口
);

// 使用反射獲取Proxy類構造器並創建動態代理類實例
FileSystem proxyInstance = (FileSystem) proxyClass.getConstructor(
      new Class[]{InvocationHandler.class}).newInstance(new Object[]{handler}
);

生成 $ProxyXXX 類代碼分析

在內存中生成代理對象的技術。整個代理過程在內存中進行,不需要手動寫代理類的代碼,也不存在代理類的編譯過程,而是直接在 Java 運行期,憑空在 JVM 中生成一個代理類對象,供我們使用。

動態代理,實際上是在內存中生成了一個對象,該對象實現了指定的目標類對象擁有的接口。所以代理類對象和目標類對象是並列關係。

java.lang.reflect.Proxy 類是通過創建一個新的 Java 類(類名爲com.sun.proxy.$ProxyXXX)的方式來實現無侵入的類方法代理功能的。

動態代理生成出來的類有如下技術細節和特性:

  1. 動態代理的必須是接口類,通過 動態生成一個接口實現類 來代理接口的方法調用(反射機制)。
  2. 動態代理類會由 java.lang.reflect.Proxy.ProxyClassFactory 創建。
  3. ProxyClassFactory 會調用 sun.misc.ProxyGenerator 類生成該類的字節碼,並調用java.lang.reflect.Proxy.defineClass0() 方法將該類註冊到 JVM
  4. 該類繼承於 java.lang.reflect.Proxy 並實現了需要被代理的接口類,因爲 java.lang.reflect.Proxy 類實現了 java.io.Serializable 接口,所以被代理的類支持 序列化與反序列化。
  5. 該類實現了代理接口類(示例中的接口類是com.anbai.sec.proxy.FileSystem),會通過ProxyGenerator動態生成接口類(FileSystem)的所有方法,
  6. 該類因爲實現了代理的接口類,所以當前類是代理的接口類的實例(proxyInstance instanceof FileSystemtrue),但不是代理接口類的實現類的實例(proxyInstance instanceof UnixFileSystemfalse)。
  7. 該類方法中包含了被代理的接口類的所有方法,通過調用動態代理處理類(InvocationHandler)的invoke方法獲取方法執行結果。
  8. 該類代理的方式重寫了 java.lang.Object 類的 toStringhashCodeequals方法。
  9. 如果動過動態代理生成了多個動態代理類,新生成的類名中的0會自增,如com.sun.proxy.$Proxy0/$Proxy1/$Proxy2

動態代理生成的com.sun.proxy.$Proxy0類代碼:

copypackage com.sun.proxy.$Proxy0;

import java.io.File;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements FileSystem {

    private static Method m1;

  // 實現的FileSystem接口方法,如果FileSystem裏面有多個方法那麼在這個類中將從m3開始n個成員變量
    private static Method m3;

    private static Method m0;

    private static Method m2;

    public $Proxy0(InvocationHandler var1) {
        super(var1);
    }

    public final boolean equals(Object var1) {
        try {
            return (Boolean) super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final String[] list(File var1) {
        try {
            return (String[]) super.h.invoke(this, m3, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final int hashCode() {
        try {
            return (Integer) super.h.invoke(this, m0, (Object[]) null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final String toString() {
        try {
            return (String) super.h.invoke(this, m2, (Object[]) null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m3 = Class.forName("com.anbai.sec.proxy.FileSystem").getMethod("list", Class.forName("java.io.File"));
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
            m2 = Class.forName("java.lang.Object").getMethod("toString");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

序列化問題

動態代理類符合 Java 對象序列化條件,並且在 序列化/反序列化時 會被ObjectInputStream/ObjectOutputStream特殊處理。

FileSystemProxySerializationTest 示例代碼:

package com.anbai.sec.proxy;

import java.io.*;
import java.lang.reflect.Proxy;

/**
 * Creator: yz
 * Date: 2020/1/14
 */
public class FileSystemProxySerializationTest {

   public static void main(String[] args) {
      try {
         // 創建UnixFileSystem類實例
         FileSystem fileSystem = new UnixFileSystem();

         // 使用JDK動態代理生成FileSystem動態代理類實例
         FileSystem proxyInstance = (FileSystem) Proxy.newProxyInstance(
               FileSystem.class.getClassLoader(),// 指定動態代理類的類加載器
               new Class[]{FileSystem.class}, // 定義動態代理生成的類實現的接口
               new JDKInvocationHandler(fileSystem)// 動態代理處理類
         );

         ByteArrayOutputStream baos = new ByteArrayOutputStream();

         // 創建Java對象序列化輸出流對象
         ObjectOutputStream out = new ObjectOutputStream(baos);

         // 序列化動態代理類
         out.writeObject(proxyInstance);
         out.flush();
         out.close();

         // 利用動態代理類生成的二進制數組創建二進制輸入流對象用於反序列化操作
         ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());

         // 通過反序列化輸入流(bais),創建Java對象輸入流(ObjectInputStream)對象
         ObjectInputStream in = new ObjectInputStream(bais);

         // 反序列化輸入流數據爲FileSystem對象
         FileSystem test = (FileSystem) in.readObject();

         System.out.println("反序列化類實例類名:" + test.getClass());
         System.out.println("反序列化類實例toString:" + test.toString());
      } catch (IOException e) {
         e.printStackTrace();
      } catch (ClassNotFoundException e) {
         e.printStackTrace();
      }

   }

}

程序執行結果:

反序列化類實例類名:class com.sun.proxy.$Proxy0
反序列化類實例toString:com.anbai.sec.proxy.UnixFileSystem@b07848

動態代理生成的類在反序列化/反序列化時不會序列化該類的成員變量,並且serialVersionUID0L ,也將是說將該類的Class對象傳遞給java.io.ObjectStreamClass的靜態lookup方法時,返回的ObjectStreamClass實例將具有以下特性:

  1. 調用其getSerialVersionUID方法將返回0L
  2. 調用其getFields方法將返回長度爲零的數組。
  3. 調用其getField方法將返回null

但其父類(java.lang.reflect.Proxy)在序列化時不受影響,父類中的h變量(InvocationHandler)將會被序列化,這個h存儲了動態代理類的處理類實例以及動態代理的接口類的實現類的實例。

動態代理生成的對象(com.sun.proxy.$ProxyXXX)序列化的時候會使用一個特殊的協議:TC_PROXYCLASSDESC(0x7D),這個常量在java.io.ObjectStreamConstants中定義的。在反序列化時也不會調用java.io.ObjectInputStream類的resolveClass方法而是調用resolveProxyClass方法來轉換成類對象的。

詳細描述請參考: Dynamic Proxy Classes-Serialization

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章