本博文中項目代碼已開源下載地址:GitHub
Java代理和動態代理機制分析和應用
概述
代理是一種常用的設計模式,其目的就是為其他對象提供一個代理以控制對某個對象的訪問。代理類負責為委托類預處理消息,過濾消息並轉發消息,以及進行消息被委托類執行后的后續處理。根據代理類的生成時間不同可以將代理分為靜態代理和動態代理兩種。
代理模式一般涉及到的角色有4種
- 主題接口:定義代理類和真實主題的公共對外方法,也是代理類代理真實主題的方法;
- 真實主題:真正實現業務邏輯的類;
- 代理類:用來代理和封裝真實主題;
- 客戶端:使用代理類和主題接口完成一些工作。
在代理模式中真實主題角色對於客戶端角色來說的透明的,也就是客戶端不知道也無需知道真實主題的存在。
為了保持行為的一致性,代理類和委托類通常會實現相同的接口,所以在訪問者看來兩者沒有絲毫的區別。通過代理類這中間一層,能有效控制對委托類對象的直接訪問,也可以很好地隱藏和保護委托類對象,同時也為實施不同控制策略預留了空間,從而在設計上獲得了更大的靈活性。
代理的優點
- 隱藏委托類的實現,調用者只需要和代理類進行交互即可。
- 解耦,在不改變委托類代碼情況下做一些額外處理,比如添加初始判斷及其他公共操作
代理模式的應用場景
代理的使用場景很多,struts2中的 action 調用, hibernate的懶加載, spring的 AOP無一不用到代理。總結起來可分為以下幾類:
1. 在原方法執行之前和之后做一些操作,可以用代理來實現(比如記錄Log,做事務控制等)。
2. 封裝真實的主題類,將真實的業務邏輯隱藏,只暴露給調用者公共的主題接口。
3. 在延遲加載上的應用。
靜態代理
所謂靜態代理也就是在程序運行前就已經存在代理類的字節碼文件,代理類和委托類的關系在運行前就確定了。
下面有個場景,一個房主要出售自己的房子,但房主不知道誰要買房,也沒有時間去接待每一個看房者。
現在我們就用靜態代理的方式來實現房主的這一需求。
首先,將出售房屋抽象成公共代理接口(Sales)
/**
* 主題接口:定義代理類和真實主題的公共對外方法,也是代理類代理真實主題的方法。<br>
* 在這里表示銷售
* @author Penn
*/
public interface Sales {
void sell();
}
Salese接口里面提供了sell方法表示出售房屋。
其次,房主做為整個事件的主角,自然而然的就成了真實主題,也是代理的委托者
/**
* 真實主題,具體處理業務。<br>
* 在這里表示房東
* @author Penn
*/
public class Owner implements Sales{
@Override
public void sell() {
System.out.println("我是房東我正在出售我的房產");
}
}
真實主題Owner實現了Sales接口,在接口提供的sell()方法里出售自己的房屋。
再次,給房主找個中介(Agents),作為房主出售房屋的代理
/**
* 代理類:用來代理和封裝真實主題<br>
* 在這里表示房產中介
* @author Penn
*/
public class Agents implements Sales {
private Owner owner;
public Agents() {
}
@Override
public void sell() {
System.out.println("我是房產中介,正在核實買房者是否符合購買該房屋的資格");
getOwner().sell();
System.out.println("我是房產中介,正在收取提成");
}
private Owner getOwner() {
if (owner==null) {
owner=new Owner();
}
return owner;
}
}
為了幫房主出售房屋,Agents代理也實現了Sales接口。同時Agents也擁有Owner的成員對象,在實現的sell()接口方法里,代理Agents幫房主Owner預處理了一些消息,然后調用了owner對象的sell()方法通知房主出售房屋,在房主Owner出售完房屋后,代理Agents開始收取中介費。有心的讀者可以發現,代理Agents在訪問Owner對象的時候使用了getOwner()方法,從而達到了在客戶真正決定買房的時候才初始化owner對象,而不是在Agents初始化的時候就將Owner初始化。真實情境中,會有很多購房者要看房,但真正買的人只有一個,所以在代理Agents幫房東預處理和過濾掉所有信息之后,告訴房東你可以出售房屋了,這樣大大節省了房東的時間和簡化了售房的繁瑣過程。而這也是用代理來實現延遲加載的好處。
最后,萬事俱備只欠買房的顧客了(Customer)
/**
* 客戶端,使用代理類和主題接口完成一些工作。<br>
* 在這里表示買房子的客戶
* @author Penn
*/
public class Customer {
public static void main(String[]args) {
Sales sales=new Agents();
sales.sell();
}
}
在這里買房的顧客Customer找到房產代理Agents,告訴他要買房。整個過程房東Owner對顧客Customer來說是透明的,Customer只與Agents打交道,Owner也只與Agents打交道,Agents作為Owner和Customer通信的橋梁從而有效控制了Customer和Owner的直接交流。
觀察代碼可以發現每一個代理類只能為一個接口服務,這樣一來程序開發中必然會產生過多的代理,而且,所有的代理操作除了調用的方法不一樣之外,其他的操作都一樣,則此時肯定是重復代碼。解決這一問題最好的做法是可以通過一個代理類完成全部的代理功能,那么此時就必須使用動態代理完成。
靜態代理類優缺點
優點:業務類只需要關注業務邏輯本身,保證了業務類的重用性。這是代理的共有優點。
缺點:
1)代理對象的一個接口只服務於一種類型的對象,如果要代理的方法很多,勢必要為每一種方法都進行代理,靜態代理在程序規模稍大時就無法勝任了。
2)如果接口增加一個方法,除了所有實現類需要實現這個方法外,所有代理類也需要實現此方法。增加了代碼維護的復雜度。
另外,如果要按照上述的方法使用代理模式,那么真實角色(委托類)必須是事先已經存在的,並將其作為代理對象的內部屬性。但是實際使用時,一個真實角色必須對應一個代理角色,如果大量使用會導致類的急劇膨脹;此外,如果事先並不知道真實角色(委托類),該如何使用代理呢?這個問題可以通過Java的動態代理類來解決。
動態代理
在java的動態代理API中,有兩個重要的類和接口,一個是 InvocationHandler(Interface)、另一個則是 Proxy(Class),這一個類和接口是實現我們動態代理所必須用到的。
InvocationHandler(Interface)
InvocationHandler是負責連接代理類和委托類的中間類必須實現的接口,它自定義了一個 invoke 方法,用於集中處理在動態代理類對象上的方法調用,通常在該方法中實現對委托類的代理訪問。
InvocationHandler 的核心方法
Object invoke(Object proxy, Method method, Object[] args)
- proxy 該參數為代理類的實例
- method 被調用的方法對象
- args 調用method對象的方法參數
該方法也是InvocationHandler接口所定義的唯一的一個方法,該方法負責集中處理動態代理類上的所有方法的調用。調用處理器根據這三個參數進行預處理或分派到委托類實例上執行。
Proxy(Class)
Proxy是 Java 動態代理機制的主類,它提供了一組靜態方法來為一組接口動態地生成代理類及其對象。
Proxy 的靜態方法 static InvocationHandler getInvocationHandler(Object proxy)
該方法用於獲取指定代理對象所關聯的調用處理器 static Class getProxyClass(ClassLoader loader, Class[] interfaces)
該方法用於獲取關聯於指定類裝載器和一組接口的動態代理類的類對象 static boolean isProxyClass(Class cl)
該方法用於判斷指定類對象是否是一個動態代理類 static Object newProxyInstance(ClassLoader loader, Class[] interfaces,InvocationHandler h)
- loader 指定代理類的ClassLoader加載器
- interfaces 指定代理類要實現的接口
- h: 表示的是當我這個動態代理對象在調用方法的時候,會關聯到哪一個InvocationHandler對象上
該方法用於為指定類裝載器、一組接口及調用處理器生成動態代理類實例
使用Java 動態代理的兩個重要步驟
- 通過實現 InvocationHandler 接口創建自己的調用處理器;
- 通過為Proxy類的newProxyInstance方法指定代理類的ClassLoader 對象和代理要實現的interface以及調用處理器InvocationHandler對象 來創建動態代理類的對象;
下面通過實例來具體介紹動態代理的使用
我們還是使用上面房東出售房屋的例子,來用動態代理去實現。
委托類(Owner)和公共代理接口(Sales)和靜態代理的例子中的一樣。
實現 InvocationHandler 接口
/**
* 動態代理類對應的調用處理程序類
* @author Penn
*/
public class SalesInvocationHandler implements InvocationHandler{
//代理類持有一個委托類的對象引用
private Object delegate;
public SalesInvocationHandler(Object delegate) {
this.delegate = delegate;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println("Enter method "+method.getName());
long start=System.currentTimeMillis();
Object result=method.invoke(delegate, args);
long end=System.currentTimeMillis();
System.out.println("Exit method "+method.getName());
System.out.println("執行時間:"+(end-start));
return result;
}
}
SalesInvocationHandler實現了InvocationHandler的invoke方法,當代理對象的方法被調用時,invoke方法會被回調。其中proxy表示實現了公共代理方法的動態代理對象。
通過 Proxy 類靜態函數生成代理對象
/**
* 客戶端,使用代理類和主題接口完成一些工作。<br>
* 在這里表示買房子的客戶
* @author Penn
*/
public class Customer {
public static void main(String[]args) {
Sales delegate=new Owner();
InvocationHandler handler=new SalesInvocationHandler(delegate);
Sales proxy=(Sales)Proxy.newProxyInstance(delegate.getClass().getClassLoader(), delegate.getClass().getInterfaces(), handler);
proxy.sell();
}
}
上面代碼通過InvocationHandler handler=new SalesInvocationHandler(delegate);
將委托對象作為構造方法的參數傳遞給了SalesInvocationHandler來作為代理方法調用的對象。當我們調用代理對象的sell()方法時,該調用將會被轉發到SalesInvocationHandler對象的invoke上,從而達到動態代理的效果。從上面代碼可以看出,客戶端需要負責自己創建代理對象,顯得有點繁瑣,其實我們可以將代理對象的創建封裝到代理協調器的實現中。
改進后的代理協調器
/**
* 動態代理類對應的調用處理程序類
* @author Penn
*/
public class SalesInvocationHandler implements InvocationHandler{
//代理類持有一個委托類的對象引用
private Object delegate;
/**
* 綁定委托對象並返回一個代理類
* @param delegate
* @return
*/
public Object bind(Object delegate) {
this.delegate = delegate;
return Proxy.newProxyInstance(delegate.getClass().getClassLoader(), delegate.getClass().getInterfaces(), this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println(proxy.getClass().getInterfaces()[0]);
System.out.println("Enter method "+method.getName());
long start=System.currentTimeMillis();
Object result=method.invoke(delegate, args);
long end=System.currentTimeMillis();
System.out.println("Exit method "+method.getName());
System.out.println("執行時間:"+(end-start));
return result;
}
}
這樣客戶端就簡化成了
/**
* 客戶端,使用代理類和主題接口完成一些工作。<br>
* 在這里表示買房子的客戶
* @author Penn
*/
public class Customer {
public static void main(String[]args) {
Sales delegate=new Owner();
Sales proxy=(Sales) new SalesInvocationHandler().bind(delegate);
proxy.sell();
}
}
執行結果
Enter method sell
我是房東我正在出售我的房產
Exit method sell
執行時間:0
動態代理類優缺點
優點
1.動態代理類的字節碼在程序運行時由Java反射機制動態生成,無需程序員手工編寫它的源代碼。
2.動態代理類不僅簡化了編程工作,而且提高了軟件系統的可擴展性,因為Java 反射機制可以生成任意類型的動態代理類。
缺點
JDK的動態代理機制只能代理實現了接口的類,而不能實現接口的類就不能實現JDK的動態代理,cglib是針對類來實現代理的,他的原理是對指定的目標類生成一個子類,並覆蓋其中方法實現增強,但因為采用的是繼承,所以不能對final修飾的類進行代理。