【設計模式】代理模式那些事兒:靜態代理,動態代理,JDK的動態代理,cglib,Spring AOP
theme: cyanosis
吹NB不負責:這可能是你從未見過的全新版本!
引言
《雪地裡的小畫家》
下雪啦,下雪啦!
雪地裡來了一群小畫家。
小雞畫竹葉,小狗畫梅花,
小鴨畫楓葉,小馬畫月牙。
不用顏料不用筆,
幾步就成一幅畫。
青蛙為什麼沒參加?
他在洞裡睡著啦。
還記得上小學時候的這篇課文嗎?這是我記憶深刻的一篇語文課文,哈哈,在這裡提出來讓大家也回憶一下小學的故事。
這裡面提到了小雞,小狗,小馬,小鴨,青蛙,他們都會在雪地裡畫畫,我們以這些小動物為物件
,來說明一些問題吧。
靜態代理
這些會畫畫小動物我們抽象出一個 畫家 Painter
介面來,讓小動物實現 Painter
,完成 paint()
方法。
小畫家 Painter
java
public interface Painter {
void paint();
}
小狗 Puppy
畫梅花
java
public class Puppy implements Painter {
@Override
public void paint() {
System.out.println("小狗畫梅花");
//隨機睡10s以內,假裝這是處理業務邏輯
try {
Thread.sleep(new Random().nextInt(10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
小馬 Pony
畫月牙
java
public class Pony implements Painter {
@Override
public void paint() {
System.out.println("小馬畫月牙");
//隨機睡10s以內,假裝這是處理業務邏輯
try {
Thread.sleep(new Random().nextInt(10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
兩個就夠了,其他幾個小畫家就不模擬了,手動捂臉~
老師 Teacher
想要看Pony
畫畫:
java
public class Teacher {
public static void main(String[] args) {
new Pony().paint();
}
}
執行結果:
``` 小馬畫月牙
Process finished with exit code 0
``
因為畫的方法裡有
隨機睡x秒的業務處理邏輯,
Teacher` 現在想知道具體睡了多少秒,怎麼辦呢?
這還不簡單,在 paint()
方法中加開始、結束時間,然後相減就可以了:
java
public class Pony implements Painter {
@Override
public void paint() {
//加上時間記錄,計算業務處理執行的時間
long start = System.currentTimeMillis();
System.out.println("小馬畫月牙");
//隨機睡10s以內,假裝這是處理業務邏輯
try {
Thread.sleep(new Random().nextInt(10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("paint 畫畫耗時:" + (end - start) + "毫秒");
}
}
當然,小狗 Puppy
的 paint()
方法也要加這一段。
老師 Teacher
的問題又來了,他還想讓畫畫的時候記錄下日誌,那麼可以做如下修改:
java
public class Pony implements Painter {
@Override
public void paint() {
//加上日誌記錄
System.out.println("日誌:開始作畫");
//加上時間記錄,計算業務處理執行的時間
long start = System.currentTimeMillis();
System.out.println("小馬畫月牙");
//隨機睡10s以內,假裝這是處理業務邏輯
try {
Thread.sleep(new Random().nextInt(10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("日誌:畫完了");
System.out.println("paint 畫畫耗時:" + (end - start) + "毫秒");
}
}
可以看出,如果想要Pony
在畫畫的時候新增一些諸如 記錄執行時間
, 記錄日誌
這樣的動作的時候,就要在實現paint()
方法的時候新增時間
、日誌
這些東西。
但是,這不合理呀,我Pony
明明只需要處理畫畫的邏輯就行了啊!也簡單,把時間處理
、日誌處理
這些東西交給別人
去做,可以把別人
看成代理,這些代理分別持有paint()
方法,在代理內部實現畫畫之外的事情。
代理
Teacher
將來只和代理
打交道,所以代理
必須也“會畫畫”的業務,除此之外,才是代理
處理特殊的業務。
so,代理可以看成是 具有額外功能的Painter
,那就也讓他實現Painter
介面,並且持有具體小畫家(比如Pony
)物件(因為代理需要會畫畫)
處理時間的代理 TimeProxy
```java public class TimeProxy implements Painter {
private Pony pony;
public TimeProxy(Pony pony) {
this.pony = pony;
}
@Override
public void paint() {
long start = System.currentTimeMillis();
//呼叫小馬畫畫
pony.paint();
long end = System.currentTimeMillis();
System.out.println("paint 畫畫耗時:" + (end - start) + "毫秒");
}
}
``
這時的
Pony` 開心了,只處理自己的邏輯即可,去掉時間、日誌:
java
public class Pony implements Painter {
@Override
public void paint() {
System.out.println("小馬畫月牙");
//隨機睡10s以內,假裝這是處理業務邏輯
try {
Thread.sleep(new Random().nextInt(10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
來,Teacher
走一遍,讓代理給我辦事:
java
public class Teacher {
public static void main(String[] args) {
new TimeProxy(new Pony()).paint();
}
}
執行結果:
```
小馬畫月牙
paint 畫畫耗時:3221毫秒
Process finished with exit code 0 ```
大家想想,這樣寫有什麼缺陷沒有?
有的,這裡只是持有了Pony
的物件,也就是說這個代理只能代理Pony
畫畫的時間處理,事實上,這個TimeProxy
也能代理Puppy
等其他小畫家的,那如何做呢?
把具體的Pony
物件換成抽象的Painter
,Teacher
想看誰畫畫就給代理傳哪個畫家就行了!
改一下TimeProxy
:
```java public class TimeProxy implements Painter { private Painter painter;
public TimeProxy(Painter painter) {
this.painter = painter;
}
@Override
public void paint() {
long start = System.currentTimeMillis();
painter.paint();
long end = System.currentTimeMillis();
System.out.println("paint 畫畫耗時:" + (end - start) + "毫秒");
}
}
``
這次呼叫小狗
Puppy`來畫:
java
new TimeProxy(new Puppy()).paint();
``` 小狗畫梅花 paint 畫畫耗時:2152毫秒
Process finished with exit code 0 ```
very ok 了!別急,來把日誌的代理也加進去。
```java public class LogProxy implements Painter { private Painter painter;
public LogProxy(Painter painter) {
this.painter = painter;
}
@Override
public void paint() {
System.out.println("日誌:開始作畫");
painter.paint();
System.out.println("日誌:畫完了");
}
}
``
現在想一下,
Teacher`該怎麼呼叫這兩個代理,既能列印執行時間,又能列印處理日誌,還能畫畫?
我們看一下代理的構造方法,他裡面傳的是抽象的畫家
,並不是具體的,而代理本身也是一種特殊的畫家
-代理本身是實現Painter
這個介面的,所以呼叫的時候可以把代理作為引數傳遞到另一個代理
!!!
java
public class Teacher {
public static void main(String[] args) {
//new TimeProxy(new Puppy()).paint();
new TimeProxy(new LogProxy(new Puppy())).paint();
}
}
執行:
``` 日誌:開始作畫 小狗畫梅花 日誌:畫完了 paint 畫畫耗時:8489毫秒
Process finished with exit code 0 ``` 既有日誌處理,又有時間處理,還有畫畫本身的邏輯處理,大功告成!
上面的例子詮釋了一種設計模式-代理模式,這是一種靜態代理模式。
動態代理
從前面的例子我們可以看到,靜態代理只能作為某一特定的介面的代理,比如前面的TimeProxy
只能代理Painter
。
像這種記錄執行時間
的操作,應該可以應用於所有物件的方法上,具有普遍性,如果要實現把TimeProxy
使用到別的地方,其他Object,該怎麼做呢?
分離代理行為與被代理物件,使用jdk的動態代理。
JDK的動態代理
jdk的 Proxy
類來自於 java.lang.reflect
包,沒錯,就是大名鼎鼎的 反射機制 ,反射是根據已經編譯好的二進位制位元組碼來分析類的屬性和方法,只要給我一個 .class 我就能分析出他的內容。
上程式碼:
java
public class Teacher {
public static void main(String[] args) {
Pony pony = new Pony();
Painter painter = (Painter) Proxy.newProxyInstance(
Pony.class.getClassLoader(),
Pony.class.getInterfaces(),//new Class[]{Painter.class}
new TimeProxyHandler(pony));
painter.paint();
}
}
Proxy.newProxyInstance
有三個引數,第一個是被代理類的類載入器,第二個是實現的介面陣列,也可以寫成:
java
new Class[]{Painter.class}
重點是第三個引數,該引數是一個InvocationHandler
,動態代理方法在執行時,會呼叫InvocationHandler類裡面的invoke方法去執行。
類TimeProxyHandler
的具體實現:
```java public class TimeProxyHandler implements InvocationHandler { private Pony pony;
public TimeProxyHandler(Pony pony) {
this.pony = pony;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long start = System.currentTimeMillis();
Object o = method.invoke(pony, args);
long end = System.currentTimeMillis();
System.out.println("執行耗時:" + (end - start) + "毫秒");
return o;
}
}
``
執行
Teacher.main()`執行結果:
``` 小馬畫月牙 執行耗時:7881毫秒
Process finished with exit code 0 ```
以上我們是用JDK的動態代理可以分離代理行為和被代理的物件,這裡的Pony
可以換成其他物件。
我的main方法裡只調用了painter.paint();
啊,怎麼連執行耗時:7881毫秒
這句話也打印出來了呢?
JDK動態代理原理分析
執行結果列印了執行耗時:7881毫秒
,說明程式必然運行了TimeProxyHandler
的invoke
方法,我們來分析一下下面這句
java
Painter painter = (Painter) Proxy.newProxyInstance(
Pony.class.getClassLoader(),
//new Class[]{Painter.class}
Pony.class.getInterfaces(),
new TimeProxyHandler(pony));
Proxy.newProxyInstance
這一句建立了一箇中間類,我們通過如下手段System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true")
把它弄出來看看:
java
public class Teacher {
public static void main(String[] args) {
Pony pony = new Pony();
//將proxy內部呼叫invoke方法 生成的中間類 儲存下來
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
Painter painter = (Painter) Proxy.newProxyInstance(
Pony.class.getClassLoader(),
new Class[]{Painter.class},
new TimeProxyHandler(pony));
painter.paint();
}
}
再次執行,發現專案目錄多了這個:
開啟 $Proxy0 看看,就能明白個差不多了
java
public final class $Proxy0 extends Proxy implements Painter {
private static Method m1;
private static Method m3;
private static Method m2;
private static Method m0;
jdk幫我們生成的 $Proxy0
繼承 Proxy
實現 Painter
```java static { try { m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object")); m3 = Class.forName("com.xblzer.dp.proxy.dynamicproxy.Painter").getMethod("paint"); m2 = Class.forName("java.lang.Object").getMethod("toString"); m0 = Class.forName("java.lang.Object").getMethod("hashCode"); } catch (NoSuchMethodException var2) { throw new NoSuchMethodError(var2.getMessage()); } catch (ClassNotFoundException var3) { throw new NoClassDefFoundError(var3.getMessage()); } }
...
public final void paint() throws {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
``
當我們
main裡面呼叫
painter.paint()時,實際上執行了
super.h.invoke(this, m3, (Object[])null),這裡的
m3` :
java
m3 = Class.forName("com.xblzer.dp.proxy.staticproxy.Painter").getMethod("paint");
cglib
引入Spring相關依賴包,org.springframework.cglib
cglib底層也是基於asm實現的,並且它不需要實現任何介面。
來看效果:
```java /* * cglib-code generate library * cglib實現動態代理不需要實現介面 * 底層用的也是asm * @author 行百里者 / public class Main { public static void main(String[] args) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(Pony.class); enhancer.setCallback(new TimeMethodInterceptor()); Pony pony = (Pony) enhancer.create(); pony.paint(); } }
class TimeMethodInterceptor implements MethodInterceptor { @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { System.out.println(o.getClass().getSuperclass().getName()); System.out.println("before..."); Object result = methodProxy.invokeSuper(o, objects); System.out.println("after"); return result; } }
class Pony { public void paint() { System.out.println("小馬畫月牙"); //隨機睡10s以內,假裝這是處理業務邏輯 try { Thread.sleep(new Random().nextInt(10000)); } catch (InterruptedException e) { e.printStackTrace(); } } } ```
程式執行結果:
Spring AOP
現在我們知道了,動態代理可以對任何方法的任何地方切入代理所執行的邏輯,比如執行時間,記錄日誌,處理事務等。
我們可以在Pony
的paint()
方法執行前切入before()
,在執行後切入after()
,也就是說可以在指定的點切入代理所要做的事情,這就是簡單的面向切面了。
Spring AOP就是面向切面,AOP是Spring的核心之一。
下面用程式碼演示一下,AOP是怎麼切入代理處理邏輯的。
Spring配置檔案app_aop.xml
:
```xml
``
LogProxy`
```java public class LogProxy { public void before() { System.out.println("日誌:開始作畫"); }
public void after() {
System.out.println("日誌:畫完了");
}
}
``
Pony還是那個
Pony`,不贅述。
使用:
java
public class Teacher {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("app_aop.xml");
Pony pony = (Pony) ctx.getBean("pony");
pony.paint();
}
}
執行結果:
``` 日誌:開始作畫 日誌:畫完了 小馬畫月牙
Process finished with exit code 0
``
Spring AOP`就是這麼方便!!!
小結
代理模式應用得非常廣泛,大到一個系統框架、企業平臺,小到程式碼片段、事務處理,用到代理模式的概率是非常大的。
有了AOP大家寫代理就更加簡單了,有類似Spring AOP這樣非常優秀的工具,拿來主義即可!
另外,我們看原始碼,特別是除錯時,只要看到類似 $Proxy0 這樣的結構,我們不妨開啟它看看,這樣能夠幫助我們更容易理解動態代理。
點個贊再走吧~
本文程式碼 Github https://github.com/xblzer/JavaJourney
我正在參與掘金技術社群創作者簽約計劃招募活動,點選連結報名投稿。
- Java最強大的技術之一:反射
- 【設計模式】通過一個簡單的案例理解-訪問者模式(Visitor Pattern)
- 【設計模式】模板模式,學會它咱也寫出優雅健壯的程式碼!
- 【設計模式】通俗易懂版責任鏈模式
- 【設計模式】代理模式那些事兒:靜態代理,動態代理,JDK的動態代理,cglib,Spring AOP
- 萬字長文 | Spring Cloud Alibaba元件之Nacos實戰及Nacos客戶端服務註冊原始碼解析
- 【設計模式】只需體驗三分鐘,你就會跟我一樣瞭解Facade和Mediator模式
- 【設計模式】工廠系列-FactoryMethod,AbstractFactory,Spring IOC
- 【資料結構】| 連結串列資料結構及其簡單玩法解析
- 系統架構演進與Spring Cloud Alibaba簡介
- 【FastDFS】一文學會一個分散式檔案系統!
- 【MySQL 8】MySQL 5.7都即將停只維護了,是時候學習一波MySQL 8了!
- 使用Rancher部署管理K8S叢集,真香!
- 玩轉Docker映象倉庫-Docker Registry及Harbor
- 【設計模式】各個擊破單例模式的8種寫法
- 容器化技術之Docker-從入地到上天
- 【RocketMQ】RocketMQ叢集,RocketMQ-on-DLedger可容災叢集
- 【RocketMQ】基於RocketMQ的分散式事務
- 【RocketMQ】近距離感受RocketMQ如何收發訊息,有備而來!
- 【RocketMQ】RocketMQ入門之閃電三連鞭:訊息佇列、RocketMQ介紹及安裝使用