【設計模式】代理模式

概述:

1. 什麼是代理

2. 代理的分類

3. Spring AOP對動態代理的應用

 

一、什麼是代理

你需要乘飛機,但是去不了機場,機票代理點就能讓你實現買機票的需求。

你需要辦理車過戶,但是你不知道流程,在門口找一個專門代你辦理的人,他都給你辦了,這就是代理。

可見代理是個中間商,他代替原來的事務部門,滿足你的需求,這就是代理模式的意義。

想像一下,你想修改某個類以實現特殊的功能,但是這個類在SDK包里,或者在遠程機器上,怎麼辦?

這時候你可以找個代理,不就是想實現自定義功能嗎?不用去改原始類了,你在我這隨便改,我把原始類集成進來,這樣我既有原始類的功能,又有你自定義的功能,不就完美了。

這就是代理模式。

二、代理的分類

1. 靜態代理

這個 不好類比說明,因為java程序中有運行中的概念,靜態代理就相當於運行前,你就已經寫好了代理類,然後編譯直接調用。

比如有如下場景,目前有個生產玩具的類,在不改變這個類的前提下,增加統計這個類生產玩具方法用時的功能,這個怎麼實現?

 1 /**
 2  * 委託者,原始類,一個生產玩偶的工廠
 3  */
 4 public class ToyFactory implements Produce {
 5     @Override
 6     public void produce_cat() {
 7         System.out.println("生產了一隻小貓");
 8         try {
 9             Thread.sleep(new Random().nextInt(1000));
10         } catch (InterruptedException e) {
11             e.printStackTrace();
12         }
13     }
14 
15     @Override
16     public void produce_deer() {
17         System.out.println("生產了一隻小鹿");
18         try {
19             Thread.sleep(new Random().nextInt(1000));
20         } catch (InterruptedException e) {
21             e.printStackTrace();
22         }
23     }
24 }

 

 1 /**
 2  * 生產方法統計時間的代理類
 3  */
 4 public class ToyFactoryTimeProxy implements Produce{
 5     private ToyFactory toyFactory;
 6 
 7 
 8     public ToyFactoryTimeProxy(ToyFactory toyFactory) {
 9         this.toyFactory = toyFactory;
10     }
11 
12     @Override
13     public void produce_cat() {
14         long startTime = System.currentTimeMillis();
15         this.toyFactory.produce_cat();
16         long endTime = System.currentTimeMillis();
17         long takeTime = endTime - startTime;
18         System.out.println("log-----cat take time="+takeTime);
19     }
20 
21     @Override
22     public void produce_deer() {
23         long startTime = System.currentTimeMillis();
24         this.toyFactory.produce_deer();
25         long endTime = System.currentTimeMillis();
26         long takeTime = endTime - startTime;
27         System.out.println("log-----deer take time="+takeTime);
28         
29     }
30 }

 

1 public static void main(String[] args) {
2         ToyFactory toyFactory = new ToyFactory();
3         ToyFactoryTimeProxy toyFactoryTimeProxy = new ToyFactoryTimeProxy(toyFactory);
4         toyFactoryTimeProxy.produce_cat();
5     }

 

執行結果:

生產了一隻小貓
log-----cat take time=226

 

這就是靜態代理的實現,這種方式屬於聚合的方式,其實還有一種方式能實現類似的效果,就是繼承。

我們可以繼承工廠類,然後重寫造小貓的方法,在這方法中寫統計時間的邏輯,但是繼承方式有弊端,如果我們再要一個功能,就是在統計完時間後,還打印日誌,這無非就是再寫一個子類,繼承時間代理類,但是如果新的需求是先打印日誌,再統計時間,對於繼承來說,之前寫的就要不了了,得再寫一個工廠類的子類,作為日誌代理類,再寫一個日誌代理類的子類,作為時間代理類。

然而通過聚合的方式,可以利用java多態的特性,既然所有的代理類和委託類都需要實現同一個接口,那麼我們就直接都聚合接口,而不是具體的委託類,這樣就可以實現代理類之間也可以互相代理了。

