Java動態代理設計模式

本文主要介紹Java中兩種常見的動態代理方式:JDK原生動態代理CGLIB動態代理

什麼是代理模式

就是爲其他對象提供一種代理以控制對這個對象的訪問。代理可以在不改動目標對象的基礎上,增加其他額外的功能(擴展功能)。

代理模式角色分爲 3 種:

  • Subject(抽象主題角色):定義代理類和真實主題的公共對外方法,也是代理類代理真實主題的方法;
  • RealSubject(真實主題角色):真正實現業務邏輯的類;
  • Proxy(代理主題角色):用來代理和封裝真實主題;

如果根據字節碼的創建時機來分類,可以分爲靜態代理和動態代理:

  • 所謂靜態也就是在程序運行前就已經存在代理類的字節碼文件,代理類和真實主題角色的關係在運行前就確定了。
  • 而動態代理的源碼是在程序運行期間由JVM根據反射等機制動態的生成,所以在運行前並不存在代理類的字節碼文件

靜態代理

學習動態代理前,有必要來學習一下靜態代理。

靜態代理在使用時,需要定義接口或者父類,被代理對象(目標對象)與代理對象(Proxy)一起實現相同的接口或者是繼承相同父類。

來看一個例子,模擬小貓走路的時間。

// 接口
public interface Walkable {
    void walk();
}

// 實現類
public class Cat implements Walkable {

    @Override
    public void walk() {
        System.out.println("cat is walking...");
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

如果我想知道走路的時間怎麼辦?可以將實現類Cat修改爲:

public class Cat implements Walkable {

    @Override
    public void walk() {
        long start = System.currentTimeMillis();
        System.out.println("cat is walking...");
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("walk time = " + (end - start));
    }
}

這裏已經侵入了源代碼,如果源代碼是不能改動的,這樣寫顯然是不行的,這裏可以引入時間代理類CatTimeProxy

public class CatTimeProxy implements Walkable {
    private Walkable walkable;

    public CatTimeProxy(Walkable walkable) {
        this.walkable = walkable;
    }

    @Override
    public void walk() {
        long start = System.currentTimeMillis();

        walkable.walk();

        long end = System.currentTimeMillis();
        System.out.println("Walk time = " + (end - start));
    }
}

如果這時候還要加上常見的日誌功能,我們還需要創建一個日誌代理類CatLogProxy

public class CatLogProxy implements Walkable {
    private Walkable walkable;

    public CatLogProxy(Walkable walkable) {
        this.walkable = walkable;
    }

    @Override
    public void walk() {
        System.out.println("Cat walk start...");

        walkable.walk();

        System.out.println("Cat walk end...");
        
    }
}

如果我們需要先記錄日誌,再獲取行走時間,可以在調用的地方這麼做:

public static void main(String[] args) {
    Cat cat = new Cat();
    CatLogProxy p1 = new CatLogProxy(cat);
    CatTimeProxy p2 = new CatTimeProxy(p1);

    p2.walk();
}

這樣的話,計時是包括打日誌的時間的。

靜態代理的問題

如果我們需要計算SDK中100個方法的運行時間,同樣的代碼至少需要重複100次,並且創建至少100個代理類。往小了說,如果Cat類有多個方法,我們需要知道其他方法的運行時間,同樣的代碼也至少需要重複多次。因此,靜態代理至少有以下兩個侷限性問題:

  • 如果同時代理多個類,依然會導致類無限制擴展
  • 如果類中有多個方法,同樣的邏輯需要反覆實現

所以,我們需要一個通用的代理類來代理所有的類的所有方法,這就需要用到動態代理技術。

動態代理

學習任何一門技術,一定要問一問自己,這到底有什麼用。其實,在這篇文章的講解過程中,我們已經說出了它的主要用途。你發現沒,使用動態代理我們居然可以在不改變源碼的情況下,直接在方法中插入自定義邏輯。這有點不太符合我們的一條線走到底的編程邏輯,這種編程模型有一個專業名稱叫AOP。所謂的AOP,就像刀一樣,抓住時機,趁機插入。

Jdk動態代理

JDK實現代理只需要使用newProxyInstance方法,但是該方法需要接收三個參數:

@CallerSensitive
    public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {
        Objects.requireNonNull(h);

        final Class<?>[] intfs = interfaces.clone();
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
        }

        /*
         * Look up or generate the designated proxy class.
         */
        Class<?> cl = getProxyClass0(loader, intfs);

        /*
         * Invoke its constructor with the designated invocation handler.
         */
        try {
            if (sm != null) {
                checkNewProxyPermission(Reflection.getCallerClass(), cl);
            }

            final Constructor<?> cons = cl.getConstructor(constructorParams);
            final InvocationHandler ih = h;
            if (!Modifier.isPublic(cl.getModifiers())) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        cons.setAccessible(true);
                        return null;
                    }
                });
            }
            return cons.newInstance(new Object[]{h});
        } catch (IllegalAccessException|InstantiationException e) {
            throw new InternalError(e.toString(), e);
        } catch (InvocationTargetException e) {
            Throwable t = e.getCause();
            if (t instanceof RuntimeException) {
                throw (RuntimeException) t;
            } else {
                throw new InternalError(t.toString(), t);
            }
        } catch (NoSuchMethodException e) {
            throw new InternalError(e.toString(), e);
        }
    }

