【設計模式】代理模式那些事兒:靜態代理,動態代理,JDK的動態代理,cglib,Spring AOP

語言: CN / TW / HK

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) + "毫秒"); } } 當然,小狗 Puppypaint() 方法也要加這一段。

老師 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物件換成抽象的PainterTeacher想看誰畫畫就給代理傳哪個畫家就行了!

改一下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

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毫秒,說明程式必然運行了TimeProxyHandlerinvoke方法,我們來分析一下下面這句

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"); jdk動態代理

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

現在我們知道了,動態代理可以對任何方法的任何地方切入代理所執行的邏輯,比如執行時間,記錄日誌,處理事務等。

我們可以在Ponypaint()方法執行前切入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

我正在參與掘金技術社群創作者簽約計劃招募活動,點選連結報名投稿