首先把時間代理類中的ToyFactory改成Produce。

 1 /**
 2  * 生產方法統計時間的代理類
 3  */
 4 public class ToyFactoryTimeProxy implements Produce{
 5     private Produce produce;
 6 
 7 
 8     public ToyFactoryTimeProxy(Produce produce) {
 9         this.produce = produce;
10     }
11 
12     @Override
13     public void produce_cat() {
14         long startTime = System.currentTimeMillis();
15         this.produce.produce_cat();
16         long endTime = System.currentTimeMillis();
17         long takeTime = endTime - startTime;
18         System.out.println("log-----cat take time="+takeTime);
19     }
20 
21     @Override
22     public void produce_deer() {
23         long startTime = System.currentTimeMillis();
24         this.produce.produce_deer();
25         long endTime = System.currentTimeMillis();
26         long takeTime = endTime - startTime;
27         System.out.println("log-----deer take time="+takeTime);
28         
29     }
30 }
 1 /**
 2  * 這是個生產方法打日誌的代理類
 3  */
 4 public class ToyFactoryLogProxy implements Produce{
 5     private Produce Produce;
 6 
 7 
 8     public ToyFactoryLogProxy(Produce Produce) {
 9         this.Produce = Produce;
10     }
11 
12     @Override
13     public void produce_cat() {
14         this.Produce.produce_cat();
15         System.out.println("log-----cat is produced");
16     }
17 
18     @Override
19     public void produce_deer() {
20         this.Produce.produce_deer();
21         System.out.println("log-----deer is produced");
22     }
23 }
1     public static void main(String[] args) {
2         ToyFactory toyFactory = new ToyFactory();
3         ToyFactoryTimeProxy toyFactoryTimeProxy = new ToyFactoryTimeProxy(toyFactory);
4         ToyFactoryLogProxy toyFactoryLogProxy = new ToyFactoryLogProxy(toyFactoryTimeProxy);
5         toyFactoryLogProxy.produce_cat();
6     }

 

執行結果:

生產了一隻小貓
log-----cat take time=914
log-----cat is produced

 

如果想反過來,只需要把main方法中的聚合順序調整一下就可以了。

這裡跑題一下,積累一下多態的知識:

面向接口編程的概念,用電腦主板和顯卡來舉例。
如果你主板上鏈接的是具體的某個內存條,那麼會造成一種什麼情況:
在組裝電腦之處,你對內存的要求就是2G就能滿足,你new了一個2G的內存條,隨着使用2G不夠了,這個時候你已經沒法切換了。
《面向對象軟件構造(Object Oriented Software Construction)》中提出了開閉原則,它的原文是這樣:「Software entities should be open for extension,but closed for modification」。
翻譯過來就是:「軟件實體應當對擴展開放,對修改關閉」。這句話說得略微有點專業,我們把它講得更通俗一點,也就是:軟件系統中包含的各種組件,例如模塊(Modules)、類(Classes)以及功能(Functions)等等,應該在不修改現有代碼的基礎上,引入新功能。
開閉原則中「開」,是指對於組件功能的擴展是開放的,是允許對其進行功能擴展的;開閉原則中「閉」,是指對於原有代碼的修改是封閉的,即修改原有的代碼對外部的使用是透明的。 然而要解決這個問題,答案其實就在接口上了,你約定好了一個規範接口,所有顯卡對象要想接入主板,必須實現這個接口,那麼你在設計功能的時候,壓根不用考慮具體實現,
2G不夠直接拔了換4G,ArrayList不行就直接換LinkList,具體實現與主體類就實現了解耦,而面向接口編程,本質上就是運用了java多態的特性。

 

靜態代理雖然也實現了功能,但是存在兩個問題:

  1. 如果SDK包里有100個委託類需要代理,那麼就得寫100個代理類,這個在現實工作中並不稀奇,最常用到的就是AOP,你需要攔截符合條件的所有類的方法,給他們附加上功能,這個要用靜態代理實現就是把每個類都加上代碼。

  2. 就算委託類很少,但是裏面的方法很多,也會造成很大的工作量,而且同樣的代碼會重複寫很多次,100個方法就得寫一百次統計時間的那段代碼,極其繁瑣。

  如果我們自己去解決這兩個問題,會怎麼寫,首先,需要根據委託類靈活的去生成對應的代理類,這個必須是一個自動的過程,如問題1,可能巨量的類需要代理,必須全自動才能解決量的問題。

  再有就是對於委託類中方法的解決方案,如果你動態生成的代理類里,還是一個一個的去實現方法,問題2就解決不掉,最好是有一個通用的方法,這個方法能代表委託類中的所有方法(或者符合條件的方法),然後在這個類中加上你想加的代碼,就等於所有方法都有了,實現了這兩種解決方案的,就是動態代理。

 2. 動態代理

繼續上面的思路,我們的問題轉移到怎麼生成一個動態代理上面來了。

繼續思考,你要生成這個樣的一個代理,首先你要獲取到委託類實現了哪些接口,因為我們將要生成的代理類也要實現接口,其次是咱們要這個代理類幹啥活,打日誌也好,篩選返回值也好,你得告訴它。

我們看看JDK的是否跟我們說的一樣:


