代理模式
- 代理模式
- 代理模式的應用場景
- 先理解什么是代理,再理解動靜態
- 舉例
- 舉例所用代碼
- 動靜態的區別
- 靜態代理
- 動態代理
- 動態代理的優點
- 代理模式與裝飾者模式的區別
代理模式
代理模式在設計模式中是7種結構型模式中的一種,而代理模式有分動態代理,靜態代理,一般來說,動態代理更加常用一些。
代理模式的應用場景
這些應用場景除了日志記錄,我也沒有熟悉的,剛接觸代理模式的可以直接跳過
遠程代理(Remote Proxy):
當對象存在于不同的地址空間,例如在網絡中的不同服務器上時,可以使用代理模式實現遠程代理。代理對象充當本地對象的代表,隱藏了遠程對象的實際細節,使得客戶端可以像調用本地對象一樣調用遠程對象。
虛擬代理(Virtual Proxy):
虛擬代理用于按需創建昂貴或復雜的對象,以提高系統性能。代理對象在真正需要執行操作時才會實例化真實對象,而在其他情況下,它充當一個占位符。
保護代理(Protection Proxy):
保護代理用于控制對對象的訪問權限。代理對象可以根據訪問者的身份控制其對真實對象的訪問,例如,檢查用戶是否具有足夠的權限來執行某個操作。
緩存代理(Cache Proxy):
緩存代理用于緩存一些開銷較大的操作的結果,以避免重復計算。代理對象在執行真實對象的操作之前檢查是否已經有相應的結果緩存,如果有則直接返回緩存的結果。
日志記錄代理(Logging Proxy):
日志記錄代理用于在調用真實對象的操作前后記錄相關日志信息,例如,記錄方法的執行時間、參數、返回值等,以便進行調試或性能分析。
智能引用代理(Smart Reference Proxy):
智能引用代理用于在對象被引用時執行一些額外的操作,例如,對對象的引用計數進行管理,當引用計數為零時釋放對象資源。
延遲加載代理(Lazy Loading Proxy):
延遲加載代理用于延遲加載對象的實例,即在真正需要使用對象時才進行加載。這可以提高系統的啟動性能,避免在啟動時加載不必要的資源。
先理解什么是代理,再理解動靜態
代理模式的靈魂就是在不直接訪問某個對象的情況下,通過代理對象來間接訪問并控制對該對象的訪問。(在這個間接訪問的過程中代理對象通常會在執行代理對象里的操作先后時間段里執行一些被代理對象里沒有的操作)
我們先搞清楚代理模式有幾個角色,再來舉例
真實對象(被代理對象): 被間接訪問的對象
代理對象: 代理對象將間接訪問真實對象,并且代理可以幫助你做一些額外的事情,比如檢查你的權限、記錄你的請求、或者緩存結果。
抽象類或接口(一般是接口): 這是代理對象和真實對象都要實現的接口(建立一個聯系),這樣代理才可以替代真實對象。一般這個接口里的抽象方法是代理對象訪問被代理對象的關鍵。
舉例
一天,四年級三班同學舉行班級里的數學期中考試,考試時間結束后,由小明同學(數學學習委員)將試卷收好送給數學老師,數學老師批改完試卷后,小明又會將試卷拿回,并將班級數學成績統計出來。
在了解完代理模式的三個角色后,我們嘗試把上面的例子進行角色分析
真實對象(被代理對象):數學老師
你可以理解成數學老師才是期中數學考試出成績的關鍵,但在同學們知曉成績時,他并沒有出面。
代理對象:小明——數學學習委員
雖然數學老師才是數學考試出成績的關鍵,但是出成績時,他才是在同學們露面的人,并且他還額外進行了統計成績的操作(類似程序的日志記錄)。
抽象類或者接口:期中考試
期中考試是聯系數學老師和數學學習委員的一個關鍵,當然你也可以用其他的關鍵詞來描述這個接口,但是批改試卷是這個例子的關鍵,如果不用學生可以自己批改試卷,那么老師就不用出現了,就不用訪問數學老師這個對象了,所以接口里必須要有批改試卷這個方法。
舉例所用代碼
這里我們先創建抽象接口
public interface IMidterm_Examination {//批改試卷的抽象方法void markPapers();
}
再創建具體的數學老師類,也就是真實對象類或者說被代理類
// 在實現抽象接口的前提下創建真實對象
public class MathTeacher implements IMidterm_Examination {//基本屬性private int age=32;private String name="李四";private String job="數學老師";@Overridepublic void markPapers() {System.out.println("數學老師正在改試卷");}
}
在創建代理對象,創建代理對象前需先寫出代理對象類
public class MathMonitor implements IMidterm_Examination{private int age=16;private String name="小明";private String job="數學學習委員";private IMidterm_Examination target;//代理目標,即被代理對象,接口是為了更加靈活通用public MathMonitor(IMidterm_Examination target) {this.target = target;//在構造代理對象時,將被代理對象傳入}void sendPaper(){System.out.println("小明同學將試卷送給老師");}//小明額外的統計成績方法void countScores(){System.out.println("小明同學正在統計成績");}@Overridepublic void markPapers() {//重寫抽象接口里的方法//顯示小明同學將試卷送給數學老師this.sendPaper();//老師來修改試卷target.markPapers();//老師批改完試卷后,小明統計成績this.countScores();System.out.println("期中考試流程結束");}
}
測試主函數
public class Main {public static void main(String[] args) {//通過接口方式創建被代理對象,IMidterm_Examination mathTeacher=new MathTeacher();//再通過接口創建代理對象IMidterm_Examination mathMonitor=new MathMonitor(mathTeacher);//通過代理對象間接訪問數學老師這個對象mathMonitor.markPapers();}
}
運行結果
在看完上面的例子后,我們知道代理模式中的代理就是一個類似中介的效果,就像找工作一樣,小王本來想進某家電子廠的,但是需要交中介費才能進入這個廠,但是這個中介還會包你不滿意該電子廠環境拒絕進廠來回的路費一樣。
動靜態的區別
靜態代理
靜態代理在上面的舉例代碼中已經體現出了,它有以下特點:
(也許你在讀完這些特點你還是會不太理解,所以你可以在看完動態代理之后再來回顧靜態代理,才能感受到這些特點。)
編譯時確定:
在編譯期間,代理類的代碼就已經確定。這意味著代理類的結構在編譯時就已經固定,不會在運行時改變。
代理類固定:
靜態代理需要為每個被代理的類創建一個代理類。這意味著如果要代理多個類,就需要為每個類編寫一個對應的代理類。
低靈活性:
由于代理類在編譯時已經確定,因此靜態代理的靈活性相對較低。如果需要修改代理類的行為,通常需要修改代理類的源代碼,并重新編譯。
性能較高:
靜態代理的方法調用在編譯期間就已經確定,因此在運行時的性能通常比動態代理高。代理對象直接調用被代理對象的方法,不需要進行額外的方法查找或調用。
動態代理
我們將之前那個例子稍微拓展一下,四年級三班的同學上午進行了數學期中考試后,下午又進行了英語期中考試,可是四年級三班的英語課代表生病請假了,所以英語老師也麻煩小明同學(數學學習委員)來收試卷并且把試卷送給老師,最后再把試卷送到班級里。
你先別急著否認這個靜態代理做不到,靜態代理同樣能完成這件事情,我們再試著用靜態代理來完成這件事,
我們回顧一下之前小明同學的代碼:
我們的代理對象的構造函數中的參數是接口,那么理論上只要英語老師也實現這個接口,他也能傳進去,那就試試。
public class MathMonitor implements IMidterm_Examination{private int age=16;private String name="小明";private String job="數學學習委員";private IMidterm_Examination target;//代理目標,即被代理對象,接口是為了更加靈活通用public MathMonitor(IMidterm_Examination target) {this.target = target;//在構造代理對象時,將被代理對象傳入}
我們定義的接口不變
public interface IMidterm_Examination {//批改試卷的抽象方法void markPapers();
}
再來創建一個英語老師的被代理對象的類實現期中考試接口:
public class EnglishTeacher implements IMidterm_Examination{private int age=24;private String name="王雪";private String job="英語老師";@Overridepublic void markPapers() {System.out.println("英語老師正在批改英語試卷");}
}
只需要在測試主函數中傳入英語老師這個被代理對象,就行了
public static void main(String[] args) {//通過接口創建被代理對象,IMidterm_Examination mathTeacher=new MathTeacher();//再通過接口創建代理對象IMidterm_Examination mathMonitor=new MathMonitor(mathTeacher);//通過代理對象間接訪問數學老師這個對象mathMonitor.markPapers();//通過創建被代理對象,IMidterm_Examination englishTeacher=new EnglishTeacher();mathMonitor=new MathMonitor(englishTeacher);//通過代理對象間接訪問英語老師這個對象mathMonitor.markPapers();}
運行結果:
其實這么來說,靜態代理也有點“動”的意思,一個代理對象也能完成多個被代理對象的代理。
但是在真正的動態代理面前,它還差遠了。
前提是被代理類與代理類實現了相同的接口
動態代理的最大特色是代理類不需要實現與被代理類相同的接口就能實現代理,也就是說代理類在java中,動態代理一般有JDK接口和CGLib兩種方式進行實現,這里只介紹JDK接口方法。
我們動態代理來完成上面的例子
需要大改代碼的就是代理類,也就是小明這個數學學習委員的代碼,
代理類不在需要我們自定義的期中考試的接口
但它需要實現官方提供的InvocationHandler接口
public class MathMonitor implements InvocationHandler {private int age=16;private String name="小明";private String job="數學學習委員";private Object target;public MathMonitor(Object target) {this.target = target;}void sendPaper(){System.out.println("小明同學將試卷送給老師");}//小明額外的統計成績方法void countScores(){System.out.println("小明同學正在統計成績");}/** @param* Object proxy 傳入代理對象* Method method 傳入需要執行的方法* Object[] args 方法需要的參數數組* @return 返回一個Object類型的對象**/@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {//在間接訪問對象前可做的事情this.sendPaper();Object result = method.invoke(target, args);//在間接訪問對象后可做的事情this.countScores();return result;//在Java的反射API中,Method 類的 invoke 方法用于動態地調用一個方法。在你提供的 invoke 方法中,//這個 method.invoke(target, args) 調用實際上是在執行被代理對象(target)上的方法,// 并且傳遞了參數(args)。//method.invoke 的返回值就是該方法調用的結果。換句話說,它返回了被代理對象上被調用的方法的返回值。//如果方法類型是void,那么result值是null值}
}
測試主函數里的代碼也有點變化
//創建兩個被代理對象IMidterm_Examination mathTeacher =new MathTeacher();IMidterm_Examination englishTeacher=new EnglishTeacher();//先代理數學老師MathMonitor mathMonitor=new MathMonitor(mathTeacher);IMidterm_Examination proxy = (IMidterm_Examination) Proxy.newProxyInstance(Main.class.getClassLoader(),//獲取類加載器new Class[]{IMidterm_Examination.class},mathMonitor);proxy.markPapers();mathMonitor=new MathMonitor(englishTeacher);proxy = (IMidterm_Examination) Proxy.newProxyInstance(Main.class.getClassLoader(),//獲取類加載器new Class[]{IMidterm_Examination.class},mathMonitor);proxy.markPapers();}
}
Proxy.newProxyInstance 是 Java 動態代理的核心方法,用于創建一個新的代理實例。這個方法需要三個參數:
類加載器(ClassLoader):Main.class.getClassLoader()
類加載器用于加載代理類。在 Java 中,每個類都有一個類加載器,它負責加載類的字節碼文件。在動態代理中,代理類是在運行時生成的,因此需要一個類加載器來加載這個新生成的類。在這個例子中,使用 Main.class.getClassLoader() 獲取 Main 類的類加載器來加載代理類。
代理接口數組(Class<?>[] interfaces):new Class[]{IMidterm_Examination.class}
這個參數指定了代理實例需要實現的接口列表。代理實例將實現這些接口中定義的所有方法。當代理實例上的這些方法被調用時,它們將被轉發到 InvocationHandler 的 invoke 方法。在這個例子中,代理實例將實現 IMidterm_Examination 接口。
調用處理器(InvocationHandler):mathMonitor
InvocationHandler 是一個接口,它里面定義了一個 invoke 方法,用于處理代理實例上的方法調用。即傳入代理對象
動態代理的優點
動態代理是一種在運行時動態創建代理對象的機制,它允許你在調用實際對象之前或之后執行額外的操作。以下是動態代理的一些優點:
靈活性:
動態代理允許你在運行時創建代理對象,因此你可以根據需要動態地選擇要代理的對象,而無需在編譯時確定。這使得代碼更加靈活和可擴展。
減少重復代碼:
通過使用動態代理,你可以將一些通用的代碼邏輯(例如日志記錄、性能監控、事務管理等)從業務邏輯中分離出來,并將其放入代理對象中。這樣可以減少重復代碼,提高代碼的可維護性。
簡化代碼結構:
動態代理可以幫助你將關注點分離(Separation of Concerns),將橫切關注點(cross-cutting concerns)從核心業務邏輯中解耦。這樣可以使得代碼結構更加清晰,易于理解和維護。
提高代碼復用性:
通過將通用的功能封裝在代理對象中,可以使得這些功能在多個地方被重復使用,從而提高了代碼的復用性。
動態性:
由于動態代理是在運行時創建的,因此你可以根據需要動態地添加、修改或刪除代理對象的行為,而無需修改原始對象或重新編譯代碼。這使得系統更加靈活和動態。
代理模式與裝飾者模式的區別
在學習中,我很容易把裝飾器模式和代理模式混淆,老師說在現實開發中,確實是兩個都會混著用的,但是它們還是有一點小區別的。
意圖不同:
代理模式的主要目的是控制對對象的訪問。代理對象通常作為原始對象的接口,允許你在不直接訪問原始對象的情況下控制對其的訪問。
裝飾者模式的主要目的是為對象動態添加新的功能。裝飾者模式允許你通過將對象包裝在一個或多個裝飾者中,來動態地添加或修改對象的行為,而不需要改變其接口。
關注點不同:
代理模式的關注點在于控制對對象的訪問,通常涉及在訪問原始對象之前或之后執行額外的操作,如權限驗證、延遲加載、緩存等。
裝飾者模式的關注點在于動態地為對象添加新的行為,通常涉及在對象的行為上面添加修飾,如增加新的功能、改變行為等。
組合方式不同:
代理模式通常是一對一的關系,即每個代理對象只代理一個真實對象,并通過這個代理對象來控制對真實對象的訪問。
裝飾者模式則可以是多對一的關系,即一個對象可以被多個裝飾者對象裝飾,每個裝飾者對象可以在不影響其他裝飾者的情況下獨立地添加新的行為。
生命周期不同:
代理模式的生命周期通常與被代理對象相關聯,代理對象的創建和銷毀由被代理對象的創建和銷毀來管理。
裝飾者模式的生命周期則通常是短暫的,裝飾者對象通常是在運行時動態添加到被裝飾對象上,可以根據需要隨時添加或刪除。