菜鳥學SSH(十四)——Spring容器AOP的實現原理——動態代理

劉水鏡發表於2014-07-16

之前寫了一篇關於IOC的部落格——《Spring容器IOC解析及簡單實現》,今天再來聊聊AOP。大家都知道Spring的兩大特性是IOC和AOP,換句話說,容器的兩大特性就是IOC和AOP。

IOC負責將物件動態的注入到容器,從而達到一種需要誰就注入誰,什麼時候需要就什麼時候注入的效果,可謂是招之則來,揮之則去。想想都覺得爽,如果現實生活中也有這本事那就爽歪歪了,至於有多爽,各位自己腦補吧;而AOP呢,它實現的就是容器的另一大好處了,就是可以讓容器中的物件都享有容器中的公共服務。那麼容器是怎麼做到的呢?它怎麼就能讓在它裡面的物件自動擁有它提供的公共性服務呢?答案就是我們今天要討論的內容——動態代理。

動態代理其實並不是什麼新鮮的東西,學過設計模式的人都應該知道代理模式,代理模式是一種靜態代理,而動態代理就是利用反射和動態編譯將代理模式變成動態的。原理跟動態注入一樣,代理模式在編譯的時候就已經確定代理類將要代理誰,而動態代理在執行的時候才知道自己要代理誰。

Spring的動態代理有兩種:一是JDK的動態代理;另一個是cglib動態代理(通過修改位元組碼來實現代理)。今天我們們主要討論JDK動態代理的方式。JDK的代理方式主要就是通過反射跟動態編譯來實現的,下面我們們就通過程式碼來看看它具體是怎麼實現的。

假設我們要對下面這個使用者管理進行代理:

//使用者管理介面
package com.tgb.proxy;

public interface UserMgr {
    void addUser();
    void delUser();
}

//使用者管理的實現
package com.tgb.proxy;

public class UserMgrImpl implements UserMgr {

    @Override
    public void addUser() {
        System.out.println("新增使用者.....");
    }

    @Override
    public void delUser() {
        System.out.println("刪除使用者.....");
    }
    
}

 


按照代理模式的實現方式,肯定是用一個代理類,讓它也實現UserMgr介面,然後在其內部宣告一個UserMgrImpl,然後分別呼叫addUser和delUser方法,並在呼叫前後加上我們需要的其他操作。但是這樣很顯然都是寫死的,我們怎麼做到動態呢?別急,接著看。

 

我們知道,要實現代理,那麼我們的代理類跟被代理類都要實現同一介面,但是動態代理的話我們根本不知道我們將要代理誰,也就不知道我們要實現哪個介面,那麼要怎麼辦呢?我們只有知道要代理誰以後,才能給出相應的代理類,那麼我們何不等知道要代理誰以後再去生成一個代理類呢?想到這裡,我們好像找到了解決的辦法,就是動態生成代理類!

這時候我們親愛的反射又有了用武之地,我們可以寫一個方法來接收被代理類,這樣我們就可以通過反射知道它的一切資訊——包括它的型別、它的方法等等(如果你不知道怎麼得到,請先去看看我寫的反射的部落格《反射一》《反射二》)。


JDK動態代理的兩個核心分別是InvocationHandler和Proxy,下面我們就用簡單的程式碼來模擬一下它們是怎麼實現的:


InvocationHandler介面:

package com.tgb.proxy;

import java.lang.reflect.Method;

public interface InvocationHandler {
    public void invoke(Object o, Method m);
}

 

 

實現動態代理的關鍵部分,通過Proxy動態生成我們具體的代理類:

package com.tgb.proxy;

import java.io.File;
import java.io.FileWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import javax.tools.JavaCompiler;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import javax.tools.JavaCompiler.CompilationTask;

