切換語言為:簡體

為什麼 Spring 代理物件的屬性沒有值?

  • 爱糖宝
  • 2024-08-15
  • 2055
  • 0
  • 0

先看程式碼:

@Service
@Transactional
public class CodeUpService {

    private String name = "codeup";

    public final void test() {
        System.out.println(name);
    }
}


關鍵點:

  1. 加了@Transactional,所以CodeUpService會生成代理物件作為Bean物件

  2. name屬性有預設值“codeup”

  3. 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底層原理的同學應該會想到,代理物件執行方法時,邏輯是這樣的:

  1. 代理物件先執行自己的test()方法,從而執行切面邏輯

  2. 然後執行被代理物件的test()方法,從而執行原本邏輯

代理物件對應的是代理類,是CodeUpService E n h a n c e r B y S p r i n g C G L I B EnhancerBySpringCGLIB f4bc73d9類 被代理物件對應的是被代理類,就是CodeUpService類

一般情況下:

  1. 代理類的父類是被代理類

  2. 代理類會重寫父類裡面被代理的方法,比如test()方法

  3. 代理類會在自己的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來解析一下這個技術: 為什麼 Spring 代理物件的屬性沒有值?

假如,我們用Objenesis來建立一個物件,並列印name屬性:

Objenesis objenesis = new ObjenesisStd();
CodeUpService codeUpService = objenesis.newInstance(CodeUpService.class);
System.out.println(codeUpService.name);

結果為null,因為使用Objenesis建立物件根本就沒有走屬性初始化這一步。

而Spring AOP裡預設就會用這個技術,對應的類為ObjenesisCglibAopProxy,關鍵程式碼為: 為什麼 Spring 代理物件的屬性沒有值?

透過上面的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()方法中了。

分析了這麼多,是不是有點暈了,最後,我再來給大家梳理一下:

  1. Spring會用cglib來建立代理類,會用Objenesis來建立代理物件,因此不會初始化代理物件中的屬性,這是可以理解的,因為代理物件的作用是去代理方法,而不是代理屬性,所以代理物件不關心屬性,使用Objenesis可以更快的建立代理物件,但是會導致代理物件中的屬性為null

  2. 如果方法加了final,那麼就不能被代理到,導致列印的是代理物件的name屬性,如果不是final,就被代理到了,導致列印的是被代理物件的name屬性

  3. final的屬性很有可能會被編譯內聯到方法中

以上,正式結束。

0則評論

您的電子郵件等資訊不會被公開,以下所有項目均必填

OK! You can skip this field.