詳述代理模式及動態代理簡單實現

前言:本文章總結於馬士兵老師系列教程,是根據視頻中提出的問題的思維爲大綱寫的

體會設計模式

可能接觸過設計模式的人都會有一種疑惑:感覺很多設計模式的實現方式都非常的相似,就比如說代理模式和裝飾模式。確實有些設計模式的實現方式是差不多的,但是他們是從不同的場景出發,解決不同的問題的,我們需要從思想的角度來體會設計模式。

代理模式

由一個實際問題來表達代理模式的思想
注:源代碼在最後,過程中的代碼不一定能運行,請參看源代碼

提出問題

新建一個Car類,包含一個drive方法,現在要求是在不改變Car代碼的前提下計算drive運行的時間
Car代碼:

public class Car {
    public void drive() throws InterruptedException {
        System.out.println("開車了...");
        Thread.sleep(1000);
    }
}

解決統計時間的問題

如果要統計時間,我們現在要將代碼寫成這樣:

public class Car {
    /**
     * 解決這個問題我們需要在drive運行開始和結束的位置加入系統當前時間
     * 再得到兩個時間差
     * @throws InterruptedException
     */
    @Test
    public void drive() throws InterruptedException {
        //獲取開始時間點
        long i = System.currentTimeMillis();
        System.out.println("開車了...");
        Thread.sleep(1000);
        //獲取結束時間點
        long j = System.currentTimeMillis();
        System.out.println("車開了"+(j-i)+"millis");
    }
}

解決不能使用改動drive代碼的問題

現在是不能改動drive的那要怎麼把計算時間的代碼插入進去呢?
這個時候我們就需要用到代理模式了,Car的代碼不能改但是它的代理類可以,而代理方式也有兩種如下:
1. 通過繼承Car來代理

public class ExtendsCar extends Car {
    //這樣我們可以通過重寫Car的drive方法來實現代理
    @Override
    public void drive() throws InterruptedException {
        long i = System.currentTimeMillis();
        super.drive();
        long j = System.currentTimeMillis();
        System.out.println("車開了:"+(i+j)+"millis");
    }
}
  1. 另外一種方式通過聚合的代理

聚合,一個類中包含另外一個類。
用聚合的方式代理時,我們需要代理類和被代理類有同樣的行爲,我們可以通過實現相同的接口來實現

接口代碼:

public interface Move {
    public void drive();
}

讓Car類implement接口,這裏就不貼代碼了
代理類實現接口:

public class TimeCarProxy implements Move {

    Car c ;
    public TimeCarProxy(Car c){
        this.c=c;
    }
    @Override
    public void drive() {
        long i = System.currentTimeMillis();
        System.out.println("開始時間:" + i);
        move.drive();
        long j = System.currentTimeMillis();
        System.out.println("結束時間:" + j);
        System.out.println("車開了:"+(j-i)+"millis");
    }
}

問題:兩種實現方式那個比較好?

這裏還是以一個問題來表達:

如果我還要在前面drive前後加上啓動和停止怎麼辦?

  1. 以繼承的方式實現,我們的思路是讓它的代理類再被代理,代碼如下:
public class CarAction extends ExtendsCar {
    @Override
    public void drive() {
        System.out.println("啓動車!");
        super.drive();
        System.out.println("停止車!");
    }
}
這種方式的問題:
    1. 繼承被佔用,不能再繼承其它類
    2. 主要的問題:如果我要將他們的順序反過來,先啓動停止在計算時間的話,那不就意味着我們要重新寫它的代理類嗎?此時繼承的侷限性就顯現出來了

2. 以聚合的方式實現:我們可以再來一個代理實現Move接口,代碼如下:

public class Action implements Move {
    ImplementMove move ;
    public Action(ImplementMove move){
        this.move=move;
    }
    @Override
    public void drive() {
        System.out.println("車啓動!");
        move.drive();
        System.out.println("車停止!");
    }
}

到這裏可能會有疑問,這不是跟繼承存在一樣的問題嗎?別急,請看下面一種實現:

