動態代理詳解

  • 摘要

本文動態代理得意義、主要介紹動態代理得實現原理以及由動態代理引申出來的一些知識點。

  • 插曲

最近在研究javamelody實現的原理,發現他對JDBC的監控就是通過動態代理實現的。由於之前對於動態代理只是大概知道怎麼回事,沒有細緻的去研究,所以上網百度了一下。發現網上的東西要麼注重原理而忽略應用場景導致空泛、要麼注重場景而忽略原理、要麼就是隻有基於接口的動態代理而沒有基於cglib的。因此這裏本文儘量做到大而全。其實想總結一下的原因是公司進行代碼review的時候,老大提出同一個類中一個方法調用本類其他方法,其他方法的事務不會生效,本質上我是持懷疑態度的。當時我是出於基於Cglib代理的角度考慮,而實際不會生效是基於動態代理的方式,採用cglib還是會生效,後面會講到。本人作文比較推崇簡約易懂的方式,儘量避免過於斯文的名詞出現。

  • 一、動態代理的意義

 首先明白一點,動態代理就是用來生成代理對象的。我們知道傳統的代理模式,通常是先定義一個代理類,該代理類需要持有目標對象(也有叫被代理對象,我覺得都行吧)。假設我們有1000個不同的目標對象(這1000個對象不是同一個類),那麼我們需要預先定義1000個代理類,這是我們不能容忍的。於是乎,動態代理就出現了,它本質上是生成一個外表上和目標對象一樣的代理對象,然後當我們調用代理對象的方法的時候,實際上它在他的方法裏面去調用了目標對象對應的同名方法。

  • 二、動態代理設計的核心思想

其實不要把這些設計想得多麼高尚,假如我是動態代理設計的作者,由動態代理的意義部分我們知道,我們就是要想盡一切辦法,通過目標對象生成代理對象,然後讓代理對象的方法調用作用到目標對象的方法調用。沒錯動態代理的核心思想就是這麼簡單。比如目標類爲Person,Person有一個方法叫做purchase(),此方法用於購物。我們期望purchase()方法有代理類去做處理,比如在購物前記錄下購買了哪些東西。我們知道在使用一個類之前,是需要創建一個對象的,我們就在創建的地方動手腳。所以你看到了JDK動態Proxy.newInstance()的方式,也領略過Spring的Enhancer.create()。個人比較喜歡cglib的優雅、乾淨、利落。吐槽一下JDK的InvocationHandler像極了噁心的中間商。下面是JDK動態代理UML示意圖

  • 三、JDK動態代理

1,原理

在瞭解動態代理之前,我們需要了解Java字節碼。如果不熟悉Java字節碼,你可以理解爲通過代碼動態生成一個.java文件,然後將其編譯爲class文件加載到內存中。接下來JDK中的動態代理要做的事情就是怎麼去生成一個ProxyPerson字節碼文件。其實它就是在生成字節碼的時候,持有了InvocationHandler對象,然後去實現了ProxyPerson對應的接口。在該接口的所有實現方法中,只做了一件事情就是調用invocationHandler.invoke()方法。從代碼層面來看如下所示:

public class ProxyPerson implements Purchase{


static{
   Method method;// 接口的方法
   Object[] args;// 接口參數  
}

InvocationHandler handler;
public ProxyPerson(InvocationHandler handler){
    this.handler = handler;
}


@overrde
public purchage(){
   this,handler.invoke(this,method,args);  
}

}   

 

 那麼上面這段代碼是在什麼時候生成的呢? 

Proxy.newProxyInstance()

在我們調用JDK上面的這個方法的時候,底層就會去生成一個ProxyPerson字節碼。知道了原理我們來解答一下JDK動態代理爲何只能基於接口代理而不能基於類呢?

1),受限於字節碼的生成方式,JDK本身就是基於InvocationHandler去做的代理中轉。我們看到代理對象的方法調用於目標對象的調用沒有半毛球關係,調用目標對象是我們自己在invoke方法裏面完成的。

2),受限於同名的方法只能被向上轉型成功的對象調用。比如有兩個類Boy與Girl,他們都實現了接口Purchase,如果我們先獲取到Girl的purchase()方法method,我們通過method.invoke(new Boy())這樣必定會報錯。但是如果我們獲取到Purchase接口purchase()方法method,我們通過method.invoke(new Boy())這樣是ok的,因爲new Boy()可以向上轉型爲Purchase。

2,應用

比如無論是傳統的MVC模型還是DDD模型,都離不開Service。我們知道Service方法使用@Transactional是可以開啓事務控制的。那麼這種註解式事務是如何實現的呢? 其實在工程啓動的時候,我們就會有一個Bean的後置處理器去檢查所有Bean一旦發現Bean的方法上有事務註解,他就通過Proxy.newInstance()去創建一個代理對象,將代理對象進行返回注入,而拋棄原本應該注入到容器的對象。所以我們看起來通過容器拿到的Service其實已經是代理對象了。在調用目標對象前,開啓編程式事務即可。

  • 四、cglib動態代理

 有了上面的知識,我們要有對於cglib而言只是在生成字節碼上面動手腳的覺悟。下面直觀感受與一下生成過程

 public static void main(final String[] args) {

        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(Boy.class);
        enhancer.setCallback(new MethodInterceptor(){

            @Override
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                System.out.println("proxy method "+ method.getName());
                if(method.getAnnotation(Transactional.class)!=null){
                    System.out.println(method.getName()+"發現註解");
                }
                return methodProxy.invokeSuper(o,args);
            }
        });
        Boy proxy = (Boy) enhancer.create();
        proxy.test();


    }

    public static class Boy{
        public void run(){
            System.out.println("run...");
        }
        @Transactional
        public void walk(){
            System.out.println("walk...");
        }
        @Transactional
        public void test(){
            System.out.println("test...");
            walk();
        }
    }

可以看到cglib是基於繼承的方式進行字節碼動態生成。它在子類的實現中,只是調用了注入的methodIntercptor.interceptor()方法。具體字節碼實現細節,這裏不在深究。我們在這裏探討一下,爲什麼cglib可以使同一個service方法中的其他帶有事務註解的事務生效?因爲基於繼承的動態代理,本質發起上調用的代理對象可以向上轉型爲原本的目標對象,所以它可以直接通過代理對象去調目標對象方法。

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