代理設(shè)計模式與 AOP

本文為大家講解代理模式奠货,包括靜態(tài)代理的作用和代碼實(shí)現(xiàn)、動態(tài)代理的作用已慢、使用反射實(shí)現(xiàn)動態(tài)代理的過程曲聂,從而理解 AOP 的原理。

代理模式分為:靜態(tài)代理和動態(tài)代理佑惠。代理模式實(shí)現(xiàn)的功能和我們生活中的代理一樣朋腋,類似于中介公司。也就是代理對象幫助被代理對象完成功能膜楷,被代理對象可以在代理對象已有的功能基礎(chǔ)上旭咽,擴(kuò)展代理對象的功能。

比如在已存在的多個具有相同接口的目標(biāo)類的各個方法上增加一些系統(tǒng)功能赌厅,經(jīng)常會使用到代理模式穷绵,例如:方法執(zhí)行前后的日志打印,計算方法的運(yùn)行時間特愿、異常處理仲墨、事務(wù)等等勾缭。

一、靜態(tài)代理

靜態(tài)代理的介紹與代碼實(shí)現(xiàn)

下面我們通過一個例子來認(rèn)識靜態(tài)代理的作用目养,假設(shè)我們有一個操作數(shù)據(jù)庫的 Dao 服務(wù)接口 DaoService 俩由,里面包括查詢方法 query() 和更新方法 update(),程序中需要操作數(shù)據(jù)庫的有訂單服務(wù) OrderService 類癌蚁,它實(shí)現(xiàn)了 DaoService 接口幻梯。

我們思考一個問題,如果需要在查詢方法 query() 和更新方法 update() 執(zhí)行的前后需要加入日志打印功能努释,也就是說在方法執(zhí)行前打印一行日志碘梢,表示方法開始執(zhí)行,在方法執(zhí)行后打印一行日志伐蒂,表示方法執(zhí)行完畢煞躬。

大家可以思考,以上的問題我們可以怎么解決饿自?

任何人都能想到的最直接的辦法就是汰翠,在每個實(shí)現(xiàn)了 DaoService 接口的類的 query() 方法和 update() 方法的前后加上兩行日志就可以了。這種方法其實(shí)產(chǎn)生了很多的重復(fù)代碼昭雌,不是一個好的解決辦法复唤。如果實(shí)現(xiàn)了 DaoService 接口的類有很多,那么我們就會在這些類的 query() 方法和 update() 方法加很多日志代碼烛卧。

另外一個解決辦法就是用靜態(tài)代理來實(shí)現(xiàn)佛纫,用一個代理類 DaoServiceProxy 實(shí)現(xiàn) DaoService 接口,讓它來對實(shí)現(xiàn)了 DaoService 接口的類進(jìn)行代理总放,在代理類的query() 方法和 update() 方法的前后加上兩行日志就可以了呈宇。這樣做的好處是不管有多少 DaoService 的實(shí)現(xiàn)類,只需要加一個代理類局雄,在代理類的具體方法上加兩行日志就可以了甥啄,重復(fù)代碼得到了極大的減少。

示例代碼如下:

// DaoService接口
interface DaoService {
  void query();
  void update();
}

// 被代理類OrderService
class OrderService implements DaoService {

  @Override
  public void query() {
    System.out.println("OrderService.query()");
  }

  @Override
  public void update() {
    System.out.println("OrderService.update()");
  }

}

// 代理類
class DaoServiceProxy implements DaoService {
  DaoService dao;

  // 創(chuàng)建代理類對象的時候炬搭,實(shí)際上傳入一個被代理類的對象
  public DaoServiceProxy(DaoService dao) {
    this.dao = dao;
  }

  @Override
  public void query() {
    System.out.println("query()開始執(zhí)行蜈漓!");
    dao.query();
    System.out.println("query()執(zhí)行完畢!");
  }

  @Override
  public void update() {
    System.out.println("update()開始執(zhí)行宫盔!");
    dao.update();
    System.out.println("update()執(zhí)行完畢融虽!");
  }
}
// 測試
public class TestStaticProxy1 {
  public static void main(String[] args) {
    // 創(chuàng)建被代理類的對象
    OrderService orderService = new OrderService();
    // 創(chuàng)建代理類的對象
    DaoServiceProxy orderServiceProxy = new DaoServiceProxy(orderService);
    orderServiceProxy.query();
    System.out.println("==================");
    orderServiceProxy.update();
  }
}

運(yùn)行結(jié)果:

query()開始執(zhí)行!
OrderService.query()
query()執(zhí)行完畢灼芭!
==================
update()開始執(zhí)行有额!
OrderService.update()
update()執(zhí)行完畢!