public class ActionCarProxy implements Move {
    /**
     * 因爲它們有一個共同的特點就是實現了Move,若我們把聚合對象換成
     * Move接口不就想讓誰代理就誰代理了嗎?
     * 同樣我們也要將代理時間的類改成Move
     */
    Move move ;
    public ActionCarProxy (Move move){
        this.move=move;
    }
    @Override
    public void drive() {
        System.out.println("車啓動!");
        move.drive();
        System.out.println("車停止!");
    }
}

可能會有點繞,這裏寫個測試類來理一理思路,測試類代碼:

public class TestCar {
   public static void main(String args[]){
       Car car = new Car();
       //若我們想先開始計算時間再啓動停止
//       TimeCarProxy t = new TimeCarProxy(car);
//       ActionCarProxy a = new ActionCarProxy(t);
//       a.drive();
       //若我們想先啓動停止再計算時間
       ActionCarProxy a = new ActionCarProxy(car);
       TimeCarProxy t = new TimeCarProxy(a);
       t.drive();
   }
}

運行效果:
1. 先計算時間再啓動
proxy1
2. 先啓動再計算時間
proxy2
以上就實現了一個靜態代理。從上面的效果可以看出實現了預期的效果,我們可以任意的指定誰先代理誰後代理,可以看作是橫向擴展了。

到這裏會有很多人有疑惑,這不是裝飾設計模式嗎?從實現語法的角度來講確實是很像裝飾,但是兩者的着重點不同,裝飾模式旨在擴展功能,這裏是以代理Car類去解決問題,語義,出發點是不同的,前面講到過設計模式的體會,這裏還需要讀者慢慢去體會。

動態代理

注意:這裏開始模仿JDK實現Proxy

這裏跟着上面的思路來,我們引入一個新的問題:

如過Car裏有多個方法要求計算運行時間怎麼處理?

這樣的話我們需要在TimeCarProxy 中的每個方法前後獲取當前時間並計算,那這樣的話我們會發現TimeCarProxy中出現了很多的重複代碼,當然我們可以給重複的代碼簡單封裝,當那也沒從根本上解決問題。

這個問題暫且擱置,先看下一個問題,這個時候請注意,請將重點放到TimeCarProxy 上來。

如果我們需要TimeCarProxy 不僅代理Car還能代理其它類對象

也就是一個萬能的TimeProxy代理,可以代理任意對象執行計算方法運行時間,這個時候我們需要怎麼辦?
首先我們需要一個代理對象:

//jdk中Proxy就是動態代理
public class Proxy {
    //產生並返回一個代理對象
    public static Object newProxyInstance(){
        //我們需要在這裏動態的生成代理對象
        return null;
    }
}

那我們要怎麼動態生成對象呢?


  1. 我們首先要得到要有生成對象的代碼,但是代碼不能交給程序處理,所以我們要將代碼轉化成程序能處理的形式,那就是字符串。
  2. 用字符串表示代碼後我們就可以任意的構造出我們想要的代碼,讓後將字符串輸出到一個java文件中交給程序去編譯
  3. 那程序要怎麼編譯java文件呢?

JDK6爲我們提供了Complier API ,另外還有CGlib、ASM插件可以直接生成二進制文件不用再編譯了,Spring中也支持通過CGlib方式實現動態代理
現在暫時不管生成字符串的邏輯,我們先解決編譯的問題
代碼:

