先看程式碼:
@Service @Transactional public class CodeUpService { private String name = "codeup"; public final void test() { System.out.println(name); } }
關鍵點:
加了@Transactional,所以CodeUpService會生成代理物件作為Bean物件
name屬性有預設值“codeup”
test()方法為final
現在,透過Spring容器獲取CodeUpService的Bean物件,並執行test方法列印name屬性:
ConfigurableApplicationContext applicationContext = SpringApplication.run(Main.class, args); CodeUpService codeUpService = applicationContext.getBean(CodeUpService.class); codeUpService.test();
問題來了,test()方法列印出來的name屬性值為:null !
是不是不敢相信,不信你可以自己在電腦上試試,一開始我也不信,name屬性有預設值啊,怎麼會為null呢?
熟悉AOP底層原理的同學應該會想到,代理物件執行方法時,邏輯是這樣的:
代理物件先執行自己的test()方法,從而執行切面邏輯
然後執行被代理物件的test()方法,從而執行原本邏輯
代理物件對應的是代理類,是CodeUpService f4bc73d9類 被代理物件對應的是被代理類,就是CodeUpService類
一般情況下:
代理類的父類是被代理類
代理類會重寫父類裡面被代理的方法,比如test()方法
代理類會在自己的test()方法中,執行切面邏輯,並執行被代理物件的test()方法,被代理物件就是一個CodeUpService物件
因此,當代理物件執行test()方法時,最終仍然會執行被代理物件的test()方法,從而列印被代理物件的name屬性。
如果是以上流程,那麼列印出來的name應該是有值的。
但是,上面的程式碼中,test()方法前面加了final,表示不能被子類重寫,因此代理類中是沒有test()方法的,代理物件執行的test()方法,並不是自己的test()方法,也就是不會執行切面邏輯,也就是事務會失效。
但是,自己沒有test()方法,父類有啊,所以,代理物件實際上執行的是CodeUpService類裡的test()方法,從而列印name屬性,但是列印的是代理物件的name屬性,再由於CodeUpService中的name屬性為private,因此代理類中也沒有繼承該屬性,因此代理物件中name屬性為null,這是正常的。
以上的分析沒有問題,可是,如果我把name屬性改成public呢?那代理類就可以繼承name屬性了吧,那應該就能列印出來值了吧?
震驚的地方就在這裏,列印出來的仍然是:null !
不理解了吧,子類繼承父類裡面的public屬性,這不是天經地義的嗎?
這裏麵的魔鬼在於Objenesis,第一次聽說這個技術?讓GPT來解析一下這個技術:
假如,我們用Objenesis來建立一個物件,並列印name屬性:
Objenesis objenesis = new ObjenesisStd(); CodeUpService codeUpService = objenesis.newInstance(CodeUpService.class); System.out.println(codeUpService.name);
結果為null,因為使用Objenesis建立物件根本就沒有走屬性初始化這一步。
而Spring AOP裡預設就會用這個技術,對應的類為ObjenesisCglibAopProxy,關鍵程式碼為:
透過上面的Spring AOP原始碼,發現其實可以透過開關來關閉使用Objenesis,這個開關是-Dspring.objenesis.ignore=true
,設定為true,Spring AOP就不會使用Objenesis來建立代理物件了。
因此,我們把這個開關加上,重新回到上面讓我們震驚的場景中進行測試,就能發現name屬性有值了。
因此,我們上面分析的代理物件執行方法的流程並沒有問題,代理類肯定會繼承父類的name屬性,只是代理物件在建立時預設使用的是Objenesis,建立出來的物件根本就沒有對屬性做初始化,所以最終name屬性為null,不使用Objenesis就正常了。
好了,分析到這裏文章其實可以結束了,但是,再給大家一個彩蛋。
我們把剛剛的Objenesis開關再去掉,也就是還是讓Spring使用Objenesis,只不過,我們把name屬性改為final。
你會發現,最終列印出來的name屬性還是有值的,並不是null,這又是為啥?不是說用Objenesis建立的物件不會初始化屬性嗎?難道會初始化final的屬性?
沒有這種說法,沒有說只初始化final的屬性,而不初始化非final的屬性,我們不妨看看現在的CodeUpService:
@Service @Transactional public class CodeUpService { public final String name = "CodeUp"; public final void test() { System.out.println(name); } }
仔細看看,不知道大家能不能分析出原因?如果分析出來了,記得給文章點個贊之後,就可以離開了。
如果沒分析出來,那就看看編譯後的CodeUpService:
@Service @Transactional public class CodeUpService { public final String name = "CodeUp"; public CodeUpService() { } public final void test() { System.out.println("CodeUp"); } }
明白了嗎?點讚了嗎?
甚至,你現在debug去看CodeUpService代理物件,會發現debug會顯示name屬性為null,但是最終test()方法卻能列印出來“CodeUp”。
因為,CodeUpService代理物件的name屬性確實沒有值,沒有值的原因就是Objenesis,test()方法之所以能列印出來值,是因為編譯最佳化,直接將name屬性的值內聯到test()方法中了。
分析了這麼多,是不是有點暈了,最後,我再來給大家梳理一下:
Spring會用cglib來建立代理類,會用Objenesis來建立代理物件,因此不會初始化代理物件中的屬性,這是可以理解的,因為代理物件的作用是去代理方法,而不是代理屬性,所以代理物件不關心屬性,使用Objenesis可以更快的建立代理物件,但是會導致代理物件中的屬性為null
如果方法加了final,那麼就不能被代理到,導致列印的是代理物件的name屬性,如果不是final,就被代理到了,導致列印的是被代理物件的name屬性
final的屬性很有可能會被編譯內聯到方法中
以上,正式結束。