背景

最近收到一个需求,需要使用到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 ,为每个拦截器创建代理并返回。

即:在前面的拦截器给目标对象包了一层代理后,后面的拦截器在原有代理上又包了一层代理。

所以当方法调用时,先执行最外层的代理方法。