public class FileTest {
    @Test
    public void test() throws IOException {
        //用來生成代理對象的代理類的字符串形式
        String src="" +
                "package net.hncu.test;\n" +
                "public class TmpProxy implements Move {\n" +
                "    Move move ;\n" +
                "    public TmpProxy(Move move){\n" +
                "        this.move=move;\n" +
                "    }\n" +
                "    @Override\n" +
                "    public void drive() {\n" +
                "        long i = System.currentTimeMillis();\n" +
                "        System.out.println(\"開始時間:\" + i);\n" +
                "        move.drive();\n" +
                "        long j = System.currentTimeMillis();\n" +
                "        System.out.println(\"結束時間:\" + j);\n" +
                "        System.out.println(\"車開了:\"+(j-i)+\"millis\");\n" +
                "    }\n" +
                "}";
        //將字符串保存成一個java文件System.getProperty("user.dir")獲得項目路徑
        String filename=System.getProperty("user.dir")+"/src/net/hncu/test/TmpProxy.java";
        //新建文件
        File file = new File(filename);
        //將字符串寫到文件
        FileWriter fileWriter =new FileWriter(file);
        fileWriter.write(src);
        fileWriter.close();
         /**
         *  編譯java文件這裏對編譯過程不做過多闡述,如果感興趣可以去查看api
         */
        //獲取java編譯器jdk6支持
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        //獲取一個file管理器(三個參數diagnosticListener監聽編譯過程的監聽器
        // locale國際化相關,charset指定字符集)所有參數爲空時,指定默認配置
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(null,null,null);
        //根據文件名字拿到java文件對象(可以填多個文件,獲得多個對象)返回一個文件對象的迭代器
        Iterable<? extends JavaFileObject> units = fileManager.getJavaFileObjects(filename);
        //建立一次編譯任務(參數:out輸出位置,fileManager文件管理器,diagnosticListener監聽器,
        // options編譯時的參數,暫不填,classes編譯時所需要的class文件,compilationUnits需要編譯的單元)
        JavaCompiler.CompilationTask task = compiler.getTask(null,fileManager,null,null,null,units);
        //執行編譯
        task.call();
        //關閉文件管理器
        fileManager.close();
    }
}

效果圖:
proxy3
從圖中我們可以生成了TmpProxy 的java文件和class文件
接下來我們又要考慮下一個問題了。

我們需要把我們生成的class文件加載到內存來生成一個代理對象

