面試必問系列之JDK動態代理
掃描文末二維碼或者微信搜索公眾號
小李不禿
,即可關注微信公眾號,獲取到更多 Java 相關內容。
1. 帶着問題去學習
面試中經常會問到關於 Spring 的代理方式有哪兩種?大家異口同聲的回答:JDK 動態代理和 CGLIB 動態代理。
這兩種代理有什麼區別呢?JDK 動態代理的類通過接口實現,CGLIB 動態代理是通過子類來實現的。
那 JDK 動態代理你了到底了解多少呢?有去看過代理對象的 class 文件么?下面兩個關於 JDK 動態代理的問題你能回答上來么?
- 問題1:為什麼 JDK 動態代理要基於接口實現?而不是基於繼承來實現?
- 問題2:JDK 動態代理中,目標對象調用自己的另一個方法,會經過代理對象么?
小李帶着大家更深入的了解一下 JDK 的動態代理。
2. JDK 動態代理的寫法
- JDK 動態代理需要這幾部分內容:接口、實現類、代理對象。
- 代理對象需要繼承 InvocationHandler,代理類調用方法時會調用 InvocationHandler 的 invoke 方法。
- Proxy 是所有代理類的父類,它提供了一個靜態方法 newProxyInstance 動態創建代理對象。
public interface IBuyService {
void buyItem(int userId);
void refund(int nums);
}
@Service
public class BuyServiceImpl implements IBuyService {
@Override
public void buyItem(int userId) {
System.out.println("小李不禿要買東西!小李不禿的id是: " + userId);
}
@Override
public void refund(int nums) {
System.out.println("商品過保質期了,需要退款,退款數量 :" + nums);
}
}
public class JdkProxy implements InvocationHandler {
private Object target;
public JdkProxy(Object target) {
this.target = target;
}
// 方法增強
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before(args);
Object result = method.invoke(target,args);
after(args);
return result;
}
private void after(Object result) { System.out.println("調用方法后執行!!!!" ); }
private void before(Object[] args) { System.out.println("調用方法前執行!!!!" ); }
// 獲取代理對象
public <T> T getProxy(){
return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),this);
}
}
public class JdkProxyMain {
public static void main(String[] args) {
// 標明目標 target 是 BuyServiceImpl
JdkProxy proxy = new JdkProxy(new BuyServiceImpl());
// 獲取代理對象實例
IBuyService buyItem = proxy.getProxy();
// 調用方法
buyItem.buyItem(12345);
}
}
查看運行結果
調用方法前執行!!!!
小李不禿要買東西!小李不禿的id是: 12345
調用方法后執行!!!!
我們完成了對目標方法的增強,開始對代理對象進行一個更全面的分析。
3. 剖析代理對象並解答問題
剖析代理對象的前提得是有代理對象,動態代理的對象是在運行時期創建的,我們就沒辦法通過打斷點的方式進行分析了。但是我們可以通過反編譯 .class 文件進行分析。如何獲取到 .class 文件呢?
通過在代碼中添加:System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true")
,就能夠實現將動態代理對象的 class 文件寫入到磁盤中。代碼如下:
public class JdkProxyMain {
public static void main(String[] args) {
// 代理對象的 class 文件寫入到磁盤中
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
// 標明目標 target 是 BuyServiceImpl
JdkProxy proxy = new JdkProxy(new BuyServiceImpl());
// 獲取代理對象實例
IBuyService buyItem = proxy.getProxy();
// 調用方法
buyItem.buyItem(12345);
}
}
在項目的根目錄下多了一個 $Proxy0.class
文件
看一下這個文件的內容
public final class $Proxy0 extends Proxy implements IBuyService {
private static Method m1;
private static Method m3;
private static Method m2;
private static Method m4;
private static Method m0;
public $Proxy0(InvocationHandler var1) throws {
super(var1);
}
public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final void buyItem(int var1) throws {
try {
super.h.invoke(this, m3, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final void refund(int var1) throws {
try {
super.h.invoke(this, m4, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m3 = Class.forName("com.example.springtest.service.IBuyService").getMethod("buyItem", Integer.TYPE);
m2 = Class.forName("java.lang.Object").getMethod("toString");
m4 = Class.forName("com.example.springtest.service.IBuyService").getMethod("refund", Integer.TYPE);
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
動態代理對象 $Proxy0
繼承了 Proxy
類並且實現了 IBuyService
接口。那問題 1 的答案就出來了:動態代理對象默認繼承了 Proxy 對象,而且 Java 不支持多繼承,所以 JDK 動態代理要基於接口來實現。
$Proxy0
重寫了 IBuyService
接口的方法,還有 Object
的方法。在重寫的方法中,統一調用 super.h.invoke
方法。super
指的是 Proxy
,h
代表 InvocationHandler
,這裏就是 JdkProxy
。所以這裏調用的是 JdkProxy
的 invoke
方法。
所以每次調用 buyItem
方法的時候,會先打印出 調用方法前執行!!!!
。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before(args);
// 通過反射調用方法
Object result = method.invoke(target,args);
after(args);
return result;
}
private void after(Object result) { System.out.println("調用方法后執行!!!!" ); }
private void before(Object[] args) { System.out.println("調用方法前執行!!!!" ); }
問題 2 還沒解決呢,接着往下看
@Service
public class BuyServiceImpl implements IBuyService {
@Override
public void buyItem(int userId) {
System.out.println("小李不禿要買東西!小李不禿的id是: " + userId);
refund(100);
}
@Override
public void refund(int nums) {
System.out.println("商品過保質期了,需要退款,退款數量 :" + nums);
}
}
上面這段代碼中,在 buyItem
調用內部的 refund
方法,那這個內部調用方法是否走代理對象呢?看一下執行結果:
調用方法前執行!!!!
小李不禿要買東西!小李不禿的id是: 12345
商品過保質期了,需要退款,退款數量 :100
調用方法后執行!!!!
確實是沒有走代理對象,其實我們期待的結果是下面這樣的
調用方法前執行!!!!
小李不禿要買東西!小李不禿的id是: 12345
調用方法前執行!!!!
商品過保質期了,需要退款,退款數量 :100
調用方法后執行!!!!
調用方法后執行!!!!
那為什麼會造成這種差異呢?
因為內部調用 refund
方法的調用,相當於 this.refund(100)
,而這個 this
指的是 BuyServiceImpl
對象,而不是代理對象,所以refund
方法沒有得到增強。
4. 總結和延伸
-
本篇文章了解了 JDK 動態代理的使用,通過分析 JDK 動態代理生成對象的 class 文件,解決了兩個問題:
- 問題1:為什麼 JDK 動態代理要基於接口實現?而不是基於繼承來實現?
- 解答:因為 JDK 動態代理生成的對象默認是繼承
Proxy
,Java 不支持多繼承,所以 JDK 動態代理要基於接口來實現。 - 問題2:JDK 動態代理中,目標對象調用自己的另一個方法,會經過代理對象么?
- 解答:內部調用方法使用的對象是目標對象本身,被調用的方法不會經過代理對象。
-
我們知道了 JDK 動態代理內部調用是不走代理對象的。那對於 @Transactional 和 @Async 等註解不起作用是不是就搞清楚為啥了?
-
因為 @Transactional 和 @Async 等註解是通過 Spring AOP 來進行實現的,如果動態代理使用的是 JDK 動態代理,那麼在方法的內部調用該方法中其它帶有該註解的方法,由於此時調用的不是動態代理對象,所以註解失效。
-
上面這些問題就是 JDK 動態代理的缺點,那 Spring 如何避免這個問題呢?就是另個一個動態代理:CGLIB 動態代理,我會在下篇文章進行分析。
5. 參考
- https://juejin.im/post/5d8a0799f265da5b7a752e7c#heading-6
- https://blog.csdn.net/varyall/article/details/102952365
6. 猜你喜歡
-
JSON的學習和使用
-
學習反射看這一篇就夠了
-
併發編程學習(一)Java 內存模型
掃描下方二維碼即可關注微信公眾號
小李不禿
,一起高效學習 Java。
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面
※南投搬家公司費用需注意的眉眉角角,別等搬了再說!
※教你寫出一流的銷售文案?