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圖