【設計模式】代理模式

胖達利亞發表於2021-10-17

概述:

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中使用示例。

 

 

練習原始碼:https://github.com/panda-zhu/design

全文借鑑:

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

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

 

相關文章