文章
问答
冒泡
解决Mybatis-plus 多租户插件和PageHelper冲突的问题

之前在老项目上使用了mybatis-plus的多租户插件改造老项目,觉得还是挺好用的,但是最近做业务适配的时候遇到了一个问题,就是@InterceptorIgnore(tenantLine = "true") 在pageHelper开启分页的方法上无效,网上随意找了找,都是千篇一律的提了一嘴,并没有说解决方案,于是记录下自己解决的过程.

1.现象

在使用@InterceptorIgnore(tenantLine = "true") 修饰被pageHelper开启分页的方法时,发现正常sql可以不添加租户ID条件,而pageHelper生成的代理count方法却依然添加了租户ID条件.

    @InterceptorIgnore(tenantLine = "true")
    List<SysUserEntity> queryPage();

分页方法

pageHelper正常count 插件依然生效

2,解决过程

1,跟踪源码

通过跟踪源码发现, @InterceptorIgnore 注解在 mybatisPlus的 MybatisMapperAnnotationBuilder类中引用, 并调用了initSqlParserInfoCache()方法,该方法内部 用来判断当前的代理Mapper class 和方法上是否有 @InterceptorIgnore注解,如果有的话,就获取注解中几个插件的开关情况,并存入到当前类的静态缓存中.

public class MybatisMapperAnnotationBuilder extends MapperAnnotationBuilder {

    private static final Set<Class<? extends Annotation>> statementAnnotationTypes = Stream
        .of(Select.class, Update.class, Insert.class, Delete.class, SelectProvider.class, UpdateProvider.class,
            InsertProvider.class, DeleteProvider.class)
        .collect(Collectors.toSet());

    private final Configuration configuration;
    private final MapperBuilderAssistant assistant;
    private final Class<?> type;

    public MybatisMapperAnnotationBuilder(Configuration configuration, Class<?> type) {
        super(configuration, type);
        String resource = type.getName().replace(StringPool.DOT, StringPool.SLASH) + ".java (best guess)";
        this.assistant = new MapperBuilderAssistant(configuration, resource);
        this.configuration = configuration;
        this.type = type;
    }

    @Override
    public void parse() {
        String resource = type.toString();
        if (!configuration.isResourceLoaded(resource)) {
            loadXmlResource();
            configuration.addLoadedResource(resource);
            String mapperName = type.getName();
            assistant.setCurrentNamespace(mapperName);
            parseCache();
            parseCacheRef();

            // 这里将当前代理Mapper初始化到了静态类中的 静态私有 map中
            InterceptorIgnoreHelper.InterceptorIgnoreCache cache = InterceptorIgnoreHelper.initSqlParserInfoCache(type);
            for (Method method : type.getMethods()) {
                if (!canHaveStatement(method)) {
                    continue;
                }
                if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
                    && method.getAnnotation(ResultMap.class) == null) {
                    parseResultMap(method);
                }
                try {
            // 这里将当前代理Mapper中的方法初始化到了静态类中的 静态私有 map中
                    InterceptorIgnoreHelper.initSqlParserInfoCache(cache, mapperName, method);
                    parseStatement(method);
                } catch (IncompleteElementException e) {
                    configuration.addIncompleteMethod(new MybatisMethodResolver(this, method));
                }
            }
            try {
                if (GlobalConfigUtils.isSupperMapperChildren(configuration, type)) {
                    parserInjector();
                }
            } catch (IncompleteElementException e) {
                configuration.addIncompleteMethod(new InjectorResolver(this));
            }
        }
        parsePendingMethods();
    }
」

MybatisMapperAnnotationBuilder继承了Mybaits的MapperAnnotationBuilder类,该类会在configuration的生成代理的addMapper方法中被调用parse()方法,所以这里会在每个mapper接口生成代理类的时候都被调用.并且这里调用了InterceptorIgnoreHeper的静态方法,将被@InterceptionIgnore修饰的Mapper全路径名及每个方法的全路径名缓存了下来.

public abstract class InterceptorIgnoreHelper {

    private static final Map<String, InterceptorIgnoreCache> INTERCEPTOR_IGNORE_CACHE = new ConcurrentHashMap<>();

    // 这个方法将class上拥有@InterceptionIgnore注解的全路径名缓存了下来
    public synchronized static InterceptorIgnoreCache initSqlParserInfoCache(Class<?> mapperClass) {

        InterceptorIgnore ignore = mapperClass.getAnnotation(InterceptorIgnore.class);
        if (ignore != null) {
            String key = mapperClass.getName();
            InterceptorIgnoreCache cache = buildInterceptorIgnoreCache(key, ignore);
            INTERCEPTOR_IGNORE_CACHE.put(key, cache);
            return cache;
        }
        return null;
    }

   // 这个方法将class的方法上拥有@InterceptionIgnore注解的全路径名缓存了下来
    public static void initSqlParserInfoCache(InterceptorIgnoreCache mapperAnnotation, String mapperClassName, Method method) {
        InterceptorIgnore ignore = method.getAnnotation(InterceptorIgnore.class);
        String key = mapperClassName.concat(StringPool.DOT).concat(method.getName());
        String name = mapperClassName.concat(StringPool.HASH).concat(method.getName());
        if (ignore != null) {
            InterceptorIgnoreCache methodCache = buildInterceptorIgnoreCache(name, ignore);
            if (mapperAnnotation == null) {
                INTERCEPTOR_IGNORE_CACHE.put(key, methodCache);
                return;
            }
            INTERCEPTOR_IGNORE_CACHE.put(key, chooseCache(mapperAnnotation, methodCache));
        }
    }
}

可以看到这里将缓存了下来每个拥有注解的mapper全路径名及方法名.

然后在多租户的拦截器的插件中, 代理sql解析器的前面,先 判断了当前的MappedStatement对应的是否在缓存中,有的话就认为此类或者sql方法被拦截器忽略,不添加租户字段,否则会添加.

public class TenantLineInnerInterceptor extends JsqlParserSupport implements InnerInterceptor {

    private TenantLineHandler tenantLineHandler;

    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        // 这里先判断了 方法是否存在于缓存中
        if (InterceptorIgnoreHelper.willIgnoreTenantLine(ms.getId())) return;
        PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
        mpBs.sql(parserSingle(mpBs.sql(), null));
    }
}

这里 在sql的代理前,先判断了是否需要走多租户的插件

    // 这里判断 方法是否在缓存中,传入了一个表达式
    public static boolean willIgnoreTenantLine(String id) {
        return willIgnore(id, InterceptorIgnoreCache::getTenantLine);
    }

    public static boolean willIgnore(String id, Function<InterceptorIgnoreCache, Boolean> function) {
        // 先判断是否在缓存中
        InterceptorIgnoreCache cache = INTERCEPTOR_IGNORE_CACHE.get(id);
        if (cache == null) {
            // 再判断是否当前方法的mapper是否在缓存中,mapper优先级小于方法
            cache = INTERCEPTOR_IGNORE_CACHE.get(id.substring(0, id.lastIndexOf(StringPool.DOT)));
        }
        if (cache != null) {
            // 这里传入的表达式是前面缓存的注解值,是否开启了(mybatisPlus会有多个插件)
            Boolean apply = function.apply(cache);
            return apply != null && apply;
        }
        return false;
    }

问题就在这里了,这里只判断了方法是否在缓存中, pageHepler是会生成_COUNT的方法的, 显然这个方法不在缓存中,于是就出现了前面的count的时候加上了租户,正常sql的时候没加的情况.

2,问题找到了,就应该想解决的方法了, 应该从缓存判断静态方法上作手, 此类没有被Spring管理,所以aop肯定是不行了,只能从其他方面考虑, 这时候我想到了 类加载器, 但是Spring没有使用java的类加载器,而是使用了自定义的类加载器,所以这方面也不行了, 那java agent呢, 需要生成另外的一个单独jar,并且在 启动的时候加入java 运行参数中去, 过于麻烦且不通用,还是不行.

3,这时候我想到了能不能重写mybatisplus的拦截器,然后注入的时候使用自己的拦截器.然后重载beforQuery方法, 使用自己的判断机制呢?

@Data
@NoArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class MyTenantLineInnerInterceptor extends TenantLineInnerInterceptor {

    public MyTenantLineInnerInterceptor(TenantLineHandler tenantLineHandler) {
        super(tenantLineHandler);
    }

   // 因为只是分页查询有问题,于是只重载了beforeQuery方法
    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
       // 可以看到这里调用了判断是否在缓存中,如果有的话就直接返回,不走下面的sql解析增强
        if (InterceptorIgnoreHelper.willIgnoreTenantLine(ms.getId())) return;
        PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
        mpBs.sql(parserSingle(mpBs.sql(), null));
}
}

4, 这里我有两个思路,一个是初始化缓存的时候,就将每个方法再多初始化进去一个相同的_COUNT结尾的方法,或者在判断的时候多判断一下有没有_COUNT方法,这两个思路都有弊端,后面解释, 于是这里我将原先判断 是否在缓存中的方法去掉,加上了自己的逻辑, 由于 使用的是静态类,所以我这里只能使用反射来获取.

       @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        /**
         * 修改判断,改为自行判断
         * if (InterceptorIgnoreHelper.willIgnoreTenantLine(ms.getId())) return;
         */
        // 增强查询方法 反射获取
        Class<InterceptorIgnoreHelper> interceptorIgnoreHelperClass = InterceptorIgnoreHelper.class;
        ConcurrentHashMap<String, InterceptorIgnoreHelper.InterceptorIgnoreCache>  cache = null;
        try {
            Field interceptorIgnoreCache = interceptorIgnoreHelperClass.getDeclaredField("INTERCEPTOR_IGNORE_CACHE");
           if (interceptorIgnoreCache != null) {
                interceptorIgnoreCache.setAccessible(true);
                cache = (ConcurrentHashMap<String, InterceptorIgnoreHelper.InterceptorIgnoreCache>)interceptorIgnoreCache.get(interceptorIgnoreHelperClass);
            }
        } catch (NoSuchFieldException e) {
            super.beforeQuery(executor,ms,parameter,rowBounds,resultHandler,boundSql);
            // throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            // throw new RuntimeException(e);
            super.beforeQuery(executor,ms,parameter,rowBounds,resultHandler,boundSql);
        }
       // 这里使用自己的方法
        if (willIgnore(ms.getId(), InterceptorIgnoreHelper.InterceptorIgnoreCache::getTenantLine,cache)) return;
        PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
        mpBs.sql(parserSingle(mpBs.sql(), null));
    }


public static boolean willIgnore(String id, Function<InterceptorIgnoreHelper.InterceptorIgnoreCache, Boolean> function,ConcurrentHashMap<String, InterceptorIgnoreHelper.InterceptorIgnoreCache> map) {
        InterceptorIgnoreHelper.InterceptorIgnoreCache ignoreCache = map.get(id);
        if (ignoreCache == null) {
            int i = id.lastIndexOf("_");
            if (i >= 0) {
                String substring = id.substring(0, i);
                ignoreCache = map.get(substring);
            }
        }
        if (ignoreCache == null) {
            ignoreCache = map.get(id.substring(0, id.lastIndexOf(StringPool.DOT)));
        }
        if (ignoreCache != null) {
            Boolean apply = function.apply(ignoreCache);
            return apply != null && apply;
        }
        return false;
    }

5,然后将自定义的多租户插件注入mybatisplus配置中

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new MyTenantLineInnerInterceptor(new DsfTenantLineHandler()));
        return mybatisPlusInterceptor;
    }

这里通过截取,缓存已经拿到了

count方法也正常不加租户了,有效

6, 到这里,就已经解决了@InterceptorIgore注解对pageHelper的分页无效的场景.

但我并不想局限于此, 上面说到这个方法有个弊端,就是万一别人没有使用pageHelper,并且方法也是以_COUNT方法结尾的,就误判断了,于是我又做了下面的修改.

7, 前面的修改还是一样,只不过我想到了 既然pageHelper也实现了sql解释的拦截器,那么我在这里判断一下拦截器链路,如果有pageHepler的情况下,我再判断COUNT就好了啊.

public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        Cache<String, MappedStatement> mappedStatementCache = null;
        List<Interceptor> interceptors = ms.getConfiguration().getInterceptors();
        MappedStatement mappedStatement = null;
        for (Interceptor interceptor : interceptors) {
            Class<? extends Interceptor> aClass = interceptor.getClass();
            if (aClass.getName().equals("com.github.pagehelper.PageInterceptor")) {
                try {
                    Field msCountMap = aClass.getDeclaredField("msCountMap");
                    if (msCountMap != null) {
                        msCountMap.setAccessible(true);
                        mappedStatementCache = (Cache<String, MappedStatement>)msCountMap.get(interceptor);
                        if (mappedStatementCache != null) {
                            mappedStatement = mappedStatementCache.get(ms.getId());
                        }
                    }
                }catch (NoSuchFieldException e) {
                    // throw new RuntimeException(e);
                } catch (IllegalAccessException e) {
//                    throw new RuntimeException(e);
                }
            }
        }

        // 如果缓存中有,并且pageHelper的缓存中也有,证明此方法被pageHepler代理,并且被@InterceptorIgnore 修饰
        if (mappedStatement != null) {
            Class<InterceptorIgnoreHelper> interceptorIgnoreHelperClass = InterceptorIgnoreHelper.class;
            ConcurrentHashMap<String, InterceptorIgnoreHelper.InterceptorIgnoreCache>  cache = null;
            try {
                Field interceptorIgnoreCache = interceptorIgnoreHelperClass.getDeclaredField("INTERCEPTOR_IGNORE_CACHE");
                if (interceptorIgnoreCache != null) {
                    interceptorIgnoreCache.setAccessible(true);
                    cache = (ConcurrentHashMap<String, InterceptorIgnoreHelper.InterceptorIgnoreCache>)interceptorIgnoreCache.get(interceptorIgnoreHelperClass);
                }
            } catch (NoSuchFieldException e) {
                super.beforeQuery(executor,ms,parameter,rowBounds,resultHandler,boundSql);
                // throw new RuntimeException(e);
            } catch (IllegalAccessException e) {
                // throw new RuntimeException(e);
                super.beforeQuery(executor,ms,parameter,rowBounds,resultHandler,boundSql);
            }
            if (willIgnore(ms.getId(), InterceptorIgnoreHelper.InterceptorIgnoreCache::getTenantLine,cache)) return;
        }else {
            if (InterceptorIgnoreHelper.willIgnoreTenantLine(ms.getId())) return;
        }


        PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
        mpBs.sql(parserSingle(mpBs.sql(), null));
    }

于是我将方法改成了这样,通过MappedStatement获取到configuration,再获取所有的interceptors, 然后通过反射获取到pageHepler中的msCountMap缓存(通过跟源码我发现,pageHelper会将所有生成的count全路径名缓存到这里),然后再判断缓存中有没有当前的方法,就可以了. 双重校验,更稳妥.

 

 

Mybatis-plus

关于作者

Dane.shang
快30岁了还没去过酒吧
获得点赞
文章被阅读