public class Proxy implements java.io.Serializable {
  public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) throws IllegalArgumentException
 6     {
 9         final Class<?>[] intfs = interfaces.clone();
15         /*
16          * Look up or generate the designated proxy class.
17          */
18         Class<?> cl = getProxyClass0(loader, intfs);
20         /*
21          * Invoke its constructor with the designated invocation handler.
22          */
23         try {
28             final Constructor<?> cons = cl.getConstructor(constructorParams);
29             final InvocationHandler ih = h;
30             if (!Modifier.isPublic(cl.getModifiers())) {
31                 AccessController.doPrivileged(new PrivilegedAction<Void>() {
32                     public Void run() {
33                         cons.setAccessible(true);
34                         return null;
35                     }
36                 });
37             }
38             return cons.newInstance(new Object[]{h});
48 } catch (NoSuchMethodException e) { 49 throw new InternalError(e.toString(), e); 50 } 51 }
}

 

 可以看到,在生成動態代理類的方法中,跟我們預想的只多了一個ClassLoader,委託類實現的一些接口(Class<?>[] interfaces),和我們需要的委託類做的事(InvocationHandler h),這裡都有。

 我們需要重點關注Class<?> cl = getProxyClass0(loader, intfs)這句代碼,這裡產生了代理類,這個類就是動態代理的關鍵。

可以通過java自帶的類方法ProxyGenerator.generateProxyClass,看看jdk給我生成的代理文件是什麼樣子的:

 1 byte[] Proxy0s = ProxyGenerator.generateProxyClass("12345", ToyFactory.class.getInterfaces());
 2         String path = "C:\\Users\\panda_zhu\\Desktop\\12345.class";
 3         try{
 4             FileOutputStream fos = new FileOutputStream(path);
 5             fos.write(Proxy0s);
 6             fos.flush();
 7             System.out.println("編譯文件生成完畢!");
 8         } catch (Exception e) {
 9             e.printStackTrace();
10         }

 

 委託類:

/**
 * 委託者,原始類,一個生產玩偶的工廠
 */
