简介

利用 MyBatis Plugin 插件技术实现分页功能。

分页插件实现思路如下:

  • 业务代码在 ThreadLocal 中保存分页信息;
  • MyBatis Interceptor 拦截查询请求,获取分页信息,实现分页操作,封装分页列表数据返回;

测试类:com.yjw.demo.PageTest

插件开发过程

确定需要拦截的签名

MyBatis 插件可以拦截四大对象中的任意一个,从 Plugin 源码中可以看到它需要注册签名才能够运行插件,签名需要确定一些要素。

确定需要拦截的对象

  • Executor 是执行 SQL 的全过程,包括组装参数,组装结果集返回和执行 SQL 过程,都可以拦截。
  • StatementHandler 是执行 SQL 的过程,我们可以重写执行 SQL 的过程。
  • ParameterHandler 是拦截执行 SQL 的参数组装,我们可以重写组装参数规则。
  • ResultSetHandler 用于拦截执行结果的组装,我们可以重写组装结果的规则。

拦截方法和参数

当确定了需要拦截什么对象,接下来就要确定需要拦截什么方法和方法的参数。比如分页插件需要拦截 Executor 的 query 方法,我们先看看 Executor 接口的定义,代码清单如下:

public interface Executor {

  ResultHandler NO_RESULT_HANDLER = null;

  int update(MappedStatement ms, Object parameter) throws SQLException;

  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;

  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

  <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;

  List<BatchResult> flushStatements() throws SQLException;

  void commit(boolean required) throws SQLException;

  void rollback(boolean required) throws SQLException;

  CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);

  boolean isCached(MappedStatement ms, CacheKey key);

  void clearLocalCache();

  void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);

  Transaction getTransaction();

  void close(boolean forceRollback);

  boolean isClosed();

  void setExecutorWrapper(Executor executor);

}

以上的任何方法都可以拦截,从接口定义而言,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})})

其中,@Intercepts 说明它是一个拦截器。@Signature 是注册拦截器签名的地方,type 是四大对象中的一个,method 是需要拦截的方法,args 是方法的参数。

插件接口定义

在 MyBatis 中开发插件,需要实现 Interceptor 接口,接口的定义如下:

public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  Object plugin(Object target);

  void setProperties(Properties properties);

}
  • intercept 方法:它将直接覆盖你所拦截对象原有的方法,因此它是插件的核心方法。通过 invocation 参数可以反射调度原来对象的方法。
  • plugin 方法:target 是被拦截对象,它的作用是给被拦截对象生成一个代理对象,并返回它。为了方便 MyBatis 使用 org.apache.ibatis.plugin.Plugin 中的 wrap 静态方法提供生成代理对象。
  • setProperties 方法:允许在 plugin 元素中配置所需参数,方法在插件初始化的时候就被调用了一次,然后把插件对象存入到配置中,以便后面再取出。

实现类

根据分页插件的实现思路,定义了三个类。

Page 类

Page 类继承了 ArrayList 类,用来封装分页信息和列表数据。

/**
* 分页返回对象
*
* @author yinjianwei
* @date 2018/11/05
*/
public class Page<E> extends ArrayList<E> { private static final long serialVersionUID = 1L; /**
* 页码,从1开始
*/
private int pageNum;
/**
* 页面大小
*/
private int pageSize;
/**
* 起始行
*/
private int startRow;
/**
* 末行
*/
private int endRow;
/**
* 总数
*/
private long total;
/**
* 总页数
*/
private int pages; public int getPageNum() {
return pageNum;
} public void setPageNum(int pageNum) {
this.pageNum = pageNum;
} public int getPageSize() {
return pageSize;
} public void setPageSize(int pageSize) {
this.pageSize = pageSize;
} public int getStartRow() {
return startRow;
} public void setStartRow(int startRow) {
this.startRow = startRow;
} public int getEndRow() {
return endRow;
} public void setEndRow(int endRow) {
this.endRow = endRow;
} public long getTotal() {
return total;
} public void setTotal(long total) {
this.total = total;
this.pages = (int)(total / pageSize + (total % pageSize == 0 ? 0 : 1));
if (pageNum > pages) {
pageNum = pages;
}
this.startRow = this.pageNum > 0 ? (this.pageNum - 1) * this.pageSize : 0;
this.endRow = this.startRow + this.pageSize * (this.pageNum > 0 ? 1 : 0);
} public int getPages() {
return pages;
} public void setPages(int pages) {
this.pages = pages;
} /**
* 返回当前对象
*
* @return
*/
public List<E> getResult() {
return this;
} }

