1. 簡介

代理模式的定義:為其他物件提供一種代理以控制對這個物件的訪問。在某些情況下,一個物件不適合或者不能直接引用另一個物件,而代理物件可以在客戶端和目標物件之間起到中介的作用。

比如:我們在呼叫底層框架方法時候,需要在呼叫方法的前後列印日誌,或者做一些邏輯判斷。此時我們無法去修改底層框架方法,那我們可以通過封裝一個代理類,在代理類中實現對方法的處理,然後所有的客戶端通過代理類去呼叫目標方法。

其中這裡有幾個物件:

  1. 抽象角色:通過介面或者抽象類宣告真實角色實現的業務方法,儘可能的保證代理物件的內部結構和目標物件一致,這樣我們對代理物件的操作最終都可以轉移到目標物件上。
  2. 代理角色/代理物件:實現抽象角色,是真實角色的代理,實現對目標方法的增強。
  3. 真實角色/目標物件:實現抽象角色,定義真實角色所要實現的業務邏輯,供代理角色呼叫。
  4. 呼叫角色/客戶端:呼叫代理物件。

2. 靜態代理

2.1 業務場景

假設現在有這麼個場景:王淑芬打算相親,但是自己嘴笨,於是找到媒婆,希望媒婆幫自己找個帥哥,於是找到了張鐵牛

角色分析:

  • 王淑芬:目標物件(被代理的人)。
  • 媒婆:代理物件(代理了王淑芬,實現對目標方法的增強)。
  • 張鐵牛:客戶端(訪問代理物件,即找媒婆)。
  • 抽象角色(相親):媒婆王淑芬的共同目標-相親成功。

2.2 程式碼實現

  1. 相親介面

    /**
    * 相親抽象類
    *
    * @author ludangxin
    * @date 2021/9/25
    */
    public interface BlindDateService {
    /**
    * 聊天
    */
    void chat();
    }
  2. 目標物件

    /**
    * 王淑芬 - 目標物件
    *
    * @author ludangxin
    * @date 2021/9/25
    */
    @Slf4j
    public class WangShuFen implements BlindDateService {
    @Override
    public void chat() {
    log.info("美女:王淑芬~");
    }
    }
  3. 代理物件

    /**
    * 媒婆 - 代理物件
    *
    * @author ludangxin
    * @date 2021/9/25
    */
    @Slf4j
    public class WomanMatchmaker implements BlindDateService {
    private BlindDateService bs; public WomanMatchmaker() {
    this.bs = new WangShuFen();
    } @Override
    public void chat() {
    this.introduce();
    bs.chat();
    this.praise();
    } /**
    * 介紹
    */
    private void introduce() {
    log.info("媒婆:她的工作是web前端~");
    } /**
    * 夸人
    */
    private void praise() {
    log.info("媒婆:她就是有點害羞~");
    log.info("媒婆:你看她人長的漂亮,而且溫柔賢惠,上的廳堂下的廚房~");
    log.info("媒婆:而且寫bug超厲害~");
    }
    }
  4. 客戶端

    /**
    * 張鐵牛 - client
    *
    * @author ludangxin
    * @date 2021/9/25
    */
    public class ZhangTieNiu {
    public static void main(String[] args) {
    WomanMatchmaker wm = new WomanMatchmaker();
    wm.chat();
    }
    }
  5. 執行方法輸出內容如下:

    22:44:51.184 [main] INFO proxy.staticp.WomanMatchmaker - 媒婆:她的工作是web前端~
    22:44:51.191 [main] INFO proxy.staticp.WangShuFen - 美女:你好,我叫王淑芬~
    22:44:51.191 [main] INFO proxy.staticp.WomanMatchmaker - 媒婆:她就是有點害羞~
    22:44:51.191 [main] INFO proxy.staticp.WomanMatchmaker - 媒婆:你看她人長的漂亮,而且溫柔賢惠,上的廳堂下的廚房~
    22:44:51.191 [main] INFO proxy.staticp.WomanMatchmaker - 媒婆:而且寫bug超厲害~

