1. 程式人生 > >34 mybatis-spring Mapper太多導致 StackOverflow

34 mybatis-spring Mapper太多導致 StackOverflow

前言

前幾天 專案上有這樣的一個問題, 記憶體給的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"

查看了一下 , 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

資源下載

參考