經過模擬JDK中的動態代理,由淺入深講解動態代理思想.

我的認爲動態代理在設計模式中算是比較難的, 本篇文章將從無到有, 從一個簡單代碼示例開始迭代, 逐步深刻講解動態代理思想.java

場景引入

  • 假設如今有一個坦克類, 它實現了Moveable接口, 裏面有一個move()移動的方法. 代碼以下:
class Tank implements Moveable{
    @Override
    public void move(){
        System.out.println("坦克開始移動...");
        try {
            Thread.sleep((long) (Math.random() * 5000));
            System.out.println("坦克移動結束...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

interface Moveable{
    public void move();
}
複製代碼
  • 爲了能計算坦克移動所花費的時間咱們打算在坦克的move()方法的先後添加一些代碼, 用於記錄坦克move()方法的執行時間.
  • 咱們將使用代理類, 並在代理類中執行上述操做, 首先展現的是以繼承的方式進行代理.
class MoveTimeProxy1 extends Tank{
    @Override
    public void move() {
        long start = System.currentTimeMillis();//開始時間
        super.move();//調用坦克的move()方法
        long end = System.currentTimeMillis();//結束時間
        System.out.println("執行該方法用了" + (end - start) + "毫秒");
    }
}
複製代碼
  • 接着咱們展現另一種經過聚合實現代理的方式
class MoveTimeProxy2 implements Moveable{
    Tank tank;

    public MoveTimeProxy2(Tank tank){
        this.tank = tank;
    }

    @Override
    public void move() {
        long start = System.currentTimeMillis();//開始時間
        tank.move();//調用坦克的move()方法
        long end = System.currentTimeMillis();//結束時間
        System.out.println("執行該方法用了" + (end - start) + "毫秒");
    }
}
複製代碼
  • 以上兩種均爲實現代理的方式, 若是要分個優劣的話, 繼承方式的代理會差一些. 想一想看, 若是如今除了記錄時間, 還要記錄日誌的話, 則要建立一個新的繼承代理類並重寫move()方法. 若是需求變動, 須要先記錄日誌, 再記錄時間的話, 又要建立一個新的繼承代理類. 如此下去, 代理類的建立將沒完沒了.
  • 相比之下, 聚合實現的代理類則靈活得多. 每個聚合代理類可以實現一種代理, 而且代理的順序是能夠替換的. 請看代碼(聚合代理類的代碼有所修改)
public class ProxyTest {

    public static void main(String[] args) {
        TimeProxy tp = new TimeProxy(new Tank());
        LogProxy lp = new LogProxy(tp);
        lp.move();
    }
}

class TimeProxy implements Moveable{//記錄時間的代理
    Moveable m;//再也不持有Tank引用, 而是持有Moveable接口引用

    public TimeProxy(Moveable m){
        this.m = m;
    }

    @Override
    public void move() {
        long start = System.currentTimeMillis();//開始時間
        m.move();//調用move()方法
        long end = System.currentTimeMillis();//結束時間
        System.out.println("執行該方法用了" + (end - start) + "毫秒");
    }
}

class LogProxy implements Moveable{//打印日誌的代理
    Moveable m;

    public LogProxy(Moveable m){
        this.m = m;
    }

    @Override
    public void move() {
        System.out.println("日誌: 開始測試坦克移動...");
        m.move();
        System.out.println("日誌: 坦克移動結束...");
    }
}
複製代碼

 

動態代理引入

  • 看完上面的例子, 你們應該對代理一詞有更深入的理解. 可是上面的代碼中, 爲坦克生成的代理類TimeProxy是咱們在代碼中寫死的, 因此這頂多算個靜態代理, 如何經過動態的方式產生代理呢?
  • 在講解動態代理以前咱們須要明確的是, 上面的聚合代理方式經過持有某個接口的引用完成代理, 因此咱們是針對某個接口產生代理, 而不是對某個具體的對象產生代理.
  • 爲了模擬Java中的實現, 咱們建立一個Proxy類, 裏面提供一個newProxyInstance()方法, 用於返回一個代理. 咱們但願經過以下代碼就能動態生成一個代理.
public static void main(String[] args) {
    Tank tank = new Tank();
    Moveable m = (Moveable)Proxy.newProxyInstance();//動態得到一個代理
    m.move();
}
複製代碼
  • 從上面的代碼能夠看到咱們甚至都不須要知道代理類的名字就能夠動態的獲取一個代理. 咱們以上述的記錄時間的代理爲例子, 獲取一個時間代理類.
  • newProxyInstance()方法中, 咱們先把原來TimeProxy的源代碼以字符串的方式存放, 再經過寫入文件的方式建立出TimeProxy.java文件. 而後經過Java原生的編譯api將TimeProxy.java編譯成TimeProxy.class文件. 最後把該class文件加載到內存中, 並調用其構造方法建立對象, 返回該代理對象.
  • 舒適提示: 本段代碼不是專門教你們如何動態生成類, 由於有不少開源工具好比CGLib, ASM等能夠更專業地完成這件事情, 這裏僅使用Java原生API完成, 主要爲了展示動態生成一個代理對象背後的過程.
class Proxy{
    public static Object newProxyInstance() throws Exception {
        //把整個TimeProxy類的實現寫入字符串, 經過編譯這一字符串獲得TimeProxy對象
        String src = "package designPattern.proxy;\n" +
                "\n" +
                "class TimeProxy implements Moveable{\n" +
                " Moveable m;//再也不持有Tank引用, 而是持有Moveable接口引用\n" +
                "\n" +
                " public TimeProxy(Moveable m){\n" +
                " this.m = m;\n" +
                " }\n" +
                "\n" +
                " @Override\n" +
                " public void move() {\n" +
                " long start = System.currentTimeMillis();//開始時間\n" +
                " m.move();//調用坦克的move()方法\n" +
                " long end = System.currentTimeMillis();//結束時間\n" +
                " System.out.println(\"執行該方法用了\" + (end - start) + \"毫秒\");\n" +
                " }\n" +
                "}";
        String filename = System.getProperty("user.dir")
                + "/src/main/java/designPattern/proxy/TimeProxy.java";//文件名(生成類的路徑)
        File f = new File(filename);
        FileWriter fw = new FileWriter(f);
        fw.write(src);
        fw.flush();
        fw.close();

        //編譯
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();//拿到系統當前默認的編譯器, 即Javac
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
        Iterable units = fileManager.getJavaFileObjects(filename);//獲得文件對象
        JavaCompiler.CompilationTask t = compiler.getTask(null, fileManager, null, null, null, units);
        t.call();//進行編譯
        fileManager.close();

        //把class文件加載進內存並建立對象
        URL[] urls = new URL[]{new URL("file:/" + System.getProperty("user.dir") + "/src")};
        URLClassLoader ul = new URLClassLoader(urls);
        Class c = ul.loadClass("designPattern.proxy.TimeProxy");//拿到class對象

        Constructor ctr = c.getConstructor(Moveable.class);//拿到參數爲Moveable的構造方法
        Moveable m = (Moveable)ctr.newInstance(new Tank());//建立代理對象
        return m;
    }
}
複製代碼
  • 咱們繼續對上面的代碼進行優化, 目前代碼中指定生成的是實現了moveable接口的代理對象. 而上面咱們提到過動態代理是基於某個接口的(聚合型代理), 因此咱們但願可以動態地指定接口, 並生成相應的代理類.
public class ProxyTest {

    public static void main(String[] args) throws Exception {
        Tank tank = new Tank();
        Moveable m = (Moveable)Proxy.newProxyInstance(Moveable.class);//傳入接口參數動態得到一個代理
        m.move();
    }
}

class Proxy{
    public static Object newProxyInstance(Class intfce) throws Exception {
        //把整個TimeProxy類的實現寫入字符串, 經過編譯這一字符串獲得TimeProxy對象
        String methodStr = "";
        String n = "\n";

        Method[] methods = intfce.getMethods();//拿到接口中的全部方法
        for(Method m : methods){//拼接方法
            methodStr += " @Override\n" +
                    " public void " + m.getName() + "() {\n" +
                    " long start = System.currentTimeMillis();//開始時間\n" +
                    " m.move();//調用坦克的move()方法\n" +
                    " long end = System.currentTimeMillis();//結束時間\n" +
                    " System.out.println(\"執行該方法用了\" + (end - start) + \"毫秒\");\n" +
                    " }\n";
        }
        //拼接出整個類
        String src = "package designPattern.proxy;\n" +
                "\n" +
                "class TimeProxy implements " + intfce.getName() + "{\n" +
                " Moveable m;//再也不持有Tank引用, 而是持有Moveable接口引用\n" +
                "\n" +
                " public TimeProxy(Moveable m){\n" +
                " this.m = m;\n" +
                " }\n" +
                "\n" + methodStr +
                "}";

        String filename = System.getProperty("user.dir")
                + "/src/main/java/designPattern/proxy/TimeProxy.java";//文件名(生成類的路徑)
        File f = new File(filename);
        FileWriter fw = new FileWriter(f);
        fw.write(src);
        fw.flush();
        fw.close();

        //編譯
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();//拿到系統當前默認的編譯器, 即Javac
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
        Iterable units = fileManager.getJavaFileObjects(filename);//獲得文件對象
        JavaCompiler.CompilationTask t = compiler.getTask(null, fileManager, null, null, null, units);
        t.call();//進行編譯
        fileManager.close();

        //把class文件加載進內存並建立對象
        URL[] urls = new URL[]{new URL("file:/" + System.getProperty("user.dir") + "/src")};
        URLClassLoader ul = new URLClassLoader(urls);
        Class c = ul.loadClass("designPattern.proxy.TimeProxy");//拿到class對象

        Constructor ctr = c.getConstructor(Moveable.class);//拿到參數爲Moveable的構造方法
        Object m = ctr.newInstance(new Tank());//建立代理對象
        return m;
    }
}
複製代碼

 

動態代理進階

  • 在上一個版本中咱們已經可以動態地生成一個代理對象了, 可是還有一個最重要的也是最難的點沒有實現. 在上面的代碼中咱們對被代理對象進行的操做是記錄方法的運行時間, 是在代碼裏面寫死的. 咱們但願可讓用戶自定義加強手段, 好比說記錄時間(TimeProxy), 輸出日誌(LogProxy), 事務操做等等.
  • 對於這種在被代理對象先後進行加強的操做, 咱們定義一個InvocationHandler接口, 並在它的實現類中給出具體的操做. 咱們以初始的記錄時間操做爲例.
  • 下面給出完整的代碼, 若是看不懂能夠結合代碼後面的總結來看.
public class ProxyTest {

    public static void main(String[] args) throws Exception {
        Tank tank = new Tank();
        InvocationHandler h = new TimeHandler(tank);
        Moveable m = (Moveable)Proxy.newProxyInstance(Moveable.class, h);//動態得到一個代理
        m.move();
    }
}

interface InvocationHandler{
    public void invoke(Object o, Method m);//參數o指定執行對象(代理對象, 可能會用到), m指定執行的方法
}

class TimeHandler implements InvocationHandler{
    private Object target;

    public TimeHandler(Object target){
        this.target = target;
    }

    @Override
    public void invoke(Object o, Method m) {
        long start = System.currentTimeMillis();//這行是用戶本身加的加強代碼
        try{
            m.invoke(target);
        } catch (Exception e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();//這行是用戶本身加的加強代碼
        System.out.println("執行該方法用了" + (end - start) + "毫秒");//這行是用戶本身加的加強代碼
    }
}

class Proxy{
    public static Object newProxyInstance(Class intfce, InvocationHandler h) throws Exception {
        //把整個TimeProxy類的實現寫入字符串, 經過編譯這一字符串獲得TimeProxy對象
        String methodStr = "";

        Method[] methods = intfce.getMethods();//拿到接口中的全部方法
        for(Method m : methods){//拼接方法
            methodStr += " @Override\n" +
                    " public void " + m.getName() + "() {\n" +
                    " try{\n" +
                    " Method md = " + intfce.getName() + ".class.getMethod(\"" + m.getName() + "\");\n" +
                    " h.invoke(this, md);\n" +
                    " }catch(Exception e){e.printStackTrace();}\n" +
                    " }\n";
        }
        //拼接出整個類
        String src = "package designPattern.proxy;\n" +
                "import java.lang.reflect.Method;\n" +
                "\n" +
                "class $Proxy1 implements " + intfce.getName() + "{\n" +
                " designPattern.proxy.InvocationHandler h;\n" +
                "\n" +
                " public $Proxy1(InvocationHandler h){\n" +
                " this.h = h;\n" +
                " }\n" +
                "\n" + methodStr +
                "}";

        String filename = System.getProperty("user.dir")
                + "/src/main/java/designPattern/proxy/$Proxy1.java";//文件名(生成類的路徑)
        File f = new File(filename);
        FileWriter fw = new FileWriter(f);
        fw.write(src);
        fw.flush();
        fw.close();

        //編譯
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();//拿到系統當前默認的編譯器, 即Javac
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
        Iterable units = fileManager.getJavaFileObjects(filename);//獲得文件對象
        JavaCompiler.CompilationTask t = compiler.getTask(null, fileManager, null, null, null, units);
        t.call();//進行編譯
        fileManager.close();

        //把class文件加載進內存並建立對象
        URL[] urls = new URL[]{new URL("file:/" + System.getProperty("user.dir") + "/src")};
        URLClassLoader ul = new URLClassLoader(urls);
        Class c = ul.loadClass("designPattern.proxy.$Proxy1");//拿到class對象

        Constructor ctr = c.getConstructor(InvocationHandler.class);//拿到參數爲Moveable的構造方法
        Object m = ctr.newInstance(h);//建立代理對象
        return m;
    }
}

class Tank implements Moveable{
    @Override
    public void move(){
        System.out.println("坦克開始移動...");
        try {
            Thread.sleep((long) (Math.random() * 5000));
            System.out.println("坦克移動結束...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

//輸出結果
坦克開始移動...
坦克移動結束...
執行該方法用了4302毫秒
複製代碼

 

總結

  • 在這裏有必要對上面整個動態代理的實現總結一下.
  • 首先要明確咱們是基於一個接口進行代理, 好比本文中給出了一個Moveable接口, 而Tank坦克類實現了Moveable接口, 並實現了move()方法.
  • 如今咱們想對move()方法進行加強, 好比說記錄這個方法的執行時間, 咱們須要動態地得到一個代理類.
  • 並且爲了讓加強具備可擴展性, 咱們建立了InvocationHandler接口, 裏面有一個invoke(Object o, Method m)方法. 調用invoke()方法時, 須要傳遞兩個參數, 一個是代理對象的引用o(可能會用上), 另外一個是須要被加強的方法, 本例中是move()方法.
  • invoke()方法中咱們能夠在被加強方法的先後添加加強代碼.
public void invoke(Object o, Method m) {
    long start = System.currentTimeMillis();//這行是用戶本身加的加強代碼
    try{
        m.invoke(target);//執行被加強的方法, 例子中的move()
    } catch (Exception e) {
        e.printStackTrace();
    }
    long end = System.currentTimeMillis();//這行是用戶本身加的加強代碼
    System.out.println("執行該方法用了" + (end - start) + "毫秒");//這行是用戶本身加的加強代碼
}
複製代碼
  • 補充一點, 要建立InvocationHandler的具體對象, 好比這裏的TimeHander, 須要傳入被加強的對象, 這裏是tank, 由於被加強方法move()須要由被加強對象執行.

 

  • 搞定InvocationHandler後, 回頭看爲咱們動態產生代理的Proxy類, 這個類須要有一個屬性字段InvocationHandler h, 由於在進行加強時, 調用的是InvocationHandler實現類中的invoke()方法. 在動態代理進階一節的最後版本代碼中, 咱們動態生成的代理類源碼是這樣的:
class $Proxy1 implements designPattern.proxy.Moveable{
    designPattern.proxy.InvocationHandler h;

    public $Proxy1(InvocationHandler h){
        this.h = h;
    }

    @Override
    public void move() {
        try{
            Method md = designPattern.proxy.Moveable.class.getMethod("move");
            h.invoke(this, md);
        }catch(Exception e){e.printStackTrace();}
    }
}
複製代碼
  • 由源碼能夠看到當調用代理類的move()方法進行加強時, 會調用InvocaitonHandler的實現類中的invoke()方法, 傳入代理類自身和被加強的方法, 這樣就可使用自定義的加強代碼進行加強了.

 

  • 動態代理有什麼好處?
  1. 對於任意一個實現了某個接口的類, 咱們均可以對其實現的接口中定義的方法進行加強.
  2. 能夠在被加強方法先後自定義加強的邏輯.
  3. 能夠進行多層嵌套代理.
相關文章
相關標籤/搜索