一个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被上层依赖后,是如何被创建和注入,以及发生创建失败的。
findAutowiringMetadata方法是将需要绑定的(注解有@Autowired,高版本Spring支持@Autowired/@Value/@Inject)的变量(AutowiredFieldElement)和方法(AutowiredMethodElement)收集至InjectionMetadata中。
这里又回到AutowiredAnnotationBeanPostProcessor的内部类AutowiredFieldElement的inject方法中,里面会使用核心类DefaultListableBeanFactory的resolveDependency(..., beanName, ...)方法来寻找注入Bean也就是Cache Bean。
resolveDependency方法的每一段if-else if都是在处理注入Bean是否是集合类型,非集合也就在最后的else里我们看到了启动日志里的异常信息,位置和日志堆栈里的调用信息也一致。其实若能熟练阅读错误日志,可以直接从日志里找到resolveDependency方法开始走读。
说明findAutowireCandidates(beanName, type, descriptor)方法没有通过名称找到注入Bean。
BeanFactoryUtils.beanNamesForTypeIncludingAncestors这个方法像是在找bean了,点进去发现其实又回到beanFactory用核心的getBeanNamesForType方法来找bean。
通过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="¶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 的实现类。就是此处return false导致关键的第二个isTypeMatch返回false,于是创建Cache Bean失败。
很好奇为什么getTypeForFactoryBean返回net.sf.ehcache.Ehcache,打开来看里面要创建EhCacheFactoryBean,但创建出来的EhCacheFactoryBean没有完全初始化,debug发现成员Ehcache cache=null,所以此时调用getObjectType方法直接返回默认值Ehcache.class了。
解决问题
既然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去做类型匹配。