Java代理模式及其應用
摘要:
代理根據代理類的產生方式和時機分為靜態代理和動態代理兩種。代理類不僅可以有效的將具體的實現與呼叫方進行解耦,通過面向介面進行編碼完全將具體的實現隱藏在內部,而且還可以在符合開閉原則的前提下,對目標類進行進一步的增強。典型地,Spring AOP 是對JDK動態代理的經典應用。
一. 代理與代理模式
1). 代理
代理模式其實很常見,比如買火車票這件小事:黃牛相當於是我們本人的的代理,我們可以通過黃牛買票。通過黃牛買票,我們可以避免與火車站的直接互動,可以省很多事,並且還能享受到黃牛更好的服務(如果錢給夠的話)。在軟體開發中,代理也具有類似的作用,並且一般可以分為靜態代理和動態代理兩種,上述的這個黃牛買票的例子就是靜態代理。
那麼,靜態代理與動態代理的區別是什麼呢?所謂靜態代理,其實質是自己手寫(或者用工具生成)代理類,也就是在程式執行前就已經存在的編譯好的代理類。但是,如果我們需要很多的代理,每一個都這麼去建立實屬浪費時間,而且會有大量的重複程式碼,此時我們就可以採用動態代理,動態代理可以在程式執行期間根據需要動態的建立代理類及其例項來完成具體的功能。總的來說,根據代理類的建立時機和建立方式的不同,我們可以將代理分為靜態代理和動態代理兩種形式。
就像我們也可以自己直接去買票一樣,在軟體開發中,方法直接呼叫就可以完成功能,為什麼非要通過代理呢?原因是採用代理模式可以有效的將具體的實現(買票過程)與呼叫方(買票者)進行解耦,通過面向介面進行編碼完全將具體的實現(買票過程)隱藏在內部(黃牛)。此外,代理類不僅僅是一個隔離客戶端和目標類的中介,我們還可以藉助代理來在增加一些功能,而不需要修改原有程式碼,是開閉原則的典型應用。代理類主要負責為目標類預處理訊息、過濾訊息、把訊息轉發給目標類,以及事後處理訊息等。代理類與目標類之間通常會存在關聯關係,一個代理類的物件與一個目標類的物件關聯,代理類的物件本身並不真正實現服務,而是通過呼叫目標類的物件的相關方法,來提供特定的服務。總的來說,
代理物件存在的價值主要用於攔截對真實業務物件的訪問(我們不需要直接與火車站打交道,而是把這個任務委託給黃牛);
代理物件應該具有和目標物件(真實業務物件)相同的方法,即實現共同的介面或繼承於同一個類;
代理物件應該是目標物件的增強,否則我們就沒有必要使用代理了。
事實上,真正的業務功能還是由目標類來實現,代理類只是用於擴充套件、增強目標類的行為。例如,在專案開發中我們沒有加入緩衝、日誌這些功能而後期需要加入,我們就可以使用代理來實現,而沒有必要去直接修改已經封裝好的目標類。
2). 代理模式
代理模式是較常見的模式之一,在許多框架中經常見到,比如Spring的面向切面的程式設計,MyBatis中快取機制對PooledConnection的管理等。代理模式使得客戶端在使用目標物件的時候間接通過操作代理物件進行,代理物件是對目標物件的增強,代理模式的UML示意圖如下:
代理模式包含如下幾個角色:
客戶端:客戶端面向介面程式設計,使用代理角色完成某項功能。
抽象主題:一般實現為介面,是對(被代理物件的)行為的抽象。
被代理角色(目標類):直接實現上述介面,是抽象主題的具體實現。
代理角色(代理類):實現上述介面,是對被代理角色的增強。
代理模式的使用本質上是對開閉原則(Open for Extension, Close for Modification)的直接支援。
二. 靜態代理
靜態代理的實現模式一般是:首先建立一個介面(JDK代理都是面向介面的),然後建立具體實現類來實現這個介面,然後再建立一個代理類同樣實現這個介面,不同之處在於,具體實現類的方法中需要將介面中定義的方法的業務邏輯功能實現,而代理類中的方法只要呼叫具體類中的對應方法即可,這樣我們在需要使用介面中的某個方法的功能時直接呼叫代理類的方法即可,將具體的實現類隱藏在底層。下面我們借用CSDN博友frank909在其《輕鬆學,Java 中的代理模式及動態代理》一文中的例子來了解靜態代理:
我們平常去電影院看電影的時候,在電影開始的階段是不是經常會放廣告呢?
電影是電影公司委託給影院進行播放的,但是影院可以在播放電影的時候,產生一些自己的經濟收益,比如賣爆米花、可樂等,然後在影片開始結束時播放一些廣告,現在用程式碼來進行模擬。
1). 抽象主題(介面)
首先得有一個介面,通用的介面是代理模式實現的基礎。這個介面我們命名為Movie,代表電影這個主題。
package com.frank.test;
public interface Movie {
void play();
}
2). 被代理角色(目標類)與代理角色(代理類)
然後,我們要有一個真正的實現這個 Movie 介面的類和一個只是實現介面的代理類。
package com.frank.test;
public class RealMovie implements Movie {
@Override
public void play() {
// TODO Auto-generated method stub
System.out.println("您正在觀看電影 《肖申克的救贖》");
}
}
這個表示真正的影片。它實現了 Movie 介面,play()方法呼叫時,影片就開始播放。那麼代理類呢?
package com.frank.test;
public class Cinema implements Movie {
RealMovie movie;
public Cinema(RealMovie movie) {
super();
this.movie = movie;
}
@Override
public void play() {
guanggao(true); // 代理類的增強處理
movie.play(); // 代理類把具體業務委託給目標類,並沒有直接實現
guanggao(false); // 代理類的增強處理
}
public void guanggao(boolean isStart){
if ( isStart ) {
System.out.println("電影馬上開始了,爆米花、可樂、口香糖9.8折,快來買啊!");
} else {
System.out.println("電影馬上結束了,爆米花、可樂、口香糖9.8折,買回家吃吧!");
}
}
}
Cinema 就是代理物件,它有一個 play() 方法。不過呼叫 play() 方法時,它進行了一些相關利益的處理,那就是廣告。也就是說,Cinema(代理類) 與 RealMovie(目標類) 都可以播放電影,但是除此之外,Cinema(代理類)還對“播放電影”這個行為進行進一步增強,即增加了額外的處理,同時不影響RealMovie(目標類)的實現。
3). 客戶端
package com.frank.test;
public class ProxyTest {
public static void main(String[] args) {
RealMovie realmovie = new RealMovie();
Movie movie = new Cinema(realmovie);
movie.play();
}
}/** Output
電影馬上開始了,爆米花、可樂、口香糖9.8折,快來買啊!
您正在觀看電影 《肖申克的救贖》
電影馬上結束了,爆米花、可樂、口香糖9.8折,買回家吃吧!
**/
現在可以看到,代理模式可以在不修改被代理物件的基礎上(符合開閉原則),通過擴充套件代理類,進行一些功能的附加與增強。值得注意的是,代理類和被代理類應該共同實現一個介面,或者是共同繼承某個類。如前所述,由於Cinema(代理類)是事先編寫、編譯好的,而不是在程式執行過程中動態生成的,因此這個例子是一個靜態代理的應用。
三. 動態代理
在第一節我們已經提到,動態代理可以在程式執行期間根據需要動態的建立代理類及其例項來完成具體的功能,下面我們結合具體例項來介紹JDK動態代理。
1). 抽象主題(介面)
同樣地,首先得有一個介面,通用的介面是代理模式實現的基礎。
package cn.inter;
public interface Subject {
public void doSomething();
}
2). 被代理角色(目標類)
然後,我們要有一個真正的實現這個 Subject 介面的類,以便代理。
package cn.impl;
import cn.inter.Subject;
public class RealSubject implements Subject {
public void doSomething() {
System.out.println("call doSomething()");
}
}
3). 代理角色(代理類)與客戶端
在動態代理中,代理類及其例項是程式自動生成的,因此我們不需要手動去建立代理類。在Java的動態代理機制中,InvocationHandler(Interface)介面和Proxy(Class)類是實現我們動態代理所必須用到的。事實上,Proxy通過使用InvocationHandler物件生成具體的代理代理物件,下面我們看一下對InvocationHandler介面的實現:
package cn.handler;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
/**
* Title: InvocationHandler 的實現
* Description: 每個代理的例項都有一個與之關聯的 InvocationHandler
* 實現類,如果代理的方法被呼叫,那麼代理便會通知和轉發給內部的 InvocationHandler 實現類,由它呼叫invoke()去處理。
*
* @author rico
* @created 2017年7月3日 下午3:08:55
*/
public class ProxyHandler implements InvocationHandler {
private Object proxied; // 被代理物件
public ProxyHandler(Object proxied) {
this.proxied = proxied;
}
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
// 在轉調具體目標物件之前,可以執行一些功能處理
System.out.println("前置增強處理: yoyoyo...");
// 轉調具體目標物件的方法(三要素:例項物件 + 例項方法 + 例項方法的引數)
Object obj = method.invoke(proxied, args);
// 在轉調具體目標物件之後,可以執行一些功能處理
System.out.println("後置增強處理:hahaha...");
return obj;
}
}
在實現了InvocationHandler介面後,我們就可以建立代理物件了。在Java的動態代理機制中,我們使用Proxy類的靜態方法newProxyInstance建立代理物件,如下:
package cn.client;
import java.lang.reflect.Proxy;
import cn.handler.ProxyHandler;
import cn.impl.RealSubject;
import cn.inter.Subject;
public class Test {
public static void main(String args[]) {
// 真實物件real
Subject real = new RealSubject();
// 生成real的代理物件
Subject proxySubject = (Subject) Proxy.newProxyInstance(
Subject.class.getClassLoader(), new Class[] { Subject.class },
new ProxyHandler(real));
proxySubject.doSomething();
System.out.println("代理物件的型別 : " + proxySubject.getClass().getName());
System.out.println("代理物件所在類的父型別 : " + proxySubject.getClass().getGenericSuperclass());
}
}/** Output
前置增強處理: yoyoyo...
call doSomething()
後置增強處理:hahaha...
代理物件的型別 : com.sun.proxy.$Proxy0
代理物件所在類的父型別 : class java.lang.reflect.Proxy
**/
到此為止,我們給出了完整的基於JDK動態代理機制的代理模式的實現。我們從上面的例項中可以看到,代理物件proxySubject的型別為”com.sun.proxy.$Proxy0”,這恰好印證了proxySubject物件是一個代理物件。除此之外,我們還發現代理物件proxySubject所對應的類繼承自java.lang.reflect.Proxy類,這也正是JDK動態代理機制無法實現對class的動態代理的原因:Java只允許單繼承。
4). JDK中InvocationHandler介面與Proxy類
(1). InvocationHandler介面
InvocationHandler 是一個介面,官方文件解釋說:每個代理的例項都有一個與之關聯的 InvocationHandler 實現類,如果代理的方法被呼叫,那麼代理便會通知和轉發給內部的 InvocationHandler 實現類,由它決定處理。
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
InvocationHandler中的invoke() 方法決定了怎麼樣處理代理傳遞過來的方法呼叫。
(2). Proxy類
JDK通過 Proxy 的靜態方法 newProxyInstance 動態地建立代理,該方法在Java中的宣告如下:
/**
* @description
* @author rico
* @created 2017年7月3日 下午3:16:49
* @param loader 類載入器
* @param interfaces 目標類所實現的介面
* @param h InvocationHandler 例項
* @return
*/
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
事實上,Proxy 動態產生的代理物件呼叫目標方法時,代理物件會呼叫 InvocationHandler 實現類,所以 InvocationHandler 是實際執行者。
五. Spring AOP 與 動態代理
AOP 專門用於處理系統中分佈於各個模組(不同方法)中的交叉關注點的問題,在 Java EE 應用中,常常通過 AOP 來處理一些具有橫切性質的系統級服務,如事務管理、安全檢查、快取、物件池管理等,AOP 已經成為一種非常常用的解決方案。
AOP機制是 Spring 所提供的核心功能之一,其既是Java動態代理機制的經典應用,也是動態AOP實現的代表。Spring AOP預設使用Java動態代理來建立AOP代理,具體通過以下幾個步驟來完成:
Spring IOC 容器建立Bean(目標類物件);
Bean建立完成後,Bean後處理器(BeanPostProcessor)根據具體的切面邏輯及Bean本身使用Java動態代理技術生成代理物件;
應用程式使用上述生成的代理物件替代原物件來完成業務邏輯,從而達到增強處理的目的。