【Java】Java中的動態代理以及在框架中的應用

一、靜態代理&動態代理

1. 靜態代理

我們先假設現在有怎麼一個需求,要求你在不改動原有代碼的情況下在所有類的方法前後打印日誌。我們很容易想到靜態代理,具體做法如下:

  • 爲現有的所有類都編寫一個對應的代理類,並且還需要讓代理類與原有類實現相同的接口;

  • 在創建代理對象時,通過構造器傳入一個目標對象,然後在代理對象的方法內部調用目標對象同名方法,並且在調用方法的前後打印日誌。換而言之,代理對象=增強代碼+原對象。有了代理對象後,我們在客戶端就不再使用源對象,而是使用代理對象了。

靜態代理的缺陷:從上面的靜態代理實現方式上,我們很容易發現靜態代理的缺陷。假設我們現在有很多類,那麼就需要手動去實現很多個代理類,這樣並不現實,那麼我們應該考慮將這個任務交由計算機完成,接下來我們就來討論動態代理的實現。

2. 動態代理

在講解動態代理實現之前,我們先來回顧一下對象的創建過程。

從上面我們可以看出,創建一個對象並不僅僅是寫一行 new 這麼簡單,底層還是隱含了許多信息的。不過我們至少可以瞭解到,一個對象的生成至少經歷了以下這幾個階段:

那麼到這裏我們應該有大概的思路了。我們或許可以不寫代理類,然後通過攔截器得到我們要代理的Class對象,之後再根據它加上反射機制創建代理實例(JDK動態代理的實現);或者讓代理對象的class文件加載進來,然後通過修改其字節碼來生成一個子類從而完成我們要做到的效果(CGLIB動態代理的實現)。

二、動態代理的實現

1. JDK動態代理

JDK動態代理的實現是利用攔截器(這個攔截器需要實現InvocationHandler接口),以及反射機制最終實現一個代理接口的匿名類。

所以在JDK中,提供了java.lang.reflect.InvocationHandler接口,此外還有一個比較重要的類java.lang.reflect.Proxy類。利用這兩個類之間的相互配合完成動態代理的配置。那接下來我們來看代碼實現:

首先定義一個接口和一個實現類:

public interface UserService {
    void addUser(String username, String password);
}

---------------------------------------------------
public class UserServiceImpl implements UserService{
    @Override
    public void addUser(String username, String password) {
        System.out.println("UserService.addUser(String, String)方法被調用");
    }
}

接下來我們就需要去定義一個攔截器去實現InvocationHandler以實現我們的業務邏輯,代碼如下:

public class JDKProxyHandler implements InvocationHandler {

    // 被代理的原對象
    private Object targetObject;

    /**
     * 傳入一個目標對象,生成一個代理對象並返回
     *
     * @param targetObject 原對象(目標對象)
     * @return 代理對象
     */
    public Object newProxy(Object targetObject) {
        this.targetObject = targetObject;
        // 返回一個代理對象
        return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(),
                                      targetObject.getClass().getInterfaces(), 
                                      this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 調用日誌打印
        log("權限校驗中...");
        // 聲明方法的返回值
        Object ret = null;
        // 調用invoke方法,所返回的值賦值給ret
        ret = method.invoke(targetObject, args);
        return ret;
    }

    /**
     * 模擬日誌打印
     *
     * @param message 信息
     */
    private void log(String message) {
        System.out.println("【" + new SimpleDateFormat("yy-MM-dd hh:mm:ss").format(new Date()) + "】" 
                           + message);
    }
}

接下來我們就可以在客戶端進行測試了:

public class Client {
    public static void main(String[] args) {
        JDKProxyHandler jdkProxyHandler = new JDKProxyHandler();
        UserService userService = (UserService) jdkProxyHandler.newProxy(new UserServiceImpl());
        userService.addUser("jo", "8820");
    }
}

最終結果如下:

2. CGLIB動態代理

CGLIB採用了非常底層的字節碼技術,其原理是通過目標類(原來的類)的字節碼創建一個新的子類,並在子類中採用方法攔截的技術攔截所有父類方法的調用,順勢植入增強代碼,所以代理類會將目標類作爲自己的父類併爲其中每個方法創建兩個方法:

  1. 一個是於目標方法簽名相同的類,它在方法中通過調用super來調用目標類中的方法;

  2. 以及另外一個Callback回調方法,它會判斷這個方法是否綁定了攔截器(即實現了MethodInterceptor接口的對象),若存在則將調用intercept方法對目標方法進行代理,也就是在前後加上一些增強邏輯。intercept中就會調用上面介紹的簽名相同的方法。

簡而言之,就是CGLIB底層使用了ASM字節碼處理框架,來修改字節碼並生成新的類。那麼接下來我們就用CGLIB來實現動態代理。

首先接口和實現接口的業務類還是複用上面的代碼,不過我們還需要引入cglib的依賴,如下:

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.1</version>
</dependency>

實現一個代理類處理類:

public class CGLIBProxyHandler implements MethodInterceptor {

    // CGLIB 需要代理的目標對象
    private Object targetObject;

    /**
     * 創建一個代理對象
     *
     * @param targetObject 目標類
     * @return 代理對象
     */
    public Object crateProxyObject(Object targetObject) {
        this.targetObject = targetObject;
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(targetObject.getClass());
        enhancer.setCallback(this);
        Object proxyObj = enhancer.create();
        return proxyObj;
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        Object ret = null;
        // 過濾方法
        if ("addUser".equals(method.getName())) {
            // 打印日誌
            log("權限檢驗中...");
        }
        ret = method.invoke(targetObject, objects);
        return ret;
    }

    /**
     * 模擬日誌打印
     *
     * @param message 信息
     */
    private void log(String message) {
        System.out.println("【" + new SimpleDateFormat("yy-MM-dd hh:mm:ss").format(new Date()) + "】" + message);
    }
}

客戶端調用:

public class Client {
    public static void main(String[] args) {
        CGLIBProxyHandler cglibProxyHandler = new CGLIBProxyHandler();
        UserService userService = (UserService) cglibProxyHandler.crateProxyObject(new UserServiceImpl());
        userService.addUser("jo", "8820");
    }
}

最終執行結果如下:

以上就是JDK以及CGLIB兩種實現動態代理方式的演示了。

三、CGLIB和JDK兩種動態代理的應用與區別

1. 兩者間區別

其中最主要的區別莫過於JDK是針對接口類生成代理,而不是針對類。而CGLIB則是針對類實現的動態代理。除此之外,上面我們提到CGLIB實現是通過目標類的字節碼生成一個子類,所以我們可以很明顯知道,這種方式不能適用與被final修飾的類。

2. Spring中的動態代理

2.1 Spring何時使用JDK/CGLIB實現AOP

  • 如果目標對象實現了接口,默認情況下Spring會採用JDK的動態代理實現AOP(不過可以通過配置強制使用CGLIB實現);
  • 如果目標對象沒有實現接口,那麼Spring就只會採用CGLIB庫來完成動態代理。

2.2 如何強制使用CGLIB

  1. 添加CGLIB庫的引用(aspectjrt-xxx.jar、aspectjweaver-xxx.jar、cglib-nodep-xxx.jar);

  2. 在Spring配置文件中加入<aop:aspectj-autoproxy proxy-target-class="true"/>

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