掃描文末二維碼或者微信搜索公眾號(hào)
小李不禿
,即可關(guān)注微信公眾號(hào)黑忱,獲取到更多 Java 相關(guān)內(nèi)容宴抚。
1. 帶著問(wèn)題去學(xué)習(xí)
面試中經(jīng)常會(huì)問(wèn)到關(guān)于 Spring 的代理方式有哪兩種?大家異口同聲的回答:JDK 動(dòng)態(tài)代理和 CGLIB 動(dòng)態(tài)代理甫煞。
這兩種代理有什么區(qū)別呢菇曲?JDK 動(dòng)態(tài)代理的類通過(guò)接口實(shí)現(xiàn),CGLIB 動(dòng)態(tài)代理是通過(guò)子類來(lái)實(shí)現(xiàn)的抚吠。
那 JDK 動(dòng)態(tài)代理你了到底了解多少呢常潮?有去看過(guò)代理對(duì)象的 class 文件么?下面兩個(gè)關(guān)于 JDK 動(dòng)態(tài)代理的問(wèn)題你能回答上來(lái)么楷力?
- 問(wèn)題1:為什么 JDK 動(dòng)態(tài)代理要基于接口實(shí)現(xiàn)喊式?而不是基于繼承來(lái)實(shí)現(xiàn)?
- 問(wèn)題2:JDK 動(dòng)態(tài)代理中萧朝,目標(biāo)對(duì)象調(diào)用自己的另一個(gè)方法岔留,會(huì)經(jīng)過(guò)代理對(duì)象么?
小李帶著大家更深入的了解一下 JDK 的動(dòng)態(tài)代理剪勿。
2. JDK 動(dòng)態(tài)代理的寫法
- JDK 動(dòng)態(tài)代理需要這幾部分內(nèi)容:接口贸诚、實(shí)現(xiàn)類、代理對(duì)象厕吉。
- 代理對(duì)象需要繼承 InvocationHandler酱固,代理類調(diào)用方法時(shí)會(huì)調(diào)用 InvocationHandler 的 invoke 方法。
- Proxy 是所有代理類的父類头朱,它提供了一個(gè)靜態(tài)方法 newProxyInstance 動(dòng)態(tài)創(chuàng)建代理對(duì)象运悲。
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("商品過(guò)保質(zhì)期了项钮,需要退款班眯,退款數(shù)量 :" + nums);
}
}
public class JdkProxy implements InvocationHandler {
private Object target;
public JdkProxy(Object target) {
this.target = target;
}
// 方法增強(qiáng)
@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("調(diào)用方法后執(zhí)行OM!!J鸢宠能!" ); }
private void before(Object[] args) { System.out.println("調(diào)用方法前執(zhí)行!4挪汀Nコ纭!" ); }
// 獲取代理對(duì)象
public <T> T getProxy(){
return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),this);
}
}
public class JdkProxyMain {
public static void main(String[] args) {
// 標(biāo)明目標(biāo) target 是 BuyServiceImpl
JdkProxy proxy = new JdkProxy(new BuyServiceImpl());
// 獲取代理對(duì)象實(shí)例
IBuyService buyItem = proxy.getProxy();
// 調(diào)用方法
buyItem.buyItem(12345);
}
}
查看運(yùn)行結(jié)果
調(diào)用方法前執(zhí)行U锱P哐印!脾还!
小李不禿要買東西伴箩!小李不禿的id是: 12345
調(diào)用方法后執(zhí)行!1陕`脱琛!
我們完成了對(duì)目標(biāo)方法的增強(qiáng)怔蚌,開始對(duì)代理對(duì)象進(jìn)行一個(gè)更全面的分析呵恢。
3. 剖析代理對(duì)象并解答問(wèn)題
剖析代理對(duì)象的前提得是有代理對(duì)象,動(dòng)態(tài)代理的對(duì)象是在運(yùn)行時(shí)期創(chuàng)建的媚创,我們就沒(méi)辦法通過(guò)打斷點(diǎn)的方式進(jìn)行分析了渗钉。但是我們可以通過(guò)反編譯 .class 文件進(jìn)行分析。如何獲取到 .class 文件呢钞钙?
通過(guò)在代碼中添加:System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true")
鳄橘,就能夠?qū)崿F(xiàn)將動(dòng)態(tài)代理對(duì)象的 class 文件寫入到磁盤中。代碼如下:
public class JdkProxyMain {
public static void main(String[] args) {
// 代理對(duì)象的 class 文件寫入到磁盤中
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
// 標(biāo)明目標(biāo) target 是 BuyServiceImpl
JdkProxy proxy = new JdkProxy(new BuyServiceImpl());
// 獲取代理對(duì)象實(shí)例
IBuyService buyItem = proxy.getProxy();
// 調(diào)用方法
buyItem.buyItem(12345);
}
}
在項(xiàng)目的根目錄下多了一個(gè) $Proxy0.class
文件
看一下這個(gè)文件的內(nèi)容
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());
}
}
}
動(dòng)態(tài)代理對(duì)象 $Proxy0
繼承了 Proxy
類并且實(shí)現(xiàn)了 IBuyService
接口芒炼。那問(wèn)題 1 的答案就出來(lái)了:動(dòng)態(tài)代理對(duì)象默認(rèn)繼承了 Proxy 對(duì)象瘫怜,而且 Java 不支持多繼承,所以 JDK 動(dòng)態(tài)代理要基于接口來(lái)實(shí)現(xiàn)本刽。
$Proxy0
重寫了 IBuyService
接口的方法鲸湃,還有 Object
的方法。在重寫的方法中子寓,統(tǒng)一調(diào)用 super.h.invoke
方法暗挑。super
指的是 Proxy
,h
代表 InvocationHandler
斜友,這里就是 JdkProxy
炸裆。所以這里調(diào)用的是 JdkProxy
的 invoke
方法。
所以每次調(diào)用 buyItem
方法的時(shí)候鲜屏,會(huì)先打印出 調(diào)用方法前執(zhí)行E肟础9础!惯殊!
酱吝。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before(args);
// 通過(guò)反射調(diào)用方法
Object result = method.invoke(target,args);
after(args);
return result;
}
private void after(Object result) { System.out.println("調(diào)用方法后執(zhí)行!M了肌5敉!" ); }
private void before(Object[] args) { System.out.println("調(diào)用方法前執(zhí)行@四!v帧址愿!" ); }
問(wèn)題 2 還沒(méi)解決呢,接著往下看
@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("商品過(guò)保質(zhì)期了响谓,需要退款,退款數(shù)量 :" + nums);
}
}
上面這段代碼中省艳,在 buyItem
調(diào)用內(nèi)部的 refund
方法娘纷,那這個(gè)內(nèi)部調(diào)用方法是否走代理對(duì)象呢?看一下執(zhí)行結(jié)果:
調(diào)用方法前執(zhí)行0峡弧@稻А!辐烂!
小李不禿要買東西遏插!小李不禿的id是: 12345
商品過(guò)保質(zhì)期了,需要退款纠修,退款數(shù)量 :100
調(diào)用方法后執(zhí)行8斐啊!?鄄荨了牛!
確實(shí)是沒(méi)有走代理對(duì)象,其實(shí)我們期待的結(jié)果是下面這樣的
調(diào)用方法前執(zhí)行3矫睢Sセ觥!密浑!
小李不禿要買東西福荸!小李不禿的id是: 12345
調(diào)用方法前執(zhí)行!k戎馈>慈瘛背传!
商品過(guò)保質(zhì)期了,需要退款台夺,退款數(shù)量 :100
調(diào)用方法后執(zhí)行>毒痢!2椤梳星!
調(diào)用方法后執(zhí)行!9龆洹T┰帧!
那為什么會(huì)造成這種差異呢辕近?
因?yàn)閮?nèi)部調(diào)用 refund
方法的調(diào)用韵吨,相當(dāng)于 this.refund(100)
,而這個(gè) this
指的是 BuyServiceImpl
對(duì)象移宅,而不是代理對(duì)象归粉,所以refund
方法沒(méi)有得到增強(qiáng)。
4. 總結(jié)和延伸
-
本篇文章了解了 JDK 動(dòng)態(tài)代理的使用漏峰,通過(guò)分析 JDK 動(dòng)態(tài)代理生成對(duì)象的 class 文件糠悼,解決了兩個(gè)問(wèn)題:
- 問(wèn)題1:為什么 JDK 動(dòng)態(tài)代理要基于接口實(shí)現(xiàn)?而不是基于繼承來(lái)實(shí)現(xiàn)浅乔?
-
解答:因?yàn)?JDK 動(dòng)態(tài)代理生成的對(duì)象默認(rèn)是繼承
Proxy
倔喂,Java 不支持多繼承,所以 JDK 動(dòng)態(tài)代理要基于接口來(lái)實(shí)現(xiàn)靖苇。 - 問(wèn)題2:JDK 動(dòng)態(tài)代理中滴劲,目標(biāo)對(duì)象調(diào)用自己的另一個(gè)方法,會(huì)經(jīng)過(guò)代理對(duì)象么顾复?
- 解答:內(nèi)部調(diào)用方法使用的對(duì)象是目標(biāo)對(duì)象本身班挖,被調(diào)用的方法不會(huì)經(jīng)過(guò)代理對(duì)象。
-
我們知道了 JDK 動(dòng)態(tài)代理內(nèi)部調(diào)用是不走代理對(duì)象的芯砸。那對(duì)于 @Transactional 和 @Async 等注解不起作用是不是就搞清楚為啥了萧芙?
- 因?yàn)? @Transactional 和 @Async 等注解是通過(guò) Spring AOP 來(lái)進(jìn)行實(shí)現(xiàn)的,如果動(dòng)態(tài)代理使用的是 JDK 動(dòng)態(tài)代理假丧,那么在方法的內(nèi)部調(diào)用該方法中其它帶有該注解的方法双揪,由于此時(shí)調(diào)用的不是動(dòng)態(tài)代理對(duì)象,所以注解失效包帚。
-
上面這些問(wèn)題就是 JDK 動(dòng)態(tài)代理的缺點(diǎn)渔期,那 Spring 如何避免這個(gè)問(wèn)題呢?就是另個(gè)一個(gè)動(dòng)態(tài)代理:CGLIB 動(dòng)態(tài)代理,我會(huì)在下篇文章進(jìn)行分析疯趟。
5. 參考
- https://juejin.im/post/5d8a0799f265da5b7a752e7c#heading-6
- https://blog.csdn.net/varyall/article/details/102952365
6. 猜你喜歡
掃描下方二維碼即可關(guān)注微信公眾號(hào)
小李不禿
拘哨,一起高效學(xué)習(xí) Java。