這裏只貼部分代碼:

 //加載class 文件到內存,
        //直接從指定URL位置加載class文件到內存,其實我們也可以直接將class存到bin目錄下,但是可能會造成衝突
        // 首先我們需要一個URL數組指定加載class文件的路徑,
        URL[] urls = new URL[]{new URL("file:/"+System.getProperty("user.dir")+"/src")};
        //新建一個URL類加載器
        URLClassLoader classLoader = new URLClassLoader(urls);
        //加載路徑下的指定class文件
        Class aClass = null;
        try {
            aClass = classLoader.loadClass("net.hncu.test.TmpProxy");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        //System.out.println("aClass: "+aClass+" from: "+"FileTest.test");

        //利用反射操作class對象
        //構造一個實例
        Constructor constructor = aClass.getConstructor(inter);
        return  constructor.newInstance(object);

好了,到此爲止,我們達到了動態生成代理對象的目的了,但是我們會發現還是隻能動態生成TimeProxy,但是你別忘了,TimeProxy是由字符串生成的,而我們動態修改字符串是不是容易多了。接下來我們要做的就是修改字符串。

問題來了,我們需要如何修改字符串讓其動態生成我們需要的代碼呢?

  1. 首先我們需要生成任意對象的代理類,我們需要告訴它我們要生成代理類的規範,即被代理類的接口

    "public class TmpProxy implements "+inter.getName()+" {\n" +
    
                    "    public TmpProxy("+handler.getClass().getName()+" tmp){\n" +
                    "        this.tmp=tmp;\n" +
                    "    }\n" +
  2. 然後我們需要得到接口裏的方法對代理類中的方法進行重新編排

    //根據接口的class文件動態實現多個方法的代理字符串拼接
    String methodStr="";
    Method[] methods = inter.getMethods();
    for (Method method : methods) {
    methodStr+=
    " @Override\n" +
    //這裏方法名要改成當前的方法名
    " public void "+method.getName()+"() {\n" +
    " try{\n"+
    " Method method = "+inter.getName()+".class.getMethod(\""+method.getName()+"\");\n"+
    " tmp.invoke(this,method);\n" +
    " }catch(Exception e){\n"+
    " e.printStackTrace();\n"+
    " }\n"+
    " }\n";
  3. 修改好後我們可以動態的生成任何對象的代理對象,只是生成的代理對象固定的只能統計運行時間業務,所以我們還需要一個處理業務的邏輯。

那麼我們需要怎樣來修改業務邏輯呢?

因爲業務邏輯是需要用戶自己來定義的,所以不能寫死在字符串中,當是業務邏輯需要有一定的編寫規範,所以最好的選擇就是通過一個接口來規範業務邏輯處理,讓後讓用戶來實現接口定義自己的業務邏輯。

public interface ProxyHandler {
    //在自定義方法模塊時我們肯定要執行被代理類本身的方法,
    //所以我們至少需要以下兩個參數
    public void invoke(Object object,Method m);
}

分析:該接口定義了目標類方法的實現規則,所以我們在實現該接口的時候需要告知它目標類。
實現代碼例子:

public class TimeHandler implements ProxyHandler {
    Object target;
    public TimeHandler(Object target){
        this.target=target;
    }
    @Override
    public void invoke(Object object,Method m) {
        long start = System.currentTimeMillis();
        System.out.println("開始時間:"+start);
        try {
            m.invoke(target);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        long end= System.currentTimeMillis();
        System.out.println("結束時間:"+end);
        System.out.println("用時:"+(end-start));
    }
}

注意點:Proxy中的invoke實際上是調用的是handler中的invoke
如此一來,就可以實現處理任何的業務邏輯了,同時簡單的代理模式也實現了。

結論

目前這個只是簡單的實現,只能做沒有參數列表、返回值的,後續有時間再去完善

源碼

注意:

在第一遍運行時會出現ClassNoFound異常,那是因爲編譯的class文件並沒有馬上寫到目錄下,重新運行就可以出來結果了,至於如何改這個bug我還沒研究出來。
Car:

package net.hncu.test;

import org.junit.Test;

/**
 * Project: String1
 * Desc: proxy test
 * Author: AMX50B
 * Date: 2017-10-20 19:08
 */
public class Car implements Move {
    /**
     * 解決這個問題我們需要在drive運行開始和結束的位置加入系統當前時間
     * 再得到兩個時間差
     */
    @Test
    public void drive(){
        System.out.println("開車了...");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void flay() {
        System.out.println("起飛了...");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Move:

package net.hncu.test;

/**
 * Created by AMX50B on 2017/10/20
 */
public interface Move {
    public void  drive();
    public void  flay();
}

Proxy:

package net.hncu.test;

import org.junit.Test;

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

/**
 * Project: String1
 * Desc: proxy
 * Author: AMX50B
 * Date: 2017-10-21 12:51
 */
public class Proxy {
    //產生並返回一個代理對象
    @Test
    public  static Object newProxyInstance(Class inter,ProxyHandler handler) throws IOException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

        //用來生成代理對象的代理類的字符串形式

        //根據接口的class文件動態實現多個方法的代理字符串拼接
        String methodStr="";
        Method[] methods = inter.getMethods();
        for (Method method : methods) {
            methodStr+=
                    "    @Override\n" +
                    //這裏方法名要改成當前的方法名
                    "    public void "+method.getName()+"() {\n" +
                    "       try{\n"+
                    "         Method method = "+inter.getName()+".class.getMethod(\""+method.getName()+"\");\n"+
                    "         tmp.invoke(this,method);\n" +
                    "         }catch(Exception e){\n"+
                    "              e.printStackTrace();\n"+
                    "         }\n"+
                    "    }\n";
        }
        String src="" +
                "package net.hncu.test;\n" +
                "import java.lang.reflect.Method;\n"+
                //傳入接口名稱,使代理類能動態代理任意我們指定的接口
                "public class TmpProxy implements "+inter.getName()+" {\n" +
                "    "+handler.getClass().getName()+" tmp ;\n" +
                "    public TmpProxy("+handler.getClass().getName()+" tmp){\n" +
                "        this.tmp=tmp;\n" +
                "    }\n" +
               methodStr +
                "}\n";
        //將字符串保存成一個java文件System.getProperty("user.dir")獲得項目路徑
        String filename=System.getProperty("user.dir")+"/src/net/hncu/test/TmpProxy.java";
        //新建文件
        File file = new File(filename);
        //將字符串寫到文件
        FileWriter fileWriter =new FileWriter(file);
        fileWriter.write(src);
        fileWriter.close();
        /**
         *  編譯java文件這裏對編譯過程不做過多闡述,如果感興趣可以去查看api
         */
        //獲取java編譯器jdk6支持
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        //獲取一個file管理器(三個參數diagnosticListener監聽編譯過程的監聽器
        // locale國際化相關,charset指定字符集)所有參數爲空時,指定默認配置
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(null,null,null);
        //根據文件名字拿到java文件對象(可以填多個文件,獲得多個對象)返回一個文件對象的迭代器
        Iterable<? extends JavaFileObject> units = fileManager.getJavaFileObjects(filename);
        //建立一次編譯任務(參數:out輸出位置,fileManager文件管理器,diagnosticListener監聽器,
        // options編譯時的參數,暫不填,classes編譯時所需要的class文件,compilationUnits需要編譯的單元)
        JavaCompiler.CompilationTask task = compiler.getTask(null,fileManager,null,null,null,units);
        //執行編譯
        task.call();
        //關閉文件管理器
        fileManager.close();

        //加載class 文件到內存,
        //直接從指定URL位置加載class文件到內存,其實我們也可以直接將class存到bin目錄下,但是可能會造成衝突
        // 首先我們需要一個URL數組指定加載class文件的路徑,
        URL[] urls = new URL[]{new URL("file:/"+System.getProperty("user.dir")+"/src")};
        //新建一個URL類加載器
        URLClassLoader classLoader = new URLClassLoader(urls);
        //加載路徑下的指定class文件
        Class aClass = null;
        try {
            aClass = classLoader.loadClass("net.hncu.test.TmpProxy");
        } catch (ClassNotFoundException e) {
            try {
                Thread.sleep(1000);
                aClass= classLoader.loadClass("net.hncu.test.TmpProxy");
            } catch (InterruptedException e1) {
                e1.printStackTrace();
            } catch (ClassNotFoundException e1) {
                e1.printStackTrace();
            }
        }
        //System.out.println("aClass: "+aClass+" from: "+"FileTest.test");

        //利用反射操作class對象
        //構造一個實例
        Constructor constructor = aClass.getConstructor(handler.getClass());
        return  constructor.newInstance(handler);
//        move.drive();

    }
}

ProxyHandler:

package net.hncu.test;

import java.lang.reflect.Method;

/**
 * Created by AMX50B on 2017/10/23
 */
public interface ProxyHandler {
    //在自定義方法模塊時我們肯定要執行被代理類本身的方法,
    //所以我們至少需要以下兩個參數
    public void invoke(Object object,Method m);
}

TimeProxy:

package net.hncu.test;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * Project: String1
 * Desc: time handler
 * Author: AMX50B
 * Date: 2017-10-23 18:40
 */
public class TimeHandler implements ProxyHandler {
    Object target;
    public TimeHandler(Object target){
        this.target=target;
    }
    @Override
    public void invoke(Object object,Method m) {
        long start = System.currentTimeMillis();
        System.out.println("開始時間:"+start);
        try {
            m.invoke(target);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        long end= System.currentTimeMillis();
        System.out.println("結束時間:"+end);
        System.out.println("用時:"+(end-start));
    }
}

Test:

package net.hncu.test;

/**
 * Project: String1
 * Desc: test car
 * Author: AMX50B
 * Date: 2017-10-21 9:58
 */
public class TestCar {
   public static void main(String args[]){
       Car car = new Car();
       //若我們想先開始計算時間再啓動停止
//       TimeCarProxy t = new TimeCarProxy(car);
//       ActionCarProxy a = new ActionCarProxy(t);
//       a.drive();
       //若我們想先啓動停止再計算時間
//       ActionCarProxy a = new ActionCarProxy(car);
//       TimeCarProxy t = new TimeCarProxy(a);
//       t.drive();
       try {

           ProxyHandler proxyHandler = new TimeHandler(car);
           Move m = (Move) Proxy.newProxyInstance(Move.class,proxyHandler);
           m.drive();
           m.flay();
       } catch (Exception e) {
           e.printStackTrace();
       }
   }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章