软件编程
位置:首页>> 软件编程>> java编程>> 解决mybatis分页插件PageHelper导致自定义 * 失效

解决mybatis分页插件PageHelper导致自定义 * 失效

作者:吃葡萄不吐葡糖皮  发布时间:2023-10-13 07:24:29 

标签:mybatis,PageHelper,分页插件, ,

问题背景

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

mybatis * 使用

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

使用方法:

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

@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)拦截的方法(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语法构建的处理。

官方代码示例:

@Intercepts({@Signature(type = Executor.class, method = "query",
       args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class TestInterceptor implements Interceptor {
  public Object intercept(Invocation invocation) throws Throwable {
    Object target = invocation.getTarget(); //被代理对象
    Method method = invocation.getMethod(); //代理方法
    Object[] args = invocation.getArgs(); //方法参数
    // do something ...... 方法拦截前执行代码块
    Object result = invocation.proceed();
    // do something .......方法拦截后执行代码块
    return result;
  }
  public Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }
}

setProperties方法

因为mybatis框架本身就是一个可以独立使用的框架,没有像Spring这种做了很多的依赖注入。 如果我们的 * 需要一些变量对象,而且这个对象是支持可配置的。

类似于Spring中的@Value("${}")从application.properties文件中获取。

使用方法:

mybatis-config.xml配置:

<plugin interceptor="com.plugin.mybatis.MyInterceptor">
    <property name="username" value="xxx"/>
    <property name="password" value="xxx"/>
</plugin>

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

bug内容:

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

自定义 * 部分代码

@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})
})
public class SQLInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
.....
}
@Override
public Object plugin(Object target) {
   return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
.....
}
}

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

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

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

PageInterceptor源码:

/**
* Mybatis - 通用分页 *
* <p>
* GitHub: https://github.com/pagehelper/Mybatis-PageHelper
* <p>
* Gitee : https://gitee.com/free/Mybatis_PageHelper
*
* @author liuzh/abel533/isea533
* @version 5.0.0
*/
@SuppressWarnings({"rawtypes", "unchecked"})
@Intercepts(
       {
               @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
               @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
       }
)
public class PageInterceptor implements Interceptor {
   private volatile Dialect dialect;
   private String countSuffix = "_COUNT";
   protected Cache<String, MappedStatement> msCountMap = null;
   private String default_dialect_class = "com.github.pagehelper.PageHelper";
   @Override
   public Object intercept(Invocation invocation) throws Throwable {
       try {
           Object[] args = invocation.getArgs();
           MappedStatement ms = (MappedStatement) args[0];
           Object parameter = args[1];
           RowBounds rowBounds = (RowBounds) args[2];
           ResultHandler resultHandler = (ResultHandler) args[3];
           Executor executor = (Executor) invocation.getTarget();
           CacheKey cacheKey;
           BoundSql boundSql;
           //由于逻辑关系,只会进入一次
           if (args.length == 4) {
               //4 个参数时
               boundSql = ms.getBoundSql(parameter);
               cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
           } else {
               //6 个参数时
               cacheKey = (CacheKey) args[4];
               boundSql = (BoundSql) args[5];
           }
           checkDialectExists();
           List resultList;
           //调用方法判断是否需要进行分页,如果不需要,直接返回结果
           if (!dialect.skip(ms, parameter, rowBounds)) {
               //判断是否需要进行 count 查询
               if (dialect.beforeCount(ms, parameter, rowBounds)) {
                   //查询总数
                   Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                   //处理查询总数,返回 true 时继续分页查询,false 时直接返回
                   if (!dialect.afterCount(count, parameter, rowBounds)) {
                       //当查询总数为 0 时,直接返回空的结果
                       return dialect.afterPage(new ArrayList(), parameter, rowBounds);
                   }
               }
               resultList = ExecutorUtil.pageQuery(dialect, executor,
                       ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
           } else {
               //rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
               resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
           }
           return dialect.afterPage(resultList, parameter, rowBounds);
       } finally {
           if(dialect != null){
               dialect.afterAll();
           }
       }
   }
   /**
    * Spring bean 方式配置时,如果没有配置属性就不会执行下面的 setProperties 方法,就不会初始化
    * <p>
    * 因此这里会出现 null 的情况 fixed #26
    */
   private void checkDialectExists() {
       if (dialect == null) {
           synchronized (default_dialect_class) {
               if (dialect == null) {
                   setProperties(new Properties());
               }
           }
       }
   }
   private Long count(Executor executor, MappedStatement ms, Object parameter,
                      RowBounds rowBounds, ResultHandler resultHandler,
                      BoundSql boundSql) throws SQLException {
       String countMsId = ms.getId() + countSuffix;
       Long count;
       //先判断是否存在手写的 count 查询
       MappedStatement countMs = ExecutorUtil.getExistedMappedStatement(ms.getConfiguration(), countMsId);
       if (countMs != null) {
           count = ExecutorUtil.executeManualCount(executor, countMs, parameter, boundSql, resultHandler);
       } else {
           countMs = msCountMap.get(countMsId);
           //自动创建
           if (countMs == null) {
               //根据当前的 ms 创建一个返回值为 Long 类型的 ms
               countMs = MSUtils.newCountMappedStatement(ms, countMsId);
               msCountMap.put(countMsId, countMs);
           }
           count = ExecutorUtil.executeAutoCount(dialect, executor, countMs, parameter, boundSql, rowBounds, resultHandler);
       }
       return count;
   }
   @Override
   public Object plugin(Object target) {
       return Plugin.wrap(target, this);
   }
   @Override
   public void setProperties(Properties properties) {
       //缓存 count ms
       msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties);
       String dialectClass = properties.getProperty("dialect");
       if (StringUtil.isEmpty(dialectClass)) {
           dialectClass = default_dialect_class;
       }
       try {
           Class<?> aClass = Class.forName(dialectClass);
           dialect = (Dialect) aClass.newInstance();
       } catch (Exception e) {
           throw new PageException(e);
       }
       dialect.setProperties(properties);
       String countSuffix = properties.getProperty("countSuffix");
       if (StringUtil.isNotEmpty(countSuffix)) {
           this.countSuffix = countSuffix;
       }
   }
}
}

解决方法:

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

  • 调整 * 顺序 让自定义 * 先执行

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

解决方案一 调整执行顺序

mybatis-config.xml 代码

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

<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="com.a.b.common.sql.SQLInterceptor"/>
</plugins>

注意点!!!

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

错误依赖:

<dependency>-->
   <!--<groupId>com.github.pagehelper</groupId>-->
   <!--<artifactId>pagehelper-spring-boot-starter</artifactId>-->
   <!--<version>1.2.12</version>-->
</dependency>

正确依赖

<dependency>
   <groupId>com.github.pagehelper</groupId>
   <artifactId>pagehelper</artifactId>
   <version>5.1.10</version>
</dependency>

解决方案二 修改 * 注解定义

@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})
})

或者

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

来源:https://juejin.cn/post/7132404138649862174

0
投稿

猜你喜欢

手机版 软件编程 asp之家 www.aspxhome.com