方法是在Proxy類中是靜態方法,且接收的三個參數依次爲:

  • ClassLoader loader //指定當前目標對象使用類加載器
  • Class<?>[] interfaces //目標對象實現的接口的類型,使用泛型方式確認類型
  • InvocationHandler h //事件處理器

主要是完成InvocationHandler h的編寫工作。

接口類UserService

public interface UserService {

    public void select();

    public void update();
}

接口實現類,即要代理的類UserServiceImpl

public class UserServiceImpl implements UserService {
    @Override
    public void select() {
        System.out.println("查詢 selectById");
    }

    @Override
    public void update() {
        System.out.println("更新 update");
    }
}

代理類UserServiceProxy

public class UserServiceProxy implements UserService {
    private UserService target;

    public UserServiceProxy(UserService target){
        this.target = target;
    }

    @Override
    public void select() {
        before();
        target.select();
        after();
    }

    @Override
    public void update() {
        before();
        target.update();
        after();
    }

    private void before() {     // 在執行方法之前執行
        System.out.println(String.format("log start time [%s] ", new Date()));
    }
    private void after() {      // 在執行方法之後執行
        System.out.println(String.format("log end time [%s] ", new Date()));
    }
}

主程序類:

public class UserServiceProxyJDKMain {
    public static void main(String[] args) {

        // 1. 創建被代理的對象,即UserService的實現類
        UserServiceImpl userServiceImpl = new UserServiceImpl();

        // 2. 獲取對應的classLoader
        ClassLoader classLoader = userServiceImpl.getClass().getClassLoader();

        // 3. 獲取所有接口的Class, 這裏的userServiceImpl只實現了一個接口UserService,
        Class[] interfaces = userServiceImpl.getClass().getInterfaces();

        // 4. 創建一個將傳給代理類的調用請求處理器,處理所有的代理對象上的方法調用
        //     這裏創建的是一個自定義的日誌處理器,須傳入實際的執行對象 userServiceImpl
        InvocationHandler logHandler = new LogHandler(userServiceImpl);

        /*
		   5.根據上面提供的信息,創建代理對象 在這個過程中,
               a.JDK會通過根據傳入的參數信息動態地在內存中創建和.class 文件等同的字節碼
               b.然後根據相應的字節碼轉換成對應的class,
               c.然後調用newInstance()創建代理實例
		 */

        // 會動態生成UserServiceProxy代理類,並且用代理對象實例化LogHandler,調用代理對象的.invoke()方法即可
        UserService proxy = (UserService) Proxy.newProxyInstance(classLoader, interfaces, logHandler);

        // 調用代理的方法
        proxy.select();
        proxy.update();
        
        // 生成class文件的名稱
        ProxyUtils.generateClassFile(userServiceImpl.getClass(), "UserServiceJDKProxy");
    }
}

這裏可以保存下來代理生成的實現了接口的代理對象:

