PageHelper 通过集成 MyBatis 的 Interceptor 接口,来实现分页插件的功能。
首先要使用 PageHelper ,需要的 pom.xml 文件中引入 PageHelper 的依赖:
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.8</version>
</dependency>
然后,在 MyBatis 中配置 PageHelper 插件:
<?xml version = "1.0" encoding = "UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0 //EN"
"http//mybatis.org/dtd/mybatis-3-config.dtd" >
<configuration>
<settings>
...
</settings>
<typeAliases>
...
</typeAliases>
// 配置插件
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
</plugins>
<environments default="development">
...
</environments>
<databaseIdProvider type="VENDOR">
...
</databaseIdProvider>
<mappers>
...
</mappers>
</configuration>
由于我们在这里配置了插件的实现类 com.github.pagehelper.PageInterceptor,所以,在初始化 MyBatis 配置时,会在初始化完执行器后,对执行器对象通过 PageInterceptor 在进行一层动态代理,源码如下:
Configuration.class
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
// 初始化执行器
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 对执行器进行动态代理
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
InterceptorChain.class
// 这里的 interceptor 即为我们在 mybatis-config.xml 中配置的 plugin 的实现
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
PageInterceptor.class
// 调用 MyBatis 的 Plugin.wrap(...) ,用 this 对象来对 taget 进行动态代理,返回一个由 PageHelper 代理的对象
// @Param target 即 executor
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
上图中可以看出, PageHelper 分页功能的实现,主要的流程可以分为:
在 mybatis-config.xml 中配置插件
通过 MyBatis 的 Plugin 中的 wrap(…) 方法生成由 Plugin 代理的 executor 对象
当调用 executor 进行增删改操作时,会被转发到 Plugin 的 invoke(…) 方法;该方法内部会调用拦截器的 interceptor(…) ,在该方法内,会先进行一次 count 查询,如果返回条数大于 0 ,则进行分页查询,等于 0 则不进行分页查询,直接返回。
Plugin.class
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
// 当前方法在被拦截的方法之列,则调用 PageInterceptor 进行拦截
return interceptor.intercept(new Invocation(target, method, args));
}
// 不在则放行
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
PageInterceptor.class
@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
...
// 进行 count 查询
Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
//判断查询结果是否有数据
if (!dialect.afterCount(count, parameter, rowBounds)) {
//当查询总数为 0 时,直接返回空的结果
return dialect.afterPage(new ArrayList(), parameter, rowBounds);
}
...
// 执行 limit 查询
resultList = ExecutorUtil.pageQuery(dialect, executor,
ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
...
}
PageHelper 的分页功能大致的实现就是这样,下面来自己实现一个插件,功能是在查询前打印 SQL 语句。
首先,创建一个自己的拦截器类:
MyBatisInterceptor.class
// 只拦截两个 query 方法
@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 MyBatisInterceptor implements Interceptor {
// 分别重写 Interceptor 接口的三个方法
// 拦截器方法
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
Executor executor = (Executor) invocation.getTarget();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
RowBounds rowBounds = (RowBounds) args[2];
ResultHandler resultHandler = (ResultHandler) args[3];
CacheKey cacheKey;
BoundSql boundSql;
// 由于我们这里拦截的是 executor 中的两个 query 方法,这两个方法唯一的区别是参数个数的不同,所以,我们这里通过参数个数的不同来获取具体的 SQL 语句
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];
}
// 打印 执行 SQL
System.err.println("执行查询前打印查询 SQL : " + boundSql.getSql());
List<Object> resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
return resultList;
}
// 生成动态代理对象,使用当前对象作为拦截器,被拦截对象为 target 对象,也就是 executor 对象
@Override
public Object plugin(Object target) {
Object wrap = Plugin.wrap(target, this);
return wrap;
}
// 拦截器的一些参数设置,我这里没有,就没有实现
@Override
public void setProperties(Properties properties) {
}
}
然后,将 mybatis-config.xml 中的 plugin 标签的 interceptor 属性设置为我们这个类的类路径。
<plugins>
<plugin interceptor="com.mybatis.simple.interceptor.MyBatisInterceptor"></plugin>
</plugins>
然后执行测试方法,测试方法如下:
@Test
public void testMyPlugin() {
SysUserMapper mapper = sqlSession.getMapper(SysUserMapper.class);
SysUser user = mapper.selectById(1004);
Assert.assertNotNull(user);
}
便可以在控制开看到打印如下结果:
// 拦截器的 interceptor 方法打印
执行查询前打印查询 SQL : select * from sys_user where id = ?
DEBUG [main] - ==> Preparing: select * from sys_user where id = ?
DEBUG [main] - ==> Parameters: 1004(Integer)
TRACE [main] - <== Columns: id, user_name, user_password, user_email, user_info, head_img, create_time
TRACE [main] - <== Row: 1004, 孙悟空, 123456, SWK@xiyouji.com, <<BLOB>>, <<BLOB>>, 2020-06-15 06:53:56
DEBUG [main] - <== Total: 1