1. 示例
首先,定義一個介面:
public interface Staff {
void work();
}
然后,新增一個類并實作上面的介面:
public class Coder implements Staff {
@Override
public void work() {
System.out.println("認真寫bug……");
}
}
假設現在有這么一個需求:在不改動以上類代碼的前提下,對該方法增加一些前置操作或者后置操作,
接下來就來講解下,如何使用JDK動態代理來實作這個需求,
首先,自定義一個呼叫處理器,實作java.lang.reflect.InvocationHandler
介面并重寫invoke
方法:
public class AttendanceInvocationHandler implements InvocationHandler {
private final Object target;
public AttendanceInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("上班打卡……");
Object invoke = method.invoke(target, args);
System.out.println("下班打卡……");
return invoke;
}
}
重點看下Object invoke = method.invoke(target, args);
,該行代碼會執行真正的目標方法,在這前后,我們可以添加一些增強邏輯,
然后,新建個測驗類,看下JDK動態代理如何使用:
public class JdkProxyTest {
public static void main(String[] args) {
Coder coder = new Coder();
AttendanceInvocationHandler h = new AttendanceInvocationHandler(coder);
// 創建代理物件
Object proxyInstance = Proxy.newProxyInstance(coder.getClass().getClassLoader(),
coder.getClass().getInterfaces(),
h);
Staff staff = (Staff) proxyInstance;
staff.work();
}
}
運行以上代碼,效果如下圖所示:
從運行結果可以看出,在目標方法的前后,執行了自定義的操作,
2. 原理
這里理解2個概念,目標物件和代理物件,
目標物件是真正要呼叫的物件,上面示例中的Coder類就是目標物件,
代理物件是JDK自動生成的物件,在代理物件內部會去呼叫目標物件的目標方法,
JDK動態代理的核心就是上面示例中的Proxy.newProxyInstance
方法,方法簽名如下圖所示:
第1個引數傳入的是目標物件的ClassLoader,第2個引數傳入的是目標物件的介面資訊,第3個引數傳入的是自定義的InvocationHandler,
然后看下該方法的實作邏輯,先看第1處重點:
注釋翻譯過來是:查找或者生成指定的代理類,
該方法會生成代理類的位元組碼檔案(也可能是從快取中讀取),核心邏輯在ProxyClassFactory
類的apply
方法中,
該方法中定義了生成的代理類的包名以及檔案名:
因此默認情況下,自動生成的代理類名稱是com.sun.proxy.$Proxy0
,
該方法最后會生成代理類的位元組碼,默認情況下不會保存到檔案系統,但可以通過引數指定保存到檔案系統:
可以看出,保存不保存到檔案系統,受saveGeneratedFiles的影響,其定義如下所示:
private static final boolean saveGeneratedFiles = (Boolean)AccessController.doPrivileged(new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles"));
所以可以通過指定sun.misc.ProxyGenerator.saveGeneratedFiles
引數來讓生成的代理類位元組碼檔案保存到檔案系統中,
然后看第2處重點:
先是獲取建構式,然后是生成代理類物件的實體,
3. 為什么必須要基于介面?
思考一個問題,為什么JDK動態代理必須要基于介面,帶著這個問題,我們看下動態生成的代理類com.sun.proxy.$Proxy0
長什么樣子?
JVM引數里添加引數-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true,然后啟動上面示例中的測驗代碼:
生成的代理類位元組碼檔案保存在專案根目錄下的com/sun/proxy目錄下:
在IDEA中打開后,如下圖所示:
在靜態代碼塊中,對靜態變數m0、m1、m2、m3進行了賦值,其中m3是要執行的目標方法,
在構造方法中,執行的是super(var1);
,也就是父類Proxy的構造方法:
該方法是將我們自定義的InvocationHandler賦值給了父類的變數h,
而以下測驗代碼實際執行的是代理類$Proxy0里的work方法:
Staff staff = (Staff) proxyInstance;
staff.work();
代理類$Proxy0里的work方法實際執行的是自定義InvocationHandler里的invoke方法:
因此在執行目標方法前后,執行了自定義的前置操作和后置操作,
了解了這個呼叫程序,就理解了為什么JDK動態代理必須要基于介面,因為動態生成的代理類已經繼承了類java.lang.reflect.Proxy
,
而Java又是單繼承的,如果想要繼續對類進行擴展,只能通過實作介面的方式,
文章持續更新,歡迎關注微信公眾號「申城異鄉人」第一時間閱讀!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/550300.html
標籤:其他
下一篇:我的編程學習小圈子