前言
前几天 项目上有这样的一个问题, 内存给的2G, 项目似乎是启动不起来, 然后 之后调整为 4G 项目就启动起来了
然后 这个问题, 我们最开始都没有太在意, 认为可能 初始化项目需要的内存稍微需要大一些吧 ?
然后 上周五, 和测试同事发布项目的时候, 又出现了这个问题, 然后 在测试环境 周四是能够启动起来, 然后 期间有一些小调整, 然后 周五发布项目 就发布不起了
还有一些其他的现象, 预发布环境是发布不了的, 然后 就没有往下走了, 大家下班了
我本地 启动项目, mountServices 是可以启动起的, 并且没有什么异常
然后 我在我们开发环境我试了一下 第一次也没得问题, 能够启动成功, 然后 第二次 我又重新打了一个包, 更新了所有的依赖, 看了一下日志, 发现 启动的过程中 有很多 "2018-10-12 19:24:52,352 [main] WARN org.springframework.beans.factory.support.AbstractBeanFactory (AbstractBeanFactory.java:1490) - Bean creation exception on non-lazy FactoryBean type check: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'trainReportPerDayDetailMapper' defined in URL [jar:file:/home/dubbo/xxx-base/deploy/xxx-base-service-0.2-SNAPSHOT.jar!/com/xxx/dao/generator/XXXMapper.class]: Unsatisfied dependency expressed through bean property 'sqlSessionFactory': Error creating bean with name 'sqlSessionFactory' defined in class path resource [spring/applicationContext-data.xml]: Invocation of init method failed; nested exception is java.lang.StackOverflowError; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sqlSessionFactory' defined in class path resource [spring/applicationContext-data.xml]: Invocation of init method failed; nested exception is java.lang.StackOverflowError" 这种样子的错误
当然 还能从日志文件里面 找到最关键的一个信息, 一个"完整的StackTrace", 参见文件下载
项目基本信息 : dubbo项目, spring-* 4.3.0 + dubbo 2.8.4 + mybatis 3.2.7 + mybatis-spring 1.2.2
问题的排查
看到这串 StackTrace, 我就意识到 问题并不是那么简单, 因此 周五还是早早的下班比较好, 还是找一个心情好的时间 来看下这个问题, 不然 搞到半夜也浪费时间, 还对身体不好, 而且还有一个问题, 同事 还等着的呢, 算了 下班了
那么 如何解决这个问题呢?, 自然 要复现啊, 但是 我本地又没得这个问题啊, 然后 抱着侥幸的心理, 在 StackTrace 上面的 DruidDataSource.init 上面打了一个断点, 然后 debug 启动了一下项目
然后 断点停了, 大致的造成项目启动不了的一个粗略的原因 也出来了
这 157 次 AbstractAutowireCapableBeanFactory. doCreateBean 的调用, 前几次 是容器中一个 实例[redisXXX], 使用 @Autowire 注入了一个 连接池
然后 中间 153 次调用是各个 mybatis 的 Mapper, ExMapper
然后 后面的几次 是, sqlSessionFactory, dataSource 的调用
在 spring容器 注入 redisXXX 连接池依赖的时候, 调用了 DefaultListableBeanFactory. doGetBeanNamesForType 来查找容器中所有的 redis连接池 对象
需要遍历 容器里面所有的对象, 在遍历到了 第一个 AMapper 的时候, 发现容器里面还没有创建 AMapper, 然后 调用了 AbstractAutowireCapableBeanFactory. getTypeForFactoryBean 来实例化 AMapper
然后 又解析 AMapper 的依赖 sqlSessionFactory
然后 调用 DefaultListableBeanFactory. doGetBeanNamesForType 方法来查找 sqlSessionFactory, 遍历到 AMapper 的时候, AMapper已经实例化了, 然后 判断了一下类型, 是否匹配 sqlSessionFactory , 不匹配 继续往下走
然后 找到了 第二个BMapper, 然后 发现容器里面还没有创建 BMapper, 然后 调用了 AbstractAutowireCapableBeanFactory. getTypeForFactoryBean 来实例化 AMapper
然后 又解析 BMapper 的依赖 sqlSessionFactory
直到 到达最后一个 Mapper, 找到了所有的 sqlSessionFactory 之后, 开始 实例化 sqlSessionFactory (参见下图)
然后 解析sqlSessionFactory 的依赖, 实例化 dataSource 等等, 然后 到了我们的 DruidDataSource.init 的断点
问题的解决
至于解决的方法, 我有几种思路, 但是 似乎不太好处理呢??
方法1. 先预先吧所有的 Mapper 对应的实例 加载好, 破坏递归的条件[初始化当前Mapper, 进而解析依赖, 进而递归到下一个Mapper的流程]
方法2. Mapper 对应的实例 的sqlSessionFactory, 注入方式 似乎是 byType, 然后 才进入了"递归", 更新一**入的方式, 就不用进入需要遍历beanNames 的"递归"
方法1
似乎是不太好找到更新优先初始化 Mapper 实例的方法
最开始, 我想的是 是否可以通过 MapperScannerConfigurer 里面配置优先级, 然后配置其他的bean的优先级, 但是 似乎实现起来并没有这么简单, 提升 Mapper 的优先级似乎是搞不定
方法2
case1. 从 ClassPathMapperScanner 里面调整一下 默认的 autowireMode, 差不多是 需要两个事情吧, 重写两个自己的 MapperScannerConfigurer, ClassPathMapperScanner, 调整一下 前者使用的 MapperScanner, 以及后者的 autowireMode
case2. 然后之后 又瞄了一下 ClassPathMapperScanner 的代码, 发现 可以显示指定, sqlSessionFactoryBeanName/sqlSessionFactory 或者sqlSessionTemplateBeanName/sqlSessionTemplate 可以直接引用 容器中的实例, 同样可以达到效果
最后使用 方法2 的后者 达到了效果, 问题 似乎是解决了, 前者 没有尝试
后记
1. tips : idea 条件断点 的一个细节, 打了改断点之后, 项目启动 初始化每一个 ServiceBean 的时候[afterPropertiesSet], 有6个 beansOfTypeIncludingAncestors 的查询, 非常慢
DefaultListableBeanFactory:440 打了一个条件断点, 条件如下
"accFormatMapper".equals(beanName) && type.type == SqlSessionFactory.class
原来这么慢, 是因为 条件断点的开销,
擦 我还以为是调整了一下, "<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />" 的开销呢,
另外 通过 overhead 这里的提示, 可以看到 这个条件断点的开销, 普通的断点 基本上是不会有太大的开销的
2. 差不多是后来一点 : 网上搜索了一下 "spring mybatis Mapper太多 StackOverflow"
是能够搜索到一篇文章的 : https://blog.csdn.net/hongxingxiaonan/article/details/50354195
查看了一下 , SqlSessionDaoSupport 的注释, setXX 这两个方法的 @Autowire 是在 1.2.0 里面删除的
/**
* Convenient super class for MyBatis SqlSession data access objects.
* It gives you access to the template which can then be used to execute SQL methods.
* <p>
* This class needs a SqlSessionTemplate or a SqlSessionFactory.
* If both are set the SqlSessionFactory will be ignored.
* <p>
* {code Autowired} was removed from setSqlSessionTemplate and setSqlSessionFactory
* in version 1.2.0.
*
* @author Putthibong Boonbong
*
* @see #setSqlSessionFactory
* @see #setSqlSessionTemplate
* @see SqlSessionTemplate
* @version $Id$
*/
@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;
}
另外 比较了一下 mybatis-spring 1.1.1 和 1.2.0 的 MapperScannerCongiurer, 1.2.0 将 doScan 的步骤封装了一层, 封装了一个 ClassPathMapperScanner
另外 就是 如果 MapperScannerCongiurer 如果没有显式的指定 sqlSessionFactoryBeanName/sqlSessionFactory 或者sqlSessionTemplateBeanName/sqlSessionTemplate, 配置 MapperFactoryBean 的 autowireMode 为 "autowire by type"
1.1.1 里面 则是, 如果显式指定 或者 通过 @Autowire(required = false), 至于二者的 先后顺序, 应该是 以用户显式指定的为准, 如果 已经显式指定, 会忽略 @Autowire
综上 1.1.1 不显示指定 sqlSessionFactoryBeanName/sqlSessionFactory 或 sqlSessionTemplateBeanName/sqlSessionTemplate, 会通过 @Autowire 来寻找 sqlSessionFactory,
1.2.0 不显示指定 sqlSessionFactoryBeanName/sqlSessionFactory 或 sqlSessionTemplateBeanName/sqlSessionTemplate, 会通过 autowireMode 为 "autowire by type" 来寻找 sqlSessionFactory
我觉得, 其实 不显示指定的话, 都会走 上面的递归的流程
但是 为什么博主问题就解决了, 可能还是 环境上有一些问题, 或者我上面的猜测 有一些错误的地方吧, 感谢 hongxingxiaonan 分享
3. 附上一个模拟该场景的代码, 方便理解
以下实例代码中, ConnectionProvider 以及相关的工具类, 可以通过其他的方式代替, 稍作改动 就能运行
package com.hx.test05;
import com.hx.log.collection.CollectionUtils;
import com.hx.mongo.connection.DefaultMysqlConnectionProvider;
import com.hx.mongo.connection.interf.ConnectionProvider;
import org.apache.commons.lang.StringUtils;
import java.util.*;
import static com.hx.log.util.Log.info;
/**
* Test28MapperStackOverFlow
*
* @author Jerry.X.He <[email protected]>
* @version 1.0
* @date 10/14/2018 9:39 AM
*/
public class Test28MapperStackOverFlow {
/**
* container
*/
private static Set<String> BEAN_NAMES = new LinkedHashSet<>();
private static Map<String, Object> BEAN_CREATED = new HashMap<>();
/**
* for debug
*/
private static int STACK_DEPTH = 0;
private static boolean DEBUG = true;
// Test28MapperStackOverFlow
public static void main(String[] args) {
// initialized
registerBean("connectionProvider", new DefaultMysqlConnectionProvider());
// not initialized
for (int i = 0; i < 10; i++) {
registerBean("The" + i + "thMapper", null);
}
List<String> result = doGetBeanNamesForType(MapperFactoryBean.class);
info(result);
}
/**
* look up 容器中所有匹配 给定类型的 beanName
*
* @param requiredType requiredType
* @return java.util.List<java.lang.String>
* @author Jerry.X.He
* @date 10/14/2018 9:42 AM
* @since 1.0
*/
public static List<String> doGetBeanNamesForType(Class requiredType) {
if (DEBUG) {
info(generatePadding(STACK_DEPTH) + "doGetBeanNamesForType, requiredType : " + requiredType.getName());
}
STACK_DEPTH++;
List<String> result = new ArrayList<>();
for (String beanName : BEAN_NAMES) {
if (isTypeMatch(beanName, requiredType)) {
result.add(beanName);
}
}
STACK_DEPTH--;
return result;
}
/**
* @param beanName beanName
* @param requiredType requiredType
* @return boolean
* @author Jerry.X.He
* @date 10/14/2018 9:40 AM
* @since 1.0
*/
public static boolean isTypeMatch(String beanName, Class requiredType) {
if (DEBUG) {
info(generatePadding(STACK_DEPTH) + "isTypeMatch, beanName : " + beanName);
}
Object beanInstance = getBean(beanName);
if (beanInstance != null) {
return doTypeMatch(beanInstance, requiredType);
}
// create bean by factoryBean
MapperFactoryBean bean = new MapperFactoryBean();
registerBean(beanName, bean);
// resolve dependencies
List<String> connectionProviderBeanNames = doGetBeanNamesForType(ConnectionProvider.class);
if (CollectionUtils.isEmpty(connectionProviderBeanNames)) {
throw new RuntimeException(" can't find bean of ConnectionProvider ! ");
}
ConnectionProvider provider = getBean(connectionProviderBeanNames.get(0), ConnectionProvider.class);
// populate attributes
bean.setProvider(provider);
return doTypeMatch(bean, requiredType);
}
public static boolean doTypeMatch(Object instance, Class requiredType) {
return requiredType.isAssignableFrom(instance.getClass());
}
/**
* 从容器中拿 beanName 对应的 bean
*
* @param beanName beanName
* @return java.lang.Object
* @author Jerry.X.He
* @date 10/14/2018 9:45 AM
* @since 1.0
*/
public static Object getBean(String beanName) {
return BEAN_CREATED.get(beanName);
}
public static <T> T getBean(String beanName, Class<T> clazz) {
Object bean = BEAN_CREATED.get(beanName);
if (!doTypeMatch(bean, clazz)) {
return null;
}
return (T) bean;
}
/**
* 注册 给定的 bean
*
* @param beanName beanName
* @param bean bean
* @return void
* @author Jerry.X.He
* @date 10/14/2018 9:58 AM
* @since 1.0
*/
public static void registerBean(String beanName, Object bean) {
BEAN_NAMES.add(beanName);
Object oldBean = BEAN_CREATED.put(beanName, bean);
// process oldBean
}
/**
* MapperFactoryBean
*
* @author Jerry.X.He <[email protected]>
* @version 1.0
* @date 10/14/2018 9:52 AM
*/
static class MapperFactoryBean {
ConnectionProvider provider;
public MapperFactoryBean() {
}
public ConnectionProvider getProvider() {
return provider;
}
public void setProvider(ConnectionProvider provider) {
this.provider = provider;
}
}
// ----------------- 辅助方法 -----------------------
/**
* 生成 len 个空格, 调试
*
* @param len len
* @return java.lang.String
* @author Jerry.X.He
* @date 10/14/2018 10:16 AM
* @since 1.0
*/
private static String generatePadding(int len) {
return StringUtils.leftPad("", len << 2);
}
}
执行的大致流程如下, 试想 将 10个Mapper 换成 100 个Mapper, 会有怎样的烟火 #_#
doGetBeanNamesForType, requiredType : com.hx.test05.Test28MapperStackOverFlow$MapperFactoryBean
isTypeMatch, beanName : connectionProvider
isTypeMatch, beanName : The0thMapper
doGetBeanNamesForType, requiredType : com.hx.mongo.connection.interf.ConnectionProvider
isTypeMatch, beanName : connectionProvider
isTypeMatch, beanName : The0thMapper
isTypeMatch, beanName : The1thMapper
doGetBeanNamesForType, requiredType : com.hx.mongo.connection.interf.ConnectionProvider
isTypeMatch, beanName : connectionProvider
isTypeMatch, beanName : The0thMapper
isTypeMatch, beanName : The1thMapper
isTypeMatch, beanName : The2thMapper
doGetBeanNamesForType, requiredType : com.hx.mongo.connection.interf.ConnectionProvider
isTypeMatch, beanName : connectionProvider
isTypeMatch, beanName : The0thMapper
isTypeMatch, beanName : The1thMapper
isTypeMatch, beanName : The2thMapper
isTypeMatch, beanName : The3thMapper
doGetBeanNamesForType, requiredType : com.hx.mongo.connection.interf.ConnectionProvider
isTypeMatch, beanName : connectionProvider
isTypeMatch, beanName : The0thMapper
isTypeMatch, beanName : The1thMapper
isTypeMatch, beanName : The2thMapper
isTypeMatch, beanName : The3thMapper
isTypeMatch, beanName : The4thMapper
doGetBeanNamesForType, requiredType : com.hx.mongo.connection.interf.ConnectionProvider
isTypeMatch, beanName : connectionProvider
isTypeMatch, beanName : The0thMapper
isTypeMatch, beanName : The1thMapper
isTypeMatch, beanName : The2thMapper
isTypeMatch, beanName : The3thMapper
isTypeMatch, beanName : The4thMapper
isTypeMatch, beanName : The5thMapper
doGetBeanNamesForType, requiredType : com.hx.mongo.connection.interf.ConnectionProvider
isTypeMatch, beanName : connectionProvider
isTypeMatch, beanName : The0thMapper
isTypeMatch, beanName : The1thMapper
isTypeMatch, beanName : The2thMapper
isTypeMatch, beanName : The3thMapper
isTypeMatch, beanName : The4thMapper
isTypeMatch, beanName : The5thMapper
isTypeMatch, beanName : The6thMapper
doGetBeanNamesForType, requiredType : com.hx.mongo.connection.interf.ConnectionProvider
isTypeMatch, beanName : connectionProvider
isTypeMatch, beanName : The0thMapper
isTypeMatch, beanName : The1thMapper
isTypeMatch, beanName : The2thMapper
isTypeMatch, beanName : The3thMapper
isTypeMatch, beanName : The4thMapper
isTypeMatch, beanName : The5thMapper
isTypeMatch, beanName : The6thMapper
isTypeMatch, beanName : The7thMapper
doGetBeanNamesForType, requiredType : com.hx.mongo.connection.interf.ConnectionProvider
isTypeMatch, beanName : connectionProvider
isTypeMatch, beanName : The0thMapper
isTypeMatch, beanName : The1thMapper
isTypeMatch, beanName : The2thMapper
isTypeMatch, beanName : The3thMapper
isTypeMatch, beanName : The4thMapper
isTypeMatch, beanName : The5thMapper
isTypeMatch, beanName : The6thMapper
isTypeMatch, beanName : The7thMapper
isTypeMatch, beanName : The8thMapper
doGetBeanNamesForType, requiredType : com.hx.mongo.connection.interf.ConnectionProvider
isTypeMatch, beanName : connectionProvider
isTypeMatch, beanName : The0thMapper
isTypeMatch, beanName : The1thMapper
isTypeMatch, beanName : The2thMapper
isTypeMatch, beanName : The3thMapper
isTypeMatch, beanName : The4thMapper
isTypeMatch, beanName : The5thMapper
isTypeMatch, beanName : The6thMapper
isTypeMatch, beanName : The7thMapper
isTypeMatch, beanName : The8thMapper
isTypeMatch, beanName : The9thMapper
doGetBeanNamesForType, requiredType : com.hx.mongo.connection.interf.ConnectionProvider
isTypeMatch, beanName : connectionProvider
isTypeMatch, beanName : The0thMapper
isTypeMatch, beanName : The1thMapper
isTypeMatch, beanName : The2thMapper
isTypeMatch, beanName : The3thMapper
isTypeMatch, beanName : The4thMapper
isTypeMatch, beanName : The5thMapper
isTypeMatch, beanName : The6thMapper
isTypeMatch, beanName : The7thMapper
isTypeMatch, beanName : The8thMapper
isTypeMatch, beanName : The9thMapper
isTypeMatch, beanName : The9thMapper
isTypeMatch, beanName : The8thMapper
isTypeMatch, beanName : The9thMapper
isTypeMatch, beanName : The7thMapper
isTypeMatch, beanName : The8thMapper
isTypeMatch, beanName : The9thMapper
isTypeMatch, beanName : The6thMapper
isTypeMatch, beanName : The7thMapper
isTypeMatch, beanName : The8thMapper
isTypeMatch, beanName : The9thMapper
isTypeMatch, beanName : The5thMapper
isTypeMatch, beanName : The6thMapper
isTypeMatch, beanName : The7thMapper
isTypeMatch, beanName : The8thMapper
isTypeMatch, beanName : The9thMapper
isTypeMatch, beanName : The4thMapper
isTypeMatch, beanName : The5thMapper
isTypeMatch, beanName : The6thMapper
isTypeMatch, beanName : The7thMapper
isTypeMatch, beanName : The8thMapper
isTypeMatch, beanName : The9thMapper
isTypeMatch, beanName : The3thMapper
isTypeMatch, beanName : The4thMapper
isTypeMatch, beanName : The5thMapper
isTypeMatch, beanName : The6thMapper
isTypeMatch, beanName : The7thMapper
isTypeMatch, beanName : The8thMapper
isTypeMatch, beanName : The9thMapper
isTypeMatch, beanName : The2thMapper
isTypeMatch, beanName : The3thMapper
isTypeMatch, beanName : The4thMapper
isTypeMatch, beanName : The5thMapper
isTypeMatch, beanName : The6thMapper
isTypeMatch, beanName : The7thMapper
isTypeMatch, beanName : The8thMapper
isTypeMatch, beanName : The9thMapper
isTypeMatch, beanName : The1thMapper
isTypeMatch, beanName : The2thMapper
isTypeMatch, beanName : The3thMapper
isTypeMatch, beanName : The4thMapper
isTypeMatch, beanName : The5thMapper
isTypeMatch, beanName : The6thMapper
isTypeMatch, beanName : The7thMapper
isTypeMatch, beanName : The8thMapper
isTypeMatch, beanName : The9thMapper
The0thMapper, The1thMapper, The2thMapper, The3thMapper, The4thMapper, The5thMapper, The6thMapper, The7thMapper, The8thMapper, The9thMapper
Process finished with exit code 0
资源下载
stackTrace : https://download.csdn.net/download/u011039332/10719234
参考
https://blog.csdn.net/hongxingxiaonan/article/details/50354195