假如我們現(xiàn)在新建了一個用戶表,需要開發(fā)用戶的查詢和更新方法的代碼巍佑,同時要求在方法執(zhí)行前后輸出日志茴迁,那么我們就可以非常方便的完成。

只需要寫一個用戶服務(wù)類 UserService 實(shí)現(xiàn) DaoService 接口句狼,在調(diào)用 UserService 方法的時候把 UserService 類的對象傳遞給代理類 DaoServiceProxy 類笋熬,然后調(diào)用代理類的 query() 方法和 update() 方法,就可以完美的打印出方法調(diào)用前后的日志腻菇。

// 用戶服務(wù)類UserService
class UserService implements DaoService {

  @Override
  public void query() {
    System.out.println("UserService.query()");
  }

  @Override
  public void update() {
    System.out.println("UserService.update()");
  }

}

public class TestStaticProxy2 {
  public static void main(String[] args) {
    // 創(chuàng)建被代理類的對象 userService
    UserService userService = new UserService();
    // 創(chuàng)建代理類的對象
    DaoServiceProxy orderServiceProxy = new DaoServiceProxy(userService);
    orderServiceProxy.query();
    System.out.println("==================");
    orderServiceProxy.update();
  }
}

運(yùn)行結(jié)果:

query()開始執(zhí)行!
UserService.query()
query()執(zhí)行完畢昔馋!
==================
update()開始執(zhí)行筹吐!
UserService.update()
update()執(zhí)行完畢!

從以上的代碼我們可以看出秘遏,使用了代理設(shè)計模式會使我們實(shí)際開發(fā)過程中的代碼量大幅減少丘薛,這就是使用設(shè)計模式的威力!

二邦危、動態(tài)代理

動態(tài)代理介紹與代碼實(shí)現(xiàn)

靜態(tài)代理的特點(diǎn)是一個代理類只能為一個接口服務(wù)洋侨,這樣的話程序開發(fā)中必然會產(chǎn)生過多的代理類。

比如程序中除了有數(shù)據(jù)庫操作 DaoService 接口倦蚪,還有支付操作 PayService 接口希坚,PayService 接口里定義了支付方法 pay(),要求在 pay() 方法的執(zhí)行前后也要加兩行日志的輸出陵且。

這樣的話裁僧,不得不再建立一個實(shí)現(xiàn)了 PayService 接口的代理類,用來代理實(shí)現(xiàn)了 PayService 接口的被代理類的操作慕购。如果還有其他的接口聊疲,則需要繼續(xù)創(chuàng)建代理類,因此最好的解決辦法是通過一個動態(tài)代理類完成所有接口的方法執(zhí)行前后的日志輸出功能沪悲。

在 Java 中實(shí)現(xiàn)動態(tài)代理機(jī)制获洲,需要使用 java.lang.reflect.InvocationHandler 接口以及 java.lang.reflect.Proxy 類。

InvocationHandler 接口的定義如下:

public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;

在 InvocationHandler 接口中只定義了一個 invoke() 方法殿如,此方法中有 3 個參數(shù):

1讼渊、Object proxy:被代理的對象
2、Method method:要調(diào)用的方法
3啸澡、Object[] args:方法調(diào)用時需要傳入的參數(shù)

Proxy 類是用來創(chuàng)建代理類的丹弱,它通過 newProxyInstance() 方法為一個或多個接口動態(tài)的生成實(shí)現(xiàn)類。

public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException

1谨胞、ClassLoader loader:類加載器
2固歪、Class<?>[] interfaces:獲取被代理類的全部接口
3、InvocationHandler h:InvocationHandler 接口的子類的實(shí)例

下面我們用動態(tài)代理來實(shí)現(xiàn)剛才提到的需求場景的日志打印

//PayService接口
interface PayService {
  void pay();
}

//被代理類WeChatPayService
class WeChatPayService implements PayService {

  @Override
  public void pay() {
    System.out.println("WeChatPayService.pay()");
  }
}

// 動態(tài)代理類DynamicProxy
class DynamicProxy implements InvocationHandler {

  Object obj; // 實(shí)現(xiàn)了接口的被代理類的對象的聲明

  // 1、給被代理類的對象實(shí)例化牢裳,并且返回一個代理類對象逢防,體會一下反射是動態(tài)語言的關(guān)鍵
  public Object getInstance(Object obj) {
    this.obj = obj;
    Class<?> clazz = obj.getClass();
    return Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), this);
  }

  // 當(dāng)通過代理類的對象發(fā)起對被重寫的方法的調(diào)用時,都會轉(zhuǎn)換為對如下的invoke方法的調(diào)用
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    System.out.println(method.getName() + "開始執(zhí)行蒲讯!");
    // returnVal就是調(diào)用被重寫的方法的返回值
    Object returnVal = method.invoke(obj, args);
    System.out.println(method.getName() + "執(zhí)行完畢忘朝!");
    return returnVal;
  }
}

// 測試
public class TestDynamicProxy {
  public static void main(String[] args) {
    System.out.println("1、動態(tài)代理orderService");
    // 被代理類的對象orderService
    OrderService orderService = new OrderService();
    // 創(chuàng)建一個實(shí)現(xiàn)了InvocationHandler接口的類的對象
    DynamicProxy proxy = new DynamicProxy();
    // 調(diào)用getInstance()方法判帮,動態(tài)的返回一個同樣實(shí)現(xiàn)了被代理類OrderService類實(shí)現(xiàn)的接口DaoService的代理類的對象
    // dao就是代理類對象
    DaoService order = (DaoService) proxy.getInstance(orderService);
    order.query(); // 調(diào)用代理類對象的方法就會轉(zhuǎn)換為調(diào)用handler類里invoke方法的調(diào)用
    System.out.println("==================");
    order.update();

    System.out.println("2局嘁、動態(tài)代理UserService");
    UserService userService = new UserService();
    DaoService user = (DaoService) proxy.getInstance(userService);
    user.query();
    System.out.println("==================");
    user.update();

    System.out.println("3、動態(tài)代理WeChatPayService");
    WeChatPayService weChatPayService = new WeChatPayService();
    PayService weChat = (PayService) proxy.getInstance(weChatPayService);
    weChat.pay();
  }
}

運(yùn)行結(jié)果:

1晦墙、動態(tài)代理orderService
query開始執(zhí)行悦昵!
OrderService.query()
query執(zhí)行完畢!
==================
update開始執(zhí)行晌畅!
OrderService.update()
update執(zhí)行完畢但指!
2、動態(tài)代理UserService
query開始執(zhí)行抗楔!
UserService.query()
query執(zhí)行完畢棋凳!
==================
update開始執(zhí)行!
UserService.update()
update執(zhí)行完畢连躏!
3剩岳、動態(tài)代理WeChatPayService
pay開始執(zhí)行!
WeChatPayService.pay()
pay執(zhí)行完畢反粥!

從以上的代碼以及運(yùn)行結(jié)果可以看出卢肃,動態(tài)代理可以對任意的接口實(shí)現(xiàn)類進(jìn)行代理,避免了靜態(tài)代理需要針對不同的接口開發(fā)對應(yīng)的代理類才顿。

動態(tài)代理可以對任何接口的所有方法實(shí)現(xiàn)前后增加相同功能的目的莫湘,Spring 框架中的 AOP 就是采用了動態(tài)代理的設(shè)計模式。AOP 切面編程對系統(tǒng)的設(shè)計與編碼具有非常重要的作用郑气,對于日志幅垮、事務(wù)、權(quán)限校驗(yàn)等可以在系統(tǒng)設(shè)計的階段不予考慮尾组,在設(shè)計后通過 AOP 的方式實(shí)現(xiàn)忙芒。

三、動態(tài)代理的實(shí)現(xiàn)模式

動態(tài)代理的實(shí)現(xiàn)模式有兩種:

1讳侨、用 JDK 實(shí)現(xiàn):代理對象必須實(shí)現(xiàn)一個接口呵萨,否則無法使用 JDK 自帶的動態(tài)代理。上面的例子就是用 JDK 實(shí)現(xiàn)的動態(tài)代理跨跨。

2潮峦、用 CGLIB 實(shí)現(xiàn):代理對象可以不實(shí)現(xiàn)接口囱皿,但是代理方法不能用 final 修飾。

用 CGLIB 實(shí)現(xiàn)動態(tài)代理

JDK 動態(tài)代理中提供一個 Proxy 類來創(chuàng)建代理類忱嘹,而在 CGLib 動態(tài)代理中也提供了一個類 Enhancer 來創(chuàng)建代理類嘱腥。使用 CGLib 創(chuàng)建動態(tài)代理類需要在項目中引入 CGLib 的 jar 包。

<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>3.2.5</version>
</dependency>

使用CGLib 實(shí)現(xiàn)動態(tài)代理拘悦,首先需要動態(tài)代理類實(shí)現(xiàn) MethodInterceptor 方法攔截器接口齿兔,然后通過構(gòu)造函數(shù)傳遞被代理對象,然后利用 Enhancer 來實(shí)例化被代理對象础米,通過 Enhancer 設(shè)置被代理對象的字節(jié)碼文件分苇、設(shè)置回調(diào)函數(shù)、創(chuàng)建被代理對象椭盏,最后覆寫 intercept() 方法组砚。

