1. 程式人生 > >一個普通類就能幹趴你的springboot,你信嗎?

一個普通類就能幹趴你的springboot,你信嗎?

  先宣告本人並不是標題黨,如果看了本篇文章並且認為沒有得到任何收穫,請您隨便留言罵我,本人絕不還口,已經對springboot瞭如指掌大大神,求放過!

  不BB了,直接上程式碼,請各位在自己的springboot專案隨便一個包下複製進去如下類(不要修改什麼東西),如果你的springboot還能站起來算我輸!

@Component
public class Environment {
}

  執行springboot的啟動類會報如下錯誤,然後你刪除這個類,你的springboot又能健步如飛了,你可能就會懷疑人生了,這程式碼有毒。先說明我的springboot是2.1.7.RELEASE,我也試了最新的2.2,報錯基本一致!

2019-11-02 00:42:46.181  INFO 13568 --- [           main] com.rdpaas.platform.demo.RunApplication  : Starting RunApplication on DESKTOP-9KL4U5L with PID 13568 (E:\project2018\platform\demo\target\classes started by 49519 in E:\project2018\platform)
2019-11-02 00:42:46.183  INFO 13568 --- [           main] com.rdpaas.platform.demo.RunApplication  : No active profile set, falling back to default profiles: default
2019-11-02 00:42:48.490  WARN 13568 --- [           main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'methodValidationPostProcessor' defined in class path resource [org/springframework/boot/autoconfigure/validation/ValidationAutoConfiguration.class]: Unsatisfied dependency expressed through method 'methodValidationPostProcessor' parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.core.env.Environment' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
2019-11-02 00:42:48.499  INFO 13568 --- [           main] ConditionEvaluationReportLoggingListener : 

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2019-11-02 00:42:48.615 ERROR 13568 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   : 

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

Description:

Parameter 0 of method methodValidationPostProcessor in org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration required a bean of type 'org.springframework.core.env.Environment' that could not be found.


Action:

Consider defining a bean of type 'org.springframework.core.env.Environment' in your configuration.

  如上很普通,誰都可能加上的類,就這樣一個簡單的類,居然可以直接導致springboot站不起來了,如果你認命了,其實也很好解決,你可能會換個名字試試,或者你壓根就不會用到這個類,或者是你給@Component("env"),加個別名,可能就碰巧解決了這個問題,那麼這時候你可能會當成springboot已經規定了你不能使用關鍵字environment作為bean的名稱,那麼這個問題就變得一文不值了,因為你已經認命了,不讓我用我不用就行了,以後一輩子都不用這個類名就好了。眼不見心不煩,我用簡稱Env還來的省事點。不過我個人認為我們遇到難題應該迎難而上,不能隨便認命,我們都是驕傲的程式設計師。應該抱著希望遇到難題的心態,積極去面對難題,多解決一些疑難雜症,用知識和經驗武裝自己,努力成長,迎娶白富美,走上人生巔峰!如果看到這裡覺得不認命的請跟著我一起看看這個問題到底為啥會出現吧!

  接下來我們一步步來找到問題的根源,為啥用了這個類,springboot就不舉了?首先我們從啟動的錯誤提示中找到唯一的關鍵資訊:method methodValidationPostProcessor in org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration required a bean of type 'org.springframework.core.env.Environment' that could not be found。從這句話可以看出來在:org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration這個類中的這個方法:methodValidationPostProcessor 中需要一個:org.springframework.core.env.Environment類的物件作為引數,但是他找不到,看這個名字和我們自己定義的一樣,先在idea中找到如上類的methodValidationPostProcessor方法的原始碼所在:

   為了驗證圖中的猜想,由於我這不是原始碼編譯的,所以只能自己模仿這個類同樣使用@Bean修飾一個方法看看是不是裡面的引數都是完全按照引數名稱注入的(可以先註釋掉之前的Environment類排除那個類的影響),如下

package com.rdpaas.platform.demo.env;
import org.springframework.stereotype.Component;
/**
 * 用作測試的bean
 * @author: rongdi
 * @date: 2019-11-2 0:12
 */
@Component
public class TestBean {
}
package com.rdpaas.platform.demo.env;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 模仿org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration
 * 使用@Configuration註解,並且提供一個static的@Bean修飾的方法
 * @author: rongdi
 * @date: 2019-11-2 0:11
 */
@Configuration
public class Config {

    @Bean
    public static TestBean create(TestBean tb) {
        System.out.println(tb);
        return tb;
    }

}

  如上我們使用預設的beanName為testBean的bean,然後Config類中注入的名稱是tb執行springboot發現可以正常打印出tb物件,說明名稱不一致同樣可以注入成功,所i有我們大概可以排除之前的猜想,想想頂頂大名的springboot也不可能這麼low逼吧!

  之前的猜測被推翻,我們只能老老實實的使用debug一步步從springboot的入口一步步跟蹤進去看看到底啥時候開始報錯的,這一步如果不熟悉spring程式碼的一定要耐心一步步找找,如下

   之前對spring底層原始碼有一定了解的應該知道spring是先把那些註解和xml宣告的類載入到一個map裡然後再進行初始化的,這個map就是beanDefinitionMap,一步步斷點到spring最核心的方法refresh中

   當執行到上圖藍色位置時,也就是執行完invokeBeanFactoryPostProcessors(beanFactory)方法後,當前beanFactory裡的beanDefinitionMap物件中找到了我們宣告的environment物件的身影,如下

  斷點的過程中發現程式碼太多要是一步步找過去,很容易就放棄,所以我們再從上面的錯誤日誌找找有用資訊,然後通過全域性搜尋看看到底時哪裡報出的錯誤,如通過報錯裡的警告資訊:expected at least 1 bean which qualifies as autowire candidate. Dependency annotations直接使用全域性搜尋(前提時先斷點跑一邊,然後根據idea提示下載好spring的原始碼)

   點選上述方法後如下一如既往的打個斷點重新跑一邊看看

   這時候可以把斷點打在if那行再進去看看是啥情況導致進入了這裡,然後我們可以確定只要是if返回了true,就必然會導致報錯,然後我們註釋掉自己的Environment類看看還能否進入到這裡,通過註釋和不註釋的對比我們發現兩種情況斷點之後有如下差別

   所以問題的關鍵就是加入了自己的Environment類導致matchingBeans的map為空而產生了本例中的報錯資訊。

   接下來我們為了除錯的效率,在每個出現beanName引數的方法打斷點都使用這個條件斷點,現在問題就回歸到為啥加上了自己的Environment類後給matchingBeans提供資料的方法findAutowireCandidates為空了。一如既往的條件斷點打到裡面

   使用同樣的方式在如下方法也加上條件斷點,再次重複執行斷點直到進去如下

   耐心的再用如上同樣方式進入這個方法,這裡由於有多個類請使用F5(斷點進入方法,可能快捷鍵不一樣)

 

 

   從上看出,剛進去迴圈的陣列中明顯有environment,但是結果為啥就成了空陣列,進一步斷點發現

   對比以上兩個結果,很明顯當我們自己添加了Environment類後,singletonObjects肯定有一個移除操作,然後我們找到所有singletonObjects.remove()的地方打一個條件斷點:beanName.equals("environment"),很明顯從邏輯上看,只要springboot不是全部清空,必然會有一個 remove("environment")才能解釋以上兩者的差別。

  然後我們再在singletonObjects.put()相關的方法都打上同樣的條件斷點,放心大膽的繼續重新斷點執行一遍,第一次進入斷點如下

   上圖如果執行過addSingleton方法後this.singletonObjects中確實會放入以environment為key,以spring的StandardServetEnvironment為value的鍵值對進去,這裡就不截圖了,免得又要重新跑一次斷點,直接點選左邊呼叫棧那個679行後如下:

  這裡可以先記錄下左邊環境物件到底是在spring最重要的refresh方法的那一步

  根據上面得出的結論,之所以報錯最根本的原因就是這個singletonObjects找不到這個environment了,而這裡有,所以肯定有地方刪除了這個key,因為這個map看起來如此重要,spring不會無緣無故直接clear吧,所以只要找到唯一的刪除key的方式singletonObjects.remove(),並打上上面說的條件斷點,這一點上面其實說過了,那我們繼續跑斷點,直到找到在哪刪除了這個key

 其實覆盤一下整個除錯過程,發現其實源頭如下

   其實我也不知道這算不算是springboot的bug,還是其實只是一個關鍵字的限定,因為最終解釋權不在於我,就像mybatis中的xml裡大於符號要用>不然別人根本解析不了,從這一點來說mybatis使用xml存放sql實際上限制了我們使用大於小於等等這些符號的權力,只能用轉義字元類似別名的東西替代。其實這裡也是類似,也可以理解成人家系統需要,你要用這個請改個名字或者取個別名,比如@Component("env)。不過我還是希望springboot能還我們使用單詞的自由,希望英文好的朋友可以發發郵件讓springboot團隊考慮下,哈哈!

最後來個篇中總結:

  1) 從文筆上來說,一如既往的沒有文筆,請各位大大海涵,真的盡力了,奈何胸無點墨!

  2) 從排版來說,一如既往的沒有排版,我是個純技術人,這些花裡胡哨的東西,真的一點不會,同樣請各位包涵

  3) 從知識點來說,其實這篇部落格主要是給小白們分享一下看原始碼的技巧和基本的除錯能力,還有遇到問題的處理態度。首先從這篇文章中應該能清晰的get到逆向思考一步步找的問題的方法,其次應該能獲取到一些斷點除錯原始碼的技巧,最後也應該能學會方法呼叫棧的作用。其實懂這三點基本就夠了,spring這些原始碼是否看過也不會影響你最終能找到這個問題的根源這一結果,最多會影響你找到根源的時間。

  4) 從用心程度來說,這篇部落格自認為是足夠用心,週五晚上從下班回家一邊一步步斷點一遍寫這篇部落格,直到凌晨三點多才衝忙洗洗睡。文章裡基本上把我知道的關於這個知識點的所有東西通過清晰的圖文方式一步步展現,本人熱愛技術,也喜歡分享技術,希望與廣大程式猿們相濡以沫,共同進步!

  5) 從文章質量來說,對大牛一文不值,對小白有一定幫助,希望大牛們多多包涵,不要噴我,有錯誤之處請多多指正。