【趣味設計模式系列】之【代理模式1--基本原理、實戰及框架應用】

小豬爸爸發表於2020-08-09

1. 簡介

代理模式(Proxy Pattern):為其他物件提供一種代理以控制對這個物件的訪問。簡而言之,既能使被代理物件無入侵,又能附加代理自己的操作,使方法增強功能

2. 圖解

水果店代理銷售海南芝麻蕉,此外還銷售蘋果、橘子等其他水果。

代理的主要實現技術與方法如下圖所示,本篇主要講靜態代理與動態代理的主要實現方式,原理部分的深入,以及ASM位元組碼技術,將放到後續篇幅講解。

3. 案例實現

下面分多個版本,通過逐步演進的方式,講解代理模式,其中版本1到版本6為靜態代理的逐步演進過程,版本7-9為JDK動態代理內容,AspjectJ靜態代理與Cglib動態代理單獨演示。

3.1 版本v1

假設水果店有待銷蘋果,並參與秒殺活動,定義Apple類,待銷水果介面Sellable,介面秒殺方法secKill(),程式碼如下:

package com.wzj.proxy.v1;

/**
 * @Author: wzj
 * @Date: 2020/8/3 10:29
 * @Desc: 蘋果
 */
public class Apple implements Sellalbe {

    @Override
    public void secKill() {
        System.out.println("蘋果正在秒殺中...");
        try {
            Thread.sleep(new Random().nextInt(3000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

package com.wzj.proxy.v1;

/**
 * @Author: wzj
 * @Date: 2020/8/3 10:56
 * @Desc: 待銷水果
 */
public interface Sellalbe {
    /**
     * 秒殺
     */
    public void secKill();
}

現有個問題1,如需記錄秒殺時間,該如何修改程式碼呢?

3.2 版本v2

針對問題1,直接修改秒殺方法

package com.wzj.proxy.v2;

import java.util.Random;

/**
 * @Author: wzj
 * @Date: 2020/8/3 10:29
 * @Desc: 待銷蘋果
 * 問題1:記錄蘋果秒殺的具體時間
 * 最簡單的做法就是修改程式碼
 */
public class Apple implements Sellalbe {

    @Override
    public void secKill() {
        Long start = System.currentTimeMillis();
        System.out.println("蘋果正在秒殺中...");
        try {
            Thread.sleep(new Random().nextInt(3000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Long end = System.currentTimeMillis();

        System.out.println("記錄秒殺時間為:" + (end - start));
    }
}

測試程式碼如下

package com.wzj.proxy.v2;

/**
 * @Author: wzj
 * @Date: 2020/8/3 15:13
 * @Desc:
 */
public class Test2 {
    public static void main(String[] args) {
        new Apple().secKill();
    }
}

結果:

蘋果正在秒殺中...
記錄秒殺時間為:2944

現在遇上問題2,如果如法改變方法原始碼呢,對原始碼無入侵,該如何做呢?

3.3 版本v3

針對問題2,很容易想到,用繼承解決。新增一個類Apple2,從Apple繼承。

package com.wzj.proxy.v3;

/**
 * @Author: wzj
 * @Date: 2020/8/3 15:25
 * @Desc: 繼承實現
 */
public class Apple2 extends Apple {

    @Override
    public void secKill() {
        long start = System.currentTimeMillis();
        super.secKill();
        long end = System.currentTimeMillis();
        System.out.println("記錄秒殺時間為:" + (end - start));
    }

}

測試類:

package com.wzj.proxy.v3;

/**
 * @Author: wzj
 * @Date: 2020/8/3 15:13
 * @Desc:
 */
public class Test3 {
    public static void main(String[] args) {
        new Apple2().secKill();
    }
}

結果:

蘋果正在秒殺中...
記錄秒殺時間為:1660

需求總是變化不斷,問題3又來了,如果需要記錄秒殺開始前與開始後的日誌,該如何修改呢?可能想到設計一個日誌類Apple3,從Apple類繼承;
如果需求變化了,先要記錄日誌,再記錄秒殺時間,是不是要繼續設計一個先日誌後時間的類Apple4,從Apple3繼承?
如果需求又變了,先記錄秒殺時間,再記錄秒殺前後日誌,是不是又得重新設計Apple5,從Apple4繼承?
如果需求再次變化,需要先記錄許可權,然後記錄秒殺時間,最後秒殺日誌,發現需要一直不停的設計新的類,並繼承於各種派生出來的類,容易產生類爆炸,且難以複用,不靈活,該如何解決呢?

3.4 版本v4

出現問題3的根源是,繼承破壞封裝,集合框架的創始人Joshua Bloch,在其著作《effective java》一書時,指出複合優於繼承這一原則,很好的解決了繼承帶來的脆弱性。針對問題3,設計一個類記錄秒殺時間的代理類AppleTimeProxy

package com.wzj.proxy.v4;

/**
 * @Author: wzj
 * @Date: 2020/8/3 15:46
 * @Desc: 記錄時間的代理
 * 組合優於繼承
 */
public class AppleTimeProxy implements Sellalbe{

    Apple apple;

    public AppleTimeProxy(Apple apple) {
        this.apple = apple;
    }

    @Override
    public void secKill() {
        long start = System.currentTimeMillis();
        apple.secKill();
        long end = System.currentTimeMillis();
        System.out.println("記錄秒殺時間為:" + (end - start));
    }
}

3.5 版本v5

針對問題3,如果要代理各種型別,比如代理記錄秒殺時間,代理記錄日誌,每個類只需要單一代理各自的功能,只需要增加程式碼如下:

package com.wzj.proxy.v5;

/**
 * @Author: wzj
 * @Date: 2020/8/3 21:39
 * @Desc: 記錄日誌的代理類
 */
public class AppleLogProxy implements Sellalbe {

    Apple apple;

    public AppleLogProxy(Apple apple) {
        this.apple = apple;
    }

    @Override
    public void secKill() {
        System.out.println("秒殺開始...");
        apple.secKill();
        System.out.println("秒殺結束...");
    }
}

package com.wzj.proxy.v5;

/**
 * @Author: wzj
 * @Date: 2020/8/3 15:46
 * @Desc: 記錄時間的代理
 * 組合優於繼承
 */
public class AppleTimeProxy implements Sellalbe {

    Apple apple;

    public AppleTimeProxy(Apple apple) {
        this.apple = apple;
    }

    @Override
    public void secKill() {
        long start = System.currentTimeMillis();
        apple.secKill();
        long end = System.currentTimeMillis();
        System.out.println("記錄秒殺時間為:" + (end - start));
    }
}

3.6 版本v6

進一步優化上述程式碼,將組合進來的型別Apple,改造成介面sellable

package com.wzj.proxy.v6;

/**
 * @Author: wzj
 * @Date: 2020/8/3 21:39
 * @Desc: 記錄日誌的代理類
 */
public class AppleLogProxy implements Sellalbe {

    Sellalbe sellalbe;

    public AppleLogProxy(Sellalbe sellalbe) {
        this.sellalbe = sellalbe;
    }

    @Override
    public void secKill() {
        System.out.println("秒殺開始...");
        sellalbe.secKill();
        System.out.println("秒殺結束...");
    }
}

package com.wzj.proxy.v6;

/**
 * @Author: wzj
 * @Date: 2020/8/3 15:46
 * @Desc: 記錄時間的代理
 * 組合優於繼承
 */
public class AppleTimeProxy implements Sellalbe {

    Sellalbe sellalbe;

    public AppleTimeProxy(Sellalbe sellalbe) {
        this.sellalbe = sellalbe;
    }

    @Override
    public void secKill() {
        long start = System.currentTimeMillis();
        sellalbe.secKill();
        long end = System.currentTimeMillis();
        System.out.println("記錄秒殺時間為:" + (end - start));
    }
}

這樣做可以達到,在不增加類的情況下,可以實現自由巢狀先記錄時間,後列印日誌,或者先列印日誌,後記錄時間,可以在客戶端發起,不用修改原始碼。測試程式碼如下:

package com.wzj.proxy.v6;

/**
 * @Author: wzj
 * @Date: 2020/8/3 21:44
 * @Desc: 如何實現代理的各種組合?繼承?Decorator?
 *  代理的物件改成Sellable型別-越來越像decorator了,為了實現巢狀
 */
public class Test6 {
    public static void main(String[] args) {
        //先記錄時間,後列印日誌
        new AppleLogProxy(new AppleTimeProxy(new Apple())).secKill();
        System.out.println("===========================");
        //先列印日誌,後記錄時間
        new AppleTimeProxy(new AppleLogProxy(new Apple())).secKill();
    }
}

結果如下:

秒殺開始...
蘋果正在秒殺中...
記錄秒殺時間為:2047
秒殺結束...
===========================
秒殺開始...
蘋果正在秒殺中...
秒殺結束...
記錄秒殺時間為:2099

針對問題3的需求,如果要增加記錄許可權的功能,或者是許可權與記錄時間的功能組合,或者許可權與記錄日誌的功能組合,或者許可權、時間、日誌三個功能按照任意順序的組合,都不需要像v3版本那樣,每個需求的變更,都需要設計新的類,各類之間冗餘度高,且臃腫。直接設計一個單獨許可權的代理類,在客戶端做各種巢狀呼叫即可。

3.7 Aspject實現靜態代理

第一種方式為配置實現,在maven中加入Aspject相關依賴

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.4</version>
        </dependency>

目標物件

package com.wzj.spring.v1;

import com.wzj.proxy.v9.Sellalbe;

import java.util.Random;

/**
 * @Author: wzj
 * @Date: 2020/8/3 10:29
 * @Desc: 待銷蘋果
 */
public class Apple {

    public void secKill() {
        System.out.println("蘋果正在秒殺中...");
        try {
            Thread.sleep(new Random().nextInt(3000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

切面類

package com.wzj.spring.v1;

import com.wzj.proxy.v6.Sellalbe;

import java.lang.reflect.Method;

/**
 * @Author: wzj
 * @Date: 2020/8/3 15:46
 * @Desc: 記錄時間的代理
 */
public class AppleTimeProxy {

    private void after() {
        System.out.println("方法執行結束時間:" + System.currentTimeMillis());
    }

    private void before() {
        System.out.println("方法執行開始時間:" + System.currentTimeMillis());
    }
}

app.xml配置檔案

    <bean id="tank" class="com.mashibing.dp.spring.v1.Tank"/>
    <bean id="timeProxy" class="com.mashibing.dp.spring.v1.TimeProxy"/>

    <aop:config>
        <aop:aspect id="time" ref="timeProxy">
            <aop:pointcut id="onmove" expression="execution(void com.mashibing.dp.spring.v1.Tank.move())"/>
            <aop:before method="before" pointcut-ref="onmove"/>
            <aop:after method="after" pointcut-ref="onmove"/>
        </aop:aspect>
    </aop:config>

測試類

package com.wzj.spring.v1;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * @Author: wzj
 * @Date: 2020/8/5 15:09
 * @Desc:
 */
public class TestAOP {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("app.xml");
        Apple apple = (Apple)context.getBean("apple");
        apple.secKill();
    }
}

第二種方式為註解實現,切面類如下

package com.wzj.spring.v2;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

/**
 * @Author: wzj
 * @Date: 2020/8/3 15:46
 * @Desc: 記錄時間的代理
 */
@Aspect
public class AppleTimeProxy {

    @Before("execution (void com.wzj.spring.v2.Apple.secKill())")
    private void after() {
        System.out.println("方法執行結束時間:" + System.currentTimeMillis());
    }

    @After("execution (void com.wzj.spring.v2.Apple.secKill())")
    private void before() {
        System.out.println("方法執行開始時間:" + System.currentTimeMillis());
    }
}

配置類app-auto.xml

   <aop:aspectj-autoproxy/>
    
    <bean id="apple" class="com.wzj.spring.v2.Apple"/>
    <bean id="timeProxy" class="com.wzj.spring.v2.AppleTimeProxy"/>

測試類

package com.wzj.spring.v2;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * @Author: wzj
 * @Date: 2020/8/5 15:09
 * @Desc:
 */
public class TestAOP {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("app-auto.xml");
        Apple apple = (Apple)context.getBean("apple");
        apple.secKill();
    }
}

結果

方法執行開始時間:1596979946478
蘋果正在秒殺中...
方法執行結束時間:1596979948708

3.8 靜態代理與動態代理

3.8.1 靜態代理

以上版本都是基於靜態代理的實現,代理類由程式設計師編寫原始碼,再編譯成位元組碼檔案,在編譯期間生成,靜態代理類圖

缺點

  • 每一個代理類只能服務於一種型別的物件,比如上面的水果店例子,只能代理Sellable型別的物件,如果該水果店除了賣水果,門口還有小孩玩的投幣玩具車,新增一個Player介面,那麼需要新增代理類;如果一個系統有100多個類需要通過代理來增強功能,程式規模龐大時無法勝任;
  • 如果介面增加方法,實現類與代理類都需要增加,程式碼維護複雜性增加。

3.8.2 動態代理

代理物件在執行期間動態生成,可以代理任何物件任何方法
優點

  • 代理物件在執行期間生成,原始碼量大大減少;
  • 介面中的所有方法都被轉移到一個集中的方法中處理(例如JDK動態代理的InvocationHandlerinvoke()方法)。

3.8.3 實現方式

3.8.3.1 JDK實現

實現步驟如下:

  • 通過實現InvocationHandlet介面,並實現invoke()方法建立自己的呼叫處理器;
  • 通過Proxy.newProxyInstance方法建立代理物件,該方法有三個引數,分別為被代理類的類載入器、被代理類介面、以及InvocationHandlet的實現類;
    程式碼實現版本V7如下
package com.wzj.proxy.v7;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
 * @Author: wzj
 * @Date: 2020/8/4 8:45
 * @Desc: 通過實現InvocationHandlet介面建立自己的呼叫處理器
 */
public class LogHandler implements InvocationHandler {

    Apple apple;

    public LogHandler(Apple apple) {
        this.apple = apple;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("方法執行日誌開始,方法名為:" + method.getName());
        Object o = method.invoke(apple, args);
        System.out.println("方法執行日誌結束,方法名為:" + method.getName());

        return o;
    }
}

package com.wzj.proxy.v7;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

/**
 * @Author: wzj
 * @Date: 2020/8/4 8:55
 * @Desc: JDK實現動態代理
 */
public class ProxyGenerator {
    public Object createProxy(Apple apple, InvocationHandler handler) {
        Object o = Proxy.newProxyInstance(apple.getClass().getClassLoader(),
                apple.getClass().getInterfaces(), handler);
        return o;
    }
}

測試類

package com.wzj.proxy.v7;

import com.wzj.proxy.v6.AppleLogProxy;
import com.wzj.proxy.v6.AppleTimeProxy;

import java.lang.reflect.InvocationHandler;

/**
 * @Author: wzj
 * @Date: 2020/8/3 21:44
 * @Desc: 如果想讓LogProxy可以重用,不僅可以代理Apple,還可以代理任何其他可以代理的型別 Object
 *  (畢竟日誌記錄,時間計算是很多方法都需要的東西),這時該怎麼做呢?
 *  分離代理行為與被代理物件
 *  使用jdk的動態代理
 */
public class Test7 {
    public static void main(String[] args) {
        Apple apple = new Apple();
        InvocationHandler handler = new LogHandler(apple);
        Sellalbe sellalbe = (Sellalbe) new ProxyGenerator().createProxy(apple, handler);
        sellalbe.secKill();
    }
}

結果:

方法執行日誌開始,方法名為:secKill
蘋果正在秒殺中...
方法執行日誌結束,方法名為:secKill

針對版本7,如果現在有個橘子類Orange,實現了打折介面Discount,也需要在打折介面前後列印日誌,如何使用動態代理呢,畢竟列印日誌是通用功能,
該如何修改程式碼呢?

版本8,通過泛型實現,程式碼如下

package com.wzj.proxy.v8;

/**
 * @Author: wzj
 * @Date: 2020/8/4 17:45
 * @Desc: 待銷橘子
 */
public class Orange implements Discount {

    /**
     * 打折優惠
     */
    @Override
    public int calculateBySourcePrice(int price) {
        int i= 9;
        System.out.println("橘子打折優惠, 一律9元");
        return i;
    }
}

package com.wzj.proxy.v8;

/**
 * @Author: wzj
 * @Date: 2020/8/5 20:56
 * @Desc: 折扣優惠介面
 */
public interface Discount {
    public int calculateBySourcePrice(int price);
}

package com.wzj.proxy.v8;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
 * @Author: wzj
 * @Date: 2020/8/4 8:45
 * @Desc: 使用泛型,代理任何物件的任何行為
 */
public class LogHandler<T> implements InvocationHandler {

    T t;

    public LogHandler(T t) {
        this.t = t;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before(method);
        Object o = method.invoke(t, args);
        after(method);
        return o;
    }

    private void after(Method method) {
        System.out.println("方法執行日誌結束,方法名為:" + method.getName());
    }

    private void before(Method method) {
        System.out.println("方法執行日誌開始,方法名為:" + method.getName());
    }
}

package com.wzj.proxy.v8;

import com.wzj.proxy.v8.LogHandler;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

/**
 * @Author: wzj
 * @Date: 2020/8/4 8:55
 * @Desc: 使用泛型,代理任何類的任何介面
 */
public class ProxyGenerator<T> {
    public Object createProxy(T t, InvocationHandler handler) {
        Object o = Proxy.newProxyInstance(t.getClass().getClassLoader(),
                t.getClass().getInterfaces(), handler);
        return o;
    }
}

測試類:

package com.wzj.proxy.v8;


import java.lang.reflect.InvocationHandler;

/**
 * @Author: wzj
 * @Date: 2020/8/3 21:44
 * @Desc: 如果想讓LogProxy可以重用,不僅可以代理Tank,還可以代理任何其他可以代理的型別 Object
 *  (畢竟日誌記錄,時間計算是很多方法都需要的東西),這時該怎麼做呢?
 *  分離代理行為與被代理物件
 *  使用jdk的動態代理
 *
 *  增加泛型,既可以代理蘋果秒殺的介面,也可以代理橘子打折介面,實現任何物件任何介面的代理
 */
public class Test8 {
    public static void main(String[] args) {
        Apple apple = new Apple();
        InvocationHandler appHandler = new LogHandler(apple);
        Sellalbe sellalbe = (Sellalbe) new ProxyGenerator().createProxy(apple, appHandler);
        sellalbe.secKill();

        System.out.println("=================================");

        Orange orange = new Orange();
        InvocationHandler orgHandler = new LogHandler(orange);
        Discount discount = (Discount) new ProxyGenerator().createProxy(orange, orgHandler);
        discount.calculateBySourcePrice(10);
    }
}

結果:

方法執行日誌開始,方法名為:secKill
蘋果正在秒殺中...
方法執行日誌結束,方法名為:secKill
=================================
方法執行日誌開始,方法名為:calculateBySourcePrice
橘子打折優惠, 一律9元
方法執行日誌結束,方法名為:calculateBySourcePrice

有人會有疑問,invoke()方法並沒有顯示呼叫,為何會執行呢?下面的版本9將代理類生成出來就會揭曉答案。

版本9

package com.wzj.proxy.v9;

/**
 * @Author: wzj
 * @Date: 2020/8/4 21:23
 * @Desc: 列印生成的代理
 */
public class Test9 {
    public static void main(String[] args) {
        Apple apple = new Apple();
        //在專案目錄下生成代理類
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
        LogHandler handler = new LogHandler(apple);
        Sellalbe sellalbe = (Sellalbe) new ProxyGenerator().createProxy(apple, handler);
        sellalbe.secKill();
    }
}

代理類如圖:

發現生成了一個$Proxy0的類,

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.sun.proxy;

import com.wzj.proxy.v9.Sellalbe;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements Sellalbe {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m0;

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final void secKill() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m3 = Class.forName("com.wzj.proxy.v9.Sellalbe").getMethod("secKill");
            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());
        }
    }
}

進入該類,在呼叫secKill()方法裡面有這麼一段程式碼super.h.invoke(this, m3, (Object[])null),顯然是在代理類呼叫secKill()方法時,裡面呼叫了自定義的handlerinvoke()方法。下圖來通過除錯驗證一下

顯然h是自定義的LogHandler

3.8.3.2 Cglib實現

圖解如下:

程式碼實現:

package com.wzj.cglib;

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**
 * @Author: wzj
 * @Date: 2020/8/7 11:09
 * @Desc:
 */
public class LogMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        //代理類的父類資訊
        System.out.println("代理類的父類資訊:" + o.getClass().getSuperclass().getName());
        //前置增強
        before(method);
        Object result = null;
        //呼叫被代理物件的方法
        result = methodProxy.invokeSuper(o, objects);
        //後置增強
        after(method);
        return result;


    }

    private void after(Method method) {
        System.out.println("方法執行日誌結束,方法名為:" + method.getName());
    }

    private void before(Method method) {
        System.out.println("方法執行日誌開始,方法名為:" + method.getName());
    }


}

package com.wzj.cglib;

import net.sf.cglib.proxy.Enhancer;

/**
 * @Author: wzj
 * @Date: 2020/8/7 15:13
 * @Desc:
 */
public class CglibProxy<T> {
    public T createProxy(T t) {
        //增強器
        Enhancer enhancer = new Enhancer();
        //設定父類
        enhancer.setSuperclass(t.getClass());
        //設定回撥的攔截器
        enhancer.setCallback(new LogMethodInterceptor());
        T proxy = (T) enhancer.create();
        return proxy;
    }
}

package com.wzj.cglib;


import java.util.Random;

/**
 * @Author: wzj
 * @Date: 2020/8/3 10:29
 * @Desc: 待銷蘋果
 */
public class Apple  {

    public void secKill() {
        System.out.println("蘋果正在秒殺中...");
        try {
            Thread.sleep(new Random().nextInt(3000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

測試程式碼:

package com.wzj.cglib;

import net.sf.cglib.core.DebuggingClassWriter;
import net.sf.cglib.proxy.Enhancer;

/**
 * @Author: wzj
 * @Date: 2020/8/7 14:16
 * @Desc:
 */
public class CglibTest {
    public static void main(String[] args) {
        //列印代理類
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, (String)System.getProperties().get("user.dir"));
        Apple apple = new Apple();
        Apple proxy = (Apple) new CglibProxy().createProxy(apple);
        proxy.secKill();
    }
}

結果:

代理類的父類資訊:com.wzj.cglib.Apple
方法執行日誌開始,方法名為:secKill
蘋果正在秒殺中...
方法執行日誌結束,方法名為:secKill

同樣,代理類的資訊會列印在工程所在目錄下,如圖:

這裡會有三個類的生成,只有一個是代理類,繼承Apple,其他兩個類繼承FastClass,其中一個class為生成的代理類中的每個方法建立了索引,另外一個則為我們被代理類的所有方法包含其父類的方法建立了索引。

public class Apple$$EnhancerByCGLIB$$3a0529f7 extends Apple implements Factory 
public class Apple$$EnhancerByCGLIB$$3a0529f7$$FastClassByCGLIB$$17333818 extends FastClass
public class Apple$$FastClassByCGLIB$$ca2d2eb9 extends FastClass
3.8.3.3 JDK與Cglib實現動態代理時的區別
  • JDK動態代理的被代理類必須要實現介面。

  • cglib動態代理是通過繼承被代理類來實現,如果被代理類為final字所修飾的非protect與public類,則沒法代理。

3.8.3.4 JDK與Cglib效能比較

效能測試設計瞭如下5個類JdkDynamicProxyTestCglibProxyTestTargetTargetImplProxyPerformanceTest

package com.wzj.proxy.v10;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * @Author: wzj
 * @Date: 2020/8/7 21:58
 * @Desc: JDK動態代理
 */
public class JdkDynamicProxyTest implements InvocationHandler {

    private Target target;

    private JdkDynamicProxyTest(Target target) {
        this.target = target;
    }

    public static Target newProxyInstance(Target target) {
        return (Target) Proxy.newProxyInstance(JdkDynamicProxyTest.class.getClassLoader(),
                new Class<?>[]{Target.class},
                new JdkDynamicProxyTest(target));

    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return method.invoke(target, args);
    }
}

package com.wzj.proxy.v10;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**
 * @Author: wzj
 * @Date: 2020/8/7 22:02
 * @Desc: Cglib代理測試
 */
public class CglibProxyTest implements MethodInterceptor {

    private CglibProxyTest() {

    }

    public static <T extends Target> Target newProxyInstance(Class<T> targetInstanceClazz) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(targetInstanceClazz);
        enhancer.setCallback(new CglibProxyTest());
        return (Target) enhancer.create();
    }
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        return methodProxy.invokeSuper(o, objects);
    }
}

package com.wzj.proxy.v10;

/**
 * @Author: wzj
 * @Date: 2020/8/7 21:54
 * @Desc: 被代理類
 */
public interface Target {

    int test(int i);

}

package com.wzj.proxy.v10;

/**
 * @Author: wzj
 * @Date: 2020/8/7 21:57
 * @Desc:
 */
public class TargetImpl implements Target {

    @Override
    public int test(int i) {
        return i + 1;
    }
}

package com.wzj.proxy.v10;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @Author: wzj
 * @Date: 2020/8/7 21:53
 * @Desc: 代理效能測試
 */
public class ProxyPerformanceTest {
    public static void main(String[] args) {
        //建立測試物件
        Target nativeTest = new TargetImpl();
        Target dynamicProxy = JdkDynamicProxyTest.newProxyInstance(nativeTest);
        Target cglibProxy = CglibProxyTest.newProxyInstance(TargetImpl.class);

        //預熱一下
        int preRunCount = 10000;
        runWithoutMonitor(nativeTest, preRunCount);
        runWithoutMonitor(cglibProxy, preRunCount);
        runWithoutMonitor(dynamicProxy, preRunCount);

        //執行測試
        Map<String, Target> tests = new LinkedHashMap<String, Target>();
        tests.put("Native   ", nativeTest);
        tests.put("Dynamic  ", dynamicProxy);
        tests.put("Cglib    ", cglibProxy);
        int repeatCount = 3;
        int runCount = 1000000;
        runTest(repeatCount, runCount, tests);
        runCount = 50000000;
        runTest(repeatCount, runCount, tests);
    }


    private static void runTest(int repeatCount, int runCount, Map<String, Target> tests) {
        System.out.println(
                String.format("\n===== run test : [repeatCount=%s] [runCount=%s] [java.version=%s] =====",
                        repeatCount, runCount, System.getProperty("java.version")));
        for (int i = 0; i < repeatCount; i++) {
            System.out.println(String.format("\n--------- test : [%s] ---------", (i + 1)));
            for (String key : tests.keySet()) {
                runWithMonitor(tests.get(key), runCount, key);
            }
        }
    }

    private static void runWithoutMonitor(Target target, int runCount) {
        for (int i = 0; i < runCount; i++) {
            target.test(i);
        }
    }

    private static void runWithMonitor(Target target, int runCount, String tag) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < runCount; i++) {
            target.test(i);
        }
        long end = System.currentTimeMillis();
        System.out.println("[" + tag + "] Total Time:" + (end - start) + "ms");
    }
}

測試結果:
JDK1.6.0_43

===== run test : [repeatCount=3] [runCount=1000000] [java.version=1.6.0_43] =====

--------- test : [1] ---------
[Native   ] Total Time:4ms
[Dynamic  ] Total Time:57ms
[Cglib    ] Total Time:59ms

--------- test : [2] ---------
[Native   ] Total Time:7ms
[Dynamic  ] Total Time:34ms
[Cglib    ] Total Time:40ms

--------- test : [3] ---------
[Native   ] Total Time:6ms
[Dynamic  ] Total Time:27ms
[Cglib    ] Total Time:42ms

===== run test : [repeatCount=3] [runCount=50000000] [java.version=1.6.0_43] =====

--------- test : [1] ---------
[Native   ] Total Time:358ms
[Dynamic  ] Total Time:985ms
[Cglib    ] Total Time:1340ms

--------- test : [2] ---------
[Native   ] Total Time:230ms
[Dynamic  ] Total Time:564ms
[Cglib    ] Total Time:817ms

--------- test : [3] ---------
[Native   ] Total Time:177ms
[Dynamic  ] Total Time:404ms
[Cglib    ] Total Time:726ms

JDK1.7.0_79

===== run test : [repeatCount=3] [runCount=1000000] [java.version=1.7.0_79] =====

--------- test : [1] ---------
[Native   ] Total Time:0ms
[Dynamic  ] Total Time:40ms
[Cglib    ] Total Time:59ms

--------- test : [2] ---------
[Native   ] Total Time:10ms
[Dynamic  ] Total Time:10ms
[Cglib    ] Total Time:70ms

--------- test : [3] ---------
[Native   ] Total Time:0ms
[Dynamic  ] Total Time:30ms
[Cglib    ] Total Time:50ms

===== run test : [repeatCount=3] [runCount=50000000] [java.version=1.7.0_79] =====

--------- test : [1] ---------
[Native   ] Total Time:500ms
[Dynamic  ] Total Time:933ms
[Cglib    ] Total Time:1490ms

--------- test : [2] ---------
[Native   ] Total Time:262ms
[Dynamic  ] Total Time:595ms
[Cglib    ] Total Time:781ms

--------- test : [3] ---------
[Native   ] Total Time:172ms
[Dynamic  ] Total Time:406ms
[Cglib    ] Total Time:693ms

JDK1.8.0_161

===== run test : [repeatCount=3] [runCount=1000000] [java.version=1.8.0_161] =====

--------- test : [1] ---------
[Native   ] Total Time:6ms
[Dynamic  ] Total Time:40ms
[Cglib    ] Total Time:94ms

--------- test : [2] ---------
[Native   ] Total Time:5ms
[Dynamic  ] Total Time:23ms
[Cglib    ] Total Time:30ms

--------- test : [3] ---------
[Native   ] Total Time:11ms
[Dynamic  ] Total Time:20ms
[Cglib    ] Total Time:28ms

===== run test : [repeatCount=3] [runCount=50000000] [java.version=1.8.0_161] =====

--------- test : [1] ---------
[Native   ] Total Time:273ms
[Dynamic  ] Total Time:990ms
[Cglib    ] Total Time:1419ms

--------- test : [2] ---------
[Native   ] Total Time:241ms
[Dynamic  ] Total Time:562ms
[Cglib    ] Total Time:851ms

--------- test : [3] ---------
[Native   ] Total Time:210ms
[Dynamic  ] Total Time:551ms
[Cglib    ] Total Time:855ms

筆者機器cpu是英特爾酷睿I5,4核,從測試結果看出,JDK動態代理的速度已經比CGLib動態代理的速度要稍微快一點,並不像一些資料或部落格所說的那樣,cglib比jdk快幾倍。

4. Spring框架中的動態代理原始碼解析

筆者用的Spring版本是5.1.6,其中有個建立代理的類為DefaultAopProxyFactory,部分原始碼如下

public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {

	@Override
	public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
                //config.isOptimize()   是否對代理類的生成使用策略優化 其作用是和isProxyTargetClass是一樣的 預設為false
                //config.isProxyTargetClass() 是否使用Cglib的方式建立代理物件 預設為false
                //hasNoUserSuppliedProxyInterfaces目標類是否有介面存在 且只有一個介面的時候介面型別不是
                //SpringProxy型別
		if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
                        //上面的三個方法有一個為true的話,則進入到這裡
			Class<?> targetClass = config.getTargetClass();
			if (targetClass == null) {
				throw new AopConfigException("TargetSource cannot determine target class: " +
						"Either an interface or a target is required for proxy creation.");
			}
                        //判斷目標類是否是介面,如果目標類是介面的話,則使用JDK的方式生成代理物件
                        //如果目標類是Proxy型別,則還是使用JDK的方式生成代理物件
			if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
				return new JdkDynamicAopProxy(config);
			}
                        //配置了使用Cglib進行動態代理  或者目標類沒有介面 那麼使用Cglib的方式建立代理物件
			return new ObjenesisCglibAopProxy(config);
		}
		else {
                        //上面的三個方法沒有一個為true 那使用JDK的提供的代理方式生成代理物件
			return new JdkDynamicAopProxy(config);
		}
	}

可見SpringAOP底層原始碼在實現時採用了JDK與Cglib兩種動態代理方式

5. 總結

本篇主要講了代理的好處,即對被代理類無入侵,同時又可以使原目標類功能增強;接著講述了靜態代理的迭代演進過程,以及靜態代理與動態代理的主要技術實現、與區別,最後通過Spring原始碼分析了底層所用的代理技術,後續將深入原始碼分析動態代理是如何一步一步生成代理類的整個過程,敬請期待。


附:githup原始碼下載地址:https://github.com/wuzhujun2006/design-patterns

相關文章