使用CGLib 實(shí)現(xiàn)動態(tài)代理的示例代碼如下:

// CGLIB動態(tài)代理類
class CglibProxy implements MethodInterceptor {

  private Object obj; // 被代理對象

  public CglibProxy(Object obj) {
    this.obj = obj;
  }

  // 給目標(biāo)對象創(chuàng)建一個被代理對象的示例
  public Object getInstance() {
    // 創(chuàng)建Enhancer對象,類似于JDK動態(tài)代理的Proxy類
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(obj.getClass()); // 設(shè)置被代理對象的字節(jié)碼文件
    enhancer.setCallback(this);// 設(shè)置回調(diào)函數(shù)
    return enhancer.create();// 創(chuàng)建被代理對象(子類)
  }

  @Override
  public Object intercept(Object obj, Method method, Object[] objects, MethodProxy proyx) throws Throwable {
    System.out.println("cglib動態(tài)代理開始");
    Object object = proyx.invokeSuper(obj, objects); // 關(guān)鍵代碼
    System.out.println("cglib動態(tài)代理結(jié)束");
    return object;
  }
}

// 測試
public class TestCglib {
  public static void main(String[] args) {
    System.out.println("1掏颊、動態(tài)代理orderService");
    DaoService orderService = new OrderService();
    CglibProxy orderProxy = new CglibProxy(orderService);
    OrderService order = (OrderService) orderProxy.getInstance();
    order.query();
    System.out.println("==================");
    order.update();

    System.out.println("2、動態(tài)代理UserService");
    DaoService userService = new UserService();
    CglibProxy userProxy = new CglibProxy(userService);
    UserService user = (UserService) userProxy.getInstance();
    user.query();
    System.out.println("==================");
    user.update();

    System.out.println("3艾帐、動態(tài)代理WeChatPayService");
    PayService payService = new WeChatPayService();
    CglibProxy payProxy = new CglibProxy(payService);
    WeChatPayService weChat = (WeChatPayService) payProxy.getInstance();
    weChat.pay();
  }
}

本文詳細(xì)介紹了靜態(tài)代理和動態(tài)代理的作用和實(shí)現(xiàn)方式乌叶,并介紹了動態(tài)代理實(shí)現(xiàn)的兩種方式,在一般的開發(fā)中很少會使用到動態(tài)代理柒爸,但是在編寫一些底層代碼或者框架代碼的時候動態(tài)代理模式就比較常用了准浴,掌握動態(tài)代理的實(shí)現(xiàn)模式是一個程序員走向高級的必備技能。

下一篇文章將會為大家介紹如何使用動態(tài)代理模擬 Spring 框架的 AOP 切面編程捎稚,實(shí)現(xiàn)統(tǒng)計方法執(zhí)行前后共花費(fèi)的時間功能乐横,敬請關(guān)注.....

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市今野,隨后出現(xiàn)的幾起案子葡公,更是在濱河造成了極大的恐慌,老刑警劉巖条霜,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件催什,死亡現(xiàn)場離奇詭異,居然都是意外死亡宰睡,警方通過查閱死者的電腦和手機(jī)蒲凶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來拆内,“玉大人旋圆,你說我怎么就攤上這事◆锘校” “怎么了灵巧?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我孩等,道長艾君,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任肄方,我火速辦了婚禮冰垄,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘权她。我一直安慰自己虹茶,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布隅要。 她就那樣靜靜地躺著蝴罪,像睡著了一般。 火紅的嫁衣襯著肌膚如雪步清。 梳的紋絲不亂的頭發(fā)上要门,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機(jī)與錄音廓啊,去河邊找鬼欢搜。 笑死,一個胖子當(dāng)著我的面吹牛谴轮,可吹牛的內(nèi)容都是我干的炒瘟。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼第步,長吁一口氣:“原來是場噩夢啊……” “哼疮装!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起粘都,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤廓推,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后驯杜,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體受啥,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年鸽心,在試婚紗的時候發(fā)現(xiàn)自己被綠了滚局。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡顽频,死狀恐怖藤肢,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情糯景,我是刑警寧澤嘁圈,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布省骂,位于F島的核電站,受9級特大地震影響最住,放射性物質(zhì)發(fā)生泄漏钞澳。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一涨缚、第九天 我趴在偏房一處隱蔽的房頂上張望轧粟。 院中可真熱鬧,春花似錦脓魏、人聲如沸兰吟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽混蔼。三九已至,卻和暖如春珊燎,著一層夾襖步出監(jiān)牢的瞬間惭嚣,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工悔政, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留料按,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓卓箫,卻偏偏與公主長得像,于是被迫代替她去往敵國和親垄潮。 傳聞我的和親對象是個殘疾皇子烹卒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345