1. 程式人生 > 實用技巧 >動態代理-你不必知道我的存在

動態代理-你不必知道我的存在

一、舉例

要計算某個類的某個方法運行了多長時間?比如Tank類的move方法,要計算坦克移動了多長時間。

坦克可以移動,抽象出介面Moveable,裡面一個move() 方法。

實現類Tank,實現Moveable介面

public class Tank implements Moveable{

    @Override
    public void move() {
        //計算方法運行了多長時間
        long start = System.currentTimeMillis();
        System.out.println("Tank Moving...");

        
try { Thread.sleep(new Random().nextInt(10000)); } catch (InterruptedException e) { e.printStackTrace(); } long end = System.currentTimeMillis(); System.out.println("time:"+(end-start)); } }

如果你能修改原始碼,可以在move方法內部的前後,計算開始、結束時間,相減就是move方法執行的時間。

如果你不能修改原始碼,怎麼辦?

1,用繼承

新建Tank2 ,繼承Tank,重寫move()方法,在move方法的前後,加上計算時間的邏輯

public class Tank2 extends Tank{

    @Override
    public void move() {
        //計算方法運行了多長時間
        long start = System.currentTimeMillis();
        super.move();
        long end = System.currentTimeMillis();
        System.out.println(
"time:"+(end-start)); } }

2,用聚合

新建Tank3 ,實現Moveable介面,重寫move()方法。Tank3有一個成員變數Tankmove()方法裡呼叫Tankmove方法,Tank3其實就是Tank的一個代理。

public class Tank3 implements Moveable{

    Tank t;
    public Tank3(Tank t) {
        super();
        this.t = t;
    }
    @Override
    public void move() {
        //計算方法運行了多長時間
        long start = System.currentTimeMillis();
        t.move();
        long end = System.currentTimeMillis();
        System.out.println("time:"+(end-start));
    }

}

繼承和聚合,都能實現計算move方法執行時長的問題,但是聚合更靈活。

Tank3Tank2都是Tank的一個代理。這裡就是靜態的代理

假設現在想要實現一個功能,先記錄執行時間,再記錄日誌,那麼如果用繼承,就得這樣寫:

新建一個類,繼承Tank2(記錄執行時間的代理)

public class Tank2_1 extends Tank2{

    @Override
    public void move() {
        //記錄日誌
        System.out.println("Tank start....");
        super.move();
        System.out.println("Tank end....");
    }    
}

這樣Test測試列印:

Tank start....

Tank Moving...

time:9528

Tank end....

如果想先記錄時間,再記錄日誌呢?就要再新建一個類,順序是,用時間的代理類,去繼承日誌的代理類,如果還有其他的代理,如許可權檢查的代理,等等,調換記錄順序,會更麻煩。。。代理類會無限制的多下去。

如果用聚合實現代理之間的組合呢?

用聚合實現代理,代理物件被代理對象要實現同一個介面:

TankLogProxy

public class TankLogProxy implements Moveable{

    Moveable m;
    public TankLogProxy(Moveable m) {
        super();
        this.m = m;
    }

    @Override
    public void move() {
        System.out.println("Tank start....");
        m.move();
        System.out.println("Tank end....");
    }

}

TankTimeProxy

public class TankTimeProxy implements Moveable{

    Moveable m;
    
    public TankTimeProxy(Moveable m) {
        super();
        this.m = m;
    }

    @Override
    public void move() {
        //計算方法運行了多長時間
        long start = System.currentTimeMillis();
        System.out.println("start:"+start);
        m.move();
        long end = System.currentTimeMillis();
        System.out.println("time:"+(end-start));
    }

}

測試:

先時間,再日誌:

Tank tank = new Tank();
TankLogProxy tlp = new TankLogProxy(tank);
TankTimeProxy ttp = new TankTimeProxy(tlp);
ttp.move();

列印:

start:1581495475807

Tank start....

Tank Moving...

Tank end....

time:6543

先日誌,再時間,只要調換測試類的代理順序即可:

Tank tank = new Tank();
TankTimeProxy ttp = new TankTimeProxy(tank);
TankLogProxy tlp = new TankLogProxy(ttp);
tlp.move();

列印結果:

Tank start....

start:1581495785543

Tank Moving...

time:2139

Tank end....

可以看到,用聚合實現代理,要比用繼承靈活的多!

第二個問題,先只考慮TimeProxy

Moveable介面:新新增stop()方法

public interface Moveable {
    void move();
    void stop();
}

Tank也實現stop方法

