動態代理-你不必知道我的存在
一、舉例
要計算某個類的某個方法運行了多長時間?比如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有一個成員變數Tank,move()方法裡呼叫Tank的move方法,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方法執行時長的問題,但是聚合更靈活。
Tank3和Tank2都是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)); } }
如果一段程式碼重複出現了多次,就要考慮封裝了,move和stop方法,都有計算時間的邏輯,可以考慮將他們封裝成為方法。
現在如果要有個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