在正式說hibernate延遲加載時,先說說一個比較奇怪的現象吧:hibernate中,在many-to-one時,如果我們設置了延遲加載,會發現我們在eclipse的調試框中查看one對應對象時,它的內部成員變量全是null的(因為這個原因我還調了好久的代碼!),貼張圖給你們感受下:
左邊是設置延遲加載的調試圖,右邊是沒設置延遲加載的示意圖;
ok,估計大家也理解我說什么了,下面就從這個現象進作為入口,闡述hibernate延遲加載背后的原理----動態代理。
一、hibernate的延遲加載與動態代理
1、hibernate中的延遲加載:get VS load
我們知道,在hibernate方法中,直接涉及到延遲加載的方法有get和load,使用get時,不會延遲加載,load則反之。另外,在many-to-one等關系配置中,我們也可以通過lazy屬性設置是否延遲加載,這是我們對hibernate最直觀的認識。
2、現象解釋----動態代理機制
所以,開頭說到的奇怪現象的原因是什么呢?其實在hibernate設置延遲加載后,hibernate返回給我們的對象(要延遲加載的對象)是一個代理對象,並不是真實的對象,該對象沒有真實對象的數據,只有真正需要用到對象數據(調用getter等方法時)時,才會觸發hibernate去數據庫查對應數據,而且查回來的數據不會存儲在代理對象中,所以這些數據是無法在調試窗口查看到的。
如果在調試是要查看該數據,我們可以查看代理對象中的hadler屬性中的target變量,該對象變量才是真實的對象,看下面截圖:
也就是說,我們user變量僅僅是一個代理類,target才是真正數據庫中獲取的數據。當我們在調用getter方法式,hibernate會利用動態代理的方法,直接調用target中的getter方法發揮對應的值。這樣也解釋了為什么hibernate可以延遲加載:通過代理類進行加載時間的控制,在外界正真調用getter等方法操作數據時才會對相應的方法進行攔截,然后讀取數據庫。
二、動態代理原理
上面也簡單介紹了hibernate延遲加載是通過動態代理實現,所以上面是動態代理呢?
1、理解代理的概念。
代理是一個中間者,它的主要作用之一是我們可以利用代理對象來增強對真正對象的控制:例如在hibernate中控制數據加載的時間在正真調用數據時發生。具體的話,后面個人會寫一篇代理模式的博客簡單總結下代理模式,讀者也可以去查查代理模式以加深理解,這里不詳細講解。
在jdk中的代理,主要通過一個叫做InvocationHandler的委托接口和Proxy的代理類來實現動態代理,一般來說,Proxy會通過調用InvocationHandler的invoke方法進行代理委托:也就是invoke方法才是真正的代理方法,這個后面的代碼例子會詳細講解。所以,java中要動態代理的話,必須有一個InvocationHandler的具體實現類。
2、java動態代理的詳細實現方式
上面也提到了,java中動態代理中至少涉及三個對象:代理調用對象(參數代理實例),被代理對象,被委托的Handler對象即代理對象。下面就從三個對象,進行一個簡單的動態代理實現
首先,我們寫一個真實的類,該類要被代理對象代理。這里需要注意的是:java中的動態代理是只能支持接口的動態代理的,所以我們在實現具體類前必須抽象該類的方法,定義一個接口,至於為什么在java中只能支持接口動態代理,后面會詳細講解,下面貼上我的代碼,大家注意看注釋:
/* * 首先定義一個接口被真實的類實現,jdk中的動態代理只能代理接口類對象 */ interface RealClassIfc{ public void method1(String myName); } /* * 這個是真實的對象 */ class RealClass implements RealClassIfc{ public void method1(String myName){ System.out.println(this.getClass().getName() + " method1Name:" + myName); } }
然后,我們定義一個代理類也就是InvocationHandler接口的具體實現類Handler,該類的對象對應代理對象,具體的代碼說明在注釋中,請注意看:
/* * 代理類,用於給jdk代理類Proxy進行委托,該類需要實現一個接口InvocationHandler,該接口只有一個方法invoke */ class ProxyClass implements InvocationHandler { /* * 參數說明: * proxy:代理對象,該對象用於查詢代理對象的其他信息,更具體作用可以參考這篇博客: * http://blog.csdn.net/bu2_int/article/details/60150319; * method:真實對象所對應的方法 * args:執行上面method所需要的參數 * 有需要該方法可以選擇返回值 */ //真實對象,invoke方法中需要用到 Object realClass = null; public ProxyClass(Object realClass){ this.realClass = realClass; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //在invoke方法體內部執行織入的代碼 System.out.println("這個是RealClass method1執行前要執行的代碼"); method.invoke(realClass,args); System.out.println("這個是RealClass method1執行后要執行的代碼"); return null; } }
這里簡單說明下:InvacationHandler中只有一個方法(具體可以查看jdk源碼)invoke,而我們便是在該方法內部進行我們的代碼織入操作(這個可以聯系spring中的AOP思想,其實Spring AOP 的實現大體就是這樣,更詳細的AOP可以參考本人這篇博客)。所以,到這里我們可以大概猜到:hibernate的查詢操作大概就是在invoke方法中調用正式的getter等獲取數據方法前進行的,動態代理幫我們攔截了getter等獲取數據方法並對應地在這之前進行了數據查詢等操作,具體可以參考查看hibernate源碼。
然后,我們需要一個客戶端類來幫我們獲取代理實例,完成代理過程,請看下面代碼:
/* * 這個是客戶端類,在hibernate中,如果延遲加載被設置了,我們獲取的對象只是代理對象,就是對應這個類的 * 該類會通過jdk 的Proxy類的getInstance方法獲取一個代理類,該代理類會自動幫你實現真實類的所有接口對應方法 */ class ClientClass { //該屬性是真實類 Object realClassInterface = null; //在構造代理類時初始化該類 public ClientClass(Object realClassInterface){ this.realClassInterface = realClassInterface; } //獲取代理實例方法,該方法用於獲取代理實例 public void proxyMethod(){ InvocationHandler handler = new Handler(realClassInterface); RealClassIfc realClass = (RealClassIfc)Proxy.newProxyInstance(Handler.class.getClassLoader(), RealClass.class.getInterfaces(), handler); realClass.method1("動態代理的方法");
//打印發現realClass的真實類是一個jre運行時生成的一個代理類
System.out.println(realClass.getClass().getName());
}
}
一般來說,客戶端類不會直接調用InvocationHandler對象的invoke方法,而是通過jdk的Proxy類獲取一個代理對象實例(對應上面的realClass對象),然后通過該對象來直接調用真實類的同名方法,這樣才能給調用者一個“我調用的是真實的類”,因為對調用者而言,代理類應當是透明的。
這里簡單說說Proxy的newProxyInstance方法的幾個參數:
第一個參數是代理類的加載器,代理對象通過該加載器來以反射的方式獲得委托的InvocationHadler具體實現類的對象,然后通過接口調用對應invoke方法來實現真實類的方法調用,需要類加載器是因為Proxy生成動態代理對象過程會生成一個描述代理類的字節碼,該字節碼加載時正需要一個classLoader;
第二個參數是真實的類的所有接口信息,該信息給代理類有的作用:代理類會對應“實現”真實類的所有接口(其實應該是調用invoke方法+真實類對應的方法),這樣也正是代理類的真正運行機理:這種方法可以讓我們有機會在正真方法執行前攔截到該方法,然后織入代碼,這也是動態代理實現AOP的大概原理,hibernate的延遲加載正是這樣實現的。從這里我們可以回答上面提出的問題:java中為什么只能通過接口實現動態代理了,這是因為Proxy的newInstance方法限制的,更本質的原因其實就是java不支持多繼承,所以代理對象不得不通過操作接口操作真實對象。
第三個參數就是真正的代理類對象了,該對象才是正真的負責代理操作。
注意的是,打印realClass的類名字可以得這樣結果:review.blog.hibernate.$Proxy0,並非是RealClass。從這里可以看出,jdk動態代理機制的確生成一個動態代理類字節碼,代理實例就是通過該字節碼對應的類來創建的。更具體可以參考這個鏈接。
動態代理是AOP實現的一種重要方式,通過InvocationHandler接口進行方法的攔截並利用反射機制執行一定的代碼正是AOP中織入代碼的重要手段,理解動態代理的原理對於我們理解更好的理解hibernate和spring等框架具有重要意義。