Mybatis3.2不支持Ant通配符TypeAliasesPackage扫描的解决方案

业务场景

业务场景:首先项目进行分布式拆分之后,按照模块再分为为api层和service层,web层。
其中订单业务的实体类放在com.muses.taoshop.item.entity,而用户相关的实体类放在com.muses.taoshop.user.entity。所以就这样,通过通配符方式去setTypeAliasesPackage ,com.muses.taoshop.*.entity

Ant通配符的3中风格:
(1) ?:匹配文件名中的一个字符 eg: com/test/entity? 匹配 com/test/entityaa
(2) * : 匹配文件名中的任意字符 eg: com//entity 匹配 com/test/entity
(3) ** : 匹配文件名中的多重路径 eg: com/
*/entity 匹配 com/test/test1/entity

mybatis配置类写在common工程,数据库操作有些是可以共用的,不需要每个web工程都进行重复配置。
所以写了个Mybatis配置类:

package com.muses.taoshop.common.core.database.config;
public class BaseConfig {

    /**
     * 设置主数据源名称
     */
    public static final String DATA_SOURCE_NAME = "shop";

    /**
     * 加载配置文件信息
     */
    public static final String DATA_SOURCE_PROPERTIES = "spring.datasource.shop";

    /**
     * repository 所在包
     */
    public static final String REPOSITORY_PACKAGES = "com.muses.taoshop.**.repository";

    /**
     * mapper 所在包
     */
    public static final String MAPPER_PACKAGES = "com.muses.taoshop.**.mapper";

    /**
     * 实体类 所在包
     */
    public static final String ENTITY_PACKAGES = "com.muses.taoshop.*.entity";
...

}

贴一下配置类代码,主要关注: factoryBean.setTypeAliasesPackage(ENTITY_PACKAGES);之前的写法是这样的。ENTITY_PACKAGES是个常量:public static final String ENTITY_PACKAGES = "com.muses.taoshop.*.entity";,ps:注意了,这里用了通配符

package com.muses.taoshop.common.core.database.config;
//省略代码
@MapperScan(
        basePackages = MAPPER_PACKAGES,
        annotationClass = MybatisRepository.class,
        sqlSessionFactoryRef = SQL_SESSION_FACTORY
)
@EnableTransactionManagement
@Configuration
public class MybatisConfig {
    //省略其它代码,主要看sqlSessionFactory配置
    @Primary
    @Bean(name = SQL_SESSION_FACTORY)
    public SqlSessionFactory sqlSessionFactory(@Qualifier(DATA_SOURCE_NAME)DataSource dataSource)throws Exception{
        //SpringBoot默认使用DefaultVFS进行扫描,但是没有扫描到jar里的实体类
        VFS.addImplClass(SpringBootVFS.class);
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        //factoryBean.setConfigLocation(new ClassPathResource("mybatis-config.xml"));
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        try{
            factoryBean.setMapperLocations(resolver.getResources("classpath*:/mybatis/*Mapper.xml"));
            //String typeAliasesPackage = packageScanner.getTypeAliasesPackages();
            //设置一下TypeAliasesPackage
            factoryBean.setTypeAliasesPackage(ENTITY_PACKAGES);
            SqlSessionFactory sqlSessionFactory = factoryBean.getObject();
            return sqlSessionFactory;
        }catch (Exception e){
            e.printStackTrace();
            throw new RuntimeException();
        }
    }

...

}

ps:原先做法:在sqlSessionFactory方法里进行TypeAliasesPackage设置,(让Mybatis能够扫描到实体类,在xml文件里就不需要写全实体类的全包名了。)factoryBean.setTypeAliasesPackage(ENTITY_PACKAGES);

但是运行之后都会报错,提示实体类不能扫描到,因为我的service工程里都是这样写的,ResultType进行用别名ItemCategory,例子:

 <!-- 获取所有的商品品类信息-->
    <select id="listCategory" resultType="ItemCategory">
        SELECT 
        <include refid="BaseColumnList" />
        FROM item_category t
    </select>

源码简单分析

