Springboot Mybatis 打包jar掃描bean與mapper問題研究與解決

SpringBootLean 是對springboot學習與研究項目,是根據實際項目的形式對進行配置與處理,歡迎star與fork。
[oschina 地址]
http://git.oschina.net/cmlbeliever/SpringBootLearning
[github 地址]
https://github.com/cmlbeliever/SpringBootLearning

最近在項目中集成以全註解的方式Mybatis,配置了自動bean包與mapper所在包

db.mybatis.mapperLocations=classpath*:com/cml/springboot/sample/db/resource/*
db.mybatis.typeAliasesPackage=com.cml.springboot.sample.bean
db.mybatis.typeHandlerPackage=com.cml.springboot.framework.mybatis.typehandler

這些配置直接在ide上運行都是ok的,但是經過打包成jar包後,就一直報錯提示找打不到對應的bean或者對應的mapper。主要錯誤如下:

 Error resolving class. Cause: org.apache.ibatis.type.TypeException: Could not resolve type alias 'logbean'.  Cause: java.lang.ClassNotFoundException: Cannot find class: logbean

***************************
APPLICATION FAILED TO START
***************************

Description:

Field logMapper in com.cml.springboot.sample.service.impl.LogServiceImpl required a bean of type 'com.cml.springboot.sample.db.LogMapper' that could not be found.


Action:

Consider defining a bean of type 'com.cml.springboot.sample.db.LogMapper' in your configuration.

針對以上問題,在新開的分支deploy_jar_bugfind上進行研究,找到了一個臨時解決辦法,就是配置mybat-configuration.xml將對應的bean都配置到xml上。

<configuration>
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true" />
    </settings>
    <typeAliases>
        <typeAlias type="com.cml.springboot.sample.bean.LogBean" alias="logbean"/>
        <typeAlias type="com.cml.springboot.sample.bean.User" alias="user"/>
    </typeAliases>
    <typeHandlers>
        <typeHandler javaType="org.joda.time.DateTime"
            handler="com.cml.springboot.framework.mybatis.typehandler.JodaDateTimeTypeHandler" />
        <typeHandler javaType="org.joda.time.LocalTime"
            handler="com.cml.springboot.framework.mybatis.typehandler.JodaLocalTimeTypeHandler" />
    </typeHandlers>
</configuration>

但是這個僅僅適合小項目並且bean少的情況,假如在打項目上,可能有幾十上百的bean,都要一一配置豈不是累死人了,而且容易出bug。

於是繼續研究,首先自定義DefaultSqlSessionFactoryBean取代默認的SqlSessionFactoryBean,並且在buildSqlSessionFactory()方法上對bean掃描的配置進行一步步log拆分,主要代碼如下:

if (hasLength(this.typeAliasesPackage)) {
            String[] typeAliasPackageArray = tokenizeToStringArray(this.typeAliasesPackage,
                    ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
            for (String packageToScan : typeAliasPackageArray) {

                logger.debug("##################################################################################");
                List<String> children = VFS.getInstance().list(packageToScan.replace(".", "/"));
                logger.debug("findclass:" + children);
                logger.debug("##################################################################################");

                logger.debug("DefaultSqlSessionFactoryBean.buildSqlSessionFactory.registerAliases:" + packageToScan);

                ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
                resolverUtil.find(new ResolverUtil.IsA(Object.class), packageToScan);
                Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();

                logger.debug(
                        "DefaultSqlSessionFactoryBean.buildSqlSessionFactory.typeAliasesPackage.scanClass:" + typeSet);

                for (Class<?> type : typeSet) {
                    if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
                        String alias = type.getSimpleName();
                        String key = alias.toLowerCase(Locale.ENGLISH);
                        logger.debug(
                                "DefaultSqlSessionFactoryBean.buildSqlSessionFactory.typeAliasesPackage.scan:" + key);

                    }
                }

                configuration.getTypeAliasRegistry().registerAliases(packageToScan,
                        typeAliasesSuperType == null ? Object.class : typeAliasesSuperType);
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Scanned package: '" + packageToScan + "' for aliases");
                }
            }
        }

發現在ide上運行的時候是可以掃描到指定的bean,但是在jar上就掃描不到了。主要問題找到了,就是對代碼的掃描問題,原本想自定義configuration的,但是發現好多類都是依賴configuration這個類,於是便放棄這個想法,轉而研究掃描不到bean的問題。

繼續研究源碼,得知bean的掃描是通過ResolverUtil這個類進行的,並且ResolverUtil掃描是通過VFS進行掃描的,主要代碼:

public ResolverUtil<T> find(Test test, String packageName) {
    String path = getPackagePath(packageName);

    try {
      List<String> children = VFS.getInstance().list(path);
      for (String child : children) {
        if (child.endsWith(".class"))
          addIfMatching(test, child);
      }
    } catch (IOException ioe) {
      log.error("Could not read package: " + packageName, ioe);
    }

    return this;
  }

結合log上的信息,工程上默認使用的是Mybatis的DefaultVFS進行掃描。查看這個類代碼後也沒發現什麼問題,轉而求救於百度與谷歌,經過多方搜索,找到了Mybatis官網issue (地址:https://github.com/mybatis/mybatis-3/issues/325)這裏有對這些問題進行說明。根據issue的描述與各大神的回答,定位到了問題是DefaultVFS在獲取jar上的class問題。

在mybat/spring-boot-starter工程上找到了SpringBootVFS,這個類重寫了class掃描功能,通過spring進行掃描。

於是將SpringBootVFS拷貝到工程上,並且添加到VFS實現上去,代碼如下:

@Bean(name = "sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(DataSource datasource, MybatisConfigurationProperties properties)
            throws Exception {

        log.info("*************************sqlSessionFactory:begin***********************" + properties);

        VFS.addImplClass(SpringBootVFS.class);

        DefaultSqlSessionFactoryBean sessionFactory = new DefaultSqlSessionFactoryBean();
        sessionFactory.setDataSource(datasource);
        sessionFactory.setTypeAliasesPackage(properties.typeAliasesPackage);
        sessionFactory.setTypeHandlersPackage(properties.typeHandlerPackage);

        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        sessionFactory.setMapperLocations(resolver.getResources(properties.mapperLocations));

//      sessionFactory
//              .setConfigLocation(new PathMatchingResourcePatternResolver().getResource(properties.configLocation));

        SqlSessionFactory resultSessionFactory = sessionFactory.getObject();

        log.info("===typealias==>" + resultSessionFactory.getConfiguration().getTypeAliasRegistry().getTypeAliases());

        log.info("*************************sqlSessionFactory:successs:" + resultSessionFactory
                + "***********************" + properties);

        return resultSessionFactory;

    }

這樣mybaits打包jar後掃描問題完美解決。


綜上,解決SpringBoot Mybatis打包jar後問題處理完畢,已提交到git項目master分支和deploy_jar_bugfind
http://git.oschina.net/cmlbeliever/SpringBootLearning

解決辦法:

  1. 通過mybatis-configuration.xml進行配置
  2. 通過SpringBootVFS進行自動掃描配置(推薦)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章