之前在老项目上使用了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全路径名缓存到这里),然后再判断缓存中有没有当前的方法,就可以了. 双重校验,更稳妥.