背景
最近收到一个需求,需要使用到mybatis的拦截器对SQL进行一些处理。
但是拦截器开发完毕后,发现系统运行过程中该拦截器并没有被调用。
问题定位
经过一番搜寻和debug,发现是由于系统中使用了PageHelper插件。
那为什么PageHelper插件导致自定义拦截器失效了呢?
首先你得知道,Mybatis 采用责任链模式,通过动态代理组织多个拦截器(插件),在拦截器intercept方法内,最后一个语句一定要是 return invocation.proceed() ,否则拦截器链就断了,剩下的拦截器直接当摆设。
而偏偏 PageHelper 就是没有向下传递。
综上所述,稍微思考一下,得出一个结论:PageHelper 先于 自定义拦截器 执行!
无效探索
1.mybatis-config.xml
通过上面的分析,我们只需要让 自定义拦截器 先于 PageHelper 执行即可。
直接动用mybatis-config.xml 文件,手动更改拦截器顺序。
注意:拦截器先注册后执行,即越先注册的拦截器执行顺序越靠后(见附录)。
<plugins>
<!-- com.github.pagehelper为PageHelper类所在包名 -->
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 使用下面的方式配置参数,后面会有所有的参数介绍 -->
<!-- reasonable:分页合理化参数,默认值为false。当该参数设置为 true 时,pageNum<=0 时会查询第一页, pageNum>pages(超过总数时),会查询最后一页。默认false 时,直接根据参数进行查询。-->
<property name="reasonable" value="true"/>
<!-- supportMethodsArguments:支持通过 Mapper 接口参数来传递分页参数,默认值false,分页插件会从查询方法的参数值中,自动根据上面 params 配置的字段中取值,查找到合适的值时就会自动分页。 使用方法可以参考测试代码中的 com.github.pagehelper.test.basic 包下的 ArgumentsMapTest 和 ArgumentsObjTest。-->
<property name="supportMethodsArguments" value="true"/>
<!-- autoRuntimeDialect:默认值为 false。设置为 true 时,允许在运行时根据多数据源自动识别对应方言的分页 (不支持自动选择sqlserver2012,只能使用sqlserver),用法和注意事项参考下面的场景五-->
<property name="autoRuntimeDialect" value="true"/>
<!-- params:为了支持startPage(Object params)方法,增加了该参数来配置参数映射,用于从对象中根据属性名取值, 可以配置 pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用默认值, 默认值为pageNum=pageNum;pageSize=pageSize;count=countSql;reasonable=reasonable;pageSizeZero=pageSizeZero。-->
</plugin>
<plugin interceptor="top.huqz.mybatis.interceptor.ReplaceTableInterceptor"/>
</plugins>结果:失败!
原因:待补充。
2. @Intercepts
看网上说的另外一种解决方案:修改拦截器注解参数
也就是再添加一个 全参数的 query 类型
原注解:
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})更改后:
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})结果:失败!
原因:待补充。
3.使用注解 @AutoConfigureAfter
网上还有一种解决方案,通过该注解确保自定义拦截器比PageHelper拦截器晚添加。
@Configuration
@AutoConfigureAfter(PageHelperAutoConfiguration.class)
public class MybatisInterceptorAutoConfiguration {
@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;
private final ReplaceTableInterceptor interceptor = new ReplaceTableInterceptor();
@PostConstruct
public void addMyInterceptor() {
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
// List<Interceptor> interceptors = sqlSessionFactory.getConfiguration().getInterceptors();
sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
}
}
}
结果:失败!
原因:首先,这段代码在本系统上压根就无法运行。本系统虽然使用了PageHelper插件,但是却没有 PageHelperAutoConfiguration 这个类。
扩展:去掉 @AutoConfigureAfter 注解后,自定义拦截器仍能正确添加,只是无法调整拦截器的顺序。经过进一步的调试,发现当系统执行到 .addInterceptor(interceptor) 时,拦截器列表仍然没有PageHelper的拦截器。。。 也就是说,PageHelper还在我们的后面!恐怖如斯!
终极解决方案
通过上面的探索,我们已经知道普通方法已经没用了,我们直接祭出大杀器:ApplicationListener
@Component
public class CustomerInterceptorRegister implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
ReplaceTableInterceptor interceptor = new ReplaceTableInterceptor();
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
org.apache.ibatis.session.Configuration configuration = sqlSessionFactory.getConfiguration();
if (configuration.getInterceptors().stream().noneMatch(i -> i == interceptor)) {
configuration.addInterceptor(interceptor);
}
}
}
}当系统启动或上下文刷新时,onApplicationEvent方法会被调用,而在这个方法执行时,PageHelper拦截器已经乖乖的躺在拦截器列表了。
桀桀桀,我们只需要 configuration.addInterceptor(interceptor); 一切问题都将得到解决!
总结
还是太弱了,对 mybatis的拦截器原理知道的太少了。
但是我是不会告诉你,在无效探索第三步中,跟另一位大佬反射半天结果发现方向全错的事情的!
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactories) {
Class<? extends org.apache.ibatis.session.Configuration> aClass = sqlSessionFactory.getConfiguration().getClass();
try {
Field interceptorChainField = aClass.getSuperclass().getDeclaredField("interceptorChain");
interceptorChainField.setAccessible(true);
Object interceptorChain = interceptorChainField.get(sqlSessionFactory.getConfiguration());
interceptorChainField.setAccessible(false);
Class<?> chainClass = interceptorChain.getClass();
Field interceptorsField = chainClass.getDeclaredField("interceptors");
interceptorsField.setAccessible(true);
Object interceptors = interceptorsField.get(interceptorChain);
interceptorsField.setAccessible(false);
((List<Interceptor>) interceptors).add(0, interceptor);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}附录
关于拦截器链中,先加载的拦截器反而最后执行
拦截器链部分源代码:
package org.apache.ibatis.plugin;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<>();
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}在 pluginAll 方法中,遍历 interceptors ,为每个拦截器创建代理并返回。
即:在前面的拦截器给目标对象包了一层代理后,后面的拦截器在原有代理上又包了一层代理。
所以当方法调用时,先执行最外层的代理方法。
pagehelper导致mybatis自定义拦截器失效
本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
评论交流
欢迎留下你的想法