解决mybatis分页插件PageHelper导致自定义 * 失效
作者:吃葡萄不吐葡糖皮 发布时间:2023-10-13 07:24:29
问题背景
在最近的项目开发中遇到一个需求 需要对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) |
---|---|
Executor | update, query, flushStatements, commit, rollback,getTransaction, close, isClosed |
ParameterHandler | getParameterObject, setParameters |
StatementHandler | prepare, parameterize, batch, update, query |
ResultSetHandler | handleResultSets, 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


猜你喜欢
- 一、项目简述功能javaweb 网上商城系统,前台+后台管理,用户注册,登录,上哦展示,分组展示,搜索,收货地址管理,购物车管理,添加,购买
- SpringBoot停止启动时测试检查rabbitmq问题在Springboot项目中配置rabbitmq后,总是在每次启动时自动测试MQ的
- 前提准备:1. 项目中至少需要引入的jar包,注意版本: a) core-renderer.jar&nb
- 简单了解计算机为什么要采用二进制表示信息:因为计算机作为一种电子计算机工具,是由大量的电子器件组成的,在这些电子器件中,电路的通断,电位的高
- jpa多对多的属性查询第一:采用JPQL方式使用@Query拼接jpql语句完成多对多的查询;@query(SELECT User FROM
- 标准c++中string类函数介绍注意不是CString之所以抛弃char*的字符串而选用C++标准程序库中的string类,是因为他和前者
- 基石想要盖起高楼的话肯定要打好基石,同样,想要学好 Compose 动画也需要一些“基石”,一起来看
- 之前学习 Java 的时候,感觉最难做的一件事情就是配置 jdk 的环境。那叫一个困难啊,Path, JAVA_HOME, CLASSPAT
- 很多小伙伴都有修改Eclipse启动画面或图标的需求,寻找自己的个性化嘛,今天我就给大家说一下如何修改。一、 启动画面自定义第一种情况:.纯
- 在application.properties中配置了static的默认路径我的static目录结构是这样的index.html中这样引用c
- 本篇文章主要内容来自于Android Doc,我翻译之后又做了些加工,英文好的朋友也可以直接去读原文。http://developer.an
- 目录一、什么是Spring二、什么是IOC三、快速搭建框架环境四、spring之依赖注入五、详解Spring框架的IOC之注解方式七、Spr
- 首先,我们使用使用命令创建模板项目,创建的命令如下。taro init myApp然后,使用 yarn 或者 npm install安装依赖
- 测试环境为Adnroid 2.1以上。 1.AndroidManifest.xml 权限配置: 添加互联网访问权限: <uses-pe
- java操作json对象出现StackOverflow错误今天在做项目的时候,遇到一个非常怪异的问题。一个java实体类中存在多个属性,每一
- Maven 多profile及指定编译要点项目A依赖项目B,项目A、B都有对应的多个profile,通过mvn –P参数指定profile,
- 本文实例为大家分享了C语言实现学生信息管理系统的具体代码,供大家参考,具体内容如下#define _CRT_SECURE_NO_WARNIN
- 直接上代码,看下最简单也是最常用的方法,将Object 转为 JSON 以及将Json转为Object方式public class Test
- 1 前言项目中,目前主流的当然是微服务项目。为了应对高并发,以及保证自己的服务比较稳定,通常会把服务按照模块,或者具体的业务划分为多个独立的
- 一、MySql实现分页查询的SQL语句 1、分页需求:客户端通过传递pageNo(页码),counter(每页显示的条数)两个参数去分页查询