关于MyBatis四大对象及插件原理请参看https://www.jianshu.com/p/f704c9ae600e 和 https://www.cnblogs.com/chenpi/p/10498921.html,这里我们使用插件原理实现MySQL分页,限制查询记录数。
插件开发实现步骤
- 确定需要拦截的签名
正如MyBatis插件可以拦截四大对象中的任意一个一样。从Plugin源码中我们可以看到它需要注册签名才能够运行插件。签名需要确定一些要素。
首先要根据功能来确定你需要拦截什么对象。
- Executor是执行SQL的全过程,包括组装参数,组装结果集返回和执行SQL过程,都可以拦截,较为广泛,我们一般用的不算太多。
- StatementHandler是执行SQL的过程,我们可以重写执行SQL的过程。这是我们最常用的拦截对象。
ParameterHandler,很明显它主要是拦截执行SQL的参数组装,你可以重写组装参数规则。 -
ResultSetHandler用于拦截执行结果的组装,你可以重写组装结果的规则。
image.png
-
拦截方法和参数
当你确定了需要拦截什么对象,接下来就要确定需要拦截什么方法及方法的参数,这些都是在你理解了MyBatis四大对象运作的基础上才能确定的。
查询的过程是通过Executor调度StatementHandler来完成的。调度StatementHandler的prepare方法预编译SQL,于是我们需要拦截的方法便是prepare方法,在此之前完成SQL的重新编写。
image.png
实现拦截方法
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import java.sql.Connection;
import java.util.Properties;
/**
* @author: Jay Mitter
* @date: 2020-07-26 17:26
* @description:
*/
@Intercepts({@Signature(
type = StatementHandler.class, // 确定需要拦截的对象
method = "prepare", // 确定需要拦截的对象方法
args = {Connection.class, Integer.class} // 拦截方法的参数
)})
public class QueryLimitPlugin implements Interceptor {
/**
* 默认限制查询返回行数
*/
private int limit;
private String dbType;
/**
* 限制表中间别名。避免表重名,所以起得怪些
*/
private static final String LMT_TABLE_NAME = "limit_Table_Name_xxx";
/**
* 代理拦截对象方法的内容
* 这里的intercept犯法就会覆盖StatementHandler的prepare方法,我们先从代理对象分离出真实的对象,然后根据需要修改的SQL,来达到限制返回行数的需求。
* 最后使用invocation.proceed()来调度真实的StatementHandler的prepare方法来完成SQL的预编译,最后需要在MyBatis的配置文件洪配置才能运行这个插件
* <plugins>
* <plugin interceptor="com.pengjs.kkb.mybatis.plugin.QueryLimitPlugin">
* <property name="dbType" value="mysql"/>
* <property name="limit" value="10"/>
* </plugin>
* </plugins>
* @param invocation 责任链对象
* @return
* @throws Throwable
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 取出被拦截对象
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
// 分离代理对象,从而形成多次代理,通过两次循环最原始的被代理类,MyBatis使用JDK代理
while (metaObject.hasGetter("h")) {
Object object = metaObject.getValue("h");
metaObject = SystemMetaObject.forObject(object);
}
// 分离最后一个代理的目标类
while (metaObject.hasGetter("target")) {
Object object = metaObject.getValue("target");
metaObject = SystemMetaObject.forObject(object);
}
// 取出即将要执行的SQL
String sql = (String) metaObject.getValue("delegate.boundSql.sql");
String limitSql;
// 判断参数是不是MySQL数据库且SQL有没有被插件重写过
if ("mysql".equals(this.dbType) && !sql.contains(LMT_TABLE_NAME)) {
// 去掉前后空格
sql = sql.trim();
// 将参数写入SQL,相当于子查询
limitSql = "select * from (" + sql + ") " + LMT_TABLE_NAME + " limit " + limit;
// 重新要执行SQL
metaObject.setValue("delegate.boundSql.sql", limitSql);
}
// 调用原来你对象的方法,进入责任链的下一层级
return invocation.proceed();
}
/**
* 生成对象的代理,这里常用MyBatis提供的Plugin的wrap方法
* @param target 被代理的对象
* @return
*/
@Override
public Object plugin(Object target) {
// 使用MyBatis默认的类生成代理对象,JDK动态代理
// 插件最终会进入plugin的invoke方法,在invoke方法中最终还是使用到了拦截器的intercept方法,如下:
// return interceptor.intercept(new Invocation(target, method, args));
return Plugin.wrap(target, this);
}
/**
* 获取插件配置的属性,我们在MyBatis的配置文件中配置的属性
* @param properties 是MyBatis配置的参数
*/
@Override
public void setProperties(Properties properties) {
String strLimit = properties.getProperty("limit", "50");
this.limit = Integer.parseInt(strLimit);
// 这里我们读取设置的数据库类型
this.dbType = properties.getProperty("dbtType", "mysql");
}
}
配置和运行
image.png观察日志发现预编译SQL已经经过插件按照我们的规则做了修改:
image.png
网友评论