PageHelper 类

PageHelper 类是分页的帮助类,主要利用 ThreadLocal 线程变量存储分页信息。代码清单如下:

/**
* 分页帮助类
*
* @author yinjianwei
* @date 2018/11/05
*/
@SuppressWarnings("rawtypes")
public class PageHelper { private static final ThreadLocal<Page> PAGE_THREADLOCAT = new ThreadLocal<Page>(); /**
* 设置线程局部变量分页信息
*
* @param page
*/
public static void setPageThreadLocal(Page page) {
PAGE_THREADLOCAT.set(page);
} /**
* 获取线程局部变量分页信息
*
* @return
*/
public static Page getPageThreadLocal() {
return PAGE_THREADLOCAT.get();
} /**
* 清空线程局部变量分页信息
*/
public static void pageThreadLocalClear() {
PAGE_THREADLOCAT.remove();
} /**
* 设置分页参数
*
* @param pageNum
* @param pageSize
*/
public static void startPage(Integer pageNum, Integer pageSize) {
Page page = new Page();
page.setPageNum(pageNum);
page.setPageSize(pageSize);
setPageThreadLocal(page);
} }

PageInterceptor 类

PageInterceptor 类实现了 Interceptor 接口,是分页插件的核心类。代码清单如下:

/**
* 分页拦截器
*
* @author yinjianwei
* @date 2018/11/05
*/
@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 Field additionalParametersField; @SuppressWarnings({"rawtypes", "unchecked"})
@Override
public Object intercept(Invocation invocation) throws Throwable {
Executor executor = (Executor)invocation.getTarget();
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement)args[0];
Object parameter = args[1];
RowBounds rowBounds = (RowBounds)args[2];
ResultHandler resultHandler = (ResultHandler)args[3];
CacheKey cacheKey;
BoundSql boundSql;
// 4个参数
if (args.length == 4) {
boundSql = ms.getBoundSql(parameter);
cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
}
// 6个参数
else {
cacheKey = (CacheKey)args[4];
boundSql = (BoundSql)args[5];
}
// 判断是否需要分页
Page page = PageHelper.getPageThreadLocal();
// 不执行分页
if (page.getPageNum() <= 0) {
return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}
// count查询
MappedStatement countMs = newCountMappedStatement(ms);
String sql = boundSql.getSql();
String countSql = "select count(1) from (" + sql + ") _count";
BoundSql countBoundSql =
new BoundSql(ms.getConfiguration(), countSql, boundSql.getParameterMappings(), parameter);
Map<String, Object> additionalParameters = (Map<String, Object>)additionalParametersField.get(boundSql);
for (Entry<String, Object> additionalParameter : additionalParameters.entrySet()) {
countBoundSql.setAdditionalParameter(additionalParameter.getKey(), additionalParameter.getValue());
}
CacheKey countCacheKey = executor.createCacheKey(countMs, parameter, rowBounds, countBoundSql);
Object countResult =
executor.query(countMs, parameter, RowBounds.DEFAULT, resultHandler, countCacheKey, countBoundSql);
Long count = (Long)((List)countResult).get(0);
page.setTotal(count);
// 分页查询
String pageSql = sql + " limit " + page.getStartRow() + "," + page.getPageSize();
BoundSql pageBoundSql =
new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);
for (Entry<String, Object> additionalParameter : additionalParameters.entrySet()) {
pageBoundSql.setAdditionalParameter(additionalParameter.getKey(), additionalParameter.getValue());
}
CacheKey pageCacheKey = executor.createCacheKey(ms, parameter, rowBounds, pageBoundSql);
List listResult = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageCacheKey, pageBoundSql);
page.addAll(listResult);
// 清空线程局部变量分页信息
PageHelper.pageThreadLocalClear();
return page;
} @Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
} @Override
public void setProperties(Properties properties) {
try {
additionalParametersField = BoundSql.class.getDeclaredField("additionalParameters");
additionalParametersField.setAccessible(true);
} catch (NoSuchFieldException | SecurityException e) {
e.printStackTrace();
}
} /**
* 创建count的MappedStatement
*
* @param ms
* @return
*/
private MappedStatement newCountMappedStatement(MappedStatement ms) {
MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId() + "_count",
ms.getSqlSource(), ms.getSqlCommandType());
builder.resource(ms.getResource());
builder.fetchSize(ms.getFetchSize());
builder.statementType(ms.getStatementType());
builder.keyGenerator(ms.getKeyGenerator());
if (ms.getKeyProperties() != null && ms.getKeyProperties().length != 0) {
StringBuilder keyProperties = new StringBuilder();
for (String keyProperty : ms.getKeyProperties()) {
keyProperties.append(keyProperty).append(",");
}
keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
builder.keyProperty(keyProperties.toString());
}
builder.timeout(ms.getTimeout());
builder.parameterMap(ms.getParameterMap());
// count查询返回值int
List<ResultMap> resultMaps = new ArrayList<ResultMap>();
ResultMap resultMap = new ResultMap.Builder(ms.getConfiguration(), ms.getId() + "_count", Long.class,
new ArrayList<ResultMapping>(0)).build();
resultMaps.add(resultMap);
builder.resultMaps(resultMaps);
builder.resultSetType(ms.getResultSetType());
builder.cache(ms.getCache());
builder.flushCacheRequired(ms.isFlushCacheRequired());
builder.useCache(ms.isUseCache()); return builder.build();
} }

