Java,JDK動態代理的原理分析
1. 代理基本概念:
以下是代理概念的百度解釋:代理(百度百科)
總之一句話:三個元素,資料--->代理物件--->真實物件;複雜一點的可以理解為五個元素:輸入資料--->代理物件--->真實物件--->代理物件--->輸出資料。
2. JDK的動態代理概念:
JDK的動態代理和正常的代理邏輯有些區別。
首先先明確一下術語:類 class ,介面 interface。
JDK動態代理是基於 interface 建立的,而不是真正的物件;也就是說,即使沒有真正的物件,JDK依然可以建立代理物件。下面用程式碼來解釋:
public class JDKProxy implements InvocationHandler{ public Object getObject(TestInterface ref){ return Proxy.newProxyInstance(getClass().getClassLoader(), ref.getClass().getInterfaces(), this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throwsThrowable { return null; } }
當然,實際中使用的情況是有真正的物件的,像下面這樣:
public class JDKProxy implements InvocationHandler{ TestInterface ref; public Object getObject(TestInterface ref){ this.ref = ref; return Proxy.newProxyInstance(getClass().getClassLoader(), ref.getClass().getInterfaces(), this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("doBefore"); Object o = method.invoke(ref, args); System.out.println("doAfter"); return o; } }
那麼,和正常的代理邏輯區別就在這裡了,JDK的動態代理多依賴一個元素,就是被代理物件ref所實現的介面。如果ref物件沒有實現任何介面,那麼這個物件是無法被代理的。
那麼問題來了: 為什麼Java自帶的動態代理 選擇 要基於介面 ?基於什麼考慮,或者說Java如果 選擇 直接 代理真正的物件會有什麼問題?
3. 進入正題:JDK動態代理是如何實現的?(基於JDK1.8)
Java中涉及到的關鍵先生:InvocationHandler , Proxy
3.1 使用方法及引數詳細解釋
程式碼使用方法:
//代理類,實現InvocationHandler
public class JDKProxy implements InvocationHandler{ private UserService userServiceRef;
//獲取代理物件 public UserService getProxy(UserService userServiceRef){ this.userServiceRef = userServiceRef;
//Proxy生成代理物件 return (UserService) Proxy.newProxyInstance(getClass().getClassLoader(), userServiceRef.getClass().getInterfaces(), this); }
//代理物件做的事 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("someone is logining"); Object returnObject = method.invoke(userServiceRef, args); System.out.println("someone login success"); return returnObject; } }
下面是InvocationHandler的介面描述:
package java.lang.reflect;
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}
其中的引數:
proxy: 代理物件本身,也就是 getProxy(UserService userServiceRef) 獲取到的物件。大家思考一下,為什麼要把這個代理物件作為引數傳進來?
我個人覺得這是個完全沒有必要的引數。
method:userServiceRef中的方法。
args: method方法的引數。
再來看一下Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)方法引數:
loader:類載入器,用來載入代理類的,即Proxy.newProxyInstance()的返回結果的類位元組碼。
interfaces:代理物件所實現的介面。 這裡介面是陣列引數,通常被代理只實現一個介面。那實現多個介面時使用代理物件有什麼問題?其實也沒問題,就是呼叫不同介面的方法前需要先強轉為對應的介面類,麻煩。
h:實現InvocationHandler的類,也就是示例程式碼中的JDKProxy類 。
測試程式碼:
public static void main(String[] args) { UserService userService = new UserServiceImpl(); JDKProxy proxy = new JDKProxy(); UserService userServiceProxy = proxy.getProxy(userService); userServiceProxy.login(); }
3.2 實現的原理與細節:
1.代理物件的建立過程:
建立代理物件的方法:Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
通過這個方法的引數其實可以看到一些眉目,loader用來載入代理類位元組碼,interfaces作為代理類實現的介面,h為代理物件實際呼叫的方法(即invoke方法)。
建立過程大致分為幾步:
-
- 從快取中獲取代理物件,獲取到則直接返回;
快取由WeakCache中的ConcurrentMap<Object, ConcurrentMap<Object, Supplier<V>>> map來儲存;
為什麼是兩級Map? 想一想,Java類的唯一性由ClassLoader+Class決定,所以Key Object是ClassLoader,Key Object是所有介面組成的物件().
WeakCache<ClassLoader, Class<?>[], Class<?>> proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());
其中KeyFactory的作用就是將interfaces轉換為Key Object 。
-
- 生成代理物件的類名proxyName;
由 private static final class ProxyClassFactory 來完成.
private static final String proxyClassNamePrefix = "$Proxy"; //ProxyClassFactory // 每次使用時 自增1 private static final AtomicLong nextUniqueNumber = new AtomicLong();//ProxyClassFactory
ReflectUtil.PROXY_PACKAGE = "com.sun.proxy";//RelectionUtil
proxyName = PROXY_PACKAGE + proxyClassNamePrefix + nextUniqueNumber;
-
- 生成proxyName類的位元組碼;
由 sun.misc.ProxyGenerator.generateProxyClass(String proxyName, Class<?>[] interfaces, int accessFlags) ,方法返回二進位制位元組碼。
引數: 類名,需要實現的介面,訪問標誌。
-
- 將位元組碼載入到虛擬機器中,即方法區記憶體(jdk1.7之前是永久代,jdk8之後的元資料區);
由 java.lang.reflect.Proxy.defineClass0(ClassLoader loader, String proxyName, byte[] proxyClass, int offset, int length) 完成。
private static native Class<?> defineClass0(ClassLoader loader, String name, byte[] b, int off, int len);
這是一個本地方法,通過JNI呼叫,返回代理的Class物件。
-
- 生成代理Class物件的例項;構造器例項化
生成代理物件的三個引數中的 interfaces, classLoader都使用過了,還有一個InvocationHandler h 沒有使用。
通過反射獲取代理Class的引數為InvocationHandler的構造器,通過 Constructor.newInstance(new Object[]{h}); 返回最後的代理例項物件。
建立代理的過程就完成了。
2. 代理物件的位元組碼分析:
還是以上面3.1的例子分析,測試程式碼稍作修改,如下:
public static void main(String[] args) { System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); //生成的代理物件位元組碼儲存到.class檔案中。 JDKProxy proxy = new JDKProxy(); proxy.getProxy(new UserServiceImpl()); //生成代理物件 }
執行測試程式碼之後,user.dir目錄下會多出一個目錄:com/sun/proxy,開啟後可以看到$Proxy0.class檔案。
jd-gui反編譯該class檔案:為方便閱讀,我把方法裡的try catch全部移掉了。
package com.sun.proxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.lang.reflect.UndeclaredThrowableException; import myproxy.UserService; public final class $Proxy0 extends Proxy implements UserService { private static Method m1; private static Method m2; private static Method m3; private static Method m0; public $Proxy0(InvocationHandler paramInvocationHandler){ super(paramInvocationHandler); //h引用在父類Proxy中 } public final boolean equals(Object paramObject){ return ((Boolean)this.h.invoke(this, m1, new Object[] { paramObject })).booleanValue(); } public final String toString(){ return (String)this.h.invoke(this, m2, null); } public final boolean login(){ return ((Boolean)this.h.invoke(this, m3, null)).booleanValue(); //boolean存在包裝和解包裝 } public final int hashCode(){ return ((Integer)this.h.invoke(this, m0, null)).intValue(); } static{ 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]); m3 = Class.forName("myproxy.UserService").getMethod("login", new Class[0]); m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]); return; } }
一目瞭然。主要五部分內容:
代理物件繼承了Proxy類,並實現了目標介面。
生成了以InvocationHandler為引數的構造器,例項化時將我們的自定JDKProxy(實現了InvocationHandler,並持有被代理物件)傳遞進去;
生成了Object中的三個方法:equals, hashCode, toString;
生成了介面中的所有方法,全部呼叫InvocationHandler物件的invoke方法;
生成了對應方法的Method物件屬性,傳遞給invoke方法。
3 .提示細節:
-
- 對於有參和無參方法,都是通過invoke方法呼叫,無參方法會直接傳入null,所以在invoke方法中使用args引數時一定要先進行null的判斷;
- 對於原始資料型別(int,boolean等8種),代理物件的方法中引數和返回值都進行了包裝和解包裝。
- 代理物件生成過程中用到了反射,生成位元組碼時,反射Object物件方法和反射介面方法;生成例項時,反射獲取代理物件的構造器;代理物件方法呼叫過程中是沒有使用反射的。
- 有沒有感覺跟 裝飾器模式 有一些 異曲同工 呢?
4. 以上就是個人總結分享的JDK動態代理的內容,原創內容,轉載請註明出處。
個人水平有限,有誤的地方歡迎評論中指正。
byte[]