一文學會 Java 動態代理機制

wingjay發表於2019-04-22

之前的文章裡講解過了Java的反射機制垃圾回收機制,這一次我們來講解一個更有意思的機制:動態代理。學習下Java裡為什麼出現這樣一個機制,什麼場合下會使用這個機制。

靜態代理

常規的代理模式有以下三個部分組成: 功能介面

interface IFunction {
	void doAThing();
}
複製程式碼

功能提供者

class FunctionProvider implement IFunction {
	public void doAThing {
		System.out.print("do A");
	}
}
複製程式碼

功能代理者

class Proxy implement IFunction {
	private FunctionProvider provider;
	Proxy(FunctionProvider provider) {
		this.provider = provider;
	}

	public void doAThing {
		provider.doAThing();
	}
}
複製程式碼

前兩者就是普通的介面和實現類,而第三個就是所謂的代理類。對於使用者而言,他會讓代理類去完成某件任務,並不關心這件任務具體的跑腿者。

這就是靜態代理,好處是方便調整變換具體實現類,而使用者不會受到任何影響。

不過這種方式也存在弊端:比如有多個介面需要進行代理,那麼就要為每一個功能提供者建立對應的一個代理類,那就會越來越龐大。而且,所謂的“靜態”代理,意味著必須提前知道被代理的委託類。

通過下面一個例子來說明下:

統計函式耗時--靜態代理實現

現在希望通過一個代理類,對我感興趣的方法進行耗時統計,利用靜態代理有如下實現:

interface IAFunc {
	void doA();
}
interface IBFunc {
	void doB();
}
複製程式碼
class TimeConsumeProxy implement IAFunc, IBFunc {
	private AFunc a;
	private BFunc b;
	public(AFunc a, BFunc b) {
		this.a = a;
		this.b = b;
	}
	void doA() {
		long start = System.currentMillions();

		a.doA();
		
		System.out.println("耗時:" + (System.currentMillions() - start));
	}
	void doB() {
		long start = System.currentMillions();

		b.doB();
		
		System.out.println("耗時:" + (System.currentMillions() - start));
	}
}
複製程式碼

弊端很明顯,如果介面越多,每新增一個函式都要去修改這個TimeConsumeProxy代理類:把委託類物件傳進去,實現介面,在函式執行前後統計耗時。

這種方式顯然不是可持續性的,下面來看下使用動態代理的實現方式,進行對比。

動態代理

動態代理的核心思想是通過Java Proxy類,為傳入進來的任意物件動態生成一個代理物件,這個代理物件預設實現了原始物件的所有介面。

還是通過統計函式耗時例子來說明更加直接。

統計函式耗時--動態代理實現

interface IAFunc {
	void doA();
}
interface IBFunc {
	void doB();
}
複製程式碼
class A implement IAFunc { ... }
class B implement IBFunc { ... }
複製程式碼
class TimeConsumeProxy implements InvocationHandler {
	private Object realObject;

	public Object bind(Object realObject) {
		this.realObject = realObject;
		Object proxyObject = Proxy.newInstance(
			realObject.getClass().getClassLoader(),
			realObject.getClass().getInterfaces(),
			this
		);

		return proxyObject;
	}

	@Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        long start = System.currentMillions();

        Object result = method.invoke(target, args);

        System.out.println("耗時:" + (System.currentMillions() - start));

        return result;
    }
}
複製程式碼

具體使用時:

public static void main(String[] args) {
	A a = new A();
	IAFunc aProxy = (IAFunc) new TimeConsumeProxy().bind(a);
	aProxy.doA();

	B b = new B();
	IBFunc bProxy = (IBFunc) new TimeConsumeProxy().bind(b);
	bProxy.doB();	
}
複製程式碼

這裡最大的區別就是:代理類和委託類互相透明獨立,邏輯沒有任何耦合,在執行時才繫結在一起。這也就是靜態代理與動態代理最大的不同,帶來的好處就是:無論委託類有多少個,代理類不受到任何影響,而且在編譯時無需知道具體委託類。

回到動態代理本身,上面程式碼中最重要的就是:

Object proxyObject = Proxy.newInstance(
			realObject.getClass().getClassLoader(),
			realObject.getClass().getInterfaces(),
			this
		);
複製程式碼

通過Proxy工具,把真實委託類轉換成了一個代理類,最開始提到了一個代理模式的三要素:功能介面、功能提供者、功能代理者;在這裡對應的就是:realObject.getClass().getInterfaces()realObjectTimeConsumeProxy

其實動態代理並不複雜,通過一個Proxy工具,為委託類的介面自動生成一個代理物件,後續的函式呼叫都通過這個代理物件進行發起,最終會執行到InvocationHandler#invoke方法,在這個方法裡除了呼叫真實委託類對應的方法,還可以做一些其他自定義的邏輯,比如上面的執行耗時統計等。

探索動態代理實現機制

好了,上面我們已經把動態代理的基本用法及為什麼要用動態代理進行了講解,很多文章到這裡也差不多了,不過我們還準備進一步探索一下給感興趣的讀者。

丟擲幾個問題:

  1. 上面生成的代理物件Object proxyObject究竟是個什麼東西?為什麼它可以轉型成IAFunc,還能呼叫doA()方法?
  2. 這個proxyObject是怎麼生成出來的?它是一個class嗎?

下面我先給出答案,再一步步探究這個答案是如何來的。

問題一: proxyObject究竟是個什麼 -> 動態生成的$Proxy0.class檔案

在呼叫Proxy.newInstance後,Java最終會為委託類A生成一個真實的class檔案:$Proxy0.class,而proxyObject就是這個class的一個例項。

猜一下,這個$Proxy0.class類長什麼樣呢,包含了什麼方法呢?回看下剛剛的程式碼:

