zoukankan      html  css  js  c++  java
  • 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 = ",; 	
    ";`
    */
        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>> 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();
            }
        }
    
    
  • 相关阅读:
    ubuntu 安装PHP
    修改rm 防止误删除
    游标输出
    微软已经提供了好几个开源的小项目供学习参考
    数据库存取时间比较
    UML建模工具比较
    16进制与BYTE类型转换
    显示有换行字符的提示框
    ORACLE数据库中主要字段类型的读写例子(包括:Long、Raw、Blob)
    .net 动态sql 参数应用 oracle和sql server的比较
  • 原文地址:https://www.cnblogs.com/mzq123/p/10055185.html
Copyright © 2011-2022 走看看