1. 程式人生 > >黑馬程式設計師——java拾遺之代理類及AOP

黑馬程式設計師——java拾遺之代理類及AOP

動態代理其實就是java.lang.reflect.Proxy類動態的根據您指定的所有介面生成一個class 位元組碼,該class會繼承Proxy類,並實現所有你指定的介面(您在引數中傳入的介面陣列);然後再利用您指定的classloader將 class 位元組碼載入進系統,最後生成這樣一個類的物件,並初始化該物件的一些值,如invocationHandler,以即所有的介面對應的Method成員。 初始化之後將物件返回給呼叫的客戶端。這樣客戶端拿到的就是一個實現你所有的介面的Proxy物件。

下面通過對Proxy類的使用來探究java中的代理機制:
首先,我們用Proxy來建立一個Collection介面的位元組碼,集合框架在java中地位重要,Collection接口裡面也定義了很多的方法,方便的後面對動態生成的代理類的觀察。建立的程式碼如下:
<span style="font-size:14px;">Class clazzProxy1 
	= Proxy.getProxyClass(Collection.class.getClassLoader(), Collection.class);
System.out.println(clazzProxy1.getName());</span>
列印一下這個動態生成的Class物件的名字,發現名稱是$Proxy0,看起來挺奇怪,不過這個名稱無關緊要,也和我們使用java的代理機制沒多大關係,不必理會,重要的是,這個Class物件建立成功了。

後面我們利用java的反射機制,探究一下Proxy類生成的動態物件結構是什麼樣子的,
首先,列印一下這個動態生成的Class物件實現了什麼介面:
<span style="font-size:14px;">for(Class clazz : clazzProxy1.getInterfaces()){
    System.out.println(clazz);
}</span>
發現列印的結果是:interface java.util.Collection
說明這個動態生成的Class物件實現了我們指定的介面。


接下來再看一下這個物件的父類:
<span style="font-size:14px;">System.out.println(clazzProxy1.getSuperclass());</span>
結果是:class java.lang.reflect.Proxy
可以看出,由Proxy類動態生成的Class物件,父類都是Proxy。


看完了Proxy生成的Class物件實現介面和父類,接下來看看這個動態物件的構造方法:

先看看這個物件有幾個構造方法,程式碼如下
<span style="font-size:14px;">Constructor[] constructors = clazzProxy1.getConstructors();
System.out.println(constructors.length);</span>
結果是1
看來生成的Proxy物件只有一個構造方法,我們來看看這個構造方法接收幾個引數,程式碼如下
<span style="font-size:14px;">System.out.println(constructors[0].getParameterTypes().length);</span>
結果是1
說明構造方法只接受一個引數,這樣我們來看看Proxy生成的動態的Class物件構造方法的結構,程式碼如下
<span style="font-size:14px;">Constructor[] constructors = clazzProxy1.getConstructors();
for(Constructor constructor : constructors){
    StringBuilder sb = new StringBuilder(constructor.getName());
    sb.append("(");
    for(Class clazz : constructor.getParameterTypes()){
        sb.append(clazz.getName()).append(",");
    }
    sb.deleteCharAt(sb.length()-1);
    sb.append(")");
    System.out.println(sb.toString());
}</span>
列印的結果是:$Proxy0(java.lang.reflect.InvocationHandler)
我們看到Proxy類的構造方法接收了一個介面InvocationHandler,這個介面的作用很關鍵,我們接下來詳細的探究一下。
InvocationHandler 介面在java官方JDK的API上,解釋是:是代理例項的呼叫處理程式 實現的介面。理解起來有點抽象,但是接下來我們用程式碼來解釋,會清楚很多。
上面我們瞭解了Proxy類動態生成的Class物件的構造方法結構,接下來我們就要用這個Class物件的構造方法來例項化出實現了我們指定介面的動態代理類的物件。程式碼如下:
<span style="font-size:14px;">public class ProxyDemo1
{
    public static void main(String[] args) throws Exception{
        Class<?> clazzProxy1 = Proxy.getProxyClass(Collection.class.getClassLoader(), Collection.class);
        Constructor<?> constructor = clazzProxy1.getConstructor(InvocationHandler.class);
        Collection collection = (Collection)constructor.newInstance(
            new InvocationHandler(){
                Collection target = new ArrayList();
                @Override
                public Object invoke(Object proxy, Method method, Object[] args)
                    throws Throwable {
                    System.out.println("proxy:"+proxy.getClass());
                    System.out.println("method:"+method.getName());
                    if(args != null && args.length > 0){
                        for( Object arg: args){
                            System.out.println("arg:"+arg);
                        }
                    }
                    return method.invoke(target,args);
                }
        });
        collection.toString();
        collection.add("123");
        collection.add("abc");
    }
}</span>

執行的結果是:
proxy:class $Proxy0
method:toString
proxy:class $Proxy0
method:add
arg:123
proxy:class $Proxy0
method:add
arg:abc

到這裡我們就可以看出 InvocationHandler 介面的作用了, InvocationHandler 介面的 invoke方法在 Proxy 的例項每次呼叫它所指定實現的介面的方法時,都會去呼叫它。到這裡我們就能知道invoke方法裡應該寫一些什麼了。首先,invoke方法應該保證這個 Proxy 例項每次呼叫介面中的方法時,得到的結果都和不用代理,直接呼叫介面實現類同一個方法的結果一致。否則代理沒有實現原本應該實現的功能,代理就失去了意義(代理的目的首先要實現它要代理的功能,其次才是在功能的基礎上新增自己個性化的功能需要)。例如上面程式碼中的clazzProxy1,對它呼叫add方法應該和對Collection的實現類 ArrayList 呼叫add方法作用相同。我們在最後新增一行程式碼:
<span style="font-size:14px;">System.out.println(collection);</span>
可以看到列印瞭如下結果:
proxy:class $Proxy0
method:toString
[123, abc]

可以看到,println方法其實也是呼叫了代理例項collection的toString方法,並且這個代理類$Proxy0的例項collection也能像ArrayList一樣實現往集合中新增元素的功能,根本原因是在匿名內部類的第一行,Collection target = new ArrayList(); 賦予了一個代理例項的操作物件target,這個物件就是ArrayList的物件。因此操作代理物件的時候,就是間接的操作了這個ArrayList的物件target。
看到這裡可能會疑惑,繞了一大圈,不如直接操作這個target得了,問什麼還要通過代理的方式間接的實現同樣的功能。這就要說代理模式的設計思想,在呼叫代理例項的方法的時候,這些方法都會呼叫invoke方法,而 invoke 方法的內容其實都是我們程式設計師自己寫的,這就意味著,我們可以在裡面寫很多我們自己需要的程式碼,最常見的就是在方法呼叫的時候新增日誌輸出。
在不使用代理的時候,我們要新增日誌,需要在介面實現類呼叫方法的外部進行編碼,每呼叫一次方法,這個記錄日誌的程式碼就要重複寫一遍。有了代理之後,我們把介面實現類要呼叫的方法和我們需要記錄日誌的方法一起塞進invoke方法中,這樣通過代理來呼叫原本我們使用的方法,達到的效果一樣,但是不用在重複的寫記錄日誌的程式碼。因為代理的例項會自己去呼叫我們寫在invoke方法裡面的記錄日誌方法。
說到這裡,已經引出了Spring框架的一個重要思想,面向切面程式設計AOP。用上面的例子來理解,記錄日誌的功能和我每次呼叫的return method.invoke(target,args);這個ArrayList類的方法其實沒什麼根本關係,去掉日誌功能,呼叫 ArrayList 的方法照樣正常。我把日誌的功能換成其他功能(例如計算圓周長),也一樣不會影響ArrayList的方法呼叫。用形象的比喻來說,就像在代理類呼叫實現介面方法的前後,加入了“切面”,這個切面在invoke方法中,對代理類的所有方法呼叫都起作用,但是又不影響代理的基本功能。用我上面的程式碼來講,記錄invoke方法三個引數的幾行println語句,
<span style="font-size:14px;">System.out.println("proxy:"+proxy.getClass());
System.out.println("method:"+method.getName());
if(args != null && args.length > 0){
    for( Object arg: args){
        System.out.println("arg:"+arg);
    }
}</span>
就是一個切面,它對collection物件的所有方法起作用,都在method.invoke(target,args)方法之前統一呼叫,且不影響代理的根本功能。就像一個切面,切入了collection這個代理物件的所有方法中。所以上面的invoke方法程式碼可以看成是這樣的結構:
<span style="font-size:14px;">Collection collection = (Collection)constructor.newInstance(
    new InvocationHandler(){
        Collection target = new ArrayList();
        @Override
        public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
            /*前面的切面*/ 
            System.out.println("proxy:"+proxy.getClass());
            System.out.println("method:"+method.getName());
            if(args != null && args.length > 0){
                for( Object arg: args){
                    System.out.println("arg:"+arg);
                }
            }
            Object obj = method.invoke(target,args);
            /*後面的切面*/ 
           return obj;
        }
});</span>
上面的程式碼演示的是由Proxy先獲取Class類例項,之後通過Class類例項獲取構造方法,最後通過構造方法來例項化物件的過程,Proxy類提供的便捷的方法,可以把這三個步驟合併為一步,簡化程式碼,上面的程式碼也可以寫成這樣:
<span style="font-size:14px;">Collection collection = (Collection) Proxy.newProxyInstance(
    Collection.class.getClassLoader(),
    new Class[] { Collection.class }, 
    new InvocationHandler() {
        Collection target = new ArrayList();
        @Override
        public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
            /* 前面的切面 */
            System.out.println("proxy:" + proxy.getClass());
            System.out.println("method:" + method.getName());
            if (args != null && args.length > 0) {
                for (Object arg : args) {
                    System.out.println("arg:" + arg);
                }
            }
            Object obj = method.invoke(target, args);
            /* 後面的切面 */
            return obj;
        }
    });</span>
