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 [email protected] SqlSessionFactory} instance. 
   * 
   * The default implementation uses the standard MyBatis [email protected] XMLConfigBuilder} API to build a 
   * [email protected] SqlSessionFactory} instance based on an Reader. 
   * Since 1.3.0, it can be specified a [email protected] 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; 
 
/** 
 * 
 
 *  TypeAlicsesPackage的扫描类 
 * 

*
* @author nicky
* @version 1.00.00
*

 
 * 修改记录 
 *    修改后版本:     修改人:  修改日期: 2018.12.01 18:23    修改内容: 
 * 

*/
@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(); 
        } 
    } 

原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/17052.html

(0)
上一篇 2021年7月19日
下一篇 2021年7月19日

相关推荐

发表回复

登录后才能评论