tk.mybatis与自定义classLoader带来的问题
tk.mybatis是mybatis的一个工具,提供通用mapper的能力
问题描述
public class EntityHelper {
/**
* 保存表
*/
private static final Map<Class<?>, EntityTable> entityTableMap = new ConcurrentHashMap<Class<?>, EntityTable>();
/**
* 获取表对象
*
* @param entityClass
* @return
*/
public static EntityTable getEntityTable(Class<?> entityClass) {
EntityTable entityTable = entityTableMap.get(entityClass);
if (entityTable == null) {
throw new MapperException("无法获取实体类" + entityClass.getCanonicalName() + "对应的表名!");
}
return entityTable;
}
//...
/**
* 初始化实体属性
*
* @param entityClass
* @param config
*/
public static synchronized void initEntityNameMap(Class<?> entityClass, Config config) {
if (entityTableMap.get(entityClass) != null) {
return;
}
//创建并缓存EntityTable
EntityTable entityTable = resolve.resolveEntity(entityClass, config);
entityTableMap.put(entityClass, entityTable);
}
//...
- 初始化时tk.mybatis通过扫描@MapperScan中指定的目录下的mapper,通过EntityHelper加载表名与字段存入entityTableMap,以便后续使用
- 在使用过程中发现诡异的现象,两个项目A和B,jar包和配置完全相同,但在使用Example动态sql查询时,A项目没有问题,B项目始终报
tk.mybatis.mapper.MapperException: 无法获取实体类xxx对应的表名!
问题排查
在网上搜索了该错误相关的资料,常见原因和解决办法有,这些办法均没有解决我的问题:
- 实体类没有@Table注解标记表名
- Springboot启动类上使用的@MapperScan注解为原生mybatis而非tkmybatis的
- jar包冲突问题,EntityHelper一些类在tk.mybatis.spring.boot.starter的mapper-core, mapper-spring和tk.mybatis中mapper中都存在,需排除
通过本地debug两个项目,发现了出问题的B项目在初始化时能够正常将表数据导入entityTableMap,但在实际Example使用时,entityTableMap却是空的,而正常的A项目则没有这个问题。
- 怀疑代码显示清除,但在EntityHelper未找到清除entityTableMap的方法
- 怀疑初始化的entityTableMap和Example使用的entityTableMap是不同的,实际debug看hashcode一致
定位问题
最终通过关键字全局搜索发现,在tk.mybatis.mapper.autoconfigure.MapperCacheDisabler中通过反射清除了EntityHelper中的entityTableMap
public class MapperCacheDisabler implements InitializingBean {
private static final Logger logger = LoggerFactory.getLogger(MapperCacheDisabler.class);
@Override
public void afterPropertiesSet() {
disableCaching();
}
private void disableCaching() {
try {
//因为jar包的类都是 AppClassLoader 加载的,所以此处获取的就是 AppClassLoader
ClassLoader appClassLoader = getClass().getClassLoader();
removeStaticCache(ClassUtils.forName("tk.mybatis.mapper.util.MsUtil", appClassLoader), "CLASS_CACHE");
removeStaticCache(ClassUtils.forName("tk.mybatis.mapper.genid.GenIdUtil", appClassLoader));
removeStaticCache(ClassUtils.forName("tk.mybatis.mapper.version.VersionUtil", appClassLoader));
removeEntityHelperCache(ClassUtils.forName("tk.mybatis.mapper.mapperhelper.EntityHelper", appClassLoader));
} catch (Exception ex) {
}
}
private void removeEntityHelperCache(Class<?> entityHelper) {
try {
Field cacheField = ReflectionUtils.findField(entityHelper, "entityTableMap");
if (cacheField != null) {
ReflectionUtils.makeAccessible(cacheField);
Map cache = (Map) ReflectionUtils.getField(cacheField, null);
//如果使用了 Devtools,这里获取的就是当前的 RestartClassLoader
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
for (Object key : new ArrayList(cache.keySet())) {
Class entityClass = (Class) key;
//加载表的class的classloader和当前的classloader不同则清理
if (!entityClass.getClassLoader().equals(classLoader)) {
//在这里清理了
cache.remove(entityClass);
}
}
logger.info("Clear EntityHelper entityTableMap cache.");
}
} catch (Exception ex) {
logger.warn("Failed to disable Mapper MsUtil cache. ClassCastExceptions may occur", ex);
}
}
}
查看该类的初始化,发现该类在spring.devtools.restart.enabled = true时将会自动加载MapperCacheDisabler,然后进行缓存的清理
/**
* Support Devtools Restart.
*/
@org.springframework.context.annotation.Configuration
@ConditionalOnProperty(prefix = "spring.devtools.restart", name = "enabled", matchIfMissing = true)
static class RestartConfiguration {
@Bean
public MapperCacheDisabler mapperCacheDisabler() {
return new MapperCacheDisabler();
}
}
- 原来是spring.devtools.restart.enabled这个配置导致在初始化表之后,对entityTableMap进行了清除,这个Bean的配置默认为true,因此即使使用的是2.3.4版本(移除spring.boot.devtools包的版本)的springboot也没办法幸免。
- A、B都注册了该类,但A项目没有清理表,怀疑ClassLoader的问题,进一步发现,由于B项目引入的包中有自定义的ClassLoader,导致被误判为spring.boot.devtools启动了热加载,而对表进行了清除。
大体的加载流程为
- MapperScan注解生效,加载表使用AppClassLoader
- 使用自定义ClassLoader加载,修改当前线程的ClassLoader
- MapperCacheDisabler bean注册,进行清理,发现创建的ClassLoader和当前的不同,对表信息进行了清理
- Example业务调用时,找不到对应的表信息报错。
经验教训
- 少使用不太维护的三方包,针对旧版springboot的功能在新版成了坑。
- 考虑问题时需考虑到反射