2.3 小節

好處:

  1. 耦合性降低。因為加入了代理類,呼叫者只用關心代理類即可,降低了呼叫者與目標類的耦合度。
  2. 指責清晰,目標物件只關心真實的業務邏輯。代理物件只負責對目標物件的增強。呼叫者只關心代理物件的執行結果。
  3. 代理物件實現了對目標方法的增強。也就是說:代理物件 = 增強程式碼 + 目標物件

缺陷:

每一個目標類都需要寫對應的代理類。如果當前系統已經有成百上千個類,工作量大太。所以,能不能不用寫那麼多代理類,就能實現對目標方法的增強呢?

3. 動態代理

我們常見的動態代理一般有兩種:JDK動態代理CGLib動態代理,本章只講JDK動態代理

在瞭解JDK動態代理之前,先了解兩個重要的類。

3.1 Proxy

從JDK的幫助文件中可知:

Proxy提供了建立動態代理和例項的靜態方法。即:

Proxy::newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)

引數:

  • loader - 定義代理類的類載入器

  • interfaces - 代理類要實現的介面列表

  • h - 指派方法呼叫的呼叫處理程式

前兩個引數沒啥好說的,主要還需要了解下InvocationHandler

3.2 InvocationHandler

從JDK的幫助文件中可知:

InvocationHandler是一個介面,並且是呼叫處理邏輯實現的介面。在呼叫代理物件方法時,實際上是呼叫實現介面的invoke方法。

Object InvocationHandler::invoke(Object proxy, Method method, Object[] args)

引數:

  • proxy - 呼叫方法的代理例項。

  • method - 對應於在代理例項上呼叫的介面方法的 Method 例項。 Method 物件的宣告類將是在其中宣告方法的介面,該介面可以是代理類賴以繼承方法的代理介面的超介面。

  • args - 包含傳入代理例項上方法呼叫的引數值的物件陣列,如果介面方法不使用引數,則為 null。基本型別的引數被包裝在適當基本包裝器類(如 java.lang.Integerjava.lang.Boolean)的例項中。

返回值:

從代理例項的方法呼叫返回的值。如果介面方法的宣告返回型別是基本型別,則此方法返回的值一定是相應基本包裝物件類的例項;否則,它一定是可分配到宣告返回型別的型別。如果此方法返回的值為 null 並且介面方法的返回型別是基本型別,則代理例項上的方法呼叫將丟擲 NullPointerException。否則,如果此方法返回的值與上述介面方法的宣告返回型別不相容,則代理例項上的方法呼叫將丟擲 ClassCastException

