假設如今項目經理有一個需求:在項目現有全部類的方法先後打印日誌。java
你如何在不修改已有代碼的前提下,完成這個需求?程序員
靜態代理數組
具體作法以下:app
1.爲現有的每個類都編寫一個對應的代理類,而且讓它實現和目標類相同的接口(假設都有)ide
2.在建立代理對象時,經過構造器塞入一個目標對象,而後在代理對象的方法內部調用目標對象同名方法,並在調用先後打印日誌。也就是說,代理對象 = 加強代碼 + 目標對象(原對象),有了代理對象後,就不用原對象了學習
靜態代理的缺陷測試
程序員要手動爲每個目標類,編寫對應的代理類。若是當前系統已經有成百上千個類,工做量太大了。因此,如今咱們的努力方向是:如何少寫或者不寫代理類,卻能完成代理功能?ui
複習對象的建立過程this
首先,在不少初學者的印象中,類和對象的關係是這樣的:編碼
雖然知道源代碼通過javac命令編譯後會在磁盤中獲得字節碼文件(.class文件),也知道java命令會啓動JVM將字節碼文件加載進內存,但也僅僅止步於此了。至於從字節碼文件加載進內存到堆中產生對象,期間具體發生了什麼,他們並不清楚。
所謂「萬物皆對象」,字節碼文件也難逃「被對象」的命運。它被加載進內存後,JVM爲其建立了一個對象,之後全部該類的實例,皆以它爲模板。這個對象叫Class對象,它是Class類的實例。
你們想一想,Class類是用來描述全部類的,好比Person類,Student類...那我如何經過Class類建立Person類的Class對象呢?這樣嗎:
Class clazz = new Class();
好像不對吧,我說這是Student類的Class對象也行啊。有點暈了...
其實,程序員是沒法本身new一個Class對象的,它僅由JVM建立。
因此藉此機會,咱們不妨換種方式看待類和對象:
也就是說,要獲得一個類的實例,關鍵是先獲得該類的Class對象!只不過new這個關鍵字實在太方便,爲咱們隱藏了底層不少細節,我在剛開始學習Java時甚至沒意識到Class對象的存在。
接口Class和類Class的區別
來分析一下接口Class和類Class的區別。以Calculator接口的Class對象和CalculatorImpl實現類的Class對象爲例:
import java.lang.reflect.Constructor; import java.lang.reflect.Executable; import java.lang.reflect.Method; public class ProxyTest { public static void main(String[] args) { /*Calculator接口的Class對象 獲得Class對象的三種方式:1.Class.forName(xxx) 2.xxx.class 3.xxx.getClass() 注意,這並非咱們new了一個Class對象,而是讓虛擬機加載並建立Class對象 */ Class<Calculator> calculatorClazz = Calculator.class; //Calculator接口的構造器信息 Constructor[] calculatorClazzConstructors = calculatorClazz.getConstructors(); //Calculator接口的方法信息 Method[] calculatorClazzMethods = calculatorClazz.getMethods(); //打印 System.out.println("------接口Class的構造器信息------"); printClassInfo(calculatorClazzConstructors); System.out.println("------接口Class的方法信息------"); printClassInfo(calculatorClazzMethods); //Calculator實現類的Class對象 Class<CalculatorImpl> calculatorImplClazz = CalculatorImpl.class; //Calculator實現類的構造器信息 Constructor<?>[] calculatorImplClazzConstructors = calculatorImplClazz.getConstructors(); //Calculator實現類的方法信息 Method[] calculatorImplClazzMethods = calculatorImplClazz.getMethods(); //打印 System.out.println("------實現類Class的構造器信息------"); printClassInfo(calculatorImplClazzConstructors); System.out.println("------實現類Class的方法信息------"); printClassInfo(calculatorImplClazzMethods); } public static void printClassInfo(Executable[] targets){ for (Executable target : targets) { // 構造器/方法名稱 String name = target.getName(); StringBuilder sBuilder = new StringBuilder(name); // 拼接左括號 sBuilder.append('('); Class[] clazzParams = target.getParameterTypes(); // 拼接參數 for(Class clazzParam : clazzParams){ sBuilder.append(clazzParam.getName()).append(','); } //刪除最後一個參數的逗號 if(clazzParams!=null && clazzParams.length != 0) { sBuilder.deleteCharAt(sBuilder.length()-1); } //拼接右括號 sBuilder.append(')'); //打印 構造器/方法 System.out.println(sBuilder.toString()); } } }
運行結果:
也就是說,接口和實現類的Class信息除了構造器,基本類似。
既然咱們但願經過接口建立實例,就沒法避開下面兩個問題:
1.接口方法體缺失問題
首先,接口的Class對象已經獲得,它描述了方法信息。但它沒方法體。
不要緊,反正代理對象的方法是個空殼,只要調用目標對象的方法便可。
JVM能夠在建立代理對象時,隨便糊弄一個空的方法體,反正後期咱們會想辦法把目標對象塞進去調用。
因此這個問題,勉強算是解決。
2.接口Class沒有構造器,沒法new
這個問題好像無解...畢竟這麼多年了,的確沒聽哪位仁兄直接new接口的。可是,仔細想一想,接口之因此不能new,是由於它缺乏構造器,它自己是具有完善的類結構信息的。就像一個武藝高強的大內太監(接口),他空有一身絕世神功(類結構信息),卻後繼無人。若是江湖上有一位妙手聖醫,能克隆他的一身武藝,那麼克隆人不就武藝高強的同時,還能生兒育女了嗎?
因此咱們就想,JDK有沒有提供這麼一個方法,好比getXxxClass(),咱們傳進一個接口Class對象,它幫咱們克隆一個具備相同類結構信息,又具有構造器的新的Class對象呢?
至此,分析完畢,咱們沒法根據接口直接建立對象(廢話)。
那動態代理是怎麼建立實例的呢?它到底有沒有相似getXxxClass()這樣的方法呢?
不錯,動態代理確實存在getXxxClass()這樣的方法。
咱們須要java.lang.reflect.InvocationHandler接口和 java.lang.reflect.Proxy類的支持。Proxy後面會用到InvocationHandler,所以我打算以Proxy爲切入點。首先,再次明確咱們的思路:
經過查看API,咱們發現Proxy類有一個靜態方法能夠幫助咱們。
Proxy.getProxyClass():返回代理類的Class對象。終於找到妙手聖醫。
也就說,只要傳入目標類實現的接口的Class對象,getProxyClass()方法便可返回代理Class對象,而不用實際編寫代理類。這至關於什麼概念?
廢話很少說,開搞。
import java.lang.reflect.Constructor; import java.lang.reflect.Executable; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class ProxyTest { public static void main(String[] args) { /* * 參數1:Calculator的類加載器(當初把Calculator加載進內存的類加載器) * 參數2:代理對象須要和目標對象實現相同接口Calculator * */ Class calculatorProxyClazz = Proxy.getProxyClass(Calculator.class.getClassLoader(), Calculator.class); //以Calculator實現類的Class對象做對比,看看代理Class是什麼類型 System.out.println(CalculatorImpl.class.getName()); System.out.println(calculatorProxyClazz.getName()); //打印代理Class對象的構造器 Constructor[] constructors = calculatorProxyClazz.getConstructors(); System.out.println("----構造器----"); printClassInfo(constructors); //打印代理Class對象的方法 Method[] methods = calculatorProxyClazz.getMethods(); System.out.println("----方法----"); printClassInfo(methods); } public static void printClassInfo(Executable[] targets) { for (Executable target : targets) { // 構造器/方法名稱 String name = target.getName(); StringBuilder sBuilder = new StringBuilder(name); // 拼接左括號 sBuilder.append('('); Class[] clazzParams = target.getParameterTypes(); // 拼接參數 for (Class clazzParam : clazzParams) { sBuilder.append(clazzParam.getName()).append(','); } //刪除最後一個參數的逗號 if (clazzParams != null && clazzParams.length != 0) { sBuilder.deleteCharAt(sBuilder.length() - 1); } //拼接右括號 sBuilder.append(')'); //打印 構造器/方法 System.out.println(sBuilder.toString()); } } }
運行結果:
你們還記得接口Class的打印信息嗎?
也就是說,經過給Proxy.getProxyClass()傳入類加載器和接口Class對象,咱們獲得了一個增強版的Class:即包含接口的方法信息add()、subtract(),又包含了構造器$Proxy0(InvocationHandler),還有一些本身特有的方法以及從Object繼承的方法。
梳理一下:
1.原先咱們本打算直接根據接口Class獲得代理對象,無奈接口Class只有方法信息,沒有構造器
2.因而,咱們想,有沒有辦法建立一個Class對象,既有接口Class的方法信息,同時又包含構造器方便建立代理實例呢?
3.利用Proxy類的靜態方法getProxyClass()方法,給它傳一個接口Class對象,它能返回一個增強版Class對象。也就是說getProxyClass()的本質是:用Class,造Class。
要謝謝Proxy類和JVM,讓咱們不寫代理類卻直接獲得代理Class對象,進而獲得代理對象。
靜態代理
動態代理:用Class造Class
既然Class<$Proxy0>有方法信息,又有構造器,咱們試着用它獲得代理實例吧:
咱們發現,newInstance()建立對象失敗。由於Class的newInstance()方法底層會走無參構造器。而以前打印$Proxy0的Class信息時,咱們發現它沒有無參構造,只有有參構造$Proxy0(InvocationHandler)。那就靠它了:
constructor.newInstance()須要傳入一個InvocationHandler對象,這裏採用匿名對象的方式,invoke()方法不作具體實現,直接返回null
舒服~
一個小問題
好不容易經過Proxy.getProxyClass()獲得代理Class,又經過反射最終獲得代理對象,固然要玩一玩:
尷尬,居然發生了空指針異常。縱觀整個代碼,新寫的add()和subtract()返回值是int,不會是空指針。而再往上的代碼以前編譯都是經過的,應該沒問題啊。再三思量,咱們發現匿名對象InvocationHandler的invoke()返回null。難道是它?作個實驗:讓invoke()返回1,而後觀察結果。
結果代理對象的add和subtract都返回1
巧合嗎?應該不是。我猜:每次調用代理對象的方法都會調用invoke(),且invoke()的返回值就是代理方法的返回值。若是真是如此,空指針異常就能夠解釋了:add()和suntract()期待的返回值類型是int,可是以前invoke()返回null,類型不匹配,因而空指針異常。
以防萬一,再驗證一下invoke()和代理對象方法的關係:
好了,什麼都不用說了。就目前的實驗來看,調用過程應該是這樣:
動態代理底層調用邏輯
一樣的,知道告終果後,咱們再反推原理。
靜態代理:往代理對象的構造器傳入目標對象,而後代理對象調用目標對象的同名方法。
動態代理:constructor反射建立代理對象時,須要傳入InvocationHandler,我猜,代理對象內部有一個成員變量InvocationHandler:
果真不出所料。那麼動態代理的大體設計思路就是:
爲何這麼設計?
爲了解耦,也爲了通用性。
若是JVM生成代理對象的同時生成了特定邏輯的方法體,那這個代理對象後期就沒有擴展的餘地,只能有一種玩法。而引入InvocationHandler的好處是:
因此,invocationHandler的做用,倒像是把「方法」和「方法體」分離。JVM只造一個空的代理對象給你,後面想怎麼玩,由你本身組裝。反正代理對象中有個成員變量invocationHandler,每個方法裏只有一句話:handler.invoke()。因此調任何一個代理方法,最終都會跑去調用invoke()方法。
invoke()方法是代理對象和目標對象的橋樑。
可是咱們真正想要的結果是:調用代理對象的方法時,去調用目標對象的方法。
因此,接下來努力的方向就是:設法在invoke()方法獲得目標對象,並調用目標對象的同名方法。
代理對象調用目標對象方法
那麼,如何在invoke()方法內部獲得目標對象呢?咱們來看看能不能從invoke()方法的形參上獲取點線索:
很惋惜,proxy不是代理對象。其實想一想也知道,建立代理對象的過程當中自始至終沒有目標對象參與,因此也就沒法產生關聯。並且一個接口能夠同時被多個類實現,因此JVM也沒法判斷當前代理對象想要代理哪一個目標對象。但好在咱們已經知道本次調的方法名(Method)和參數(args)。咱們接下來要作的就是獲得目標對象並調用同名方法,而後把參數給它。
如何獲得目標對象呢?沒辦法,爲今之計只能new了...哈哈哈哈。我靠,饒了一大圈,又是動態代理,又是invoke()的,結果仍是要手動new?別急,先玩玩。後面會改進的:
可是這樣的寫法顯然是倒退30年,一晚上回到解放前。咱們須要改進一下,封裝Proxy.getProxyClass(),使得目標對象能夠做爲參數傳入:
public class ProxyTest { public static void main(String[] args) throws Throwable { CalculatorImpl target = new CalculatorImpl(); //傳入目標對象 //目的:1.根據它實現的接口生成代理對象 2.代理對象調用目標對象方法 Calculator calculatorProxy = (Calculator) getProxy(target); calculatorProxy.add(1, 2); calculatorProxy.subtract(2, 1); } private static Object getProxy(final Object target) throws Exception { //參數1:隨便找個類加載器給它, 參數2:目標對象實現的接口,讓代理對象實現相同接口 Class proxyClazz = Proxy.getProxyClass(target.getClass().getClassLoader(), target.getClass().getInterfaces()); Constructor constructor = proxyClazz.getConstructor(InvocationHandler.class); Object proxy = constructor.newInstance(new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println(method.getName() + "方法開始執行..."); Object result = method.invoke(target, args); System.out.println(result); System.out.println(method.getName() + "方法執行結束..."); return result; } }); return proxy; } }
厲害厲害...惋惜,仍是太麻煩了。有沒有更簡單的方式獲取代理對象?有!
直接返回代理對象,而不是代理對象Class
從一開始就存在,哈哈。可是我以爲getProxyClass()切入更好理解。
public class ProxyTest { public static void main(String[] args) throws Throwable { CalculatorImpl target = new CalculatorImpl(); Calculator calculatorProxy = (Calculator) getProxy(target); calculatorProxy.add(1, 2); calculatorProxy.subtract(2, 1); } private static Object getProxy(final Object target) throws Exception { Object proxy = Proxy.newProxyInstance( target.getClass().getClassLoader(),/*類加載器*/ target.getClass().getInterfaces(),/*讓代理對象和目標對象實現相同接口*/ new InvocationHandler(){/*代理對象的方法最終都會被JVM導向它的invoke方法*/ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println(method.getName() + "方法開始執行..."); Object result = method.invoke(target, args); System.out.println(result); System.out.println(method.getName() + "方法執行結束..."); return result; } } ); return proxy; } }
上面的代碼,已經比上一篇開頭直接修改目標類好多了。再來看一下當時的四大缺點:
使用動態代理,讓咱們避免手寫代理類,只要給getProxy()方法傳入target就能夠生成對應的代理對象。可是日誌打印還是硬編碼在invoke()方法中。雖然修改時只要改一處,可是別忘了「開閉原則」。因此最好是能把日誌打印單獨拆出來,像目標對象同樣做爲參數傳入。
日誌打印其實就是AOP裏的通知概念。我打算定義一個Advice接口,而且寫一個MyLogger實現該接口。
通知接口
public interface Advice { void beforeMethod(Method method); void afterMethod(Method method); }
日誌打印
public class MyLogger implements Advice { public void beforeMethod(Method method) { System.out.println(method.getName() + "方法執行開始..."); } public void afterMethod(Method method) { System.out.println(method.getName() + "方法執行結束..."); } }
測試類
public class ProxyTest { public static void main(String[] args) throws Throwable { CalculatorImpl target = new CalculatorImpl(); Calculator calculatorProxy = (Calculator) getProxy(target, new MyLogger()); calculatorProxy.add(1, 2); calculatorProxy.subtract(2, 1); } private static Object getProxy(final Object target, Advice logger) throws Exception { /*代理對象的方法最終都會被JVM導向它的invoke方法*/ Object proxy = Proxy.newProxyInstance( target.getClass().getClassLoader(),/*類加載器*/ target.getClass().getInterfaces(),/*讓代理對象和目標對象實現相同接口*/ (proxy1, method, args) -> { logger.beforeMethod(method); Object result = method.invoke(target, args); System.out.println(result); logger.afterMethod(method); return result; } ); return proxy; } }
差一點完美~下篇講講更完美的作法。
初學者可能對諸如「字節碼文件」、Class對象比較陌生。因此這裏花一點點篇幅介紹一下類加載器的部分原理。若是咱們要定義類加載器,須要繼承ClassLoader類,並覆蓋findClass()方法:
@Override public Class<?> findClass(String name) throws ClassNotFoundException { try { /*本身另外寫一個getClassData() 經過IO流從指定位置讀取xxx.class文件獲得字節數組*/ byte[] datas = getClassData(name); if(datas == null) { throw new ClassNotFoundException("類沒有找到:" + name); } //調用類加載器自己的defineClass()方法,由字節碼獲得Class對象 return this.defineClass(name, datas, 0, datas.length); } catch (IOException e) { e.printStackTrace(); throw new ClassNotFoundException("類找不到:" + name); } }
因此,這就是類加載之因此能把xxx.class文件加載進內存,並建立對應Class對象的深層緣由。具體文章能夠參考基友寫的另外一篇:請叫我程序猿大人:好怕怕的類加載器
靜態代理
代理類CalculatorProxy是咱們事先寫好的,編譯後獲得Proxy.class字節碼文件。隨後和目標類一塊兒被ClassLoader(類加載器)加載進內存,生成Class對象,最後生成實例對象。代理對象中有目標對象的引用,調用同名方法並先後加上日誌打印。
優勢:不用修改目標類源碼
缺點是:高度綁定,不通用。硬編碼,不易於維護。
動態代理
咱們本想經過接口Class直接建立代理實例,無奈的是,接口Class雖然有方法信息描述,卻沒有構造器,沒法建立對象。因此咱們但願JDK能提供一套API,咱們傳入接口Class,它自動複製裏面的方法信息,造出一個有構造器、能建立實例的代理Class對象。
優勢:
上面的討論都在刻意迴避代理對象的類型,放最後來聊一聊。
最後討論一下代理對象是什麼類型。
首先,請區分兩個概念:代理Class對象和代理對象。
單從名字看,代理Class和Calculator的接口確實相去甚遠,可是咱們卻能講代理對象賦值給接口類型:
但誰說可否複製給接口是看名字的?難道不是隻要實現接口就好了嗎?
代理對象的本質就是:和目標對象實現相同接口的實例。代理Class能夠叫任何名字,whatever,只要它實現某個接口,就能成爲該接口類型。
我寫了一個MyProxy類,那麼它的Class名字必然叫MyProxy。但這和可否賦值給接口沒有任何關係。因爲它實現了Serializable和Collection,因此myProxy(代理實例)同時是這兩個接口的類型。
我想了個很騷的比喻,但願能解釋清楚:
接口Class對象是大內太監,裏面的方法和字段比作他的一身武藝,可是他沒有小DD(構造器),因此不能new實例。一身武藝後繼無人。
那怎麼辦呢?
正常途徑(implements):
寫一個類,實現該接口。這個就至關於大街上拉了一我的,認他作乾爹。一身武藝傳給他,只是比他乾爹多了小DD,能夠new實例。
非正常途徑(動態代理):
經過妙手聖醫Proxy的克隆大法(Proxy.getProxyClass()),克隆一個Class,可是有小DD。因此這個克隆人Class能夠建立實例,也就是代理對象。
代理Class其實就是附有構造器的接口Class,同樣的類結構信息,卻能建立實例。
JDK動態代理生成的實例
CGLib動態代理生成的實例
若是說繼承的父類是親爹(只有一個),那麼實現的接口是乾爹(能夠有多個)。
實現接口是一個類認乾爹的過程。接口沒法建立對象,但實現該接口的類能夠。
好比
class Student extends Person implements A, B
這個類new一個實例出來,你問它:你爸爸是誰啊?它會告訴你:我只有一個爸爸Person。
可是student instanceof A interface,或者student instanceof B interface,它會告訴你兩個都是它乾爹(true),均可以用來接收它。
然而,凡有利必有弊。
也就是說,動態代理生成的代理對象,最終均可以用接口接收,和目標對象一塊兒造成了多態,能夠隨意切換展現不一樣的功能。可是切換的同時,只能使用該接口定義的方法。