@Override
    public void stop() {
        System.out.println("Tank Stoping...");
    }

TankTimeProxy也記錄stop方法的執行時間:

public class TankTimeProxy implements Moveable{

    Moveable m;
    
    public TankTimeProxy(Moveable m) {
        super();
        this.m = m;
    }

    @Override
    public void move() {
        //計算方法運行了多長時間
        long start = System.currentTimeMillis();
        System.out.println("start:"+start);
        m.move();
        long end = System.currentTimeMillis();
        System.out.println("time:"+(end-start));
    }

    @Override
    public void stop() {
        //計算方法運行了多長時間
        long start = System.currentTimeMillis();
        System.out.println("start:"+start);
        m.stop();
        long end = System.currentTimeMillis();
        System.out.println("time:"+(end-start));
        
    }

}

如果一段程式碼重複出現了多次,就要考慮封裝了,movestop方法,都有計算時間的邏輯,可以考慮將他們封裝成為方法。

現在如果要有個Car類的move方法,要記錄汽車移動的時間,就需要再寫個CarProxy

如果再有個Animal類的eat方法,要記錄動物吃的時間,就要有個AnimalProxy

...... 如果一個系統有100個類,就要有100個代理類出現,又出現了類爆炸。

所以現在有個需求就是:

能不能產生一個代理類,可以給所有的類做代理呢???

從上邊的例子可以看出,用聚合產生代理,需要代理類和被代理類實現同一個介面。

現在假設,假設被代理的類都實現某一個介面,(Spring裡面也是這麼要求的,Spring也能用繼承實現代理但是不推薦),就能給這個類生成代理。

二,下面模擬JDK的實現

站在使用者的角度,有一個專門產生代理的類,假設現在只是產生時間的代理

    //站在使用者的角度,動態代理,Proxy產生一個代理類的物件,你根本看不到這個代理類的名字
        Moveable m = (Moveable)Proxy.newProxyInstance();
        m.move();
/**
 * 產生代理的類
 * @author dev
 *
 */
public class Proxy {

    public static Object newProxyInstance(){
        //只要能動態的 編譯這段程式碼,就能動態的產生代理類!類的名字無所謂
        //動態編譯的技術:JDK6 Compiler API,CGLib(用到了ASM) ,ASM
        //(CGLib、ASM不用原始碼來編譯,能直接生成二進位制檔案,因為java的二進位制檔案格式是公開的)
        //Spring內部,如果是實現介面就是用的JDK本身的API產生代理,否則就用CGLib
        //換行字串
        String rt = "\r\n";
        String src = 
        "package com.lhy.proxy;"+ rt +

        "public class TankTimeProxy implements Moveable{"+rt +

        "    Moveable m;"+rt +
            
        "    public TankTimeProxy(Moveable m) {"+rt +
        "        super();"+rt +
        "        this.m = m;"+rt +
        "    }"+rt +

        "    @Override" +rt +
        "    public void move() {" +rt +
                //計算方法運行了多長時間
        "        long start = System.currentTimeMillis();" +rt +
        "        System.out.println(\"start:\"+start);" +rt +
        "        m.move();"+rt +
        "        long end = System.currentTimeMillis();"+rt +
        "        System.out.println(\"time:\"+(end-start));"+rt +
        "    }"+rt +
        "}";

        return null;
    } 
}

新建測試類,測試用java程式碼產生代理類,然後進行編譯,然後load到記憶體進行載入,用反射新建一個代理類的物件。

package com.lhy.proxy;

import java.io.File;
import java.io.FileWriter;
import java.lang.reflect.Constructor;
import java.net.URL;
import java.net.URLClassLoader;

import javax.tools.JavaCompiler;
import javax.tools.JavaCompiler.CompilationTask;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;

public class TestCompiler {

