1. 示例 首先,定義一個介面: public interface Staff { void work(); } 然後,新增一個類並實現上面的介面: public class Coder implements Staff { @Override public void work() { System ...
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又是單繼承的,如果想要繼續對類進行擴展,只能通過實現介面的方式。
文章持續更新,歡迎關註微信公眾號「申城異鄉人」第一時間閱讀!