最近在復(fù)習(xí) Java 相關(guān)涮总,回顧了下代理模式。代理模式在 Java 領(lǐng)域很多地方都有應(yīng)用祷舀,它分為靜態(tài)代理和動(dòng)態(tài)代理瀑梗,其中 Spring AOP 就是動(dòng)態(tài)代理的典型例子。動(dòng)態(tài)代理又分為接口代理和 cglib (子類代理)裳扯,結(jié)合我的理解寫了幾個(gè) demo 分享給你們抛丽,這是昨晚修仙到 3 點(diǎn)寫出來(lái)的文章,不點(diǎn)在看饰豺,我覺得說(shuō)不過(guò)去了亿鲜。
代理模式在我們?nèi)粘V泻艹R姡钐幪幱写恚?/p>
- 看張學(xué)友的演唱會(huì)很難搶票哟忍,可以找黃牛排隊(duì)買
- 嫌出去吃飯麻煩狡门,可以叫外賣
無(wú)論是黃牛自点、外賣騎手都得幫我們干活。但是他們不能一手包辦(比如黃牛不能幫我吃飯)赵抢,他們只能做我們不能或者不想做的事嫩舟。
- 找黃牛可以幫我排隊(duì)買上張學(xué)友的演唱會(huì)門票
- 外賣騎手可以幫我把飯送到樓下
所以叛复,你看仔引。代理模式其實(shí)就是當(dāng)前對(duì)象不愿意做的事情,委托給別的對(duì)象做褐奥。
靜態(tài)代理
我還是以找黃牛幫我排隊(duì)買張學(xué)友的演唱會(huì)門票的例子咖耘,寫個(gè) demo 說(shuō)明。現(xiàn)在有一個(gè) Human 接口撬码,無(wú)論是我還是黃牛都實(shí)現(xiàn)了這個(gè)接口儿倒。
public interface Human {
void eat();
void sleep();
void lookConcert();
}
例如,我這個(gè)類呜笑,我會(huì)吃飯和睡覺夫否,如以下類:
public class Me implements Human{
@Override
public void eat() {
System.out.println("eat emat ....");
}
@Override
public void sleep() {
System.out.println("Go to bed at one o'clock in the morning");
}
@Override
public void lookConcert() {
System.out.println("Listen to Jacky Cheung's Concert");
}
}
有黃牛類,例如:
public class Me implements Human{
@Override
public void eat() {
}
@Override
public void sleep() {
}
@Override
public void lookConcert() {
}
}
現(xiàn)在我和黃牛都已經(jīng)準(zhǔn)備好了叫胁,怎么把這二者關(guān)聯(lián)起來(lái)呢凰慈?我們要明確的是黃牛是要幫我買票的,買票必然就需要幫我排隊(duì)驼鹅,于是有以下黃牛類:注意這里我們不關(guān)心微谓,黃牛的其他行為,我們只關(guān)心他能不能排隊(duì)買票输钩。
public class HuangNiu implements Human{
private Me me;
public HuangNiu() {
me = new Me();
}
@Override
public void eat() {
}
@Override
public void sleep() {
}
@Override
public void lookConcert() {
// 添加排隊(duì)買票方法
this.lineUp();
me.lookConcert();
}
public void lineUp() {
System.out.println("line up");
}
}
最終的 main 方法調(diào)用如下:
public class Client {
public static void main(String[] args) {
Human human = new HuangNiu();
human.lookConcert();
}
}
結(jié)果如下:
由此可見豺型,黃牛就只是做了我們不愿意做的事(排隊(duì)買票),實(shí)際看演唱會(huì)的人還是我张足〈ゴ矗客戶端也并不關(guān)心代理類代理了哪個(gè)類,因?yàn)榇a控制了客戶端對(duì)委托類的訪問(wèn)为牍『甙螅客戶端代碼表現(xiàn)為 Human human = new HuangNiu();
由于代理類實(shí)現(xiàn)了抽象角色的接口,導(dǎo)致代理類無(wú)法通用碉咆。比如抖韩,我的狗病了,想去看醫(yī)生疫铜,但是排隊(duì)掛號(hào)很麻煩茂浮,我也想有個(gè)黃牛幫我的排隊(duì)掛號(hào)看病,但是黃牛它不懂這只狗的特性(黃牛跟狗不是同一類型,黃牛屬于 Human 但狗屬于 Animal 類)但排隊(duì)掛號(hào)和排隊(duì)買票相對(duì)于黃牛來(lái)說(shuō)它兩就是一件事席揽,這個(gè)方法是不變的顽馋,現(xiàn)場(chǎng)排隊(duì)。那我們能不能找一個(gè)代理說(shuō)既可以幫人排隊(duì)買票也可以幫狗排隊(duì)掛號(hào)呢幌羞?
答案肯定是可以的寸谜,可以用動(dòng)態(tài)代理。
基于接口的動(dòng)態(tài)代理
如靜態(tài)代理的內(nèi)容所描述的属桦,靜態(tài)代理受限于接口的實(shí)現(xiàn)熊痴。動(dòng)態(tài)代理就是通過(guò)使用反射,動(dòng)態(tài)地獲取抽象接口的類型聂宾,從而獲取相關(guān)特性進(jìn)行代理果善。因動(dòng)態(tài)代理能夠?yàn)樗械奈蟹竭M(jìn)行代理,因此給代理類起個(gè)通用點(diǎn)的名字 HuangNiuHandle系谐。先看黃牛類可以變成什么樣巾陕?
public class HuangNiuHandle implements InvocationHandler {
private Object proxyTarget;
public Object getProxyInstance(Object target) {
this.proxyTarget = target;
return Proxy.newProxyInstance(proxyTarget.getClass().getClassLoader(), proxyTarget.getClass().getInterfaces(), this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object methodObject = null;
System.out.println("line up");
methodObject = method.invoke(proxyTarget, args);
System.out.println("go home and sleep");
return methodObject;
}
}
這個(gè)時(shí)候的客戶端代碼就變成這樣了
public class Client {
public static void main(String[] args) {
HuangNiuHandle huangNiuHandle = new HuangNiuHandle();
Human human = (Human) huangNiuHandle.getProxyInstance(new Me());
human.eat();
human.run();
human.lookConcert();
System.out.println("------------------");
Animal animal = (Animal) huangNiuHandle.getProxyInstance(new Dog());
animal.eat();
animal.run();
animal.seeADoctor();
}
}
使用動(dòng)態(tài)代理有三個(gè)要點(diǎn),
必須實(shí)現(xiàn) InvocationHandler 接口纪他,表明該類是一個(gè)動(dòng)態(tài)代理執(zhí)行類惜论。
InvocationHandler 接口內(nèi)有一實(shí)現(xiàn)方法如下: public Object invoke(Object proxy, Method method, Object[] args) 。使用時(shí)需要重寫這個(gè)方法
獲取代理類止喷,需要使用 Proxy.newProxyInstance(Clas loader, Class<?>[] interfaces, InvocationHandler h) 這個(gè)方法去獲取Proxy對(duì)象(Proxy 類類型的實(shí)例)。
注意到 Proxy.newProxyInstance 這個(gè)方法混聊,它需要傳入 3 個(gè)參數(shù)弹谁。解析如下:
// 第一個(gè)參數(shù),是類的加載器
// 第二個(gè)參數(shù)是委托類的接口類型句喜,證代理類返回的是同一個(gè)實(shí)現(xiàn)接口下的類型预愤,保持代理類與抽象角色行為的一致
// 第三個(gè)參數(shù)就是代理類本身,即告訴代理類咳胃,代理類遇到某個(gè)委托類的方法時(shí)該調(diào)用哪個(gè)類下的invoke方法
Proxy.newProxyInstance(Class loader, Class<?>[] interfaces, InvocationHandler h)
再來(lái)看看 invoke 方法植康,用戶調(diào)用代理對(duì)象的什么方法,實(shí)質(zhì)上都是在調(diào)用處理器的
invoke 方法展懈,通過(guò)該方法調(diào)用目標(biāo)方法销睁,它也有三個(gè)參數(shù):
// 第一個(gè)參數(shù)為 Proxy 類類型實(shí)例,如匿名的 $proxy 實(shí)例
// 第二個(gè)參數(shù)為委托類的方法對(duì)象
// 第三個(gè)參數(shù)為委托類的方法參數(shù)
// 返回類型為委托類某個(gè)方法的執(zhí)行結(jié)果
public Object invoke(Object proxy, Method method, Object[] args)
調(diào)用該代理類之后的輸出結(jié)果:
由結(jié)果可知存崖,黃牛不僅幫了(代理)我排隊(duì)買票冻记,還幫了(代理)我的狗排隊(duì)掛號(hào)。所以来惧,你看靜態(tài)代理需要自己寫代理類(代理類需要實(shí)現(xiàn)與目標(biāo)對(duì)象相同的接口)冗栗,還需要一一實(shí)現(xiàn)接口方法,但動(dòng)態(tài)代理不需要。
注意隅居,我們并不是所有的方法都需要黃牛這個(gè)代理去排隊(duì)钠至。我們知道只有我看演唱會(huì)和我的狗去看醫(yī)生時(shí),才需要黃牛胎源,如果要實(shí)現(xiàn)我們想要的方法上面添加特定的代理棉钧,可以通過(guò) invoke 方法里面的方法反射獲取 method 對(duì)象方法名稱即可實(shí)現(xiàn),所以動(dòng)態(tài)代理類可以變成這樣:
public class HuangNiuHandle implements InvocationHandler {
private Object proxyTarget;
public Object getProxyInstance(Object target) {
this.proxyTarget = target;
return Proxy.newProxyInstance(proxyTarget.getClass().getClassLoader(), proxyTarget.getClass().getInterfaces(), this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object methodObject = null;
if ("lookConcert".equals(method.getName()) ||
"seeADoctor".equals(method.getName())) {
System.out.println("line up");
// 調(diào)用目標(biāo)方法
methodObject = method.invoke(proxyTarget, args);
} else {
// 不使用第一個(gè)proxy參數(shù)作為參數(shù)乒融,否則會(huì)造成死循環(huán)
methodObject = method.invoke(proxyTarget, args);
}
return methodObject;
}
}
結(jié)果如下:可以看到我們只在特定方法求助了黃牛
由此可見掰盘,動(dòng)態(tài)代理一般應(yīng)用在記錄日志等橫向業(yè)務(wù)。
值得注意的是:
基于接口類的動(dòng)態(tài)代理模式赞季,必須具備抽象角色愧捕、委托類、代理三個(gè)基本角色申钩。委托類和代理類必須由抽象角色衍生出來(lái)次绘,否則無(wú)法使用該模式。
動(dòng)態(tài)代理模式最后返回的是具有抽象角色(頂層接口)的對(duì)象撒遣。在委托類內(nèi)被 private 或者 protected 關(guān)鍵修飾的方法將不會(huì)予以調(diào)用邮偎,即使允許調(diào)用。也無(wú)法在客戶端使用代理類轉(zhuǎn)換成子類接口义黎,對(duì)方法進(jìn)行調(diào)用禾进。也就是說(shuō)上述的動(dòng)態(tài)代理返回的是委托類(Me)或 (Dog)的就接口對(duì)象 (Human)或 (Animal)。
在 invoke 方法內(nèi)為什么不使用第一個(gè)參數(shù)進(jìn)行執(zhí)行回調(diào)廉涕。在客戶端使用getProxyInstance(new Child( ))時(shí)泻云,JDK 會(huì)返回一個(gè) proxy 的實(shí)例,實(shí)例內(nèi)有InvokecationHandler 對(duì)象及動(dòng)態(tài)繼承下來(lái)的目標(biāo) 狐蜕〕璐浚客戶端調(diào)用了目標(biāo)方法,有如下操作:首先 JDK 先查找 proxy 實(shí)例內(nèi)的 handler 對(duì)象 然后執(zhí)行 handler 內(nèi)的 invoke 方法层释。
根據(jù) public Object invoke 這個(gè)方法第一個(gè)參數(shù) proxy 就是對(duì)應(yīng)著 proxy 實(shí)例婆瓜。如果在 invoke 內(nèi)使用 method.invoke(proxy,args) ,會(huì)出現(xiàn)這樣一條方法鏈,目標(biāo)方法→invoke→目標(biāo)方法→invoke...贡羔,最終導(dǎo)致堆棧溢出廉白。
基于子類的動(dòng)態(tài)代理
為了省事,我這里并沒(méi)有繼承父類治力,但在實(shí)際開發(fā)中是需要繼承父類才比較方便擴(kuò)展的蒙秒。與基于接口實(shí)現(xiàn)類不同的是:
- CGLib (基于子類的動(dòng)態(tài)代理)使用的是方法攔截器 MethodInterceptor ,需要導(dǎo)入 cglib.jar 和 asm.jar 包
- 基于子類的動(dòng)態(tài)代理宵统,返回的是子類對(duì)象
- 方法攔截器對(duì) protected 修飾的方法可以進(jìn)行調(diào)用
代碼如下:
public class Me {
public void eat() {
System.out.println("eat meat ....");
}
public void run() {
System.out.println("I run with two legs");
}
public void lookConcert() {
System.out.println("Listen to Jacky Cheung's Concert");
}
protected void sleep() {
System.out.println("Go to bed at one o'clock in the morning");
}
}
Dog 類
public class Dog {
public void eat() {
System.out.println("eat Dog food ....");
}
public void run() {
System.out.println("Dog running with four legs");
}
public void seeADoctor() {
System.out.println("The dog go to the hospital");
}
}
黃牛代理類晕讲,注意 invoke() 這里多了一個(gè)參數(shù) methodProxy 覆获,它的作用是用于執(zhí)行目標(biāo)(委托類)的方法,至于為什么用 methodProxy 瓢省,官方的解釋是速度快且在intercep t內(nèi)調(diào)用委托類方法時(shí)不用保存委托對(duì)象引用弄息。
public class HuangNiuHandle implements MethodInterceptor {
private Object proxyTarget;
public Object getProxyInstance(Object target) {
this.proxyTarget = target;
return Enhancer.create(target.getClass(), target.getClass().getInterfaces(), this);
}
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
Object methodObject = null;
if ("lookConcert".equals(method.getName()) ||
"seeADoctor".equals(method.getName())) {
System.out.println("line up");
// 調(diào)用目標(biāo)方法
methodObject = methodProxy.invokeSuper(proxy, args);
} else {
methodObject = method.invoke(proxyTarget, args);
}
return methodObject;
}
}
client 類
public class Client {
public static void main(String[] args) {
HuangNiuHandle huangNiuHandle = new HuangNiuHandle();
Me me = (Me) huangNiuHandle.getProxyInstance(new Me());
me.eat();
me.run();
me.sleep();
me.lookConcert();
System.out.println("------------------");
Dog dog = (Dog) huangNiuHandle.getProxyInstance(new Dog());
dog.eat();
dog.run();
dog.seeADoctor();
}
}
結(jié)果:
注意到 Me 類中被 protected 修飾的方法 sleep 仍然可以被客戶端調(diào)用。這在基于接口的動(dòng)態(tài)代理中是不被允許的勤婚。
靜態(tài)代理與動(dòng)態(tài)代理的區(qū)別
靜態(tài)代理需要自己寫代理類并一一實(shí)現(xiàn)目標(biāo)方法摹量,且代理類必須實(shí)現(xiàn)與目標(biāo)對(duì)象相同的接口。
動(dòng)態(tài)代理不需要自己實(shí)現(xiàn)代理類馒胆,它是利用 JDKAPI缨称,動(dòng)態(tài)地在內(nèi)存中構(gòu)建代理對(duì)象(需要我們傳入被代理類),并且默認(rèn)實(shí)現(xiàn)所有目標(biāo)方法祝迂。
源碼下載:https://github.com/turoDog/review_java.git
后語(yǔ)
如果本文對(duì)你哪怕有一丁點(diǎn)幫助睦尽,請(qǐng)幫忙點(diǎn)好看,你的好看是我堅(jiān)持寫作的動(dòng)力型雳。關(guān)注公眾號(hào)一個(gè)優(yōu)秀的廢人回復(fù) 1024 獲取資料:Python当凡、C++、Java纠俭、Linux沿量、Go、前端冤荆、算法資料分享