原标题:从一次工程启动失败谈谈 spring 注解
檀宝权
Java 后端开发工程师,负责度假 App 后端和广告后端开发维护工作,熟悉 Tomcat,Spring,Mybatis,会点 Python,Lua。
一、背景
线上环境升级成 JDK8后, Tomcat 启动会经常失败,调整 JVM 的栈大小为 2M 后,失败频率大大降低,但是偶尔还是会失败。捕获启动异常日志,会看到下面异常信息(没找到附件上传地方,暂不上传日志附件,只截取一段,感兴趣的可以直接找我):
at org.springframework.beans.factory.support.AbstractBeanFactory.isTypeMatch(AbstractBeanFactory.java:519)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doGetBeanNamesForType(DefaultListableBeanFactory.java:343)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanNamesForType(DefaultListableBeanFactory.java:320)
at org.springframework.beans.factory.BeanFactoryUtils.beanNamesForTypeIncludingAncestors(BeanFactoryUtils.java:187)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.findAutowireCandidates(DefaultListableBeanFactory.java:861)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:818)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:735)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.inject(AutowiredAnnotationBeanPostProcessor.java:551)
at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:87)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:284)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1106)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:517)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:456)
at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:294)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:225)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:291)
at org.springframework.beans.factory.support.AbstractBeanFactory.getTypeForFactoryBean(AbstractBeanFactory.java:1356)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.getTypeForFactoryBean(AbstractAutowireCapableBeanFactory.java:710)
at org.springframework.beans.factory.support.AbstractBeanFactory.isTypeMatch(AbstractBeanFactory.java:519)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doGetBeanNamesForType(DefaultListableBeanFactory.java:343)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanNamesForType(DefaultListableBeanFactory.java:320)
at org.springframework.beans.factory.BeanFactoryUtils.beanNamesForTypeIncludingAncestors(BeanFactoryUtils.java:187)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.findAutowireCandidates(DefaultListableBeanFactory.java:861)
Caused by: java.lang.StackOverflowError
栈溢出异常,很不正常的行为,为什么启动时候会导致这么长的调用链?
二、相关的版本环境
1. JDK82. Tomcat73. Spring3.1.2.RELEASE4. Mybatis-3.0.6.jar5. Mybatis-spring-1.0.2.jar
三、分析3.1 栈调用分析
仔细分析启动日志,发现下面的异常信息不断重复,熟悉 spring 源码的同学都知道,这段代码是用来创建 bean,但是创建的过程中发现依赖的 bean 不存在,继续创建依赖的 bean。
......
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1106)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:517)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:456)
at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:294)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:225)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:291)
at org.springframework.beans.factory.support.AbstractBeanFactory.getTypeForFactoryBean(AbstractBeanFactory.java:1356)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.getTypeForFactoryBean(AbstractAutowireCapableBeanFactory.java:710)
at org.springframework.beans.factory.support.AbstractBeanFactory.isTypeMatch(AbstractBeanFactory.java:519)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doGetBeanNamesForType(DefaultListableBeanFactory.java:343)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanNamesForType(DefaultListableBeanFactory.java:320)
at org.springframework.beans.factory.BeanFactoryUtils.beanNamesForTypeIncludingAncestors(BeanFactoryUtils.java:187)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.findAutowireCandidates(DefaultListableBeanFactory.java:861)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:818)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:735)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.inject(AutowiredAnnotationBeanPostProcessor.java:551)
at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:87)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:284)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1106)
......
关于 bean 之间的依赖,我们一般使用 @Autowired, @Resource 注解来声明。看上面的异常信息,明显使用的是 @Autowired 注解。断点调试发现,这个超长调用栈最终表现形式为:
service1->dao1->dao2->dao3->.....
这就很奇怪了,我们项目中 dao 使用的是 Mybatis Mapper XML 文件形式, dao 表现形式就是一个 Interface 文件, 根本不可能存在 dao 调用 dao 的情况。
3.2 @Autowired 注解
@Autowried 是类型匹配的注解, Spring 使用 AutowiredAnnotationBeanPostProcessor 来解析依赖。具体原理比较简单,大致如下:
遍历项目中所有注入的 BeanDefinition ;
获取每个 BeanDefinition 类型,校验此类型和 @Autowired 声明的类型是否匹配,匹配就认为是候选类型;
如果有多个候选类型,选择最佳的一个,找不到最佳的,就抛出存在多个同一类型的 bean 异常,启动失败。
那么,每个 BeanDefinition 类型是什么了?
普通的 bean,在注入到 spring 中就是其 beanClass 属性;
如果是 FactoryBean ,真正的类型并不是其 beanClass 属性声明的值,而是其方法 getObjectType() 返回的值 这就导致 spring 在查找类型的时候,如果遇到 FactoryBean ,就必须先实例化这个 bean ,只有创建成功后,才能调用对象的方法 getObjectType() 。
Mybatis Mapper dao 被 MapperScannerConfigurer 扫描后,注入到 spring 中的 beanClass 值为 org.mybatis.spring.mapper.MapperFactoryBean ,正好就是个 FactoryBean 。
3.3 MapperFactoryBean
先看看 MapperFacotryBean 的声明:
public class MapperFactoryBean extends SqlSessionDaoSupport implements FactoryBean {
......
}
public abstract class SqlSessionDaoSupport extends DaoSupport {
private SqlSession sqlSession;
private boolean externalSqlSession;
@Autowired(required = false)
public final void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
if (!this.externalSqlSession) {
this.sqlSession = new SqlSessionTemplate(sqlSessionFactory);
}
}
@Autowired(required = false)
public final void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
this.sqlSession = sqlSessionTemplate;
this.externalSqlSession = true;
}
......
}
哈哈,MapperFactoryBean 也是使用了 @Autowired , spring 在实例化 MapperFactoryBean 后仍需要继续按照3.2节的过程执行遍历,导致出现这样的调用链:
service->dao1->mapper1->dao2->mapper2->dao3->mapper3->......
所以随着系统 Mybatis Mapper dao 文件慢慢增多,这个调用链会越来越长,最终超过栈深度,抛出栈溢出异常。
3.4 @Resource
spring 启动时候,在解析 @Autowired 依赖注入,需要使用很大的篇幅去寻找候选 beanName ,必须逐一遍历每个 BeanDefinition ,费时费力,即使整个系统只有一个这样类型的 BeanDefinition 。但是 spring 在注入 BeanDefinition , 除了会 add beanDefinitionNames , 还会建立 beanName-> beanDefinitionMap 。所以,使用 @Resource 注解,会大大降低依赖注入 BeanDefinition 寻找过程,也就大大降低 bean 初始化调用栈。
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
throws BeanDefinitionStoreException {
synchronized (this.beanDefinitionMap) {
Object oldBeanDefinition = this.beanDefinitionMap.get(beanName);
if (oldBeanDefinition != null) {
if (!this.allowBeanDefinitionOverriding) {
throw new BeanDefinitionStoreException(beanDefinition.getResourceDeion(), beanName,
"Cannot register bean definition [" + beanDefinition + "] for bean '" + beanName +
"': There is already [" + oldBeanDefinition + "] bound.");
}
else {
if (this.logger.isInfoEnabled()) {
this.logger.info("Overriding bean definition for bean '" + beanName +
"': replacing [" + oldBeanDefinition + "] with [" + beanDefinition + "]");
}
}
}
else {
this.beanDefinitionNames.add(beanName);
this.frozenBeanDefinitionNames = null;
}
this.beanDefinitionMap.put(beanName, beanDefinition);
resetBeanDefinition(beanName);
}
}
四、结论
上面异常的根本原因是项目大量使用 @Autowired 注解,而且 mybatis-spring 版本比较陈旧导致。最新版本的 mybatis-spring 已经去掉了 @Autowired 注解,MapperScannerConfigurer 在扫描后直接为生成的MapperFactoryBean.sqlSession 赋值,不再使用注解去建立依赖关系。
解决办法:
升级 mybatis-spring 版本 最好 mybatis, spring 版本一并升级,目前这个确实太陈旧了
尽量使用 @Resource, 弃用 @Autowired@Autowired 本意是根据注入的类型来选择一个最佳的候选 bean, 当注入到系统的中候选 bean 只有一个时,是不关心 bean 的名称的,当有多个候选 bean时候,会进行 primary 判断或者名称判断。
@Resource 是根据名称查找 BeanDefinition, 当查找的的名称没有注入到 BeanFactory 中,CommonAnnotationBeanPostProcessor 就转到 @Autowired 查找。所以我们完全有理由弃用 @Autowired。返回搜狐,查看更多
责任编辑: