點贊再看瓜贾,養(yǎng)成習慣,公眾號搜一搜【一角錢技術】關注更多原創(chuàng)技術文章携悯。本文 GitHub org_hejianhui/JavaStudy 已收錄阐虚,有我的系列文章。
前言
- 23種設計模式速記
- 單例(singleton)模式
- 工廠方法(factory method)模式
- 抽象工廠(abstract factory)模式
- 建造者/構(gòu)建器(builder)模式
- 原型(prototype)模式
- 享元(flyweight)模式
- 外觀(facade)模式
- 適配器(adapter)模式
- 裝飾(decorator)模式
- 觀察者(observer)模式
- 策略(strategy)模式
- 橋接(bridge)模式
- 模版方法(template method)模式
- 責任鏈(chain of responsibility)模式
- 組合(composite)模式
- 持續(xù)更新中......
23種設計模式快速記憶的請看上面第一篇蚌卤,本篇和大家一起來學習代理模式相關內(nèi)容。
模式定義
由于某些原因需要給某對象提供一個代理以控制對該對象的訪問奥秆。這時逊彭,訪問對象不適合或者不能直接引用目標對象,代理對象作為訪問對象和目標對象之間的中介构订。
代理模式的結(jié)構(gòu)比較簡單侮叮,主要是通過定義一個繼承抽象主題的代理來包含真實主題,從而實現(xiàn)對真實主題的訪問悼瘾。
在代碼中囊榜,一般代理會被理解為代碼增強,實際上就是在原代碼邏輯前后增加一些代碼邏輯亥宿,而使調(diào)用者無感知卸勺。
根據(jù)代理的創(chuàng)建時期,代理模式分為靜態(tài)代理和動態(tài)代理烫扼。
- 靜態(tài):由程序員創(chuàng)建代理類或特定工具自動生成源代碼再對其編譯曙求,在程序運行前代理類的 .class 文件就已經(jīng)存在了。
- 動態(tài):在程序運行時映企,運用反射機制動態(tài)創(chuàng)建而成
解決的問題
在直接訪問對象時帶來的問題悟狱,比如說:要訪問的對象在遠程的機器上。在面向?qū)ο笙到y(tǒng)中堰氓,有些對象由于某些原因(比如對象創(chuàng)建開銷很大挤渐,或者某些操作需要安全控制,或者需要進程外的訪問)双絮,直接訪問會給使用者或者系統(tǒng)結(jié)構(gòu)帶來很多麻煩浴麻,我們可以在訪問此對象時加上一個對此對象的訪問層。
模式組成
組成(角色) | 作用 |
---|---|
抽象主題(Subject)類 | 通過接口或抽象類聲明真實主題和代理對象實現(xiàn)的業(yè)務方法掷邦。 |
真實主題(Real Subject)類 | 實現(xiàn)了抽象主題中的具體業(yè)務白胀,是代理對象所代表的真實對象,是最終要引用的對象抚岗。 |
代理(Proxy)類 | 提供了與真實主題相同的接口或杠,其內(nèi)部含有對真實主題的引用,它可以訪問宣蔚、控制或擴展真實主題的功能向抢。 |
靜態(tài)代理
使用步驟
步驟1:定義抽象主題類
//抽象主題
interface Subject {
void request();
}
步驟2:定義真實主題類
//真實主題
class RealSubject implements Subject {
public void request() {
System.out.println("訪問真實主題方法...");
}
}
步驟3:定義代理類
//代理
class Proxy implements Subject {
private RealSubject realSubject;
public void request() {
if (realSubject == null) {
realSubject = new RealSubject();
}
preRequest();
realSubject.request();
postRequest();
}
public void preRequest() {
System.out.println("訪問真實主題之前的預處理认境。");
}
public void postRequest() {
System.out.println("訪問真實主題之后的后續(xù)處理。");
}
}
步驟4:測試
public class ProxyPattern {
public static void main(String[] args) {
Proxy proxy = new Proxy();
proxy.request();
}
}
輸出結(jié)果如下:
訪問真實主題之前的預處理挟鸠。
訪問真實主題方法...
訪問真實主題之后的后續(xù)處理叉信。
可以看到,主題接口是Subject艘希,真實主題是RealSubject 實現(xiàn)了Subject接口硼身,代理類是Proxy,在代理類的方法里實現(xiàn)了Subject類覆享,并且在代碼里寫死了代理前后的操作佳遂,這就是靜態(tài)代理的簡單實現(xiàn),可以看到靜態(tài)代理的實現(xiàn)優(yōu)缺點十分明顯撒顿。
靜態(tài)代理優(yōu)點
使得真實主題處理的業(yè)務更加純粹丑罪,不再去關注一些公共的事情,公共的業(yè)務由代理來完成凤壁,實現(xiàn)業(yè)務的分工吩屹,公共業(yè)務發(fā)生擴展時變得更加集中和方便。
靜態(tài)代理缺點
這種實現(xiàn)方式很直觀也很簡單拧抖,但其缺點是代理類必須提前寫好煤搜,如果主題接口發(fā)生了變化,代理類的代碼也要隨著變化唧席,有著高昂的維護成本宅楞。
針對靜態(tài)代理的缺點,是否有一種方式彌補袱吆?能夠不需要為每一個接口寫上一個代理方法厌衙,那就動態(tài)代理。
動態(tài)代理
動態(tài)代理绞绒,在java代碼里動態(tài)代理類使用字節(jié)碼動態(tài)生成加載
技術婶希,在運行時生成加載類。
生成動態(tài)代理類的方法很多蓬衡,比如:JDK 自帶的動態(tài)處理喻杈、CGLIB、Javassist狰晚、ASM 庫筒饰。
- JDK 的動態(tài)代理使用簡單,它內(nèi)置在 JDK 中壁晒,因此不需要引入第三方 Jar 包瓷们,但相對功能比較弱。
- CGLIB 和 Javassist 都是高級的字節(jié)碼生成庫,總體性能比 JDK 自帶的動態(tài)代理好谬晕,而且功能十分強大碘裕。
- ASM 是低級的字節(jié)碼生成工具,使用 ASM 已經(jīng)近乎于在使用 Java bytecode 編程攒钳,對開發(fā)人員要求最高帮孔,當然,也是性能最好的一種動態(tài)代理生成工具不撑。但 ASM 的使用很繁瑣文兢,而且性能也沒有數(shù)量級的提升,與 CGLIB 等高級字節(jié)碼生成工具相比焕檬,ASM 程序的維護性較差禽作,如果不是在對性能有苛刻要求的場合,還是推薦 CGLIB 或者 Javassist揩页。
我們這里介紹兩種非常常用的動態(tài)代理技術,面試時也會常常用到的技術:JDK 自帶的動態(tài)處理
烹俗、CGLIB
兩種爆侣。
JDK動態(tài)代理
Java提供了一個Proxy類,使用Proxy類的newInstance方法可以生成某個對象的代理對象幢妄,該方法需要三個參數(shù):
- 類裝載器【一般我們使用的是被代理類的裝載器】
- 指定接口【指定要被代理類的接口】
- 代理對象的方法里干什么事【實現(xiàn)handler接口】
使用步驟
步驟1:定義抽象主題類
//抽象主題
interface Subject {
void request();
}
步驟2:定義真實主題類
//真實主題
class RealSubject implements Subject {
public void request() {
System.out.println("訪問真實主題方法...");
}
}
步驟3:使用Proxy.newProxyInstance生成代理對象
class ProxyHandler implements InvocationHandler {
private Subject subject; // 定義主題接口
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 如果是第一次調(diào)用兔仰,生成真實主題
if (subject == null) {
subject = new RealSubject();
}
if ("request".equalsIgnoreCase(method.getName())) {
System.out.println("訪問真實主題之前的預處理。");
Object result = method.invoke(subject, args);
System.out.println("訪問真實主題之后的后續(xù)處理蕉鸳。");
return result;
} else {
// 如果不是調(diào)用request方法乎赴,返回真實主題完成實際操作
return method.invoke(subject, args);
}
}
// 使用Proxy.newProxyInstance生成代理對象
static Subject createProxy() {
Subject proxy = (Subject) Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(), // 當前類的類加載器
new Class[]{Subject.class}, //被代理的主題接口
new ProxyHandler() // 代理對象,這里是當前對象
);
return proxy;
}
}
步驟4:測試輸出
public class ProxyPattern {
public static void main(String[] args) {
Subject subject = ProxyHandler.createProxy();
subject.request();
}
}
輸出結(jié)果如下:
訪問真實主題之前的預處理潮尝。
訪問真實主題方法...
訪問真實主題之后的后續(xù)處理榕吼。
用debug的方式啟動,可以看到方法被代理到代理類中實現(xiàn)勉失,在代理類中執(zhí)行真實主題的方法前后可以進行很多操作羹蚣。
雖然這種方法實現(xiàn)看起來很方便,但是細心的同學應該也已經(jīng)觀察到了乱凿,JDK動態(tài)代理技術的實現(xiàn)是必須要一個接口才行的顽素,所以JDK動態(tài)代理的優(yōu)缺點也非常明顯
JDK動態(tài)代理優(yōu)點
- 不需要為真實主題寫一個形式上完全一樣的封裝類,減少維護成本徒蟆;
- 可以在運行時制定代理類的執(zhí)行邏輯胁出,提升系統(tǒng)的靈活性;
JDK動態(tài)代理缺點
- JDK動態(tài)代理段审,真實主題 必須實現(xiàn)的主題接口全蝶,如果真實主題 沒有實現(xiàn)主圖接口,或者沒有主題接口,則不能生成代理對象裸诽。
由于必須要有接口才能使用JDK的動態(tài)代理嫂用,那是否有一種方式可以沒有接口只有真實主題實現(xiàn)類也可以使用動態(tài)代理呢?這就是第二種動態(tài)代理:
CGLIB
丈冬;
CGLIB動態(tài)代理
使用 CGLIB
生成動態(tài)代理嘱函,首先需要生成 Enhancer
類實例,并指定用于處理代理業(yè)務的回調(diào)類埂蕊。在 Enhancer.create()
方法中往弓,會使用 DefaultGeneratorStrategy.Generate()
方法生成動態(tài)代理類的字節(jié)碼,并保存在 byte 數(shù)組中蓄氧。接著使用 ReflectUtils.defineClass()
方法函似,通過反射,調(diào)用 ClassLoader.defineClass()
方法喉童,將字節(jié)碼裝載到 ClassLoader 中撇寞,完成類的加載。最后使用 ReflectUtils.newInstance()
方法堂氯,通過反射蔑担,生成動態(tài)類的實例,并返回該實例咽白∑∥眨基本流程是根據(jù)指定的回調(diào)類生成 Class 字節(jié)碼—通過 defineClass()
將字節(jié)碼定義為類—使用反射機制生成該類的實例。
使用步驟
步驟1:定義真實主題
class WorkImpl {
void addWorkExperience() {
System.out.println("增加工作經(jīng)驗的普通方法...");
}
}
步驟2:創(chuàng)建代理類
class WorkImplProxyLib implements MethodInterceptor {
// 創(chuàng)建代理對象
Object getWorkProxyImplInstance() {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(WorkImpl.class);
// 回調(diào)方法
enhancer.setCallback(this);
// 創(chuàng)建代理對象
return enhancer.create();
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("開始...");
methodProxy.invokeSuper(obj, args);
System.out.println("結(jié)束...");
return null;
}
}
步驟3:測試輸出
public class CglibProxy {
public static void main(String[] args) {
WorkImplProxyLib cglib = new WorkImplProxyLib();
WorkImpl workCglib = (WorkImpl) cglib.getWorkProxyImplInstance();
workCglib.addWorkExperience();
}
}
輸出結(jié)果如下:
開始...
增加工作經(jīng)驗的普通方法...
結(jié)束...
CGLIB動態(tài)代理優(yōu)點
CGLIB通過繼承的方式進行代理晶框、無論目標對象沒有沒實現(xiàn)接口都可以代理排抬,彌補了JDK動態(tài)代理的缺陷。
CGLIB動態(tài)代理缺點
- CGLib創(chuàng)建的動態(tài)代理對象性能比JDK創(chuàng)建的動態(tài)代理對象的性能高不少授段,但是CGLib在創(chuàng)建代理對象時所花費的時間卻比JDK多得多蹲蒲,所以對于單例的對象,因為無需頻繁創(chuàng)建對象侵贵,用CGLib合適悠鞍,反之,使用JDK方式要更為合適一些模燥。
- 由于CGLib由于是采用動態(tài)創(chuàng)建子類的方法咖祭,對于final方法,無法進行代理蔫骂。
應用場景
當無法或不想直接引用某個對象或訪問某個對象存在困難時么翰,可以通過代理對象來間接訪問。使用代理模式主要有兩個目的:一是保護目標對象辽旋,二是增強目標對象浩嫌。
代理模式有多種應用場合檐迟,如下所述:
- 遠程代理,也就是為一個對象在不同的地址空間提供局部代表码耐,這樣可以隱藏一個對象存在于不同地址空間的事實追迟。比如說 WebService,當我們在應用程序的項目中加入一個 Web 引用骚腥,引用一個 WebService敦间,此時會在項目中聲稱一個 WebReference 的文件夾和一些文件,這個就是起代理作用的束铭,這樣可以讓那個客戶端程序調(diào)用代理解決遠程訪問的問題廓块;
- 虛擬代理,是根據(jù)需要創(chuàng)建開銷很大的對象契沫,通過它來存放實例化需要很長時間的真實對象带猴。這樣就可以達到性能的最優(yōu)化,比如打開一個網(wǎng)頁懈万,這個網(wǎng)頁里面包含了大量的文字和圖片拴清,但我們可以很快看到文字,但是圖片卻是一張一張地下載后才能看到会通,那些未打開的圖片框口予,就是通過虛擬代里來替換了真實的圖片,此時代理存儲了真實圖片的路徑和尺寸渴语;
- 安全代理,用來控制真實對象訪問時的權(quán)限昆咽。一般用于對象應該有不同的訪問權(quán)限的時候驾凶;
- 指針引用,是指當調(diào)用真實的對象時掷酗,代理處理另外一些事调违。比如計算真實對象的引用次數(shù),這樣當該對象沒有引用時泻轰,可以自動釋放它技肩,或當?shù)谝淮我靡粋€持久對象時,將它裝入內(nèi)存浮声,或是在訪問一個實際對象前虚婿,檢查是否已經(jīng)釋放它,以確保其他對象不能改變它泳挥。這些都是通過代理在訪問一個對象時附加一些內(nèi)務處理然痊;
- 延遲加載,用代理模式實現(xiàn)延遲加載的一個經(jīng)典應用就在 Hibernate 框架里面屉符。當 Hibernate 加載實體 bean 時剧浸,并不會一次性將數(shù)據(jù)庫所有的數(shù)據(jù)都裝載锹引。默認情況下,它會采取延遲加載的機制唆香,以提高系統(tǒng)的性能嫌变。Hibernate 中的延遲加載主要分為屬性的延遲加載和關聯(lián)表的延時加載兩類。實現(xiàn)原理是使用代理攔截原有的 getter 方法躬它,在真正使用對象數(shù)據(jù)時才去數(shù)據(jù)庫或者其他第三方組件加載實際的數(shù)據(jù)腾啥,從而提升系統(tǒng)性能。
其它代理模式
- 防火墻代理:內(nèi)網(wǎng)通過代理穿透防火墻虑凛,實現(xiàn)對公網(wǎng)的訪問碑宴;
- 緩存代理:比如當我們請求圖片文件資源時,先到緩存代理取桑谍,如果取不到資源再到公網(wǎng)或者數(shù)據(jù)庫取延柠,然后緩存;
- 遠程代理:遠程對象的本地代表锣披,通過它可以把遠程對象來調(diào)用贞间。遠程代理通過網(wǎng)絡和真正的遠程對象溝通;
- 同步代理:主要用在多線程編程中雹仿,完成多線程間同步工作增热。
PS:以上代碼提交在 Github :https://github.com/Niuh-Study/niuh-designpatterns.git
文章持續(xù)更新,可以公眾號搜一搜「 一角錢技術 」第一時間閱讀胧辽, 本文 GitHub org_hejianhui/JavaStudy 已經(jīng)收錄峻仇,歡迎 Star。