3.3 程式碼實現

  1. 建立動態代理類

    import lombok.extern.slf4j.Slf4j;
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    import java.util.Arrays; /**
    * 媒婆 - 代理物件
    *
    * @author ludangxin
    * @date 2021/9/25
    */
    @Slf4j
    public class ProxyTest { public static Object getProxy(final Object target) {
    return Proxy.newProxyInstance(
    //類載入器
    target.getClass().getClassLoader(),
    //讓代理物件和目標物件實現相同介面
    target.getClass().getInterfaces(),
    //代理物件的方法最終都會被JVM導向它的invoke方法
    new InvocationHandler() {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    log.info("proxy ===》{}", proxy.getClass().toGenericString());
    log.info("method ===》{}", method.toGenericString());
    log.info("args ===》{}", Arrays.toString(args));
    introduce();
    // 呼叫目標方法
    Object result = method.invoke(target, args);
    praise();
    return result;
    }
    });
    } /**
    * 介紹
    */
    private static void introduce() {
    log.info("媒婆:她的工作是web前端~");
    } /**
    * 夸人
    */
    private static void praise() {
    log.info("媒婆:她就是有點害羞~");
    log.info("媒婆:你看她人長的漂亮,而且溫柔賢惠,上的廳堂下的廚房~");
    log.info("媒婆:而且寫bug超厲害~");
    }
    }
  2. 客戶端

    /**
    * 張鐵牛 - client
    *
    * @author ludangxin
    * @date 2021/9/25
    */
    public class ZhangTieNiu {
    public static void main(String[] args) {
    BlindDateService proxy = (BlindDateService)ProxyTest.getProxy(new WangShuFen());
    proxy.chat();
    }
    }
  3. 執行方法輸出內容如下:

    21:29:22.222 [main] INFO proxy.dynamic.ProxyTest - proxy ===》public final class com.sun.proxy.$Proxy0
    21:29:22.229 [main] INFO proxy.dynamic.ProxyTest - method ===》public abstract void proxy.dynamic.BlindDateService.chat()
    21:29:22.229 [main] INFO proxy.dynamic.ProxyTest - args ===》null
    21:29:22.229 [main] INFO proxy.dynamic.ProxyTest - 媒婆:她的工作是web前端~
    21:29:22.229 [main] INFO proxy.dynamic.WangShuFen - 美女:你好,我叫王淑芬~
    21:29:22.230 [main] INFO proxy.dynamic.ProxyTest - 媒婆:她就是有點害羞~
    21:29:22.230 [main] INFO proxy.dynamic.ProxyTest - 媒婆:你看她人長的漂亮,而且溫柔賢惠,上的廳堂下的廚房~
    21:29:22.230 [main] INFO proxy.dynamic.ProxyTest - 媒婆:而且寫bug超厲害~

3.4 小節

其實程式碼很簡單,只需要通過jdk提供的代理類建立一個代理類,再通過代理類去呼叫目標方法,並實現對方法的增強即可。

從列印的日誌中可以看出JDK生成真實的代理物件為com.sun.proxy.$Proxy0,那我們能不能檢視下生成的代理物件的原始碼呢?答案肯定是可以的。我們可以藉助JDK預設提供的ProxyGenerator::generateProxyClass()來輸出動態生成的代理物件。

改造下動態代理類如下:

新增輸出動態代理類的方法。

package proxy.dynamic;

import lombok.extern.slf4j.Slf4j;
import sun.misc.ProxyGenerator;
import java.io.File;
import java.io.FileOutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays; /**
* 媒婆 - 代理物件
*
* @author ludangxin
* @date 2021/9/25
*/
@Slf4j
public class ProxyTest {
public static Object getProxy(final Object target) {
return Proxy.newProxyInstance(
//類載入器
target.getClass().getClassLoader(),
//讓代理物件和目標物件實現相同介面
target.getClass().getInterfaces(),
//代理物件的方法最終都會被JVM導向它的invoke方法
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
generateProxyClass(proxy);
log.info("proxy ===》{}", proxy.getClass().toGenericString());
log.info("method ===》{}", method.toGenericString());
log.info("args ===》{}", Arrays.toString(args));
introduce();
Object result = method.invoke(target, args);
praise();
return result;
}
});
} /**
* 輸出動態生成的代理類
* @param proxy 代理例項
*/
private static void generateProxyClass(Object proxy) {
byte[] bytes = ProxyGenerator.generateProxyClass(proxy.getClass().getName(), proxy.getClass().getInterfaces());
File file = new File("/Users/ludangxin/workspace/idea/test/target/classes/proxy/dynamic/proxy.class");
try {
FileOutputStream fileOutputStream = new FileOutputStream(file);
fileOutputStream.write(bytes);
} catch(Exception e) {
e.printStackTrace();
}
}
...
}

再次執行呼叫方法,輸出類如下:

類資訊如下:

看完代理類的方法是不是恍然大悟,我們再整理下呼叫過程:

動態代理:動態的生成代理物件。

4. 總結

其實無論是靜態代理還是動態代理本質都是最終生成代理物件,區別在於靜態代理物件需要人手動生成,而動態代理物件是執行時,JDK通過反射動態生成的代理類最終構造的物件,JDK生成的類在載入到記憶體之後就刪除了,所以看不到類檔案。