针对上面的业务场景,首先的分析一下,我们知道Mybatis的执行都会通过SQLSessionFactory去调用,调用前都是先用SqlSessionFactoryBean的setTypeAliasesPackage可以看一下SqlSessionFactoryBean的源码:

/**
   * Packages to search for type aliases.
   *
   * @since 1.0.1
   *
   * @param typeAliasesPackage package to scan for domain objects
   *
   */
  public void setTypeAliasesPackage(String typeAliasesPackage) {
    this

我们看一下SqlSessionFactoryBean的初步Build方法:

/**
   * Build a {@code SqlSessionFactory} instance.
   *
   * The default implementation uses the standard MyBatis {@code XMLConfigBuilder} API to build a
   * {@code SqlSessionFactory} instance based on an Reader.
   * Since 1.3.0, it can be specified a {@link Configuration} instance directly(without config file).
   *
   * @return SqlSessionFactory
   * @throws IOException if loading the config file failed
   */
  protected SqlSessionFactory buildSqlSessionFactory() throws IOException {

    Configuration configuration;
//创建一个XMLConfigBuilder读取配置文件的一些信息
    XMLConfigBuilder xmlConfigBuilder = null;
    if (this.configuration != null) {
      configuration = this.configuration;
      if (configuration.getVariables() == null) {
        configuration.setVariables(this.configurationProperties);
      } else if (this.configurationProperties != null) {
        configuration.getVariables().putAll(this.configurationProperties);//添加Properties属性
      }
    } else if (this.configLocation != null) {
      xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
      configuration = xmlConfigBuilder.getConfiguration();
    } else {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");
      }
      configuration = new Configuration();
      if (this.configurationProperties != null) {
        configuration.setVariables(this.configurationProperties);
      }
    }

    if (this.objectFactory != null) {
      configuration.setObjectFactory(this.objectFactory);
    }

    if (this.objectWrapperFactory != null) {
      configuration.setObjectWrapperFactory(this.objectWrapperFactory);
    }

    if (this.vfs != null) {
      configuration.setVfsImpl(this.vfs);
    }
/*
重点看这里,其它源码先不看,这里获取到typeAliasesPackage字符串之后,调用tokenizeToStringArray进行字符串分隔返回一个数组,`String CONFIG_LOCATION_DELIMITERS = ",; \t\n";`
*/
    if (hasLength(this.typeAliasesPackage)) {
      String[] typeAliasPackageArray = tokenizeToStringArray(this.typeAliasesPackage,
          ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
      for (String packageToScan : typeAliasPackageArray) {//遍历,注册到configuration对象上
        configuration.getTypeAliasRegistry().registerAliases(packageToScan,
                typeAliasesSuperType == null ? Object.class : typeAliasesSuperType);
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Scanned package: '" + packageToScan + "' for aliases");
        }
      }
    }
    ...
    //省略其它代码
}

然后可以看到注册所有别名的方法 ,registerAliases是怎么做的?

configuration.getTypeAliasRegistry().registerAliases(packageToScan,
                typeAliasesSuperType == null ? Object.class : typeAliasesSuperType);

要扫描注册所有的别名之前先要扫描包下面的所有类

public void registerAliases(String packageName, Class<?> superType) {
        ResolverUtil<Class<?>> resolverUtil = new ResolverUtil();
        resolverUtil.find(new IsA(superType), packageName);
        //通过ResolverUtil获取到的所有类都赋值给一个集合
        Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
        /*遍历集合,然后一个个注册*/
        Iterator var5 = typeSet.iterator();
        while(var5.hasNext()) {
            Class<?> type = (Class)var5.next();
            if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
                this.registerAlias(type);
            }
        }

    }

ResolverUtil是怎么通过packageName去查找的呢,可以再继续跟一下

 public ResolverUtil<T> find(ResolverUtil.Test test, String packageName) {
    //获取包路径
        String path = this.getPackagePath(packageName);

        try {
        //VFS类用单例模式实现,先获取集合
            List<String> children = VFS.getInstance().list(path);
            Iterator var5 = children.iterator();
       //遍历,.class文件的选出来
            while(var5.hasNext()) {
                String child = (String)var5.next();
                if (child.endsWith(".class")) {
                    this.addIfMatching(test, child);
                }
            }
        } catch (IOException var7) {
            log.error("Could not read package: " + packageName, var7);
        }

        return this;
    }

