1. 簡介
代理模式的定義:為其他物件提供一種代理
以控制對這個物件的訪問。在某些情況下,一個物件不適合或者不能直接引用另一個物件,而代理物件可以在客戶端和目標物件之間起到中介的作用。
比如:我們在呼叫底層框架方法時候,需要在呼叫方法的前後列印日誌,或者做一些邏輯判斷。此時我們無法去修改底層框架方法,那我們可以通過封裝一個代理類,在代理類中實現對方法的處理,然後所有的客戶端通過代理類去呼叫目標方法。
其中這裡有幾個物件:
抽象角色
:通過介面或者抽象類宣告真實角色實現的業務方法,儘可能的保證代理物件的內部結構和目標物件一致,這樣我們對代理物件的操作最終都可以轉移到目標物件上。代理角色/代理物件
:實現抽象角色,是真實角色的代理,實現對目標方法的增強。真實角色/目標物件
:實現抽象角色,定義真實角色所要實現的業務邏輯,供代理角色呼叫。呼叫角色/客戶端
:呼叫代理物件。
2. 靜態代理
2.1 業務場景
假設現在有這麼個場景:王淑芬
打算相親
,但是自己嘴笨,於是找到媒婆
,希望媒婆
幫自己找個帥哥,於是找到了張鐵牛
。
角色分析:
- 王淑芬:目標物件(被代理的人)。
- 媒婆:代理物件(代理了王淑芬,實現對目標方法的增強)。
- 張鐵牛:客戶端(訪問代理物件,即找媒婆)。
- 抽象角色(相親):
媒婆
和王淑芬
的共同目標-相親成功。
2.2 程式碼實現
相親介面
/**
* 相親抽象類
*
* @author ludangxin
* @date 2021/9/25
*/
public interface BlindDateService {
/**
* 聊天
*/
void chat();
}
目標物件
/**
* 王淑芬 - 目標物件
*
* @author ludangxin
* @date 2021/9/25
*/
@Slf4j
public class WangShuFen implements BlindDateService {
@Override
public void chat() {
log.info("美女:王淑芬~");
}
}
代理物件
/**
* 媒婆 - 代理物件
*
* @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超厲害~");
}
}
客戶端
/**
* 張鐵牛 - client
*
* @author ludangxin
* @date 2021/9/25
*/
public class ZhangTieNiu {
public static void main(String[] args) {
WomanMatchmaker wm = new WomanMatchmaker();
wm.chat();
}
}
執行方法輸出內容如下:
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 小節
好處:
- 耦合性降低。因為加入了代理類,呼叫者只用關心代理類即可,降低了呼叫者與目標類的耦合度。
- 指責清晰,目標物件只關心真實的業務邏輯。代理物件只負責對目標物件的增強。呼叫者只關心代理物件的執行結果。
- 代理物件實現了對目標方法的增強。也就是說:代理物件 = 增強程式碼 + 目標物件。
缺陷:
每一個目標類都需要寫對應的代理類。如果當前系統已經有成百上千個類,工作量大太。所以,能不能不用寫那麼多代理類,就能實現對目標方法的增強呢?
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.Integer
或java.lang.Boolean
)的例項中。
返回值:
從代理例項的方法呼叫返回的值。如果介面方法的宣告返回型別是基本型別,則此方法返回的值一定是相應基本包裝物件類的例項;否則,它一定是可分配到宣告返回型別的型別。如果此方法返回的值為 null
並且介面方法的返回型別是基本型別,則代理例項上的方法呼叫將丟擲 NullPointerException
。否則,如果此方法返回的值與上述介面方法的宣告返回型別不相容,則代理例項上的方法呼叫將丟擲 ClassCastException
。
3.3 程式碼實現
建立動態代理類
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超厲害~");
}
}
客戶端
/**
* 張鐵牛 - 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();
}
}
執行方法輸出內容如下:
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生成的類在載入到記憶體之後就刪除了,所以看不到類檔案。