配置

MyBatis 配置文件增加 plugin 配置项。

<?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>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings> <typeHandlers>
<typeHandler javaType="com.yjw.demo.mybatis.common.constant.Sex"
jdbcType="TINYINT"
handler="com.yjw.demo.mybatis.common.type.SexEnumTypeHandler"/>
</typeHandlers> <plugins>
<plugin interceptor="com.yjw.demo.mybatis.common.page.PageInterceptor">
</plugin>
</plugins>
</configuration>

MyBatis 实用篇

MyBatis 概念

MyBatis 示例-简介

MyBatis 示例-类型处理器

MyBatis 示例-传递多个参数

MyBatis 示例-主键回填

MyBatis 示例-动态 SQL

MyBatis 示例-联合查询

MyBatis 示例-缓存

MyBatis 示例-插件

MyBatis 示例-插件的更多相关文章

  1. MyBatis 示例-传递多个参数

    映射器的主要元素: 本章介绍 select 元素中传递多个参数的处理方式. 测试类:com.yjw.demo.MulParametersTest 使用 Map 传递参数(不建议使用) 使用 MyBat ...

  2. MyBatis 示例-类型处理器

    MyBatis 提供了很多默认类型处理器,参考官网地址:链接,除了官网提供的类型处理器,我们也可以自定义类型处理器. 具体做法为:实现 org.apache.ibatis.type.TypeHandl ...

  3. MyBatis 示例-简介

    简介 为了全面熟悉 MyBatis 的使用,整理一个 MyBatis 的例子,案例中包含了映射器.动态 SQL 的使用.本章先介绍项目结构和配置. 项目地址:链接 数据库表的模型关系:链接 项目结构 ...

  4. MyBatis 示例-联合查询

    简介 MyBatis 提供了两种联合查询的方式,一种是嵌套查询,一种是嵌套结果.先说结论:在项目中不建议使用嵌套查询,会出现性能问题,可以使用嵌套结果. 测试类:com.yjw.demo.JointQ ...

  5. MyBatis 示例-缓存

    MyBatis 提供两种类型的缓存,一种是一级缓存,另一种是二级缓存,本章通过例子的形式描述 MyBatis 缓存的使用. 测试类:com.yjw.demo.CacheTest 一级缓存 MyBati ...

  6. MyBatis 示例-动态 SQL

    MyBatis 的动态 SQL 包括以下几种元素: 详细的使用参考官网文档:http://www.mybatis.org/mybatis-3/zh/dynamic-sql.html 本章内容简单描述这 ...

  7. MyBatis 示例-主键回填

    测试类:com.yjw.demo.PrimaryKeyTest 自增长列 数据库表的主键为自增长列,在写业务代码的时候,经常需要在表中新增一条数据后,能获得这条数据的主键 ID,MyBatis 提供了 ...

  8. Mybatis分页插件

    mybatis配置 <!-- mybatis分页插件 --> <bean id="pagehelper" class="com.github.pageh ...

  9. Mybatis学习---Mybatis分页插件 - PageHelper

    1. Mybatis分页插件 - PageHelper说明 如果你也在用Mybatis,建议尝试该分页插件,这个一定是最方便使用的分页插件. 该插件目前支持Oracle,Mysql,MariaDB,S ...

