代理模式是我們使用率比較高的一個(gè)模式耳高。它的定義是為其他對(duì)象提供一種代理以控制對(duì)這個(gè)對(duì)象的訪問(wèn)癞蚕。
如果只是從定義上來(lái)看,可能無(wú)法理解著蛙。為什么要用代理來(lái)對(duì)這個(gè)對(duì)象來(lái)訪問(wèn)删铃,我直接訪問(wèn)不行嗎?行踏堡,當(dāng)然行了猎唁。但是我們使用代理自然是有代理的優(yōu)勢(shì),我們舉個(gè)簡(jiǎn)單例子來(lái)說(shuō)明一下顷蟆。
有一個(gè)房東诫隅,他有一座房子要出售。但是房子買(mǎi)賣(mài)呢帐偎,我們知道逐纬,需要讓買(mǎi)房的人來(lái)看房,有意向之后還需要和顧客簽訂一系列的合同肮街,實(shí)在復(fù)雜风题。我們的房東比較懶,他不想做那么多事情,他只想單純賣(mài)房子沛硅。于是他找了一個(gè)中介眼刃,由中介來(lái)代替房東做這些事情。這個(gè)中介就可以說(shuō)是我們的代理摇肌。如果覺(jué)得還是覺(jué)得抽象的話擂红,下面我們會(huì)用實(shí)際開(kāi)發(fā)代碼來(lái)演示代理模式,深入理解围小。
代理模式的三個(gè)重要角色:
Subject抽象主題角色
- 抽象主題類(lèi)可以是抽象類(lèi)也可以是接口昵骤,是一個(gè)最普通的業(yè)務(wù)類(lèi)型定義,無(wú)特殊要求肯适。
RealSubject具體主題角色
- 被委托角色变秦,被代理角色。也就是我們的房東
Proxy代理主題角色
- 也叫做委托類(lèi)框舔、代理類(lèi)蹦玫。它負(fù)責(zé)對(duì)真實(shí)角色的應(yīng)用,把所有抽象主題類(lèi)定義的方法限制委托給真實(shí)主題角色實(shí)現(xiàn)刘绣,并且在真實(shí)主題角色處理完畢前后做預(yù)處理和善后處理工作樱溉。(我們的中介)
關(guān)于我們的代理模式大致可以分成兩種,靜態(tài)代理模式和動(dòng)態(tài)代理模式纬凤。下面我們會(huì)用代碼來(lái)分別演示一下兩種代理的區(qū)別福贞。
靜態(tài)代理
靜態(tài)代理呢,就是我們的每一個(gè)具體主題角色都有自己專(zhuān)門(mén)的代理角色停士。我們用代碼來(lái)說(shuō)吧挖帘,我們開(kāi)發(fā)業(yè)務(wù)的時(shí)候,需要抽象service接口和具體實(shí)現(xiàn)向瓷。我們模擬一個(gè)增刪改查肠套。
package proxydemo;
public interface Service {
//比如我們的業(yè)務(wù)有這四個(gè)方法
void add();
void delete();
void update();
void query();
}
然后就是我們?nèi)?shí)現(xiàn)這個(gè)接口
package proxydemo;
public class ServiceImpl implements Service {
@Override
public void add() {
System.out.println("增加操作");
}
@Override
public void delete() {
System.out.println("刪除操作");
}
@Override
public void update() {
System.out.println("更新操作");
}
@Override
public void query() {
System.out.println("查詢操作");
}
}
這個(gè)service接口就可以看作是我們的抽象主題角色,具體實(shí)現(xiàn)類(lèi)就是我們的具體主題角色猖任。我們可以在客戶端使用
package proxydemo;
public class Client {
public static void main(String[] args) {
ServiceImpl service = new ServiceImpl();
service.add();
service.delete();
service.update();
service.query();
}
}
|----------控制臺(tái)輸出--------|
增加操作
刪除操作
更新操作
查詢操作
Process finished with exit code 0
但是如果這個(gè)時(shí)候來(lái)了一個(gè)需求你稚,說(shuō)要在我們使用方法后,日志能夠記錄下來(lái)是進(jìn)行了什么操作朱躺。那我們?nèi)菀紫氲降木褪窃谖覀兊膶?shí)現(xiàn)類(lèi)去修改刁赖,在使用后方法后用日志記錄下來(lái)。但是我們?cè)陂_(kāi)發(fā)中最忌諱就是修改原有的代碼长搀,因?yàn)槲覀兪且祥_(kāi)閉原則的宇弛。所有我們就可以用代理模式來(lái)
package proxydemo;
//我們的代理角色,在不修改原來(lái)的代碼情況下源请,擴(kuò)展了日志的功能
public class ProxyServiceImpl implements Service{
//使用組合模式代替繼承
private ServiceImpl service;
public void setService(ServiceImpl service) {
this.service = service;
}
@Override
public void add() {
System.out.println("日志輸出:使用add方法");
service.add();
}
@Override
public void delete() {
System.out.println("日志輸出:使用delete方法");
service.delete();
}
@Override
public void update() {
System.out.println("日志輸出:使用update方法");
service.update();
}
@Override
public void query() {
System.out.println("日志輸出:使用query方法");
service.query();
}
}
然后在我們的客戶端使用的時(shí)候枪芒,只需要給代理角色設(shè)置需要被代理的角色就行
package proxydemo;
public class Client {
public static void main(String[] args) {
//實(shí)例化我們?cè)瓉?lái)的功能
ServiceImpl service = new ServiceImpl();
//實(shí)例我們的代理角色
ProxyServiceImpl proxyService = new ProxyServiceImpl();
//設(shè)置需要被代理的角色
proxyService.setService(service);
//使用有日志功能的代理方法
proxyService.add();
proxyService.delete();
proxyService.update();
proxyService.query();
}
}
|----------控制臺(tái)輸出--------|
日志輸出:使用add方法
增加操作
日志輸出:使用delete方法
刪除操作
日志輸出:使用update方法
更新操作
日志輸出:使用query方法
查詢操作
Process finished with exit code 0
靜態(tài)代理總結(jié)
好處:
- 可以使真實(shí)角色的操作更加純粹彻况!不用去關(guān)注一些公共的業(yè)務(wù)
- 一些非核心的功能交給了代理角色,實(shí)現(xiàn)了業(yè)務(wù)的分工
- 公共業(yè)務(wù)擴(kuò)展的時(shí)候舅踪,方便集中管理
缺點(diǎn):
- 上面也說(shuō)了纽甘,每一個(gè)真實(shí)角色就會(huì)產(chǎn)生一個(gè)代理角色,代碼量翻倍
動(dòng)態(tài)代理
動(dòng)態(tài)代理就是為了解決我們上面那個(gè)代碼量大的解決問(wèn)題抽碌,也是我們真正在開(kāi)發(fā)中會(huì)去使用的代理模式悍赢。我們的動(dòng)態(tài)代理有兩種方式去實(shí)現(xiàn),一個(gè)是JDK動(dòng)態(tài)代理(面向接口)货徙,一個(gè)是CGlib動(dòng)態(tài)代理(面向類(lèi))左权。我們這里主要是介紹概念,所以就用JDK動(dòng)態(tài)代理來(lái)實(shí)現(xiàn)我們上面的例子痴颊,另一種的使用可以看這里(動(dòng)態(tài)代理的兩種方式以及區(qū)別)
(補(bǔ)充:現(xiàn)在也出了Javassit去實(shí)現(xiàn)動(dòng)態(tài)代理)
關(guān)于JDK的動(dòng)態(tài)代理赏迟,我們?cè)?code>java.lang.reflect包下,有這么一個(gè)接口
Interface InvocationHandler :
InvocationHandler
是由代理實(shí)例的調(diào)用處理程序實(shí)現(xiàn)的接口 蠢棱。
每個(gè)代理實(shí)例都有一個(gè)關(guān)聯(lián)的調(diào)用處理程序瀑梗。 當(dāng)在代理實(shí)例上調(diào)用方法時(shí),方法調(diào)用將被編碼并分派到其調(diào)用處理程序的
invoke
方法裳扯。
還有一個(gè)Proxy
類(lèi),它提供了創(chuàng)建動(dòng)態(tài)代理類(lèi)和實(shí)例的靜態(tài)方法谤职,它也是由這些方法創(chuàng)建的所有動(dòng)態(tài)代理類(lèi)的超類(lèi)饰豺。
可以通過(guò)反射來(lái)創(chuàng)建代理
Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),
new Class<?>[] { Foo.class },
handler);
話不多說(shuō),我們用代碼了實(shí)現(xiàn)一下更容易明白這些抽象的解釋允蜈。
例子還是調(diào)用我們的上面的例子冤吨,只不過(guò)這個(gè)時(shí)候,我們的代理類(lèi)不需要去實(shí)現(xiàn)了饶套,我們實(shí)現(xiàn)InvocationHandler接口寫(xiě)調(diào)用程序就行了漩蟆。
package proxydemo;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class ProxyInvocationHandler implements InvocationHandler {
//被代理的接口
public Object target;
public void setTarget(Object object) {
this.target = object;
}
//通過(guò)反射生成得到代理類(lèi)
public Object getProxy(){
//三個(gè)參數(shù),當(dāng)前的類(lèi)加載器妓蛮,被代理的接口怠李,以及一個(gè)InvocationHandler,當(dāng)前即可
return Proxy.newProxyInstance
(this.getClass().getClassLoader(),
target.getClass().getInterfaces(),this);
}
//處理代理實(shí)例蛤克,返回結(jié)果
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//通過(guò)反射來(lái)調(diào)用方法
Object result = method.invoke(target,args);
return result;
}
}
我們?cè)谖覀兊目蛻舳耸褂玫臅r(shí)候捺癞,需要去實(shí)例化一個(gè)InvocationHandler就行了。
package proxydemo;
public class Client {
public static void main(String[] args) {
//實(shí)例一個(gè)我們的需要被代理的角色
ServiceImpl service = new ServiceImpl();
//實(shí)例我們的代理調(diào)用處理
ProxyInvocationHandler pih = new ProxyInvocationHandler();
//設(shè)置被代理的角色
pih.setTarget(service);
//通過(guò)反射了來(lái)實(shí)例我們的代理類(lèi)
Service proxyService = (Service) pih.getProxy();
proxyService.add();
proxyService.delete();
proxyService.update();
proxyService.query();
}
}
|----------控制臺(tái)輸出--------|
增加操作
刪除操作
更新操作
查詢操作
Process finished with exit code 0
具體的調(diào)用過(guò)程就是
這個(gè)時(shí)候如果我們要增加一個(gè)日志輸出功能的話构挤,只需要在我們的ProxyInvocationHandler里面增加就行髓介。
package proxydemo;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class ProxyInvocationHandler implements InvocationHandler {
//被代理的接口
private Object target;
//設(shè)計(jì)被代理的接口
public void setTarget(Object object) {
this.target = object;
}
//通過(guò)反射生成得到代理類(lèi)
public Object getProxy(){
//三個(gè)參數(shù),當(dāng)前的類(lèi)加載器筋现,被代理的接口唐础,以及一個(gè)InvocationHandler箱歧,當(dāng)前即可
return Proxy.newProxyInstance
(this.getClass().getClassLoader(),
target.getClass().getInterfaces(),this);
}
//處理代理實(shí)例,返回結(jié)果
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//通過(guò)反射來(lái)調(diào)用方法
log(method.getName()); //將方法名傳給我們的輸出方法
Object result = method.invoke(target,args);
return result;
}
//新增加的日志輸出功能
public void log(String msg){
System.out.println("日志輸出一膨,調(diào)用了"+msg+"方法");
}
}
客戶端輸出:
日志輸出呀邢,調(diào)用了add方法
增加操作
日志輸出,調(diào)用了delete方法
刪除操作
日志輸出汞幢,調(diào)用了update方法
更新操作
日志輸出驼鹅,調(diào)用了query方法
查詢操作
Process finished with exit code 0
看到這里可能還是會(huì)有小伙伴不是很懂, 上面我們的靜態(tài)代理實(shí)現(xiàn)了一個(gè)代理類(lèi)森篷,這里不也是實(shí)現(xiàn)了一個(gè)調(diào)用處理嗎输钩?注意,這里我們的調(diào)用處理可是一個(gè)可以復(fù)用的仲智,只要你是實(shí)現(xiàn)了基于接口實(shí)現(xiàn)的業(yè)務(wù)就可以調(diào)用买乃。比如我們一個(gè)龐大的業(yè)務(wù)的層,有很多的servcie接口钓辆,都可以統(tǒng)一使用這個(gè)動(dòng)態(tài)代理模板剪验。我們不需要在實(shí)現(xiàn)階段去關(guān)心代理誰(shuí),在使用的時(shí)候才指定代理誰(shuí)G傲9ζ荨!
總結(jié)
關(guān)于代理我們也了解似嗤,它主要的優(yōu)點(diǎn)也很明顯
- 職責(zé)清晰啸臀。真實(shí)的角色就是實(shí)現(xiàn)實(shí)際的業(yè)務(wù)邏輯,不用關(guān)心其他非本職責(zé)的事務(wù)烁落,通過(guò)后期的代理完成一件事務(wù)乘粒,附帶的結(jié)果就是編程簡(jiǎn)潔清晰。
- 高擴(kuò)展性伤塌。具體主題角色是隨時(shí)都會(huì)發(fā)生變化的灯萍,只要它實(shí)現(xiàn)了接口,甭管它如何變化每聪,都逃不脫如來(lái)佛的手掌(接口)旦棉,那我們的代理類(lèi)完全就可以在不做任何修改的情況下使用。
- 智能化药薯。都不用自己實(shí)現(xiàn)代理他爸,只需要在使用的時(shí)候去指定就行了。
使用場(chǎng)景
關(guān)于代理模式應(yīng)用的場(chǎng)景果善,一個(gè)比較典型的動(dòng)態(tài)代理就是Spring AOP了诊笤,我們用AOP來(lái)面向切面編程。在我們完成核心業(yè)務(wù)之后巾陕,然后可以橫向擴(kuò)展我們的周邊業(yè)務(wù)讨跟,比如日志輸出纪他,監(jiān)控等。具體的可以去了解AOP晾匠,了解之后對(duì)其他的場(chǎng)景茶袒,下次一見(jiàn)到就能看出來(lái)這用到了代理模式了。
參考資料
設(shè)計(jì)模式之禪(第二版)