应用迁移CentOS 7 后Spring启动出错问题复盘

一个Spring2.5的老应用从CentOS 5 迁移到CentOS 7之后启动报错。该问题是由同事定位解决,本文是我之后的复盘和源码走读。
一句话结论:CentOS 7改变了Spring BeanFactory中的BeanDefinition顺序让另一个类排在最前先加载,该类的filed定义的是实现类而不是接口类型,该field被@Autowired标注在自动注入时会去匹配类型,由于定义的不是接口类型导致匹配失败。

发现问题

启动报错日志如下,做出少量精简:

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创建失败无法注入导致最上层的CacheServiceFacade创建失败。一个常见的Bean缺失无法层级注入的错误,一般是开发阶段Spring Bean定义遗漏导致Bean缺失无法注入,但这里只是迁移了部署环境并没有涉及开发改动。

首先我们阅读错误日志里挖掘些有用信息,第一段堆栈说的是顶层的CacheServiceFacade创建失败,堆栈调用信息里doCreateBean、populateBean之后就到了AutowiredAnnotationBeanPostProcessor.postProcessAfterInstantiation,即使对Spring加载机制不熟悉也能获知CacheServiceFacade的成员注入就是在AutowiredAnnotationBeanPostProcessor这个BeanPostProcessor的postProcessAfterInstantiation方法里完成(高版本Spring将注入逻辑移至 postProcessPropertyValues方法)。接着一段段的Caused by说的是层级注入失败,明确的事情重复说,可以忽略。最后一段堆栈就是错误源头Cache创建失败,堆栈调用信息里是从resolveDependency方法抛出的异常。

所以我们从AutowiredAnnotationBeanPostProcessor.postProcessAfterInstantiation方法开始代码走读,查找Cache被上层依赖后,是如何被创建和注入,以及发生创建失败的。

图片.png

findAutowiringMetadata方法是将需要绑定的(注解有@Autowired,高版本Spring支持@Autowired/@Value/@Inject)的变量(AutowiredFieldElement)和方法(AutowiredMethodElement)收集至InjectionMetadata中。

图片.png

这里又回到AutowiredAnnotationBeanPostProcessor的内部类AutowiredFieldElement的inject方法中,里面会使用核心类DefaultListableBeanFactory的resolveDependency(..., beanName, ...)方法来寻找注入Bean也就是Cache Bean。

图片.png

resolveDependency方法的每一段if-else if都是在处理注入Bean是否是集合类型,非集合也就在最后的else里我们看到了启动日志里的异常信息,位置和日志堆栈里的调用信息也一致。其实若能熟练阅读错误日志,可以直接从日志里找到resolveDependency方法开始走读。
说明findAutowireCandidates(beanName, type, descriptor)方法没有通过名称找到注入Bean。

图片.png

图片.png

BeanFactoryUtils.beanNamesForTypeIncludingAncestors这个方法像是在找bean了,点进去发现其实又回到beanFactory用核心的getBeanNamesForType方法来找bean。

图片.png

通过debug得到入参为type="net.sf.ehcache.Cache" includePrototypes=true allowEagerInit=true。这个核心方法需要关注几个点:
1)beanFactory拿出所有的BeanDefinition来遍历核对,Spring加载机制先收集bean信息也就是BeanDefinition,然后才挨个definition的去创建实例bean并递归创建注入依赖bean。
2)这个net.sf.ehcache.Cache是个实现类,实现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带上了个"&"前缀以区分当做FactoryBean来使用。
再次进入isTypeMatch这个方法有点难读用debug来跟踪,入参为name="&parameterCache" 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 的实现类。就是此处return false导致关键的第二个isTypeMatch返回false,于是创建Cache Bean失败。


图片.png

很好奇为什么getTypeForFactoryBean返回net.sf.ehcache.Ehcache,打开来看里面要创建EhCacheFactoryBean,但创建出来的EhCacheFactoryBean没有完全初始化,debug发现成员Ehcache cache=null,所以此时调用getObjectType方法直接返回默认值Ehcache.class了。


图片.png

解决问题

既然Spring都假设入参Type是接口类型,factoryBean返回的ObjectType是接口或实现类,那就按编码规范将@Autowired标注的field定义成接口类型net.sf.ehcache.Ehcache。

回顾问题

1)为什么本地及 CentOS 5 下启动正常,而 CentOS 7 下启动失败?
打印 BeanFactory 中注册的 BeanDefinition 列表,发现本地和 CentOS 7 下的顺序不一致,猜测是文件系统导致扫描的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出现在第一位,本地里cacheServiceFacade前有accountFacade和adsManagerFacade,如果在本地的Spring注释掉accountFacade和adsManagerFacade的定义让cacheServiceFacade排在最前面,这样的话本地也能复现同样的错误。

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去做类型匹配。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容