美文网首页
应用迁移CentOS 7 后Spring启动出错问题复盘

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

作者: 哈密朵 | 来源:发表于2019-11-07 22:51 被阅读0次

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

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/tyubbctx.html