23種設計模式(四)-代理模式
一. 什麼是代理模式
1.1 概念
代理模式給某一個物件提供一個代理物件,並由代理物件控制對原物件的引用
也就是說客戶端並不直接呼叫實際的物件,而是通過呼叫代理,來間接的呼叫實際的物件。
通俗的來講代理模式就是我們生活中常見的中介。
1.2 為什麼不直接呼叫, 而要間接的呼叫物件呢?
一般是因為客戶端不想直接訪問實際的物件, 或者不方便直接訪問實際物件,因此通過一個代理物件來完成間接的訪問。
代理模式的UML圖
代理類和真正實現都實現了同一個介面, 並且他們有相同的方法. 這樣對於客戶端呼叫來說是透明的.
二. 什麼情況下使用動態代理
想在訪問一個類時做一些控制, 在真實呼叫目標方法之前或者之後新增一些操作.
我們想買房, 但是買房的手續實在太複雜, 索性都交給中介公司. 中介公司就是代理, 我們的直接目的是買到房子, 中介公司在買房前後增加一些處理操作.
來看看程式碼實現
/** * 買房介面 */ public interface IBuyHouse { public void buyHouse(); } /** * 真正買房的人 */ public class RealBuyHouse implements IBuyHouse{ private String name; public RealBuyHouse(String name) { this.name = name; } @Override public void buyHouse() { System.out.println(this.name + "買房子"); } } /** * 代理買房 */ public class ProxyBuyHouse implements IBuyHouse{ private IBuyHouse buyHouse; public ProxyBuyHouse(IBuyHouse buyHouse) { this.buyHouse = buyHouse; } @Override public void buyHouse() { beforeBuyHouse(); buyHouse.buyHouse(); afterBuyHouse(); } public void beforeBuyHouse() { System.out.println("買房前操作--選房"); } public void afterBuyHouse() { System.out.println("買房後操作--交稅"); } } public class Client { public static void main(String[] args) { IBuyHouse buyHouse = new RealBuyHouse("張三"); IBuyHouse proxyBuyHouse = new ProxyBuyHouse(buyHouse); proxyBuyHouse.buyHouse(); } }
我們看到, 代理做的事情, 是代替主體完成買房操作, 所以, 類內部有一個主體實體物件.
代理模式有三種角色
Real Subject:真實類,也就是被代理類、委託類。用來真正完成業務服務功能;
Proxy:代理類。將自身的請求用 Real Subject 對應的功能來實現,代理類物件並不真正的去實現其業務功能;
Subject:定義 RealSubject 和 Proxy 角色都應該實現的介面
通俗來說,代理模式的主要作用是擴充套件目標物件的功能,比如說在目標物件的某個方法執行前後你可以增加一些額外的操作,並且不用修改這個方法的原有程式碼。如果大家學過 Spring 的 AOP,一定能夠很好的理解這句話。
三. 代理模式的種類
按照代理建立的時期來進行分類的可以分為:靜態代理、動態代理。
靜態代理是由程式設計師建立或特定工具自動生成原始碼,在對其編譯。在程式執行之前,代理類.class檔案就已經被建立了。
動態代理是在程式執行時通過反射機制動態建立的。
3.1 靜態代理
先來看靜態代理的實現步驟:
1)定義一個介面(Subject)
2)建立一個委託類(Real Subject)實現這個介面
3)建立一個代理類(Proxy)同樣實現這個介面
4)將委託類 Real Subject 注入進代理類 Proxy,在代理類的方法中呼叫 Real Subject 中的對應方法。這樣的話,我們就可以通過代理類遮蔽對目標物件的訪問,並且可以在目標方法執行前後做一些自己想做的事情。
從實現和應用角度來說,靜態代理中,我們對目標物件的每個方法的增強都是手動完成的,非常不靈活(比如介面一旦新增加方法,目標物件和代理物件都要進行修改)且麻煩(需要對每個目標類都單獨寫一個代理類)。 實際應用場景非常非常少,日常開發幾乎看不到使用靜態代理的場景。
從 JVM 層面來說, 靜態代理在編譯時就將介面、委託類、代理類這些都變成了一個個實際的 .class 檔案。
上面我們舉的買房的例子就是靜態代理.
原始碼見上面第二點
靜態代理總結:
優點:可以做到在符合開閉原則的情況下對目標物件進行功能擴充套件。
缺點:我們得為每一個實現類都得建立代理類,工作量太大,不易管理。同時介面一旦發生改變,代理類也得相應修改。比如: 介面Subject增加一個方法. 所有的實現類, 代理類都要想聽的增加.
3.2 動態代理
代理類是在呼叫委託類方法的前後增加了一些操作。委託類的不同,也就導致代理類的不同。
那麼為了做一個通用性的代理類出來,我們把呼叫委託類方法的這個動作抽取出來,把它封裝成一個通用性的處理類,於是就有了動態代理中的 InvocationHandler 角色(處理類)。
於是,在代理類和委託類之間就多了一個處理類的角色,這個角色主要是對代理類呼叫委託類方法的動作進行統一的呼叫,也就是由 InvocationHandler 來統一處理代理類呼叫委託類方法的操作。看下圖:
從 JVM 角度來說,動態代理是在執行時動態生成 .class 位元組碼檔案 ,並載入到 JVM 中的。
雖然動態代理在我們日常開發中使用的相對較少,但是在框架中的幾乎是必用的一門技術。學會了動態代理之後,對於我們理解和學習各種框架的原理也非常有幫助,Spring AOP、RPC 等框架的實現都依賴了動態代理。
就 Java 來說,動態代理的實現方式有很多種,比如:
- JDK 動態代理
- CGLIB 動態代理
- Javassit 動態代理
很多知名的開源框架都使用到了動態代理, 例如 Spring 中的 AOP 模組中:如果目標物件實現了介面,則預設採用 JDK 動態代理,否則採用 CGLIB 動態代理。
下面詳細講解這三種動態代理機制。
1. JDK動態代理
先來看下 JDK 動態代理機制的使用步驟:
第一步: 定義一個介面(Subject)
第二步: 建立一個委託類(Real Subject)實現這個介面
第三步: 建立一個處理類並實現 InvocationHandler 介面,重寫其 invoke 方法(在 invoke 方法中利用反射機制呼叫委託類的方法,並自定義一些處理邏輯),並將委託類注入處理類
下面來看看InvocationHandler介面
package java.lang.reflect;
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}
在InvocationHandler裡面定義了invoke方法. 該方法有三個引數:
- proxy:代理類物件(見下一步)
- method:還記得我們在上篇文章反射中講到的 Method.invoke 嗎?就是這個,我們可以通過它來呼叫委託類的方法(反射)
- args:傳給委託類方法的引數列表
第四步: 建立代理物件(Proxy):通過 Proxy.newProxyInstance() 建立委託類物件的代理物件
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException
{
.....
}
Proxy.newProxyInstance()有三個引數
- 類載入器 ClassLoader
- 委託類實現的介面陣列,至少需要傳入一個介面進去
- 呼叫的 InvocationHandler 例項處理介面方法(也就是第 3 步我們建立的類的例項)
下面來看看案例實現
/**
* 抽象介面
*/
public interface ISubject {
void operate();
}
/**
* 委託類, 也叫被代理類
* 真正的處理邏輯
*/
public class RealSubject implements ISubject{
@Override
public void operate() {
System.out.println("實際操作");
}
}
/**
* 代理物件的處理類
*/
public class ProxySubject implements InvocationHandler {
private ISubject realSubject;
public ProxySubject(ISubject subject) {
this.realSubject = subject;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("呼叫方法前---前置操作");
//動態代理呼叫RealSubject中的方法
Object result = method.invoke(realSubject, args);
System.out.println("呼叫方法後---後置操作");
return result;
}
}
/**
* 客戶端呼叫類
*/
public class JdkProxyClient {
public static void main(String[] args) {
ISubject subject = new RealSubject();
ISubject result = (ISubject)Proxy.newProxyInstance(subject.getClass().getClassLoader(), subject.getClass().getInterfaces(), new ProxySubject(subject));
result.operate();
}
}
最後的執行結果是:
呼叫方法前---前置操作
實際操作
呼叫方法後---後置操作
JDK 動態代理有一個最致命的問題是它只能代理實現了某個介面的實現類,並且代理類也只能代理介面中實現的方法,要是實現類中有自己私有的方法,而介面中沒有的話,該方法不能進行代理呼叫。
2. CGLIB動態代理
CGLIB(Code Generation Library)是一個基於 ASM 的 Java 位元組碼生成框架,它允許我們在執行時對位元組碼進行修改和動態生成。原理就是通過位元組碼技術生成一個子類,並在子類中攔截父類方法的呼叫,織入額外的業務邏輯。關鍵詞大家注意到沒有,攔截!CGLIB 引入一個新的角色就是方法攔截器 MethodInterceptor。和 JDK 中的處理類 InvocationHandler 差不多,也是用來實現方法的統一呼叫的。
CGLIB 動態代理的使用步驟:
第一步: 首先建立一個委託類(Real Subject)
第二步: 建立一個方法攔截器實現介面 MethodInterceptor,並重寫 intercept 方法。intercept 用於攔截並增強委託類的方法(和 JDK 動態代理 InvocationHandler 中的 invoke 方法類似)
package org.springframework.cglib.proxy;
import java.lang.reflect.Method;
public interface MethodInterceptor extends Callback {
Object intercept(Object var1, Method var2, Object[] var3, MethodProxy var4) throws Throwable;
}
該方法擁有四個引數:
- Object var1:委託類物件
- Method var2:被攔截的方法(委託類中需要增強的方法)
- Object[] var3:方法入參
- MethodProxy var4:用於呼叫委託類的原始方法(底層也是通過反射機制,不過不是 Method.invoke 了,而是使用 MethodProxy.invokeSuper 方法)
第三步: 建立代理物件(Proxy):通過 Enhancer.create() 建立委託類物件的代理物件.
也就是說:我們在通過 Enhancer 類的 create() 建立的代理物件在呼叫方法的時候,實際會呼叫到實現了 MethodInterceptor 介面的處理類的 intercept()方法,可以在 intercept() 方法中自定義處理邏輯,比如在方法執行前後做什麼事情。
可以發現,CGLIB 動態代理機制和 JDK 動態代理機制的步驟差不多,CGLIB 動態代理的核心是方法攔截器 MethodInterceptor 和 Enhancer,而 JDK 動態代理的核心是處理類 InvocationHandler 和 Proxy。
程式碼示例
不同於 JDK的是, JDK 動態代理不需要新增額外的依賴,CGLIB 是一個開源專案,如果你要使用它的話,需要手動新增相關依賴。
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
第一步: 建立委託類
public class RealSubject {
public void operate() {
System.out.println("實際操作的動作");
}
}
第二步: 建立攔截器類, 實現MethodInterceptor 介面. 在這裡面可以對方法進行增強處理
public class ProxyMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("呼叫真實操作之前---操作前處理");
// 呼叫真實使用者需要處理的業務邏輯
Object object = methodProxy.invokeSuper(o, args);
System.out.println("呼叫真實操作之後---操作後處理");
return object;
}
}
第三步: 建立代理物件Proxy:通過 Enhancer.create() 建立委託類物件的代理物件
public class CglibProxyFactory {
public static Object getProxy(Class<?> clazz) {
// 建立cglib動態代理的增強類
Enhancer enhancer = new Enhancer();
// 設定類載入器
enhancer.setClassLoader(clazz.getClassLoader());
// 設定委託類
enhancer.setSuperclass(clazz);
// 設定方法攔截器
enhancer.setCallback(new ProxyMethodInterceptor());
// 建立代理類
return enhancer.create();
}
}
從 setSuperclass 我們就能看出,為什麼說 CGLIB 是基於繼承的。
第四步: 客戶端呼叫
public class CglibClient {
public static void main(String[] args) {
RealSubject proxy = (RealSubject)CglibProxyFactory.getProxy(RealSubject.class);
proxy.operate();
}
}
最後的執行結果:
呼叫真實操作之前---操作前處理
實際操作的動作
呼叫真實操作之後---操作後處理
3. JDK 動態代理和 CGLIB 動態代理對比
1)JDK 動態代理是基於實現了介面的委託類,通過介面實現代理;而 CGLIB 動態代理是基於繼承了委託類的子類,通過子類實現代理。
2)JDK 動態代理只能代理實現了介面的類,且只能增強介面中現有的方法;而 CGLIB 可以代理未實現任何介面的類。
3)就二者的效率來說,大部分情況都是 JDK 動態代理的效率更高,隨著 JDK 版本的升級,這個優勢更加明顯。
4. 什麼情況下使用動態代理?
1)我們知道, 設計模式的開閉原則,對修改關閉,對擴充套件開放,在工作中, 經常會接手前人寫的程式碼,有時裡面的程式碼邏輯很複雜不容易修改,那麼這時我們就可以使用代理模式對原來的類進行增強。
2)在使用 RPC 框架的時候,框架本身並不能提前知道各個業務方要呼叫哪些介面的哪些方法 。那麼這個時候,就可用通過動態代理的方式來建立一箇中間人給客戶端使用,也方便框架進行搭建邏輯,某種程度上也是客戶端程式碼和框架鬆耦合的一種表現。
3)Spring AOP 採用了動態代理模式
5. 靜態代理和動態代理對比
1)靈活性 :動態代理更加靈活,不需要必須實現介面,可以直接代理實現類,並且可以不需要針對每個目標類都建立一個代理類。另外,靜態代理中,介面一旦新增加方法,目標物件和代理物件都要進行修改,這是非常麻煩的
2)JVM 層面 :靜態代理在編譯時就將介面、實現類、代理類這些都變成了一個個實際的 .class 位元組碼檔案。而動態代理是在執行時動態生成類位元組碼,並載入到 JVM 中的。
四. 代理模式的優缺點
優點:
1、職責清晰。
2、高擴充套件性。
3、智慧化。
缺點
1、由於在客戶端和真實主題之間增加了代理物件,因此有些型別的代理模式可能會造成請求的處理速度變慢。
2、實現代理模式需要額外的工作,有些代理模式的實現非常複雜。
五. 代理模式使用了哪幾種設計原則?
- 單一職責原則: 一個介面只做一件事
- 裡式替換原則: 任何使用了基類的地方,都可以使用子類替換. 不重寫父類方法
- 依賴倒置原則: 依賴於抽象, 而不是依賴與具體
- 介面隔離原則: 類和類之間應該建立在最小的介面上
- 迪米特法則: 一個物件應該儘可能少的和對其他物件產生關聯, 物件之間解耦
- 開閉原則: 對修改封閉, 對擴充套件開放(體現的最好的一點)
代理類除了是客戶類和委託類的中介之外,我們還可以通過給代理類增加額外的功能來擴充套件委託類的功能,這樣做我們只需要修改代理類而不需要再修改委託類,符合程式碼設計的開閉原則。代理類主要負責為委託類預處理訊息、過濾訊息、把訊息轉發給委託類,以及事後對返回結果的處理等。代理類本身並不真正實現服務,而是同過呼叫委託類的相關方法,來提供特定的服務。真正的業務功能還是由委託類來實現,但是可以在業務功能執行的前後加入一些公共的服務。例如我們想給專案加入快取、日誌這些功能,我們就可以使用代理類來完成,而沒必要開啟已經封裝好的委託類
六. 代理模式和其他模式的區別
1. 代理模式和裝飾器模式的區別
我們來看看代理模式和裝飾器模式的UML圖
- 代理模式
- 裝飾器模式
兩種模式的相似度很高. 接下來具體看看他們的區別
讓別人幫助你做你並不關心的事情,叫代理模式
為讓自己的能力增強,使得增強後的自己能夠使用更多的方法,拓展在自己基礎功能之上,叫裝飾器模式
對裝飾器模式來說,裝飾者(decorator)和被裝飾者(decoratee)都實現同一個 介面。
對代理模式來說,代理類(proxy class)和真實處理的類(real class)都實現同一個介面。
他們之間的邊界確實比較模糊,兩者都是對類的方法進行擴充套件,具體區別如下:
1、裝飾器模式強調的是增強自身,在被裝飾之後你能夠在被增強的類上使用增強後的功能。增強後你還是你,只不過能力更強了而已;代理模式強調要讓別人幫你去做一些本身與你業務沒有太多關係的職責(記錄日誌、設定快取)。代理模式是為了實現物件的控制,因為被代理的物件往往難以直接獲得或者是其內部不想暴露出來。
2、裝飾模式是以對客戶端透明的方式擴充套件物件的功能,是繼承方案的一個替代方案;代理模式則是給一個物件提供一個代理物件,並由代理物件來控制對原有物件的引用;
3、裝飾模式是為裝飾的物件增強功能;而代理模式對代理的物件施加控制,但不對物件本身的功能進行增強;
2. 代理模式和介面卡模式的區別
介面卡模式主要改變所考慮物件的介面,而代理模式不能改變所代理類的介面
來看看代理模式和介面卡模式的UML圖