關於Spring循環依賴可能存在的坑


場景重現

問題初現

今天項目編譯上線出現一個問題,項目啟動時,報了:

***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

初步排查

相信搞過Java的老鐵一看到這個就知道是除了啥問題,循環依賴了唄。所以看到這里我也是一點都不慌,看了下日志就去代碼里排查了。

  1. 看代碼
    發現確實存在循環依賴,但是在之前提交的一個版本里我已經把循環依賴的service改為通過setter方法注入了,按理來說不會再有問題:
    @Autowired
    public void setTenantService(TenantService tenantService) {
        this.tenantService = tenantService;
    }
    
  2. Debug
    然后本地拉了main分支最新代碼,編譯啟動,竟然成功啟動了,毫無問題。
  3. 甩鍋
    於是判斷是運維大佬拉的代碼是修復之前的,就讓他去排查代碼版本是不是拉錯了,就沒管了。

進一步排查

  1. 甩鍋失敗
    運維大佬經過精心檢查之后,確認代碼就是最新的,並不是代碼問題,於是鍋又回到了我的頭上。
  2. 對比代碼
    然后我讓運維大佬把jenkins打包的jar發給我,打算反編譯出來作對比。結果反編譯和本地代碼一對比,竟然是一樣的....到了這里我已經慌了,事情漸漸向着玄學前進...
  3. 控制變量
    作為一個合格的小學生,想起了當年老師教我們的控制變量法。然后就開始了瘋狂的控制變量。
  • 代碼問題? 代碼確認都是git拉的最新的,排除
  • Java版本問題?都是openjdk11(build11+28),排除
  • maven問題?都是maven3.6.3,排除
  • 電腦問題?
    • Windows下,我們三個開發都拉了最新代碼下來編譯跑,沒有問題
    • Linux下,jenkins編譯出來就有問題。然而,運維大佬換了個linux服務器,同樣的jdk,同樣的maven版本,編譯出來竟然就可以跑了。所以這是什么玄學
  1. 垂死掙扎
    接下來把本地編譯的jar包和有問題的jar包解壓出來對比,除了MANIFEST文件的timestamp不一樣,其他竟然所有文件的md5都是一模一樣的???!!!

懷疑人生

那么問題來了,同樣的代碼,同樣的jdk,同樣的maven版本,兩台linux服務器編譯,出來的jar包除了MANIFEST文件的時間戳不一樣,其他完全一致。但是跑起來,一個可以正常運行,一個卻報循環依賴錯誤。這是什么玄學?

找到同樣的受害者

抱着疑問,我找到了程序員導師:Google來求助,最終兜兜轉轉找到了github里spring-framework的一個issue,提的就是這個問題:
https://github.com/spring-projects/spring-framework/issues/18879

可以看到這個issue從2016年首次被提出,到2019年reopen,實際上一直都沒有找到過原因。issue里好幾個人遇到了和我一樣的問題:一樣的代碼,在不同的環境上編譯,出來的jar包有的能運行,有的卻報錯。
spring的維護人員可能是覺得循環依賴不應當在程序中出現,甚至目前springboot2.6版本已經完全不允許循環依賴了,所以對這個issue也就沒有動力去解決。

如何解決

關於怎么解決spring的循環依賴問題,網絡上一般就三種說法:

  • 不使用構造器來注入bean
  • 直接在field上添加@Autowired來注入
  • 使用setter方法來注入

總體來講,以上方法都不推薦

  • 直接在field上添加@Autowired來注入,不使用構造器來注入bean,是最懶惰的做法。實際上從spring4.0開始就已經不再推薦使用field injection,IDEA也會給出warning。至於為什么不推薦field injection,文章很多,不贅述
  • 使用setter方法來注入:setter方法注入的本意是表示這個bean對當前的bean是選擇性依賴的,不提供也不影響bean的正常運行。這次我就是因為出現了循環依賴,而偷懶不想通過代碼結構優化來解決,使用了setter方法來注入。但是目前看來實際上是有一定幾率觸發spring的bug,導致在不同環境編譯,出來的jar包觸發循環依賴報錯的。

根本上解決

根本上的解決方案,就是不要出現循環依賴。只要不出現循環依賴,就不會有幾率觸發這個bug。從程序設計的角度來說,循環依賴無論如何也不是個很好的設計,如果程序里存在循環依賴,我們應當反思。
實際上spring早就為我們想好了什么才是良好的程序設計,只要我們遵循他們推薦的方式來注入bean:通過構造器注入Bean。
通過構造器注入Bean,有以下兩個好處:

  • 絕對杜絕循環依賴,因為這時候只要存在循環依賴,必然會報BeanCurrentlyInCreationException。只要強制所有bean都通過構造器注入,我們的程序可以從根本上杜絕循環依賴。
  • 更能暴露出耦合性問題:往往隨着業務開發,bean注入的依賴越來越多。如果使用Field注入,往往這個問題會被忽視。但是當我們使用構造器注入時,越來越龐大的構造方法將使我們無法忽視這個問題:我們是不是違反了類的單一性原則?我們是不是違背了高內聚,低耦合的原則?這時候就是該反思程序的結構,做重構的時候了。
  • 更利於mock測試,使用field注入,單元測試就會依賴spring容器來注入依賴,但是使用構造器注入bean時,可以脫離spring而直接構造。

實在沒辦法的辦法

如果真的循環依賴無法避免,那就在注入的setter上添加@Lazy注解,進行懶加載。但是這個方法可能還是會觸發SPR-14307這個Bug(由於目前尚未找到bug原因,實際上任何循環依賴都有可能觸發這個bug,有不確定性)。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM