tk.mybatis

tk.mybatis与自定义classLoader

Posted by Link on April 7, 2021

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);
    }
    //...
  1. 初始化时tk.mybatis通过扫描@MapperScan中指定的目录下的mapper,通过EntityHelper加载表名与字段存入entityTableMap,以便后续使用
  2. 在使用过程中发现诡异的现象,两个项目A和B,jar包和配置完全相同,但在使用Example动态sql查询时,A项目没有问题,B项目始终报tk.mybatis.mapper.MapperException: 无法获取实体类xxx对应的表名!

问题排查

在网上搜索了该错误相关的资料,常见原因和解决办法有,这些办法均没有解决我的问题:

  1. 实体类没有@Table注解标记表名
  2. Springboot启动类上使用的@MapperScan注解为原生mybatis而非tkmybatis的
  3. jar包冲突问题,EntityHelper一些类在tk.mybatis.spring.boot.starter的mapper-core, mapper-spring和tk.mybatis中mapper中都存在,需排除

通过本地debug两个项目,发现了出问题的B项目在初始化时能够正常将表数据导入entityTableMap,但在实际Example使用时,entityTableMap却是空的,而正常的A项目则没有这个问题。

  1. 怀疑代码显示清除,但在EntityHelper未找到清除entityTableMap的方法
  2. 怀疑初始化的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();
        }
    }
  1. 原来是spring.devtools.restart.enabled这个配置导致在初始化表之后,对entityTableMap进行了清除,这个Bean的配置默认为true,因此即使使用的是2.3.4版本(移除spring.boot.devtools包的版本)的springboot也没办法幸免。
  2. A、B都注册了该类,但A项目没有清理表,怀疑ClassLoader的问题,进一步发现,由于B项目引入的包中有自定义的ClassLoader,导致被误判为spring.boot.devtools启动了热加载,而对表进行了清除。

大体的加载流程为

  1. MapperScan注解生效,加载表使用AppClassLoader
  2. 使用自定义ClassLoader加载,修改当前线程的ClassLoader
  3. MapperCacheDisabler bean注册,进行清理,发现创建的ClassLoader和当前的不同,对表信息进行了清理
  4. Example业务调用时,找不到对应的表信息报错。

经验教训

  1. 少使用不太维护的三方包,针对旧版springboot的功能在新版成了坑。
  2. 考虑问题时需考虑到反射