java設定模式---代理模式--動態代理模式和cglib代理模式詳解
代理模式使用場景
代理模式的定義:什麼是代理模式呢?代理模式是常用的Java設計模式,它的特徵是代理類與委託類有同樣的介面,代理類主要負責為委託類預處理訊息、過濾訊息、把訊息轉發給委託類,以及事後處理消息等。代理類和委託類之間通常會存在關聯關係,一個代理類的物件與一個委託類的物件關聯,代理類的物件本身並不是真正實現服務,而是通過呼叫委託類物件的相關的方法來提供特定的服務。
舉個例子來說明:假如說我現在想買一輛二手車,雖然我可以自己去找車源,做質量檢測等一系列的車輛過戶流程,但是這確實太浪費我得時間和精力了。我只是想買一輛車而已為什麼我還要額外做這麼多事呢?於是我就通過中介公司來買車,他們來給我找車源,幫我辦理車輛過戶流程,我只是負責選擇自己喜歡的車,然後付錢就可以了。
上述例子中:委託類就是我,
委託類的方法:選車+付錢
代理類:過戶,質量檢測,或許還有一些雜七雜八的事情,每個委託人也許雜七雜八的事情還不同,都通過代理類來處理,而委託類只需完成核心的業務,選車付錢就行了。
用圖表示如下(網圖):
為什麼要用代理模式?
中介隔離作用:在某些情況下,一個客戶類不想或者不能直接引用一個委託物件,而代理類物件可以在客戶類和委託物件之間起到中介的作用,其特徵是代理類和委託類實現相同的介面。
開閉原則,增加功能:代理類除了是客戶類和委託類的中介之外,我們還可以通過給代理類增加額外的功能來擴充套件委託類的功能,這樣做我們只需要修改代理類而不需要再修改委託類,符合程式碼設計的開閉原則。代理類主要負責為委託類預處理訊息、過濾訊息、把訊息轉發給委託類,以及事後對返回結果的處理等。代理類本身並不真正實現服務,而是同過呼叫委託類的相關方法,來提供特定的服務。真正的業務功能還是由委託類來實現,但是可以在業務功能執行的前後加入一些公共的服務。例如我們想給專案加入快取、日誌這些功能,我們就可以使用代理類來完成,而沒必要開啟已經封裝好的委託類。
靜態代理模式
由程式設計師或特定工具自動生成的原始碼,再對其編譯,在程式執行之前,代理的類編譯生成的.class檔案就已經存在了。
直接上程式碼:
介面:
public interface A {
void buyCar();
void sallCar(String string);
}
實現類(委託類):
public class AImpl implements A { public void buyCar() { System.out.println("我要買車"); } public void sallCar(String s){ System.out.println("sall car" + s); } }
代理類:
public class StaticStateProxy implements A {
private A a;
public StaticStateProxy(A a){
this.a = a;
}
public void buyCar(){
//do something
a.buyCar();
//do something
}
public void sallCar(String string){
a.sallCar(string);
}
}
測試:
public class StaticStateProxyTest{
public static void main(String[] args) {
A a = new AImpl();
a.buyCar();
StaticStateProxy buyHouseProxy = new StaticStateProxy(a);
buyHouseProxy.buyCar();
}
}
靜態代理總結:
1、代理類和實現類(委託類),繼承同一個介面類A。
2、代理類中傳入介面A的實現類,通過重寫介面A的方法,在重寫方式時,直接呼叫傳入A實現類的方法,並且在呼叫前後可以做一些其他操作,結合本章開始的例子,這裡就是通過代理類,來實現委託類方法的同事,做一些其他操作。
優缺點:
優點:可以做到在符合開閉原則的情況下對目標物件進行功能擴充套件,呼叫比較簡單。
缺點:我們得為每一個服務都得建立代理類,工作量太大,不易管理。同時介面一旦發生改變,代理類也得相應修改。
JDK動態代理模式
動態代理是在程式執行時通過反射機制動態建立的,在動態代理中我們不再需要再手動的建立代理類,我們只需要編寫一個動態處理器就可以了。真正的代理物件由JDK再執行時為我們動態的來建立。
上程式碼:
介面:
public interface A {
void buyCar();
void sallCar(String string);
}
實現類(委託類):
public class AImpl implements A {
public void buyCar() {
System.out.println("我要買車");
}
public void sallCar(String s){
System.out.println("sall car" + s);
}
}
代理類:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class DynamicProxyHandler implements InvocationHandler {
private Object object;
public DynamicProxyHandler(final Object object) {
this.object = object;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("買車前湊錢");
Object result = method.invoke(object, args);
System.out.println("買車後飆車");
return result;
}
}
測試:
import java.lang.reflect.Proxy;
public class DynamicProxyTest {
public static void main(String[] args) {
A a = new AImpl();
A proxyA = (A) Proxy.newProxyInstance(A.class.getClassLoader(), new
Class[]{A.class}, new DynamicProxyHandler(a));
proxyA.buyCar();
proxyA.sallCar("BMW");
}
}
動態代理總結:
動態代理的介面和實現類和靜態代理一樣,主要是看代理類的實現:
DynamicProxyHandler實現了InvocationHandler介面。
每一個動態代理類都必須要實現InvocationHandler這個介面,並且每個代理類的例項都關聯到了一個handler,當我們通過代理物件呼叫一個方法的時候,這個方法的呼叫就會被轉發為由InvocationHandler這個介面的 invoke 方法來進行呼叫。我們來看看InvocationHandler這個介面的唯一一個方法 invoke 方法
換句話說,我們一般呼叫方法都是採用a.method這種方式,程式碼會直接走到method方法中,而使用了動態代理類,我們需要通過InvocationHandler中的invoke來呼叫該方法,這樣做有什麼好處呢,最淺顯的理解,就是在invoke中,除了通過method.invoke(object, args)來呼叫該方法以外,還可以做其他操作,如以上程式碼所示。
在寫好代理類後,需要建立一個代理類的例項,來呼叫方法,建立方式為:
Proxy.newProxyInstance
注意Proxy.newProxyInstance()方法接受三個引數:
ClassLoader loader:指定當前目標物件使用的類載入器,也就是委託類,獲取載入器的方法是固定的
Class<?>[] interfaces:指定目標物件實現的介面的型別,也就是委託類的介面,使用泛型方式確認型別
InvocationHandler:指定動態處理器,也就是制定代理類,執行目標物件的方法時,會觸發事件處理器的方法。
動態代理和靜態代理的比較:
動態代理與靜態代理相比較,最大的好處是介面中宣告的所有方法都被轉移到呼叫處理器一個集中的方法中處理(InvocationHandler.invoke)。這樣,在介面方法數量比較多的時候,我們可以進行靈活處理,而不需要像靜態代理那樣,重實現介面的每個方法,每一個方法都需要進行中轉。而且動態代理的應用使我們的類職責更加單一,複用性更強;
但是:動態代理只能代理實現了介面的類,沒有實現介面的類不能實現JDK動態代理。這時我們就需要用到Cglib代理。
Cglib代理模式
JDK實現動態代理需要實現類通過介面定義業務方法,對於沒有介面的類,如何實現動態代理呢,這就需要CGLib了。CGLib採用了非常底層的位元組碼技術,其原理是通過位元組碼技術為一個類建立子類,並在子類中採用方法攔截的技術攔截所有父類方法的呼叫,順勢織入橫切邏輯。但因為採用的是繼承,所以不能對final修飾的類進行代理。JDK動態代理與CGLib動態代理均是實現Spring AOP的基礎。 Cglib不是java自帶的API,我們要使用cglib代理必須引入 cglib的jar包。
上程式碼,借用上述例子實現類:
public class AImpl implements A {
public void buyCar() {
System.out.println("我要買車");
}
public void sallCar(String s){
System.out.println("sall car" + s);
}
public String aa(){
return "aa";
}
}
代理類:
public class CglibMethodInterceptor implements MethodInterceptor {
public Object intercept(Object object , Method method , Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("Before");
Object result = methodProxy.invokeSuper(object,args);
System.out.println("After");
return result;
}
}
測試:
public class CglibProxyTest {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(AImpl.class);
enhancer.setCallback(new CglibMethodInterceptor());
AImpl aImpl = (AImpl) enhancer.create();
aImpl.buyCar();
aImpl.sallCar("dd");
}
}
代理類定義了一個攔截器,在呼叫目標方法之前,cglib回撥MethodInterceptor介面方法攔截,來實現自己的業務邏輯,類似於JDK中的InvocationHandler介面。也就是通過intercept 呼叫methodProxy.invokeSuper來呼叫委託類的方法,而在intercept中可以做其他工作。
public Object intercept(Object object , Method method , Object[] args, MethodProxy methodProxy)
object:為cglib動態生成的代理例項
method:為上文中實體類所呼叫的被代理的方法呼叫
args:為method引數數值列表
methodProxy:為生成代理類對方法的代理引用
返回:從代理例項方法呼叫返回的值
其中,methodProxy.invokeSuper(object,args):
呼叫代理類例項上的proxy方法的父類方法
(這裡有點拗口,簡單一點就是,呼叫代理類例項的父類方法,為什麼是父類方法,因為在建立代理類例項時,需要將代理類設定為委託類的子類,下面會有提到)
在寫好代理類後,需要建立一個代理類的例項,來呼叫方法。
代理類物件是由Enhancer類建立的。Enhancer是CGLIB的位元組碼增強器,可以很方便的對類進行拓展。
建立代理物件的幾個步驟:
Enhancer enhancer = new Enhancer(); 1
enhancer.setSuperclass(AImpl.class); 2
enhancer.setCallback(new CglibMethodInterceptor()); 3
AImpl aImpl = (AImpl) enhancer.create(); 4
aImpl.buyCar();
aImpl.sallCar("dd");
- 建立Enhancer例項。
- 繼承被委託類,也就是說enhance對應的類為Alpml的子類。
- enhancer.setCallback()這個方法,我們需要傳入CglibMethodInterceptor的例項作為回撥的物件,並且把它放入到一個Callback型別的數組裡面。然後去判斷這個陣列是否合法。
- 最後一步enhancer.create()返回的是一個增強的目標類的例項,此時的AImpl物件已經不是以前那個AImpl類的, 而是一個增強後的AImpl類的例項了,每個方法都有代理類中的增加語句。
至此,CGlib就告一段落,等後續有時間,對原始碼解析一番就更好了。
三種代理方式的對比
網上有一段CGLIB和JDK動態帶路使用場景上的對比,就借用一下啦:
CGLIB建立的動態代理物件比JDK建立的動態代理物件的效能更高,但是CGLIB建立代理物件時所花費的時間卻比JDK多得多。所以對於單例的物件,因為無需頻繁建立物件,用CGLIB合適,反之使用JDK方式要更為合適一些。同時由於CGLib由於是採用動態建立子類的方法,對於final修飾的方法無法進行代理。
代理方式 |
實現 |
優點 |
缺點 |
特點 |
JDK靜態代理 |
代理類與委託類實現同一介面,並且在代理類中需要硬編碼介面 |
實現簡單,容易理解 |
代理類需要硬編碼介面,在實際應用中可能會導致重複編碼,浪費儲存空間並且效率很低 |
好像沒啥特點 |
JDK動態代理 |
代理類與委託類實現同一介面,主要是通過代理類實現InvocationHandler並重寫invoke方法來進行動態代理的,在invoke方法中將對方法進行增強處理 |
不需要硬編碼介面,程式碼複用率高 |
只能夠代理實現了介面的委託類 |
底層使用反射機制進行方法的呼叫 |
CGLIB動態代理 |
代理類將委託類作為自己的父類併為其中的非final委託方法建立兩個方法,一個是與委託方法簽名相同的方法,它在方法中會通過super呼叫委託方法;另一個是代理類獨有的方法。在代理方法中,它會判斷是否存在實現了MethodInterceptor介面的物件,若存在則將呼叫intercept方法對委託方法進行代理 |
可以在執行時對類或者是介面進行增強操作,且委託類無需實現介面 |
不能對final類以及final方法進行代理 |
底層將方法全部存入一個數組中,通過陣列索引直接進行方法呼叫 |