    public static void main(String[] args) throws Exception{
        String rt = "\r\n";
        String src = 
        "package com.lhy.proxy;"+ rt +

        "public class TankTimeProxy implements Moveable{"+rt +

        "    Moveable m;"+rt +
            
        "    public TankTimeProxy(Moveable m) {"+rt +
        "        super();"+rt +
        "        this.m = m;"+rt +
        "    }"+rt +

        "    @Override" +rt +
        "    public void move() {" +rt +
                //計算方法運行了多長時間
        "        long start = System.currentTimeMillis();" +rt +
        "        System.out.println(\"start:\"+start);" +rt +
        "        m.move();"+rt +
        "        long end = System.currentTimeMillis();"+rt +
        "        System.out.println(\"time:\"+(end-start));"+rt +
        "    }"+rt +
        "}";
        
        //1,生成代理類
        String fileName = System.getProperty("user.dir")
                            +"/src/com/lhy/proxy/TankTimeProxy.java";//獲取專案根路徑
        File file = new File(fileName);
        FileWriter fw = new FileWriter(file);
        fw.write(src);
        fw.flush();
        fw.close();
        
        //2,將生成的類進行編譯成class檔案
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();//拿到系統預設的編譯器(其實就是javac)
        StandardJavaFileManager fileMgr = compiler.getStandardFileManager(null, null, null);//診斷監聽器;語言;編碼
        Iterable units =  fileMgr.getJavaFileObjects(fileName);
        CompilationTask task = compiler.getTask(null, fileMgr, null, null, null, units);
        task.call();
        fileMgr.close();
        
        //3,將class load到記憶體 
        URL[] urls = new URL[]{new URL("file:/"+ System.getProperty("user.dir")+"/src")};
        URLClassLoader urlClassLoader = new URLClassLoader(urls);
        Class clazz = urlClassLoader.loadClass("com.lhy.proxy.TankTimeProxy");
        //System.out.println(clazz);
        //4,,建立一個物件
        //不能用 clazz.newInstance();建立物件因為它會呼叫空構造方法
        Constructor<Moveable> constructor = clazz.getConstructor(Moveable.class);//獲取某個型別引數的構造器
        Moveable m = constructor.newInstance(new Tank());//
        m.move();
    
    }
}

列印結果

start:1581515256858

Tank Moving...

time:6611

生成的代理類和編譯後的class

測試結果可以看出,可以動態產生代理類,你看不到代理類的名字,你只要呼叫Proxy.newProxyInstance()方法就能返回一個代理類,這就是動態代理,用完你就可以吧代理類的程式碼刪了

但是現在產生的代理 是實現了Moveable介面的代理,要想產生實現任意介面的代理怎麼辦呢? 只要把介面傳給產生代理的方法就可以了。而且 ,介面的方法,也要動態生成,這就需要用到反射了:

反射拿到介面的方法程式碼:

Method[] methods = Moveable.class.getMethods();
        for(Method m : methods){
            System.err.println(m.getName());//move
        }

修改後的產生代理的類:

用反射拿到介面的所有方法,動態的構建代理類的方法

package com.lhy.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.JavaCompiler.CompilationTask;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;

/**
 * 產生代理的類
 * @author dev
 *
 */
public class Proxy {

    public static Object newProxyInstance(Class interfaces) throws Exception{//動態傳入介面,其實jdk可以傳多個介面
        //換行字串
        String rt = "\r\n";
        String methodStr = "";
        //反射拿到介面的所有的方法
        Method[] methods = interfaces.getMethods();
        for(Method m : methods){
            methodStr += "@Override"+rt +
                    "public void "+ m.getName()+ "() {"+
                    //計算方法運行了多長時間
                    "        long start = System.currentTimeMillis();" +rt +
                    "        System.out.println(\"start:\"+start);" +rt +
                    "        m."+m.getName() +"();" +rt +
                    "        long end = System.currentTimeMillis();"+rt +
                    "        System.out.println(\"time:\"+(end-start));"+rt +
                    "}";
        }
        
        //只要能動態的 編譯這段程式碼,就能動態的產生代理類!類的名字無所謂
        //動態編譯的技術:JDK6 Compiler API,CGLib(用到了ASM) ,ASM
        //(CGLib、ASM不用原始碼來編譯,能直接生成二進位制檔案,因為java的二進位制檔案格式是公開的)
        //Spring內部,如果是實現介面就是用的JDK本身的API產生代理,否則就用CGLib
        
        String src = 
        "package com.lhy.proxy;"+ rt +

        "public class TankTimeProxy implements "+ interfaces.getName() +"{"+rt +

        "    Moveable m;"+rt +
            
        "    public TankTimeProxy(Moveable m) {"+rt +
        "        super();"+rt +
        "        this.m = m;"+rt +
        "    }"+rt +

         methodStr +
        "}";
        
        //1,生成代理類
        String fileName = System.getProperty("user.dir")
                            +"/src/com/lhy/proxy/TankTimeProxy.java";//獲取專案根路徑
        File file = new File(fileName);
        FileWriter fw = new FileWriter(file);
        fw.write(src);
        fw.flush();
        fw.close();
        
        //2,將生成的類進行編譯成class檔案
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();//拿到系統預設的編譯器(其實就是javac)
        StandardJavaFileManager fileMgr = compiler.getStandardFileManager(null, null, null);//診斷監聽器;語言;編碼
        Iterable units =  fileMgr.getJavaFileObjects(fileName);
        CompilationTask task = compiler.getTask(null, fileMgr, null, null, null, units);
        task.call();
        fileMgr.close();
        
        //3,將class load到記憶體 
        URL[] urls = new URL[]{new URL("file:/"+ System.getProperty("user.dir")+"/src")};
        URLClassLoader urlClassLoader = new URLClassLoader(urls);
        Class clazz = urlClassLoader.loadClass("com.lhy.proxy.TankTimeProxy");
        System.out.println(clazz);
        //4,,建立一個物件
        //不能用 clazz.newInstance();建立物件因為它會呼叫空構造方法
        Constructor<Moveable> constructor = clazz.getConstructor(Moveable.class);//獲取某個型別引數的構造器
        Object obj = constructor.newInstance(new Tank());//
        
        return obj;
    } 
}

