面試必問系列之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。