• mybatis分页插件PageHelper导致自定义拦截器失效


    问题背景

    在最近的项目开发中遇到一个需求 需要对mysql做一些慢查询、大结果集等异常指标进行收集监控,从运维角度并没有对mysql进行统一的指标搜集,所以需要通过代码层面对指标进行收集,我采用的方法是通过mybatis的Interceptor拦截器进行指标收集在开发中出现了自定义拦截器 对于查询无法进行拦截的问题几经周折后终于解决,故进行记录学习,分享给大家下次遇到少走一些弯路;

    mybatis拦截器使用

    像springmvc一样,mybatis也提供了拦截器实现,对Executor、StatementHandler、ResultSetHandler、ParameterHandler提供了拦截器功能。

    使用方法:

    在使用时我们只需要 implements org.apache.ibatis.plugin.Interceptor类实现 方法头标注相应注解即可 如下代码会对CRUD的操作进行拦截:

    1. @Intercepts({
    2. @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
    3. @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
    4. })
    5. 复制代码

    注解参数介绍:

    • @Intercepts:标识该类是一个拦截器;
    • @Signature:指明自定义拦截器需要拦截哪一个类型,哪一个方法
    拦截的类(type)拦截的方法(method)
    Executorupdate, query, flushStatements, commit, rollback,getTransaction, close, isClosed
    ParameterHandlergetParameterObject, setParameters
    StatementHandlerprepare, parameterize, batch, update, query
    ResultSetHandlerhandleResultSets, handleOutputParameters
    • Executor:提供了增删改查的接口 拦截执行器的方法.
    • StatementHandler:负责处理Mybatis与JDBC之间Statement的交互 拦截参数的处理.
    • ResultSetHandler:负责处理Statement执行后产生的结果集,生成结果列表 拦截结果集的处理.
    • ParameterHandler:是Mybatis实现Sql入参设置的对象 拦截Sql语法构建的处理。

    官方代码示例:

    1. @Intercepts({@Signature(type = Executor.class, method = "query",
    2. args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
    3. public class TestInterceptor implements Interceptor {
    4. public Object intercept(Invocation invocation) throws Throwable {
    5. Object target = invocation.getTarget(); //被代理对象
    6. Method method = invocation.getMethod(); //代理方法
    7. Object[] args = invocation.getArgs(); //方法参数
    8. // do something ...... 方法拦截前执行代码块
    9. Object result = invocation.proceed();
    10. // do something .......方法拦截后执行代码块
    11. return result;
    12. }
    13. public Object plugin(Object target) {
    14. return Plugin.wrap(target, this);
    15. }
    16. }
    17. 复制代码

    setProperties方法

    因为mybatis框架本身就是一个可以独立使用的框架,没有像Spring这种做了很多的依赖注入。 如果我们的拦截器需要一些变量对象,而且这个对象是支持可配置的。 类似于Spring中的@Value("${}")从application.properties文件中获取。 使用方法:

    mybatis-config.xml配置:

    1. <plugin interceptor="com.plugin.mybatis.MyInterceptor">
    2. <property name="username" value="xxx"/>
    3. <property name="password" value="xxx"/>
    4. plugin>
    5. 复制代码

    方法中获取参数:properties.getProperty("username");

    bug内容:

    update类型操作可以正常拦截 query类型查询sql无法进入自定义拦截器,导致拦截失败以下为部分源码 由于涉及到公司代码以下代码做了mask的处理

    自定义拦截器部分代码

    1. @Intercepts({
    2. @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
    3. @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
    4. })
    5. public class SQLInterceptor implements Interceptor {
    6. @Override
    7. public Object intercept(Invocation invocation) throws Throwable {
    8. .....
    9. }
    10. @Override
    11. public Object plugin(Object target) {
    12. return Plugin.wrap(target, this);
    13. }
    14. @Override
    15. public void setProperties(Properties properties) {
    16. .....
    17. }
    18. }
    19. 复制代码

    自定义拦截器拦截的是Executor执行器4参数query方法和update类型方法 由于mybatis的拦截器为责任链模式调用有一个传递机制 (第一个拦截器执行完向下一个拦截器传递 具体实现可以看一下源码)

    update的操作执行确实进了自定义拦截器但是查询的操作始终进不来后通过追踪源码发现

    pagehelper插件的 PageInterceptor 拦截器 会对 Executor执行器method=query 的4参数方法进行修改转化为 6参数方法 向下传递 导致执行顺序在pagehelper后面的拦截器的Executor执行器4参数query方法不会接收到传递过来的请求导致拦截器失效

    PageInterceptor源码:

    1. /**
    2. * Mybatis - 通用分页拦截器
    3. *

    4. * GitHub: https://github.com/pagehelper/Mybatis-PageHelper
    5. *

    6. * Gitee : https://gitee.com/free/Mybatis_PageHelper
    7. *
    8. * @author liuzh/abel533/isea533
    9. * @version 5.0.0
    10. */
    11. @SuppressWarnings({"rawtypes", "unchecked"})
    12. @Intercepts(
    13. {
    14. @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
    15. @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
    16. }
    17. )
    18. public class PageInterceptor implements Interceptor {
    19. private volatile Dialect dialect;
    20. private String countSuffix = "_COUNT";
    21. protected Cache<String, MappedStatement> msCountMap = null;
    22. private String default_dialect_class = "com.github.pagehelper.PageHelper";
    23. @Override
    24. public Object intercept(Invocation invocation) throws Throwable {
    25. try {
    26. Object[] args = invocation.getArgs();
    27. MappedStatement ms = (MappedStatement) args[0];
    28. Object parameter = args[1];
    29. RowBounds rowBounds = (RowBounds) args[2];
    30. ResultHandler resultHandler = (ResultHandler) args[3];
    31. Executor executor = (Executor) invocation.getTarget();
    32. CacheKey cacheKey;
    33. BoundSql boundSql;
    34. //由于逻辑关系,只会进入一次
    35. if (args.length == 4) {
    36. //4 个参数时
    37. boundSql = ms.getBoundSql(parameter);
    38. cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
    39. } else {
    40. //6 个参数时
    41. cacheKey = (CacheKey) args[4];
    42. boundSql = (BoundSql) args[5];
    43. }
    44. checkDialectExists();
    45. List resultList;
    46. //调用方法判断是否需要进行分页,如果不需要,直接返回结果
    47. if (!dialect.skip(ms, parameter, rowBounds)) {
    48. //判断是否需要进行 count 查询
    49. if (dialect.beforeCount(ms, parameter, rowBounds)) {
    50. //查询总数
    51. Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
    52. //处理查询总数,返回 true 时继续分页查询,false 时直接返回
    53. if (!dialect.afterCount(count, parameter, rowBounds)) {
    54. //当查询总数为 0 时,直接返回空的结果
    55. return dialect.afterPage(new ArrayList(), parameter, rowBounds);
    56. }
    57. }
    58. resultList = ExecutorUtil.pageQuery(dialect, executor,
    59. ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
    60. } else {
    61. //rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
    62. resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
    63. }
    64. return dialect.afterPage(resultList, parameter, rowBounds);
    65. } finally {
    66. if(dialect != null){
    67. dialect.afterAll();
    68. }
    69. }
    70. }
    71. /**
    72. * Spring bean 方式配置时,如果没有配置属性就不会执行下面的 setProperties 方法,就不会初始化
    73. *

    74. * 因此这里会出现 null 的情况 fixed #26
    75. */
    76. private void checkDialectExists() {
    77. if (dialect == null) {
    78. synchronized (default_dialect_class) {
    79. if (dialect == null) {
    80. setProperties(new Properties());
    81. }
    82. }
    83. }
    84. }
    85. private Long count(Executor executor, MappedStatement ms, Object parameter,
    86. RowBounds rowBounds, ResultHandler resultHandler,
    87. BoundSql boundSql) throws SQLException {
    88. String countMsId = ms.getId() + countSuffix;
    89. Long count;
    90. //先判断是否存在手写的 count 查询
    91. MappedStatement countMs = ExecutorUtil.getExistedMappedStatement(ms.getConfiguration(), countMsId);
    92. if (countMs != null) {
    93. count = ExecutorUtil.executeManualCount(executor, countMs, parameter, boundSql, resultHandler);
    94. } else {
    95. countMs = msCountMap.get(countMsId);
    96. //自动创建
    97. if (countMs == null) {
    98. //根据当前的 ms 创建一个返回值为 Long 类型的 ms
    99. countMs = MSUtils.newCountMappedStatement(ms, countMsId);
    100. msCountMap.put(countMsId, countMs);
    101. }
    102. count = ExecutorUtil.executeAutoCount(dialect, executor, countMs, parameter, boundSql, rowBounds, resultHandler);
    103. }
    104. return count;
    105. }
    106. @Override
    107. public Object plugin(Object target) {
    108. return Plugin.wrap(target, this);
    109. }
    110. @Override
    111. public void setProperties(Properties properties) {
    112. //缓存 count ms
    113. msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties);
    114. String dialectClass = properties.getProperty("dialect");
    115. if (StringUtil.isEmpty(dialectClass)) {
    116. dialectClass = default_dialect_class;
    117. }
    118. try {
    119. Class aClass = Class.forName(dialectClass);
    120. dialect = (Dialect) aClass.newInstance();
    121. } catch (Exception e) {
    122. throw new PageException(e);
    123. }
    124. dialect.setProperties(properties);
    125. String countSuffix = properties.getProperty("countSuffix");
    126. if (StringUtil.isNotEmpty(countSuffix)) {
    127. this.countSuffix = countSuffix;
    128. }
    129. }
    130. }
    131. }
    132. 复制代码

    解决方法:

    通过上述我们定位到了问题产生的原因 解决起来就简单多了 有俩个方案如下:

    • 调整拦截器顺序 让自定义拦截器先执行
    • 自定义拦截器query方法也定义为 6参数方法或者不使用Executor.class执行器使用StatementHandler.class执行器也可以实现拦截

    解决方案一 调整执行顺序

    mybatis-config.xml 代码

    我们的自定义拦截器配置的执行顺序是在PageInterceptor这个拦截器前面的(先配置后执行)

    1. <plugins>
    2. <plugin interceptor="com.github.pagehelper.PageInterceptor">
    3. <property name="reasonable" value="true"/>
    4. <property name="supportMethodsArguments" value="true"/>
    5. <property name="autoRuntimeDialect" value="true"/>
    6. plugin>
    7. <plugin interceptor="com.a.b.common.sql.SQLInterceptor"/>
    8. plugins>
    9. 复制代码

    注意点!!!

    pageHelper的依赖jar一定要使用pageHelper原生的jar包 pagehelper-spring-boot-starter jar包 是和spring集成的 PageInterceptor会由spring进行管理 在mybatis加载完后就加载了PageInterceptor 会导致mybatis-config.xml 里调整拦截器顺序失效

    错误依赖:

    1. <dependency>-->
    2. dependency>
    3. 复制代码

    正确依赖

    1. <dependency>
    2. <groupId>com.github.pagehelpergroupId>
    3. <artifactId>pagehelperartifactId>
    4. <version>5.1.10version>
    5. dependency>
    6. 复制代码

    解决方案二 修改拦截器注解定义

    1. @Intercepts({
    2. @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
    3. @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
    4. })
    5. 复制代码

    或者

    1. @Intercepts({
    2. @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
    3. @Signature(type = StatementHandler.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class,ResultHandler.class})
    4. })

  • 相关阅读:
    【python】python内置函数——map()根据提供的函数对指定序列做映射
    Go 多版本管理工具
    出口欧洲玩具做EN71玩具安全测试
    使用go_concurrent_map 管理 并发更新缓存
    一、Vue3基础[生命周期]
    Springboot:如何搭建起自己第一个Springboot项目
    112、工作繁忙,随口胡说;接近胡说,敷衍而已
    Linux debian gdb dump
    [ 常用工具篇 ] 解决 kali 下载速度软件慢的问题 -- kali换源
    2007-2019年36家上市银行绿色信贷余额、绿色信贷占比、资产收益率、不良贷款率等数据
  • 原文地址:https://blog.csdn.net/m0_71777195/article/details/126384145