获取packPath只是获取一下相对路径

protected String getPackagePath(String packageName) {
        return packageName == null ? null : packageName.replace('.', '/');
    }

校验方法addIfMatching:

 protected void addIfMatching(ResolverUtil.Test test, String fqn) {
        try {
            String externalName = fqn.substring(0, fqn.indexOf(46)).replace('/', '.');
            ClassLoader loader = this.getClassLoader();//类加载器
            if (log.isDebugEnabled()) {
                log.debug("Checking to see if class " + externalName + " matches criteria [" + test + "]");
            }

            Class<?> type = loader.loadClass(externalName);//通过类加载器加载类
            if (test.matches(type)) {//校验是否符合
                this.matches.add(type);
            }
        } catch (Throwable var6) {
            log.warn("Could not examine class '" + fqn + "' due to a " + var6.getClass().getName() + " with message: " + var6.getMessage());
        }

    }

VFS类具体是怎么setInstance的?继续跟:

//这里的关键点就是getResources,Thread.currentThread().getContextClassLoader().getResources(),其实总结一下Mybatis的类扫描还是要依赖与jdk提供的类加载器
 protected static List<URL> getResources(String path) throws IOException {
 //获取到资源路径以列表形式放在集合里
        return Collections.list(Thread.currentThread().getContextClassLoader().getResources(path));
    }

    ...
    public List<String> list(String path) throws IOException {
        List<String> names = new ArrayList();
        Iterator var3 = getResources(path).iterator();
    //遍历封装成列表
        while(var3.hasNext()) {
            URL url = (URL)var3.next();
            names.addAll(this.list(url, path));
        }

        return names;
    }

ok,本博客只是稍微跟一下源码,并没有对Mybatis的源码进行比较系统更高层次的分析。
跟了一下源码知道,稍微总结一下Mybatis对别名的注册是先将从sqlSessionFactoryBean类set的别名报名进行tokenizeToStringArray拆分成数组,然后将包名数组丢给ResolverUtil类和VFS等类进行一系列类加载遍历,之后将 resolverUtil.getClasses()获取的类都赋值给Set<Class<? extends Class<?>>> typeSet 一个集合。其中也是依赖与类加载器。

支持Ant通配符方式setTypeAliasesPackage解决方案

从这个源码比较简单的分析过程,我们并没有找到支持所谓通配符的方法,通过类加载的话也是要传个相对路径去遍历,不过我上面描述的业务场景是要兼容通配符的情况的,一般不会去改包名,假如这个项目有一定规模的话。

下面给出我的解决方案:

package com.muses.taoshop.common.core.database.annotation;

import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.util.ClassUtils;

import static com.muses.taoshop.common.core.database.config.BaseConfig.ENTITY_PACKAGES;

public class AnnotationConstants {

    public static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";

    public final static String PACKAGE_PATTERN =
            ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
            + ClassUtils.convertClassNameToResourcePath(ENTITY_PACKAGES)
            + DEFAULT_RESOURCE_PATTERN;

}

写一个扫描类,代码参考Hibernate的AnnotationSessionFactoryBean源码

package com.muses.taoshop.common.core.database.annotation;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;

import java.io.IOException;
import java.util.*;

import org.springframework.core.io.Resource;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.thymeleaf.util.StringUtils;

import static com.muses.taoshop.common.core.database.annotation.AnnotationConstants.PACKAGE_PATTERN;

/**
 * <pre>
 *  TypeAlicsesPackage的扫描类
 * </pre>
 *
 * @author nicky
 * @version 1.00.00
 * <pre>
 * 修改记录
 *    修改后版本:     修改人:  修改日期: 2018.12.01 18:23    修改内容:
 * </pre>
 */
@Component
public class TypeAliasesPackageScanner {

