MyBatis插件

2020-07-26  本文已影响0人  Doooook

关于MyBatis四大对象及插件原理请参看https://www.jianshu.com/p/f704c9ae600ehttps://www.cnblogs.com/chenpi/p/10498921.html,这里我们使用插件原理实现MySQL分页,限制查询记录数。

插件开发实现步骤

  1. 确定需要拦截的签名
    正如MyBatis插件可以拦截四大对象中的任意一个一样。从Plugin源码中我们可以看到它需要注册签名才能够运行插件。签名需要确定一些要素。
    首先要根据功能来确定你需要拦截什么对象。
image.png
  1. 拦截方法和参数
    当你确定了需要拦截什么对象,接下来就要确定需要拦截什么方法及方法的参数,这些都是在你理解了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
上一篇下一篇

猜你喜欢

热点阅读