一個Spring2.5的老應用從CentOS 5 遷移到CentOS 7之后啟動報錯。該問題是由同事定位解決嗜傅,本文是我之后的復盤和源碼走讀金句。
一句話結論:CentOS 7改變了Spring BeanFactory中的BeanDefinition順序讓另一個類排在最前先加載,該類的filed定義的是實現(xiàn)類而不是接口類型吕嘀,該field被@Autowired標注在自動注入時會去匹配類型违寞,由于定義的不是接口類型導致匹配失敗。
發(fā)現(xiàn)問題
啟動報錯日志如下币他,做出少量精簡:
ERROR [org.springframework.web.context.ContextLoader.initWebApplicationContext(ContextLoader.java:214)] - Context initialization failed
BeanCreationException: Error creating bean 'cacheServiceFacade': Autowiring of fields failed (CacheService) CacheServiceFacade.cacheService
BeanCreationException: Error creating bean 'cacheServiceImpl': Autowiring of fields failed (net.sf.ehcache.Cache) CacheServiceImpl.parameterCache
NoSuchBeanDefinitionException: No unique bean of type [net.sf.ehcache.Cache] is defined: Unsatisfied dependency of type [class net.sf.ehcache.Cache]: expected at least 1 matching bean
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessAfterInstantiation(AutowiredAnnotationBeanPostProcessor.java:241)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:927)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:462)
............
Caused by:
BeanCreationException: Could not autowire field (CacheService) CacheServiceFacade.cacheService
BeanCreationException: Error creating bean with name 'cacheServiceImpl': Autowiring of fields failed
BeanCreationException: Could not autowire field (net.sf.ehcache.Cache) CacheServiceImpl.parameterCache
NoSuchBeanDefinitionException: No unique bean of type [net.sf.ehcache.Cache] is defined: Unsatisfied dependency of type [class net.sf.ehcache.Cache]: expected at least 1 matching bean
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:433)
at org.springframework.beans.factory.annotation.InjectionMetadata.injectFields(InjectionMetadata.java:104)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessAfterInstantiation(AutowiredAnnotationBeanPostProcessor.java:238)
... 35 more
Caused by:
BeanCreationException: Error creating bean with name 'cacheServiceImpl': Autowiring of fields failed
BeanCreationException: Could not autowire field (net.sf.ehcache.Cache) CacheServiceImpl.parameterCache
NoSuchBeanDefinitionException: No unique bean of type [net.sf.ehcache.Cache] is defined: Unsatisfied dependency of type [class net.sf.ehcache.Cache]: expected at least 1 matching bean
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessAfterInstantiation(AutowiredAnnotationBeanPostProcessor.java:241)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:927)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:462)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory$1.run(AbstractAutowireCapableBeanFactory.java:404)
at java.security.AccessController.doPrivileged(Native Method)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:375)
at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:263)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:170)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:260)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:184)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:163)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.findAutowireCandidates(DefaultListableBeanFactory.java:671)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:611)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:410)
... 37 more
Caused by:
BeanCreationException: Could not autowire field (net.sf.ehcache.Cache) CacheServiceImpl.parameterCache
NoSuchBeanDefinitionException: No unique bean of type [net.sf.ehcache.Cache] is defined: Unsatisfied dependency of type [class net.sf.ehcache.Cache]: expected at least 1 matching bean
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:433)
at org.springframework.beans.factory.annotation.InjectionMetadata.injectFields(InjectionMetadata.java:104)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessAfterInstantiation(AutowiredAnnotationBeanPostProcessor.java:238)
... 50 more
Caused by:
NoSuchBeanDefinitionException: No unique bean of type [net.sf.ehcache.Cache] is defined: Unsatisfied dependency of type [class net.sf.ehcache.Cache]: expected at least 1 matching bean
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:614)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:410)
... 52 more
分析問題
從描述信息可以看到是Spring啟動出錯坞靶,CacheServiceFacade -> (@Autowired) CacheServiceImpl -> (@Autowired) Cache 依賴關系上因為Cache創(chuàng)建失敗無法注入導致最上層的CacheServiceFacade創(chuàng)建失敗。一個常見的Bean缺失無法層級注入的錯誤蝴悉,一般是開發(fā)階段Spring Bean定義遺漏導致Bean缺失無法注入彰阴,但這里只是遷移了部署環(huán)境并沒有涉及開發(fā)改動。
首先我們閱讀錯誤日志里挖掘些有用信息拍冠,第一段堆棧說的是頂層的CacheServiceFacade創(chuàng)建失敗尿这,堆棧調用信息里doCreateBean、populateBean之后就到了AutowiredAnnotationBeanPostProcessor.postProcessAfterInstantiation庆杜,即使對Spring加載機制不熟悉也能獲知CacheServiceFacade的成員注入就是在AutowiredAnnotationBeanPostProcessor這個BeanPostProcessor的postProcessAfterInstantiation方法里完成(高版本Spring將注入邏輯移至 postProcessPropertyValues方法)射众。接著一段段的Caused by說的是層級注入失敗,明確的事情重復說晃财,可以忽略叨橱。最后一段堆棧就是錯誤源頭Cache創(chuàng)建失敗,堆棧調用信息里是從resolveDependency方法拋出的異常断盛。
所以我們從AutowiredAnnotationBeanPostProcessor.postProcessAfterInstantiation方法開始代碼走讀罗洗,查找Cache被上層依賴后,是如何被創(chuàng)建和注入钢猛,以及發(fā)生創(chuàng)建失敗的伙菜。
findAutowiringMetadata方法是將需要綁定的(注解有@Autowired,高版本Spring支持@Autowired/@Value/@Inject)的變量(AutowiredFieldElement)和方法(AutowiredMethodElement)收集至InjectionMetadata中命迈。
這里又回到AutowiredAnnotationBeanPostProcessor的內(nèi)部類AutowiredFieldElement的inject方法中贩绕,里面會使用核心類DefaultListableBeanFactory的resolveDependency(..., beanName, ...)方法來尋找注入Bean也就是Cache Bean。
resolveDependency方法的每一段if-else if都是在處理注入Bean是否是集合類型壶愤,非集合也就在最后的else里我們看到了啟動日志里的異常信息淑倾,位置和日志堆棧里的調用信息也一致。其實若能熟練閱讀錯誤日志征椒,可以直接從日志里找到resolveDependency方法開始走讀娇哆。
說明findAutowireCandidates(beanName, type, descriptor)方法沒有通過名稱找到注入Bean。
BeanFactoryUtils.beanNamesForTypeIncludingAncestors這個方法像是在找bean了,點進去發(fā)現(xiàn)其實又回到beanFactory用核心的getBeanNamesForType方法來找bean迂尝。
通過debug得到入?yún)閠ype="net.sf.ehcache.Cache" includePrototypes=true allowEagerInit=true。這個核心方法需要關注幾個點:
1)beanFactory拿出所有的BeanDefinition來遍歷核對剪芥,Spring加載機制先收集bean信息也就是BeanDefinition垄开,然后才挨個definition的去創(chuàng)建實例bean并遞歸創(chuàng)建注入依賴bean。
2)這個net.sf.ehcache.Cache是個實現(xiàn)類税肪,實現(xiàn)net.sf.ehcache.Ehcache接口溉躲。定義Cache時只需要定義Spring提供的EhCacheFactoryBean,由factoryBean的工廠方法來生成Cache Bean益兄。
<bean id="parameterCache" class="org.springframework.cache.ehcache.EhCacheFactoryBean">
<property name="cacheManager"><ref local="ehCacheManager" /></property>
<property name="cacheName"><value>parameterCache</value></property>
</bean>
所以definition中是沒有Cache的锻梳,只會有Cache的工廠類EhCacheFactoryBean,因為有定義id所以definition具體為"parameterCache"净捅。
3)遍歷definition時疑枯,beanName為definition也就是"parameterCache"(EhCacheFactoryBean),所以isFactoryBean為true蛔六。type依然為Cache所以第一個isTypeMatch里"parameterCache"(EhCacheFactoryBean)不會直接等于Cache所以matchFound為false荆永,進入if后第二個isTypeMatch就很關鍵了。注意此時beanName帶上了個"&"前綴以區(qū)分當做FactoryBean來使用国章。
再次進入isTypeMatch這個方法有點難讀用debug來跟蹤具钥,入?yún)閚ame="¶meterCache" targetType="net.sf.ehcache.Cache"。beanClass為EhCacheFactoryBean.class液兽,getTypeForFactoryBean返回得到的type=net.sf.ehcache.Ehcache.class骂删,導致return語句Cache.class.isAssignableFrom(net.sf.ehcache.Ehcache.class) == false 因為net.sf.ehcache.Cache 是 net.sf.ehcache.Ehcache 的實現(xiàn)類。就是此處return false導致關鍵的第二個isTypeMatch返回false四啰,于是創(chuàng)建Cache Bean失敗宁玫。
很好奇為什么getTypeForFactoryBean返回net.sf.ehcache.Ehcache,打開來看里面要創(chuàng)建EhCacheFactoryBean拟逮,但創(chuàng)建出來的EhCacheFactoryBean沒有完全初始化撬统,debug發(fā)現(xiàn)成員Ehcache cache=null,所以此時調用getObjectType方法直接返回默認值Ehcache.class了敦迄。
解決問題
既然Spring都假設入?yún)ype是接口類型恋追,factoryBean返回的ObjectType是接口或實現(xiàn)類,那就按編碼規(guī)范將@Autowired標注的field定義成接口類型net.sf.ehcache.Ehcache罚屋。
回顧問題
1)為什么本地及 CentOS 5 下啟動正常苦囱,而 CentOS 7 下啟動失敗脾猛?
打印 BeanFactory 中注冊的 BeanDefinition 列表撕彤,發(fā)現(xiàn)本地和 CentOS 7 下的順序不一致,猜測是文件系統(tǒng)導致掃描的class文件順序不一致:
本地:
rcacheManager | com.xxx.RcacheManager
redisProvider | com.xxx.RedisCacheProvider
redisCacheManager | null
ddsRedisManager | null
accountFacade | com.xxx.AccountFacade
adsManagerFacade | com.xxx.AdsManagerFacade
cacheServiceFacade | com.xxx.CacheServiceFacade
......
CentOS 7:
rcacheManager | com.xxx.RcacheManager
redisProvider | com.xxx.RedisCacheProvider
redisCacheManager | null
ddsRedisManager | null
cacheServiceFacade | com.xxx.CacheServiceFacade
adsManagerFacade | com.xxx.AdsManagerFacade
accountFacade | com.xxx.AccountFacade
......
CentOS 7里出錯的cacheServiceFacade出現(xiàn)在第一位,本地里cacheServiceFacade前有accountFacade和adsManagerFacade羹铅,如果在本地的Spring注釋掉accountFacade和adsManagerFacade的定義讓cacheServiceFacade排在最前面蚀狰,這樣的話本地也能復現(xiàn)同樣的錯誤。
2)為什么accountFacade也依賴了net.sf.ehcache.Cache parameterCache卻不會出錯呢职员?
AccountFacade -> (@Autowired) CustomerServiceImpl -> (@Autowired) AdminParameterServiceImpl | (AOP XML) ParameterCacheInterceptor -> (XML) Cache
<aop:config>
<aop:advisor pointcut="execution(* com.xxx.AdminParameterServiceImpl.listParameters(..))" advice-ref="parameterCacheInterceptor" order="3" />
</aop:config>
<bean id="parameterCacheInterceptor" class="com.xxx.ParameterCacheInterceptor">
<property name="cache"><ref local="parameterCache" /></property>
</bean>
parameterCacheInterceptor是通過XML直接定義注入Cache Bean麻蹋,所以不會像@Autowired自動注入會去遍歷BeanDefinition去做類型匹配。