    protected final static Logger LOGGER = LoggerFactory.getLogger(TypeAliasesPackageScanner.class);
    private static ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
   /* private TypeFilter[] entityTypeFilters = new TypeFilter[]{new AnnotationTypeFilter(Entity.class, false),
            new AnnotationTypeFilter(Embeddable.class, false),
            new AnnotationTypeFilter(MappedSuperclass.class, false),
            new AnnotationTypeFilter(org.hibernate.annotations.Entity.class, false)};*/


    public static String getTypeAliasesPackages() {
        Set<String> packageNames = new TreeSet<String>();
        //TreeSet packageNames = new TreeSet();
        String typeAliasesPackage ="";
        try {
            //加载所有的资源
            Resource[] resources = resourcePatternResolver.getResources(PACKAGE_PATTERN);
            MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
            //遍历资源
            for (Resource resource : resources) {
                if (resource.isReadable()) {
                    MetadataReader reader = readerFactory.getMetadataReader(resource);
                    String className = reader.getClassMetadata().getClassName();
                    //eg:com.muses.taoshop.item.entity.ItemBrand
                    LOGGER.info("className : {} "+className);
                    try{
                        //eg:com.muses.taoshop.item.entity
                        LOGGER.info("packageName : {} "+Class.forName(className).getPackage().getName());
                        packageNames.add(Class.forName(className).getPackage().getName());
                    }catch (ClassNotFoundException e){
                        LOGGER.error("classNotFoundException : {} "+e);
                    }
                }
            }
        } catch (IOException e) {
            LOGGER.error("ioException =>: {} " + e);
        }
        //集合不为空的情况,拼装一下数据
        if (!CollectionUtils.isEmpty(packageNames)) {
            typeAliasesPackage = StringUtils.join(packageNames.toArray() , ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
        }else{
            LOGGER.info("set empty,size:{} "+packageNames.size());
        }
        return typeAliasesPackage;
    }

    
}

然后刚才的Mybatis配置类改一下,主要改 String typeAliasesPackage = packageScanner.getTypeAliasesPackages();通过扫描类去获取一下,重点代码在TypeAliasesPackageScanner 扫描类

package com.muses.taoshop.common.core.database.config;
//省略代码
@MapperScan(
        basePackages = MAPPER_PACKAGES,
        annotationClass = MybatisRepository.class,
        sqlSessionFactoryRef = SQL_SESSION_FACTORY
)
@EnableTransactionManagement
@Configuration
public class MybatisConfig {
    //省略其它代码,主要看sqlSessionFactory配置
    @Primary
    @Bean(name = SQL_SESSION_FACTORY)
    public SqlSessionFactory sqlSessionFactory(@Qualifier(DATA_SOURCE_NAME)DataSource dataSource)throws Exception{
        //SpringBoot默认使用DefaultVFS进行扫描,但是没有扫描到jar里的实体类
        VFS.addImplClass(SpringBootVFS.class);
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        //factoryBean.setConfigLocation(new ClassPathResource("mybatis-config.xml"));
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        try{
            factoryBean.setMapperLocations(resolver.getResources("classpath*:/mybatis/*Mapper.xml"));
            String typeAliasesPackage = packageScanner.getTypeAliasesPackages();
            //设置一下TypeAliasesPackage
            factoryBean.setTypeAliasesPackage(typeAliasesPackage);
            SqlSessionFactory sqlSessionFactory = factoryBean.getObject();
            return sqlSessionFactory;
        }catch (Exception e){
            e.printStackTrace();
            throw new RuntimeException();
        }
    }

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容

  • 1. 简介 1.1 什么是 MyBatis ? MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的...
    笨鸟慢飞阅读 5,283评论 0 4
  • question 1 逆序遍历二分树 给定一个二分搜索树(BST),对于每一个节点,加上所有比它大的节点的和,然后...
    vincehxb阅读 120评论 0 0
  • 跨越2017,启程2018,让我们一起朗诵感恩词,懂得感恩是宇宙给我们的最大的智慧!伴着新年的钟声让我们感恩一切,...
    实现幸福的人生阅读 1,549评论 0 3
  • int main(int argc, const char * argv[]) { @autoreleas...
    cmhfx1阅读 345评论 0 0