在框架中,生成動態代理的方法往往是通用的,不會像上面這樣根據不同的介面單獨去寫不同功能的代理類。在實際應用中都是對同一個生成動態代理的方法,需要實現什麼介面,就傳進去什麼介面,而方法根據不同的介面生成不同的代理類,這樣就需要對上面我們的程式碼做一些改動。
先把上面要實現的介面子類物件target提出到InvocationHandler匿名內部類的外面,讓InvocationHandler要操作的介面型別由外部傳入,就像這樣:
<span style="font-size:14px;">final Collection target = new ArrayList();
Collection collection = (Collection) Proxy.newProxyInstance(
    Collection.class.getClassLoader(),
    new Class[] { Collection.class }, 
    new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
            /* 前面的切面 */
            System.out.println("proxy:" + proxy.getClass());
            System.out.println("method:" + method.getName());
            if (args != null && args.length > 0) {
                for (Object arg : args) {
                    System.out.println("arg:" + arg);
                }
            }
            Object obj = method.invoke(target, args);
            /* 後面的切面 */
            return obj;
        }
    });</span>
接下來把生成動態代理的物件抽象成一個方法,假如起名叫getProxy,傳入的物件型別改成通用的Object類,返回的型別也改為Object類,程式碼如下:
<span style="font-size:14px;">public static void main(String[] args) throws Exception{
    final Collection target = new ArrayList();
    Collection collection = (Collection)getProxy(target);
}