IAFunc aProxy = (IAFunc) new TimeConsumeProxy().bind(a);
aProxy.doA();
複製程式碼

推理下,顯然這個$Proxy0.class實現了 IAFunc 介面,同時它內部也實現了doA()方法,而且重點是:這個doA()方法在執行時會執行到TimeConsumeProxy#invoke()方法裡。

重點來了!下面我們來看下這個$Proxy0.class檔案,把它放進IDE反編譯下,可以看到如下內容,來驗證下剛剛的猜想:

final class $Proxy0 extends Proxy implements IAFunc {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m0;

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

    public final boolean equals(Object var1) throws  {
        // 省略
    }

    public final void doA() throws  {
        try {
        	// 劃重點
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final String toString() throws  {
        // 省略
    }

    public final int hashCode() throws  {
        // 省略
    }

    static {
        try {
        	// 劃重點
            m3 = Class.forName("proxy.IAFunc").getMethod("doA", new Class[0]);

            m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[]{Class.forName("java.lang.Object")});
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}
複製程式碼

沒錯,剛剛的猜想都中了!實現了IAFunc介面和doA()方法,不過,doA()裡是什麼鬼?

super.h.invoke(this, m3, (Object[])null);
複製程式碼

回看下,TimeConsumeProxy裡面的invoke方法,它的函式簽名是啥?

public Object invoke(Object proxy, Method method, Object[] args);
複製程式碼

沒錯,doA()裡做的就是呼叫TimeConsumeProxy#invoke()方法。

那麼也就是說,下面這段程式碼執行流程如下:

IAFunc aProxy = (IAFunc) new TimeConsumeProxy().bind(a);
aProxy.doA();
複製程式碼
  1. 基於傳入的委託類A,生成一個$Proxy0.class檔案;
  2. 建立一個$Proxy0.class物件,轉型為IAFunc介面;
  3. 呼叫aProxy.doA()時,自動呼叫TimeConsumeProxy內部的invoke方法。

問題二:proxyObject 是怎麼一步步生成出來的 -> $Proxy0.class檔案生成流程

剛剛從末尾看了結果,現在我們回到程式碼的起始端來看:

Object proxyObject = Proxy.newInstance(
			realObject.getClass().getClassLoader(),
			realObject.getClass().getInterfaces(),
			this
		);
複製程式碼

準備好,開始發車讀原始碼了。我會擷取重要的程式碼並加上註釋。

先看Proxy.newInstance():

public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h) {
    //複製要代理的介面                                  	
	final Class<?>[] intfs = interfaces.clone();

	//重點:生成 $Proxy0.class 檔案並通過 ClassLoader 載入進來
	Class<?> cl = getProxyClass0(loader, intfs);

	//對$Proxy0.class生成一個例項,就是`proxyObject`
	final Constructor<?> cons = cl.getConstructor(constructorParams);
	return cons.newInstance(new Object[]{h});
}
複製程式碼

再來看 getProxyClass0 的具體實現:ProxyClassFactory工廠類:

@Override
public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
	// 引數為ClassLoader和要代理的介面

	Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);

	// 1. 驗證ClassLoader和介面有效性
	for (Class<?> intf : interfaces) {
		// 驗證classLoader正確性
		Class<?> interfaceClass = Class.forName(intf.getName(), false, loader);
		if (interfaceClass != intf) {
            throw new IllegalArgumentException(
                intf + " is not visible from class loader");
        }

		// 驗證傳入的介面class有效
		if (!interfaceClass.isInterface()) { ... } 

		// 驗證介面是否重複
		if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) { ... }
	}

	// 2. 建立包名及類名 $Proxy0.class
	proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
	long num = nextUniqueNumber.getAndIncrement();
    String proxyName = proxyPkg + proxyClassNamePrefix + num;

    // 3. 建立class位元組碼內容
    byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags);

    // 4. 基於位元組碼和類名,生成Class<?>物件
    return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length);
}
複製程式碼

再看下第三步生成class內容 ProxyGenerator.generateProxyClass

// 新增 hashCode equals toString方法
addProxyMethod(hashCodeMethod, Object.class);
addProxyMethod(equalsMethod, Object.class);
addProxyMethod(toStringMethod, Object.class);

// 新增委託類的介面實現
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]);
    }
}

// 新增建構函式
methods.add(this.generateConstructor());
複製程式碼

這裡構造好了類的內容:新增必要的函式,實現介面,建構函式等,下面就是要寫入上一步看到的 $Proxy0.class 了。

 ByteArrayOutputStream bout = new ByteArrayOutputStream();
 DataOutputStream dout = new DataOutputStream(bout);
 dout.writeInt(0xCAFEBABE);
 ...
 dout.writeShort(ACC_PUBLIC | ACC_FINAL | ACC_SUPER);
 ...
 return bout.toByteArray();
複製程式碼

到這裡就生成了第一步看到的$Proxy0.class檔案了,完成閉環,講解完成!

動態代理小結

通過上面的講解可以看出,動態代理可以隨時為任意的委託類進行代理,並可以在InvocationHandler#invoke拿到執行時的資訊,並可以做一些切面處理。

在動態代理背後,其實是為一個委託類動態生成了一個$Proxy0.class的代理類,該代理類會實現委託類的介面,並把介面呼叫轉發到InvocationHandler#invoke上,最終呼叫到真實委託類的對應方法。

動態代理機制把委託類和代理類進行了隔離,提高了擴充套件性。

Java動態代理與Python裝飾器

這是Java語言提供的一個有意思的語言特性,而其實Python裡也提供了一種類似的特性:裝飾器,可以達到類似的面相切面程式設計思想,下次有空再把兩者做下對比,這次先到這。

--

謝謝。

wingjay

我的更多文章,歡迎關注:

公眾號

相關文章