随机推荐

  1. flask 设置配置文件的方式

    from flask import Flask from flask import current_app """ 配置参数设置与读取 """ ...

  2. tp5 回滚事务记录,其中一条语句报错,全部回滚

    #################################### 测试事务 // 启动事务 Db::startTrans(); try { //插入行为表 $data = [ 'userId' ...

  3. 爬虫之代理和cookie的处理

    代理操作 代理的目的 为解决ip被封的情况 什么是代理 代理服务器:fiddler 为什么使用代理可以改变请求的ip 本机的请求会先发送给代理服务器,代理服务器会接受本机发送过来的请求(当前请求对应的 ...

  4. 《你不知道的JavaScript(上)》笔记——动态作用域

    动态作用域让作用域作为一个在运行时就被动态确定的形式, 而不是在写代码时进行静态确定的形式.动态作用域并不关心函数和作用域是如何声明以及在何处声明的, 只关心它们从何处调用. 换句话说, 作用域链是基 ...

  5. About Xi’an

    Introduction Ancient Capital It is the birthplace of the Chinese Nation, is one of the four ancient ...

  6. SQL-W3School-基础:SQL UPDATE 语句

    ylbtech-SQL-W3School-基础:SQL UPDATE 语句 1.返回顶部 1. Update 语句 Update 语句用于修改表中的数据. 语法: UPDATE 表名称 SET 列名称 ...

  7. mySQL 插入,更新和删除数据

    插入数据: 语法: INSERT INTO table_name ( field1, field2,...fieldN ) VALUES ( value1, value2,...valueN ); 如 ...

  8. 七天学会ASP.NET MVC

    地址一: http://www.cnblogs.com/powertoolsteam/p/MVC_one.html http://www.cnblogs.com/powertoolsteam/p/MV ...

  9. 18 Flutter仿京东商城项目 商品详情顶部tab切换 顶部下拉菜单 底部浮动导航

    ProductContent.dart import 'package:flutter/material.dart'; import '../services/ScreenAdaper.dart'; ...

  10. JAVA 基础编程练习题33 【程序 33 杨辉三角】

    33 [程序 33 杨辉三角] 题目:打印出杨辉三角形(要求打印出 10 行如下图) 程序分析: 1 1 1 1 2 1 1 3 3 1 1 4 6 4 1 1 5 10 10 5 1 package ...