測試

產生的代理類TankTimeProxy

package com.lhy.proxy;

public class TankTimeProxy implements com.lhy.proxy.Moveable {
    Moveable m;

    public TankTimeProxy(Moveable m) {
        super();
        this.m = m;
    }

    @Override
    public void move() {
        long start = System.currentTimeMillis();
        System.out.println("start:" + start);
        m.move();
        long end = System.currentTimeMillis();
        System.out.println("time:" + (end - start));
    }
}

結論:

到目前為止,已經可以動態建立某個介面的代理類,並呼叫代理類的方法,但是目前的代理只是實現了時間的代理,代理的邏輯是寫死的,肯定不能寫死,那怎麼寫活呢?

思路:代理的邏輯,可以自己指定

寫一個處理代理邏輯的介面

import java.lang.reflect.Method;
public interface InvocationHandler {

    /**
     * 代理執行的邏輯
     * @param o 方法所屬的物件
     * @param m 要執行的方法
     */
    public void invoke(Object o,Method m);
}

時間的代理類的處理邏輯,實現InvocationHandler 介面

import java.lang.reflect.Method;

public class TimeHandler implements InvocationHandler{

    //被代理類
    private Object target;
    

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

    @Override
    public void invoke(Object o,Method m) {
        long start = System.currentTimeMillis();
        System.out.println("start:" + start);
        try {
            m.invoke(target, new Object[]{});
        } catch (Exception e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("time:" + (end - start));
        
    }
}

產生代理類的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.JavaCompiler.CompilationTask;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;

/**
 * 產生代理的類
 * @author dev
 *
 */
public class Proxy {
    /**
     * 
     * @param interfaces 代理實現的介面
     * @param h 代理處理邏輯
     * @return
     * @throws Exception
     */
    public static Object newProxyInstance(Class interfaces,InvocationHandler h) throws Exception{//動態傳入介面,其實jdk可以傳多個介面
        //換行字串
        String rt = "\r\n";
        String methodStr = "";
        //反射拿到介面的所有的方法
        Method[] methods = interfaces.getMethods();
        for(Method m : methods){
            methodStr += "@Override"+rt +
                    "public void "+ m.getName()+ "() {"+
                    "    try{"+rt+
                    "    Method md = "+ interfaces.getName()+".class.getMethod(\""+m.getName()+"\");"+rt+    
                    "    h.invoke(this,md);"+rt+ //this->代理物件
                        "    }catch(Exception e){e.printStackTrace();}"+
                    "}";
        }
        
        //只要能動態的 編譯這段程式碼,就能動態的產生代理類!類的名字無所謂
        //動態編譯的技術:JDK6 Compiler API,CGLib(用到了ASM) ,ASM
        //(CGLib、ASM不用原始碼來編譯,能直接生成二進位制檔案,因為java的二進位制檔案格式是公開的)
        //Spring內部,如果是實現介面就是用的JDK本身的API產生代理,否則就用CGLib
        
        String src = 
        "package com.lhy.proxy;"+ rt +
        "import java.lang.reflect.Method;"+rt+
        "public class $Proxy1 implements "+ interfaces.getName() +"{"+rt +

        "    com.lhy.proxy.InvocationHandler h;"+rt+
        "    public $Proxy1(InvocationHandler h) {"+rt +
        "        this.h = h;"+rt +
        "    }"+rt +

         methodStr +
        "}";
        
        //1,生成代理類
        String fileName = System.getProperty("user.dir")
                            +"/src/com/lhy/proxy/$Proxy1.java";//獲取專案根路徑
        File file = new File(fileName);
        FileWriter fw = new FileWriter(file);
        fw.write(src);
        fw.flush();
        fw.close();
        
        //2,將生成的類進行編譯成class檔案
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();//拿到系統預設的編譯器(其實就是javac)
        StandardJavaFileManager fileMgr = compiler.getStandardFileManager(null, null, null);//診斷監聽器;語言;編碼
        Iterable units =  fileMgr.getJavaFileObjects(fileName);
        CompilationTask task = compiler.getTask(null, fileMgr, null, null, null, units);
        task.call();
        fileMgr.close();
        
        //3,將class load到記憶體 
        URL[] urls = new URL[]{new URL("file:/"+ System.getProperty("user.dir")+"/src")};
        URLClassLoader urlClassLoader = new URLClassLoader(urls);
        Class clazz = urlClassLoader.loadClass("com.lhy.proxy.$Proxy1");
    
        //4,,建立一個物件
        //不能用 clazz.newInstance();建立物件因為它會呼叫空構造方法
        Constructor constructor = clazz.getConstructor(InvocationHandler.class);//獲取某個型別引數的構造器
        Object obj = constructor.newInstance(h);//
        
        return obj;
    } 
}

測試程式碼:

public static void main(String[] args) throws Exception{
        InvocationHandler h = new TimeHandler(new Tank());
        //站在使用者的角度,動態代理,Proxy產生一個代理類的物件,你根本看不到這個代理類的名字
        Moveable m = (Moveable)Proxy.newProxyInstance(Moveable.class,h);
        m.move();
    }

列印結果:

start:1581596505206

Tank Moving...

time:5193

產生的代理類$Proxy1:

import java.lang.reflect.Method;

public class $Proxy1 implements com.lhy.proxy.Moveable {
    com.lhy.proxy.InvocationHandler h;

