前言
大家好,我是禿頭JAVA人。你們是否在編程中經(jīng)常遇見這樣一個(gè)問題,對(duì)于訪問某個(gè)對(duì)象,我們希望給它的方法前加入一個(gè)標(biāo)記,比如對(duì)象的方法開始執(zhí)行、結(jié)束等等(比如日志記錄)。怎么辦呢,這個(gè)時(shí)候只要我們編寫一個(gè)復(fù)制的類,然后把這個(gè)對(duì)象傳給這個(gè)類,再對(duì)這個(gè)類進(jìn)行操作,不就可以了嗎。這就是代理模式,復(fù)制的類就是代理對(duì)象,通過代理對(duì)象與我們進(jìn)行打交道就可以對(duì)它原來(lái)的對(duì)象進(jìn)行改造。對(duì)于有些時(shí)候現(xiàn)有的對(duì)象不能滿足我們的需求的時(shí)候,如何對(duì)它進(jìn)行擴(kuò)展,對(duì)方法進(jìn)行改造,使其適用于我們所面臨的問題,這就是代理模式的思維出發(fā)點(diǎn)。
目錄
一:代理模式的介紹
二:實(shí)現(xiàn)靜態(tài)代理
三:代理的進(jìn)階:實(shí)現(xiàn)動(dòng)態(tài)代理
四:總結(jié)
接下來(lái)按照目錄,我們來(lái)依次講解本篇博客:
一:代理模式的介紹
1.1:目標(biāo)
為其他對(duì)象提供一種代理以控制對(duì)這個(gè)對(duì)象的訪問
解釋:在實(shí)際編程中我們會(huì)產(chǎn)生一個(gè)代理對(duì)象,然后去引用被代理對(duì)象,對(duì)被代理對(duì)象進(jìn)行控制與訪問,實(shí)現(xiàn)客戶端對(duì)原代理對(duì)象的訪問,詳情見下面的代碼示例。
1.2:適用性
在需要用比較通用和復(fù)雜的對(duì)象指針代替簡(jiǎn)單的指針的時(shí)候,使用Proxy 模式。下面是一 些可以使用Proxy 模式常見情況:
1.2.1:遠(yuǎn)程代理(Remote Proxy )為一個(gè)對(duì)象在不同的地址空間提供局部代表。 NEXTSTEP[Add94] 使用N X P r o x y 類實(shí)現(xiàn)了這一目的。
1.2.2:虛代理(Virtual Proxy )根據(jù)需要?jiǎng)?chuàng)建開銷很大的對(duì)象。在動(dòng)機(jī)一節(jié)描述的I m a g e P r o x y 就是這樣一種代理的例子。
1.2.3: 保護(hù)代理(Protection Proxy )控制對(duì)原始對(duì)象的訪問。保護(hù)代理用于對(duì)象應(yīng)該有不同 的訪問權(quán)限的時(shí)候
1.2.4: 智能指引(Smart Reference )取代了簡(jiǎn)單的指針,它在訪問對(duì)象時(shí)執(zhí)行一些附加操作。
1.3:結(jié)構(gòu)
二:實(shí)現(xiàn)靜態(tài)代理
2.1:代碼場(chǎng)景
假如我們現(xiàn)在由以下的場(chǎng)景:文件編輯器要對(duì)一個(gè)圖像文件進(jìn)行操作,遵循以下順序:加載,繪制,獲取長(zhǎng)度和寬度,存儲(chǔ)四個(gè)步驟,但是有個(gè)問題,需要被加載的圖片非常大,每次加載的時(shí)候都要耗費(fèi)很多時(shí)間。并且我們希望對(duì)圖片的操作可以記錄出來(lái)操作的步驟,比如第一步、第二步這樣便于我們?nèi)ダ斫?strong>。為了解決這個(gè)問題,我們可以先考慮解決第一個(gè)問題就是利用代理模式去新建一個(gè)代理對(duì)象,然后在代理對(duì)象里去實(shí)現(xiàn)一個(gè)緩存,這樣下次我們直接可以去緩存里面取對(duì)象,而不用去新建,這樣就省去了新建對(duì)象消耗的資源。另一方面,我們可以考慮去引用原來(lái)的方法,再給這方法基礎(chǔ)上添加我們所要做的記錄。接下里我們用java代碼來(lái)實(shí)現(xiàn)這個(gè)場(chǎng)景:
2.2:代碼示范
2.2.1:首先新建一個(gè)接口,命名為Graphic,其中主要規(guī)范了我們進(jìn)行操作的步驟
public interface Graphic {
void load();//加載
void Draw();//繪制
Extent GetExtent();//獲取長(zhǎng)度和寬度
void Store();//存儲(chǔ)
}
2.2.2:然后去新建一個(gè)Image類,用于實(shí)現(xiàn)接口,對(duì)操作進(jìn)行具體控制,注意為了其中的Extent是對(duì)寬度和長(zhǎng)度的封裝(省略get和set方法)
public class Image implements Graphic{
public Image() {
try {
Thread.sleep(2000); //模擬創(chuàng)建需要花費(fèi)很久的時(shí)間
System.out.println("正在創(chuàng)建對(duì)象");
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void load() {
System.out.println("進(jìn)行加載..");
}
@Override
public void Draw() {
System.out.println("進(jìn)行繪畫..");
}
@Override
public Extent GetExtent() {
Extent extent = new Extent("100","200");
System.out.println("獲取圖片的屬性是:"+extent.toString());
return extent;
}
@Override
public void Store() {
System.out.println("圖片進(jìn)行存儲(chǔ)在硬盤里..");
}
}
public class Extent {
private String width;
private String length;
public Extent(String width, String length) {
super();
this.width = width;
this.length = length;
}
//getter And setter方法
}
2.2.3:接下來(lái)就是很關(guān)鍵的一步了,新建我們的代理類,我們新建一個(gè)類叫做ImageProxy,然后實(shí)現(xiàn)緩存與記錄的效果:
import java.util.HashMap;
import java.util.Map;
public class ImageProxy implements Graphic{
private Image image;
private Map<String , Image> cache = new HashMap<String, Image>();//緩存
public ImageProxy() {
init();
}
public void init(){ //只需要初始化一次
if (image==null) {
image= new Image();
cache.put("image", image);//放入緩存
}else{
image=cache.get("image");
}
@Override
public void load() {
System.out.println("---第一步開始---");
image.load();
System.out.println("---第一步結(jié)束---");
}
@Override
public void Draw() {
System.out.println("---第二步開始---");
image.Draw();
System.out.println("---第二步結(jié)束---");
}
@Override
public Extent GetExtent() {
System.out.println("---第三步開始---");
Extent extent = image.GetExtent();
System.out.println("---第三步結(jié)束--");
return extent;
}
@Override
public void Store() {
System.out.println("---第四步開始---");
image.Store();
System.out.println("---第四步結(jié)束--");
}
}
2.2.4:我們的文檔編輯器現(xiàn)在要開始進(jìn)行文檔編輯了,我們來(lái)實(shí)現(xiàn)具體的代碼,我們先來(lái)引用一下原對(duì)象,看一下原來(lái)的對(duì)象會(huì)出現(xiàn)什么情況:
public class DocumentEditor {
public static void main(String[] args) {
Graphic proxy = new Image();//引用代碼
proxy.load();
proxy.Draw();
proxy.GetExtent();
proxy.Store();
}
}
2.2.5:測(cè)試代碼
正在創(chuàng)建對(duì)象
進(jìn)行加載..
進(jìn)行繪畫..
獲取圖片的屬性是:Extent [width=100, length=200]
圖片進(jìn)行存儲(chǔ)在硬盤里..
我們可以看出,它會(huì)消耗3秒才會(huì)出來(lái)具體的對(duì)象,并且沒有我們所需要的記錄。好了,我們把2.2.4的引用代碼改為: Graphic proxy = new ImageProxy();
我們?cè)賮?lái)測(cè)試一下:
正在創(chuàng)建對(duì)象
---第一步開始---
進(jìn)行加載..
---第一步結(jié)束---
---第二步開始---
進(jìn)行繪畫..
---第二步結(jié)束---
---第三步開始---
獲取圖片的屬性是:Extent [width=100, length=200]
---第三步結(jié)束--
---第四步開始---
圖片進(jìn)行存儲(chǔ)在硬盤里..
---第四步結(jié)束--
很明顯可以看出,通過訪問我們的代理對(duì)象,就可以實(shí)現(xiàn)對(duì)原方法的改造,這就是代理模式的精髓思想。不過到這里你可能會(huì)問,為什么不對(duì)原對(duì)象進(jìn)行改造呢?為什么要給他新建一個(gè)代理對(duì)象,這不是很麻煩嗎。回答這個(gè)問題,首先要提一個(gè)代碼的設(shè)計(jì)原則,也就是有名的開閉原則:對(duì)擴(kuò)展開放,對(duì)修改關(guān)閉。這句話的意思就是不建議對(duì)原有的代碼進(jìn)行修改,我們要做的事就是盡量不用動(dòng)原有的類和對(duì)象,在它的基礎(chǔ)上去改造,而不是直接去修改它。至于這個(gè)原則為什么這樣,我想其中一個(gè)原因就是因?yàn)檐浖w系中牽一發(fā)很動(dòng)全身的事情很常見,很可能你修改了這一小塊,然而與此相關(guān)的很多東西就會(huì)發(fā)生變化。所以輕易不要修改,而是擴(kuò)展。
三:實(shí)現(xiàn)動(dòng)態(tài)代理
3.1:靜態(tài)代理的不足:
通過看靜態(tài)代理可以動(dòng)態(tài)擴(kuò)展我們的對(duì)象,但是有個(gè)問題,在我們進(jìn)行方法擴(kuò)展的時(shí)候,比如我們的日志功能:每個(gè)前面都得寫第一步、第二步。如果我們要再一些其他的東西,比如權(quán)限校驗(yàn)、代碼說明,一個(gè)兩個(gè)方法還好,萬(wàn)一方法成百個(gè)呢,那我們豈不是要累死。這就是動(dòng)態(tài)代理要解決的問題,只需要寫一次就可以,究竟是怎么實(shí)現(xiàn)的呢,接下里我們來(lái)一探究竟吧。
3.2:動(dòng)態(tài)代理的準(zhǔn)備:
動(dòng)態(tài)代理需要用到JDk的Proxy類,通過它的newProxyInstance()方法可以生成一個(gè)代理類,我們來(lái)通過jdk看一下具體的說明,如何使用它:
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
返回一個(gè)指定接口的代理類實(shí)例,該接口可以將方法調(diào)用指派到指定的調(diào)用處理程序。此方法相當(dāng)于:
Proxy.getProxyClass(loader, interfaces).
getConstructor(new Class[] { InvocationHandler.class }).
newInstance(new Object[] { handler });
Proxy.newProxyInstance 拋出 IllegalArgumentException,原因與 Proxy.getProxyClass 相同。
參數(shù):loader - 定義代理類的類加載器interfaces - 代理類要實(shí)現(xiàn)的接口列表h - 指派方法調(diào)用的調(diào)用處理程序返回:一個(gè)帶有代理類的指定調(diào)用處理程序的代理實(shí)例,它由指定的類加載器定義,并實(shí)現(xiàn)指定的接口拋出:IllegalArgumentException - 如果違反傳遞到 getProxyClass 的參數(shù)上的任何限制NullPointerException - 如果 interfaces 數(shù)組參數(shù)或其任何元素為 null,或如果調(diào)用處理程序 h 為 null
從中可以看出它有三個(gè)參數(shù),分別是classlcoder、interface、InvocationHandler.只要我們把這三個(gè)參數(shù)傳遞給他,它就可以 返回給我們一個(gè)代理對(duì)象,訪問這個(gè)代理對(duì)象就可以實(shí)現(xiàn)對(duì)原對(duì)象的擴(kuò)展。接下來(lái),我們用代碼來(lái)實(shí)現(xiàn)它。
3.3:代碼場(chǎng)景
我們來(lái)做這樣一個(gè)場(chǎng)景,我們實(shí)現(xiàn)一個(gè)計(jì)算器,計(jì)算器里面有加減乘除方法,然后我們實(shí)現(xiàn)這個(gè)計(jì)算的接口,有具體的類和被代理的類,我們通過動(dòng)態(tài)代理來(lái)生成代理類,而不用自己去建了,好了,看接下來(lái)的代碼:
3.4:動(dòng)態(tài)代理的代碼實(shí)現(xiàn)
3.4.1:首先我們新建一個(gè)接口,命名為Calculator ,聲明四個(gè)方法:
public interface Calculator {
int add(int i,int j);//加
int sub(int i,int j);//減
int mul(int i,int j);//乘
double div(int i,int j);//除
}
3.4.2:新建一個(gè)實(shí)現(xiàn)類,命名為CalculatorImpl ,也就是被代理類
public class CalculatorImpl implements Calculator{
@Override
public int add(int i, int j) {
return i+j;
}
@Override
public int sub(int i, int j) {
return i-j;
}
@Override
public int mul(int i, int j) {
return i*j;
}
@Override
public double div(int i, int j) {
return (i/j);
}
}
3.4.3:新建一個(gè)類,命名為CalCulatorDynamicProxy,也就是我們的代理類,用來(lái)對(duì)上面的類進(jìn)行代理:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
public class CalCulatorDynamicProxy { //動(dòng)態(tài)代理類
private Calculator calculator;//要代理的對(duì)象
public CalCulatorDynamicProxy(Calculator calculator) {
this.calculator = calculator;
}
public Calculator getCalculator() {
Calculator proxy = null;
ClassLoader loader =calculator.getClass().getClassLoader();//獲取類加載器
Class[] interfaces = new Class[]{Calculator.class};//代理對(duì)象的類型
InvocationHandler h = new InvocationHandler() {//調(diào)用處理器
//proxy:正在返回的代理對(duì)象
//method:被調(diào)用的方法
//args:傳入的參數(shù)
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("---日志記錄開始---");
String name = method.getName();//獲取方法的名字
System.out.println("方法"+name+"()開始執(zhí)行了");
System.out.println("方法中的參數(shù)是:"+Arrays.asList(args));
Object result = method.invoke(calculator, args);
System.out.println("方法執(zhí)行后的結(jié)果是"+result);
return result;
}
};
proxy=(Calculator)Proxy.newProxyInstance(loader, interfaces, h);//代理對(duì)象
return proxy;
}
}
這里要特別強(qiáng)調(diào)的問題就是:invoke()方法,注意其中的參數(shù),分別是被代理對(duì)象、方法、和對(duì)象參數(shù),這里的原理是反射,通過獲取原對(duì)象的class對(duì)象,然后進(jìn)行處理,我們可以通過method對(duì)象拿到被代理對(duì)象的方法,也是add()、mul()、sub()、div()方法,也可以通過args對(duì)象數(shù)組取得傳入的參數(shù),比如我們具體傳入的數(shù)值,再通過method.invoke()方法進(jìn)行調(diào)用,就進(jìn)行了被代理對(duì)象的方法的執(zhí)行,然后就是返回的結(jié)果(如果方法前為void,返回的就是null)
3.4.4:我們來(lái)做具體的測(cè)試
public class Test {
public static void main(String[] args) {
Calculator cal = new CalculatorImpl();
Calculator proxy = new CalCulatorDynamicProxy(cal).getCalculator();
int add = proxy.add(29, 1);
int sub = proxy.sub(9, 2);
int mul = proxy.mul(3, 7);
double div = proxy.div(6,8);
}
}
具體的測(cè)試結(jié)果:
---日志記錄開始---
方法add()開始執(zhí)行了
方法中的參數(shù)是:[29, 1]
方法執(zhí)行后的結(jié)果是30
---日志記錄開始---
方法sub()開始執(zhí)行了
方法中的參數(shù)是:[9, 2]
方法執(zhí)行后的結(jié)果是7
---日志記錄開始---
方法mul()開始執(zhí)行了
方法中的參數(shù)是:[3, 7]
方法執(zhí)行后的結(jié)果是21
---日志記錄開始---
方法div()開始執(zhí)行了
方法中的參數(shù)是:[6, 8]
方法執(zhí)行后的結(jié)果是0.0
可以看出動(dòng)態(tài)代理模式輕松完成了對(duì)被代理對(duì)象的日志記錄功能,并且只用寫一次,這樣即便有成百上千的方法我們也不怕,這就是動(dòng)態(tài)代理領(lǐng)先于靜態(tài)代理之處,雖然實(shí)現(xiàn)起來(lái)有點(diǎn)麻煩,但是其方便,動(dòng)態(tài)的給被代理對(duì)象添加功能。我們所寫的重復(fù)代碼更少,做的事情更少。
四:總結(jié)
本篇博客介紹了動(dòng)態(tài)代理和靜態(tài)代理的概念,并對(duì)其進(jìn)行了代碼實(shí)現(xiàn),在實(shí)際的工作中,我們會(huì)經(jīng)常遇到需要代理模式的地方,希望能多多思考,促進(jìn)我們形成一定的思維模式。并且動(dòng)態(tài)代理作為SpringAop的實(shí)現(xiàn)原理,封裝了動(dòng)態(tài)代理,讓我們實(shí)現(xiàn)起來(lái)更加方便,對(duì)于這部分內(nèi)容可以只做了解,理解其背后的運(yùn)行機(jī)制即可,并不需要具體實(shí)現(xiàn),如果需要實(shí)現(xiàn),直接使用spring的Aop功能即可。
希望看完本篇,能對(duì)代理這種思維有深入的理解。好了,本篇文章就講到這里,謝謝。