[TOC] 個人認為動態代理在設計模式中算是比較難的, 本篇文章將從無到有, 從一個簡單代碼示例開始迭代, 逐步深入講解動態代理思想. 場景引入 假設現在有一個坦克類, 它實現了 介面, 裡面有一個 移動的方法. 代碼如下: 為了能計算坦克移動所花費的時間我們打算在坦克的 方法的前後添加一些代碼, ...
目錄
個人認為動態代理在設計模式中算是比較難的, 本篇文章將從無到有, 從一個簡單代碼示例開始迭代, 逐步深入講解動態代理思想.
場景引入
- 假設現在有一個坦克類, 它實現了
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()
方法, 傳入代理類自身和被增強的方法, 這樣就可以使用自定義的增強代碼進行增強了.
- 動態代理有什麼好處?
- 對於任意一個實現了某個介面的類, 我們都可以對其實現的介面中定義的方法進行增強.
- 可以在被增強方法前後自定義增強的邏輯.
- 可以進行多層嵌套代理.