一文學會 Java 動態代理機制

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

靜態代理

常規的代理模式有如下三個部分組成: 功能接口編程

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

功能提供者bash

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

功能代理者app

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

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

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

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

不過這種方式也存在弊端:好比有多個接口須要進行代理,那麼就要爲每個功能提供者建立對應的一個代理類,那就會愈來愈龐大。並且,所謂的「靜態」代理,意味着必須提早知道被代理的委託類。工具

經過下面一個例子來講明下:學習

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

如今但願經過一個代理類,對我感興趣的方法進行耗時統計,利用靜態代理有以下實現:ui

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代理類:把委託類對象傳進去,實現接口,在函數執行先後統計耗時。this

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

動態代理

動態代理的核心思想是經過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

個人更多文章,歡迎關注:

公衆號
相關文章
相關標籤/搜索