    public $Proxy1(InvocationHandler h) {
        this.h = h;
    }
    @Override
    public void move() {
        try {
            Method md = com.lhy.proxy.Moveable.class.getMethod("move");
            h.invoke(this, md);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

現在,想實現什麼代理,只要實現InvocationHandler介面,自定義代理的處理邏輯,即可實現代理,這就是動態代理。

二,實際舉例說明

UserMgr介面:

public interface UserMgr {
    void addUser();
}

UserMgr實現類

public class UserMgrImpl implements UserMgr {
    @Override
    public void addUser() {
        System.err.println("插入到資料庫user表");
        System.err.println("記錄到日誌表");
    }
}

事務代理處理邏輯TransitionHandler

import java.lang.reflect.Method;
import com.lhy.proxy.InvocationHandler;

public class TransitionHandler implements InvocationHandler{
    private Object target;
    public TransitionHandler(Object target) {
        this.target = target;
    }
    @Override
    public void invoke(Object o, Method m) {
        System.err.println("事務開始....");
        try {
            m.invoke(target, new Object[]{});
        } catch (Exception e) {
            e.printStackTrace();
            System.err.println("事務回滾....");
        }
        System.err.println("事務提交....");
    }
}

測試類:

import com.lhy.proxy.InvocationHandler;
import com.lhy.proxy.Proxy;

public class Client {
    public static void main(String[] args) throws Exception{
        UserMgr userMgr = new UserMgrImpl();
        InvocationHandler h = new TransitionHandler(userMgr);
        UserMgr proxy = (UserMgr)Proxy.newProxyInstance(UserMgr.class, h);
        proxy.addUser();
    }
}

執行:

事務開始....

插入到資料庫user

記錄到日誌表

事務提交....

產生的事務代理類:

import java.lang.reflect.Method;

public class $Proxy1 implements com.lhy.proxy.test.UserMgr {
    com.lhy.proxy.InvocationHandler h;

    public $Proxy1(InvocationHandler h) {
        this.h = h;
    }
    @Override
    public void addUser() {
        try {
            Method md = com.lhy.proxy.test.UserMgr.class.getMethod("addUser");
            h.invoke(this, md);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

從執行結果可以看出,已經控制了事務!

動態代理:不用修改原來的實現的程式碼,就能在原來基礎上前後插入一些內容

AOP:可插拔的,可以將代理配置在配置檔案,想實現什麼樣的代理就實現什麼樣的代理。代理之間是可以疊加的

AOP的運用:日誌、事務、許可權。。。。

完整程式碼在github,地址: https://github.com/lhy1234/DesignPattern_Proxy.git