public class ProxyUtils {
    /*
     * 將根據類信息 動態生成的二進制字節碼保存到硬盤中,
     * 默認的是clazz目錄下
     * params :clazz 需要生成動態代理類的類
     * proxyName : 爲動態生成的代理類的名稱
     */
    public static void generateClassFile(Class clazz, String proxyName) {

        //根據類信息和提供的代理類名稱,生成字節碼
        byte[] classFile = ProxyGenerator.generateProxyClass(proxyName, clazz.getInterfaces());
        String paths = clazz.getResource(".").getPath();
        System.out.println(paths);
        FileOutputStream out = null;

        try {
            //保留到硬盤中
            out = new FileOutputStream(paths + proxyName + ".class");
            out.write(classFile);
            out.flush();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

動態代理實現過程

  1. 通過getProxyClass0()生成代理類。JDK生成的最終真正的代理類,它繼承自Proxy並實現了我們定義的接口.
  2. 通過Proxy.newProxyInstance()生成代理類的實例對象,創建對象時傳入InvocationHandler類型的實例。
  3. 調用新實例的方法,即原InvocationHandler類中的invoke()方法。

代理對象不需要實現接口,但是目標對象一定要實現接口,否則不能用動態代理

Cglib動態代理

JDK的動態代理機制只能代理實現了接口的類,而不能實現接口的類就不能實現JDK的動態代理,cglib是針對類來實現代理的,他的原理是對指定的目標類生成一個子類,並覆蓋其中方法實現增強,但因爲採用的是繼承,所以不能對final修飾的類進行代理。

Cglib代理,也叫作子類代理,它是在內存中構建一個子類對象從而實現對目標對象功能的擴展。

Cglib子類代理實現方法:

  1. 需要引入cglibjar文件,但是Spring的核心包中已經包括了Cglib功能,所以直接引入Spring-core.jar即可.
  2. 引入功能包後,就可以在內存中動態構建子類
  3. 代理的類不能爲final,否則報錯
  4. 目標對象的方法如果爲final/static,那麼就不會被攔截,即不會執行目標對象額外的業務方法.

基本使用

<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>2.2</version>
</dependency>

方法攔截器

public class LogInterceptor implements MethodInterceptor{

    /*
     * @param o 要進行增強的對象
     * @param method 要攔截的方法
     * @param objects 參數列表,基本數據類型需要傳入其包裝類
     * @param methodProxy 對方法的代理,
     * @return 執行結果
     * @throws Throwable
     */
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        before();
        Object result = methodProxy.invokeSuper(o, objects);
        after();
        return result;

    }

    private void before() {
        System.out.println(String.format("log start time [%s] ", new Date()));
    }
    private void after() {
        System.out.println(String.format("log end time [%s] ", new Date()));
    }
}

測試用例

這裏保存了代理類的.class文件

public class CglibMain {

    public static void main(String[] args) {
        // 創建Enhancer對象,類似於JDK動態代理的Proxy類
        Enhancer enhancer = new Enhancer();
        // 設置目標類的字節碼文件
        enhancer.setSuperclass(UserDao.class);
        // 設置回調函數
        enhancer.setCallback(new LogInterceptor());
        // create會創建代理類
        UserDao userDao = (UserDao)enhancer.create();
        userDao.update();
        userDao.select();
    }
}

結果

log start time [Mon Nov 30 17:26:39 CST 2020] 
UserDao 更新 update
log end time [Mon Nov 30 17:26:39 CST 2020] 
log start time [Mon Nov 30 17:26:39 CST 2020] 
UserDao 查詢 selectById
log end time [Mon Nov 30 17:26:39 CST 2020] 

JDK動態代理與CGLIB動態代理對比

JDK 動態代理

  • 爲了解決靜態代理中,生成大量的代理類造成的冗餘;
  • JDK 動態代理只需要實現 InvocationHandler 接口,重寫 invoke 方法便可以完成代理的實現,
  • jdk的代理是利用反射生成代理類 Proxyxx.class 代理類字節碼,並生成對象
  • jdk動態代理之所以只能代理接口是因爲代理類本身已經extendsProxy,而java是不允許多重繼承的,但是允許實現多個接口

優點:解決了靜態代理中冗餘的代理實現類問題。

缺點JDK 動態代理是基於接口設計實現的,如果沒有接口,會拋異常。

CGLIB 代理

  • 由於JDK 動態代理限制了只能基於接口設計,而對於沒有接口的情況,JDK方式解決不了;
  • CGLib 採用了非常底層的字節碼技術,其原理是通過字節碼技術爲一個類創建子類,並在子類中採用方法攔截的技術攔截所有父類方法的調用,順勢織入橫切邏輯,來完成動態代理的實現。
  • 實現方式實現 MethodInterceptor 接口,重寫 intercept 方法,通過 Enhancer 類的回調方法來實現。
  • 但是CGLib在創建代理對象時所花費的時間卻比JDK多得多,所以對於單例的對象,因爲無需頻繁創建對象,用CGLib合適,反之,使用JDK方式要更爲合適一些。
  • 同時,由於CGLib由於是採用動態創建子類的方法,對於final方法,無法進行代理。

優點:沒有接口也能實現動態代理,而且採用字節碼增強技術,性能也不錯。

缺點:技術實現相對難理解些。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章