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; //省略jar進入程式碼 @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; //省略jar進入程式碼 @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(); } }