public class ToyFactory implements Produce {
    @Override
    public void produce_cat() {
        System.out.println("生產了一隻小貓");
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void produce_deer() {
        System.out.println("生產了一隻小鹿");
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

 

 

生成的class文件反編譯後如下(部分):

public final class 12345 extends Proxy implements Produce
{
  private static Method m1;
  private static Method m4;
  private static Method m2;
  private static Method m3;
  private static Method m0;

  public 12345(InvocationHandler paramInvocationHandler)throws
  {
    super(paramInvocationHandler);
  }
//從這裡可以很清晰的看到,利用反射,把委託類中的方法取出,聚合到代理類中,然後通過父類的屬性InvocationHandler中的invoke方法執行,這個方法後續說。從而實現了代理委託類的功能。
static { try { m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") }); m4 = Class.forName("com.example.design.proxy.Produce").getMethod("produce_deer", new Class[0]); m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]); m3 = Class.forName("com.example.design.proxy.Produce").getMethod("produce_cat", new Class[0]); m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]); return; } } public final boolean equals(Object paramObject) throws { try { return ((Boolean)this.h.invoke(this, m1, new Object[] { paramObject })).booleanValue(); } } public final void produce_deer() throws { try { this.h.invoke(this, m4, null); return; } } } public final void produce_cat() throws { try { this.h.invoke(this, m3, null); return; } } }

到此為止,我們至少解決了一個問題,那就是動態生成一個代理文件。

但是,這裡其實有一個疑問點,就是這個生成的代理類,是怎麼知道我的委託類是誰的,這裡也就依據和委託類實現同一個接口而寫了方法的空殼子而已,真正實現都是人家InvocationHandler的invoke方法去實現的,不論是生產鹿也好生產貓也好,就這一個方法,當然這也是咱們最初的設想,即上一節通用方法的解決方案,但是是怎麼實現的呢?又是怎麼精準定位委託類的呢?

我們看看這個類的源碼:

public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

 

就一個方法,第一個參數傳入代理類,咱們自動生成的代理類,傳入的是他自己。

第二個是需要執行的方法,這個代理類傳的是他反射出來接口的方法。

第三個是這些方法的參數,這裡為了方便看咱們沒有參數。

通過這個方法也得不出這個答案,因為代理類本來也沒法說清楚委託類是誰,第二個頂多告訴這個通用的方法,我要執行的是哪個方法,所以可以推斷,這些都應該落在自己定義的Invocation上。

/**
 *  自定義的invoaction類,
 */

public class MyInvocationHandler implements InvocationHandler {
    private ToyFactory toyFactory;

    public MyInvocationHandler(ToyFactory toyFactory){
        this.toyFactory = toyFactory;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("開始執行自定義方法。。");
        long startTime = System.currentTimeMillis();
        method.invoke(toyFactory,new Object[]{});
        long endTime = System.currentTimeMillis();
        System.out.println("執行"+method.getName()+"方法共耗時:"+(endTime-startTime));
        return null;
    }
}

 

答案在這裡,跟預想的一樣,是在自定義invacation類里聚合了委託類,並且通過method.invoke()方法,實現傳入哪個方法,調用託管類哪個方法這種靈活性的。

這裡有一個疑問,就是傳進來的proxy對象好像沒有用上,這個是幹啥的,其實這個參數是為了返回值,jdk文檔中表示,這個invoke方法的返回值必須跟傳入的proxy返回值對應。

1         ToyFactory toyFactory = new ToyFactory();
2         //使用動態代理
3         Produce o = (Produce)Proxy.newProxyInstance(toyFactory.getClass().getClassLoader(), toyFactory.getClass().getInterfaces(), new MyInvocationHandler(toyFactory));
4         o.produce_deer();
開始執行自定義方法。。
生產了一隻小鹿
執行produce_deer方法共耗時:974

 

 至此,咱們靜態變量遇到的問題就算是徹底解決了。

動態生成代理類解決了需要一直自己寫代理類的事,method.invoke方法解決了每個方法都需要寫重複代碼的問題。

 

3. Spring AOP 對動態代理的應用

Spring AOP是基於動態代理的,如果要代理的對象,實現了某個接口,那麼Spring AOP會使用JDK Proxy,去創建代理對象,而對於沒有實現接口的對象,就無法使用 JDK Proxy 去進行代理了,這時候Spring AOP會使用Cglib ,這時候Spring AOP會使用 Cglib 生成一個被代理對象的子類來作為代理。

在web開發中,我們通常將項目分為controller、service、dao層,這種分層是一種縱向的,我們為了好理解,可以把它想像層一個豎狀的圓柱形,數據從中川流不息。

而AOP則是從這個圓柱截面中插入一個濾網,也就是我們說的面向切面。

在日常開發中,日誌攔截、權限處理、異常攔截、事務等,都是基於這種切面完成的。

那spring在哪調用了動態代理呢?

final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable {
    public Object getProxy(@Nullable ClassLoader classLoader) {
    if (logger.isTraceEnabled()) {
    logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource());
    }

    return Proxy.newProxyInstance(classLoader, this.proxiedInterfaces, this);
    }
}

 

最後那個返回值是不是很眼熟了。 

題外知識點練習:

如何使用spring aop。

POM中引入依賴:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
            <version>2.2.6.RELEASE</version>
        </dependency>

 

由於springboot默認配置啟動aop,所以不用另外配置了。

 1 @Aspect
 2 @Component
 3 public class WebAspect {
 4     /*
 5     *  切點,用來匹配需要切入的並增強的目標方法
 6     *  下面的表示com.example.design.proxy.aop包下所有類的所有方法
 7     *   匹配規則很靈活可以自行百度
 8     */
 9     @Pointcut("execution(* com.example.design.proxy.aop.*.*(..))")
10     public void pointCut(){
11 
12     }
13 
14     /*
15     * 在方法執行前開始執行
16     * */
17     @Before("pointCut()")
18     public void beforeAdvice(JoinPoint joinPoint){
19         System.out.println("前置通知開始。。");
20         Signature signature = joinPoint.getSignature();
21         System.out.println("目前代理的是哪一個方法:"+ signature.getName());
22     }
23 
24     /*
25      * 在方法執行後開始執行
26      * */
27     @After("pointCut()")
28     public void afterAdvice(){
29         System.out.println("後置通知開始。。");
30     }
31 
32 
33     @AfterReturning(value = "execution(* com.example.design.proxy.aop.*.*(..))",returning = "args")
34     public void afterReturningAdvice(JoinPoint joinPoint,String args){
35         System.out.println("後置返回通知開始。。");
36         System.out.println("返回值是:"+args);
37     }
38 
39 
40 
41 }

 

寫一個切面類,確定攔截那些目標類,如果是使用動態代理的話,這一步就是挑選委託類和組裝自定義invocation的地方,這裡只是挑選了幾個基本的通知方式,其實還有環繞通知,異常通知等等,對應的是咱們在自定義invocation中方法執行不同位置寫入的增強代碼。

@RestController
@RequestMapping("/aop")
public class AopController {
    @RequestMapping("before")
    public String testBeforeAdvice(){
        return "testBeforeAdvice方法開始執行!";
    }
}
http://localhost:8080/aop/before
前置通知開始。。
目前代理的是哪一個方法:testBeforeAdvice
後置返回通知開始。。
返回值是:testBeforeAdvice方法開始執行!
後置通知開始。。

 全文涉及知識點:

1. 代理模式,包括動態代理,靜態代理。

2. java多態。

3. spring aop對於動態代理的使用。

4. aop在springboot中使用示例。

 

 

練習源碼://github.com/panda-zhu/design

全文借鑒:

Spring AOP實現原理: //blog.csdn.net/moreevan/article/details/11977115/

10分鐘看懂動態代理模式: //www.cnblogs.com/faster/p/10874371.html