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又是單繼承的,如果想要繼續對類進行擴充套件,只能透過實現介面的方式。
文章持續更新,歡迎關注微信公眾號「申城異鄉人」第一時間閱讀!