private static Object getProxy(final Object target) {
    Object obj = Proxy.newProxyInstance(
        target.getClass().getClassLoader(),
        target.getClass().getInterfaces(), 
        new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args)
                throws Throwable {
                /* 前面的切面 */
                System.out.println("proxy:" + proxy.getClass());
                System.out.println("method:" + method.getName());
                if (args != null && args.length > 0) {
                    for (Object arg : args) {
                        System.out.println("arg:" + arg);
                    }
                }
                Object obj = method.invoke(target, args);
                /* 後面的切面 */
                return obj;
            }
        });
    return obj;
}</span>
這樣一個能根據需要生成不同代理類的通用方法就寫好了,但是上面的方法還是有不令人滿意的地方,那就是對於切面的描述,上面的切面按理應該抽象成一個方法,後面要修改切面的邏輯可以直接修改切面方法,而不是去改動代理生成類的內部邏輯。我們接著來修改上面的程式碼,來達到我們滿意的效果。首先,我們需要定義一個用來描述切面的介面,裡面需要定義兩個方法,beforeMethod和afterMethod,分別表示代理類呼叫它所實現介面(例如上面的Collection介面)的方法(比如Collection介面的add方法)前後的切面。然後再定義一個自定義的切面類,裡面封裝我們要實現的切面具體實現。比如我這樣定義切面:
<span style="font-size:14px;">//描述切面的介面
public interface Advice
{
    /** 功能描述:前切面  */
    void beforeMethod(Object proxy, Method method, Object[] args);
    /** 功能描述:後切面  */
    void afterMethod(Object proxy, Method method, Object[] args);
}
//封裝具體實現的切面類
public class MyAdvice implements Advice
{
    public void beforeMethod(Object proxy, Method method, Object[] args){
        System.out.println("我是 method "+method.getName()+" 前面的切面!");
    }
    public void afterMethod(Object proxy, Method method, Object[] args){
        System.out.println("我是 method "+method.getName()+" 後面的切面!");
    }
}</span>
我們獲取動態代理類的方法現在就可以改成這樣:
<span style="font-size:14px;">private static Object getProxy(final Object target, final Advice advice) {
    Object obj = Proxy.newProxyInstance(
        target.getClass().getClassLoader(),
        target.getClass().getInterfaces(), 
        new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args)
                throws Throwable {
                advice.beforeMethod(proxy, method, args);//前切面
                Object obj = method.invoke(target, args);
                advice.afterMethod(proxy, method, args);//後切面
                return obj;
            }
        });
    return obj;
}</span>
看起來是不是清爽許多,我們再來測試下現在這個代理獲取方法看看效果:
<span style="font-size:14px;">final Collection target = new ArrayList();
Collection collection = (Collection)getProxy(target, new MyAdvice());
collection.toString();
collection.add("123");
collection.add("abc");    
System.out.println(collection);</span>
執行結果:
我是 method toString 前面的切面!
我是 method toString 後面的切面!
我是 method add 前面的切面!     
我是 method add 後面的切面!     
我是 method add 前面的切面!     
我是 method add 後面的切面! 
我是 method toString 前面的切面!    
我是 method toString 後面的切面!
[123, abc]

大功告成,一個實現了簡單AOP功能的通用動態代理類現在就寫好了。

我們來總結一下上面的內容,

1.java裡有動態代理機制,可以動態生成Class類的例項,這個例項可以實現我們指定的介面,通常來說,這個生成的動態類目的是讓我們在實現介面原有功能的基礎上,新增新的功能,而不用重複編寫這個新的功能的程式碼。

2.為了實現動態代理例項在原有介面功能上新增新功能的目的,引入了面向切面AOP的概念,基本思想就是在InvocationHandler中的invoke方法呼叫介面本身方法的前後,加入我們自定義的方法,這樣我們自定義的方法就可以在動態代理呼叫介面方法的時候,自動的被呼叫。

3.切面我們通常抽象出對應的介面和實現類,在動態代理的invoke方法中,通過切面的實現類來呼叫切面的功能,而不是直接把切面的程式碼寫進invoke方法中。