public class Proxy {
    /**
     * 
     * @param infce 被代理類的介面
     * @param h 代理類
     * @return
     * @throws Exception
     */
    public static Object newProxyInstance(Class infce, InvocationHandler h) throws Exception { 
        String methodStr = "";
        String rt = "\r\n";
        
        //利用反射得到infce的所有方法,並重新組裝
        Method[] methods = infce.getMethods();    
        for(Method m : methods) {
            methodStr += "    @Override" + rt + 
                         "    public  "+m.getReturnType()+" " + m.getName() + "() {" + rt +
                         "        try {" + rt +
                         "        Method md = " + infce.getName() + ".class.getMethod(\"" + m.getName() + "\");" + rt +
                         "        h.invoke(this, md);" + rt +
                         "        }catch(Exception e) {e.printStackTrace();}" + rt +                        
                         "    }" + rt ;
        }
        
        //生成Java原始檔
        String srcCode = 
            "package com.tgb.proxy;" +  rt +
            "import java.lang.reflect.Method;" + rt +
            "public class $Proxy1 implements " + infce.getName() + "{" + rt +
            "    public $Proxy1(InvocationHandler h) {" + rt +
            "        this.h = h;" + rt +
            "    }" + rt +            
            "    com.tgb.proxy.InvocationHandler h;" + rt +                            
            methodStr + rt +
            "}";
        String fileName = 
            "d:/src/com/tgb/proxy/$Proxy1.java";
        File f = new File(fileName);
        FileWriter fw = new FileWriter(f);
        fw.write(srcCode);
        fw.flush();
        fw.close();
        
        //將Java檔案編譯成class檔案
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager fileMgr = compiler.getStandardFileManager(null, null, null);
        Iterable units = fileMgr.getJavaFileObjects(fileName);
        CompilationTask t = compiler.getTask(null, fileMgr, null, null, null, units);
        t.call();
        fileMgr.close();
        
        //載入到記憶體,並例項化
        URL[] urls = new URL[] {new URL("file:/" + "d:/src/")};
        URLClassLoader ul = new URLClassLoader(urls);
        Class c = ul.loadClass("com.tgb.proxy.$Proxy1");
        
        Constructor ctr = c.getConstructor(InvocationHandler.class);
        Object m = ctr.newInstance(h);

        return m;
    }
    
}

 


這個類的主要功能就是,根據被代理物件的資訊,動態組裝一個代理類,生成$Proxy1.java檔案,然後將其編譯成$Proxy1.class。這樣我們就可以在執行的時候,根據我們具體的被代理物件生成我們想要的代理類了。這樣一來,我們就不需要提前知道我們要代理誰。也就是說,你想代理誰,想要什麼樣的代理,我們就給你生成一個什麼樣的代理類。

 


然後,在客戶端我們就可以隨意的進行代理了。

package com.tgb.proxy;


public class Client {
    public static void main(String[] args) throws Exception {
        UserMgr mgr = new UserMgrImpl();
        
        //為使用者管理新增事務處理
        InvocationHandler h = new TransactionHandler(mgr);
        UserMgr u = (UserMgr)Proxy.newProxyInstance(UserMgr.class,h);
        
        //為使用者管理新增顯示方法執行時間的功能
        TimeHandler h2 = new TimeHandler(u);
        u = (UserMgr)Proxy.newProxyInstance(UserMgr.class,h2);
        
        u.addUser();
        System.out.println("\r\n==========華麗的分割線==========\r\n");
        u.delUser();
    }
}

 

執行結果:

開始時間:2014年-07月-15日 15時:48分:54秒
開啟事務.....
新增使用者.....
提交事務.....
結束時間:2014年-07月-15日 15時:48分:57秒
耗時:3秒

==========華麗的分割線==========

開始時間:2014年-07月-15日 15時:48分:57秒
開啟事務.....
刪除使用者.....
提交事務.....
結束時間:2014年-07月-15日 15時:49分:00秒
耗時:3秒

 

這裡我寫了兩個代理的功能,一個是事務處理,一個是顯示方法執行時間的代理,當然都是非常簡單的寫法,只是為了說明這個原理。當然,我們可以想Spring那樣將這些AOP寫到配置檔案,因為之前那篇已經寫了怎麼通過配置檔案注入了,這裡就不重複貼了。

 

到這裡,你可能會有一個疑問:你上面說,只要放到容器裡的物件,都會有容器的公共服務,我怎麼沒看出來呢?好,那我們就繼續看一下我們的代理功能:

事務處理:

package com.tgb.proxy;

import java.lang.reflect.Method;

public class TransactionHandler implements InvocationHandler {
    
    private Object target;
    
    public TransactionHandler(Object target) {
        super();
        this.target = target;
    }

    @Override
    public void invoke(Object o, Method m) {
        System.out.println("開啟事務.....");
        try {
            m.invoke(target);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("提交事務.....");
    }

}

 


從程式碼中不難看出,我們代理的功能裡沒有涉及到任何被代理物件的具體資訊,這樣有什麼好處呢?這樣的好處就是將代理要做的事情跟被代理的物件完全分開,這樣一來我們就可以在代理和被代理之間隨意的進行組合了。也就是說同一個功能我們只需要一個。同樣的功能只有一個,那麼這個功能不就是公共的功能嗎?不管容器中有多少給物件,都可以享受容器提供的服務了。這就是容器的好處。

 


不知道我講的夠不夠清楚,歡迎大家積極交流、討論。


相關文章