Springboot整合MyBatis(七:Mybatis的xml配配置文件,详细配置之插件(plugins)监控dao层,自定义插件(浅剖分页插件实现原理))

MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)

这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心。

通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。

以上是官方文档对插件的介绍,看下来能懂一个大概,简单点说就是:sql语句从执行之前到最后返回结果会提供几个生命周期函数,在这几个生命周期函数我们可以做一些自己想做的事情,从而监听这一过程,获取我们想要的信息或者增加一些操作步骤。

下面是官方的实例:来解读一下
1、实现org.apache.ibatis.plugin.Interceptor接口,并且覆盖其方法
2、在其类上打上注解@Intercepts
3、在配置文件中进行配置
具体的每一个细节,我会在下面代码中注释

package com.osy.config;

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;

import java.util.Properties;

/**
 * Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
 * ParameterHandler (getParameterObject, setParameters)
 * ResultSetHandler (handleResultSets, handleOutputParameters)
 * StatementHandler (prepare, parameterize, batch, update, query)
 *
 * @Intercepts: Signature[] value(); 它的值是方法签名注解的一个数组,证明这个注解作用的这个类可以作用多个方法签名
 * @Signature:
 *  type:Class类型,能够传入的值为:Executor.class,ParameterHandler.class,ResultSetHandler.class,StatementHandler.class
 *  method: 方法,对应type下面的方法,可选值为上面每种类型对应的括号里面的值,不同的方法拦截的生命周期不同
 *  args:Class类型,对应的方法就传入对应类型的class对象即可。比如Executor的update方法,
 *      它的方法体是:int update(MappedStatement ms, Object parameter) throws SQLException;
 *      所以我们只需要传入MappedStatement.class,Object.class即可,其他方法同理。(没有方法参数的,就给一个空数组即可{})
 * @Intercepts是一个数组,那么下面这个也是可以写多个,拦截多个方法签名,不过个人建议一个就够了,分工明确。
 */
@Intercepts({@Signature(
        type= Executor.class,
        method = "update",
        args = {MappedStatement.class,Object.class})})
public class MybatisPlugin implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 调用他原本该调用的方法。
        /**Invocation: 三个属性
         *   private final Object target; // 目标对象
         *   private final Method method; // 目标方法
         *   private final Object[] args; // 方法参数
         *   这里就有点类似于aop思想的环绕拦截了,
         *   因为proceed方法调用的就是invoke方法
         */
        // todoSomething
        Object o = invocation.proceed();
        // todoSomething
        return o;
    }

    @Override
    public Object plugin(Object target) {
         // 这里只拦截Executor的,其他的不进行拦截
        // 判断插件是否属于Executor类型,如果属于Executor类型,那么将本身这个类传递给插件
        if (target instanceof Executor) {
        	// 此方法返回一个插件增强对象,相当于覆盖了mybatis默认的插件。他会走我们定义的当前的这个类的方法
            return Plugin.wrap(target, this);
        } else {
            // 否则不动,原来是什么插件就是返回什么插件
            return target;
        }
    }

    @Override
    public void setProperties(Properties properties) {
         // 设置属性值,当然也可以获取属性,这个属性是通过外面配置文件中传入过来的
        // 比如下面配置文件中传了一个someProperty属性。这里就可以获取
        String params = properties.getProperty("someProperty");
        System.out.println(params);
    }
}

<plugins>
  <plugin interceptor="org.mybatis.example.ExamplePlugin">
    <property name="someProperty" value="100"/>
  </plugin>
</plugins>

浅剖分页插件实现原理

简易的流程:设置分页参数 -> 放入threadLocal中 -> 然后将MappedStatement替换,将里面的sql替换成添加了limit的sql -> 将参数绑定在limit上面

当面里面会有比较复杂的逻辑,下面来粗略的看一他具体的实现。

使用这个分页插件是需要配置plugins,那么根据上面的我们自定义的例子来看,我们也许配置,那么我们就可以找打他的插件核心类:com.github.pagehelper.PageHelper

看他的类上面的注解,他拦截的是Executor的query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler)方法,(根据args的参数,可以确定是那个重载方法

@SuppressWarnings("rawtypes")
@Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
public class PageHelper implements Interceptor {}

然后看他覆盖Interceptor接口的三个方法:

	// 此方法对其进行拦截,并且增加分页查询语句 
	public Object intercept(Invocation invocation) throws Throwable {
        if (autoRuntimeDialect) {
            SqlUtil sqlUtil = getSqlUtil(invocation);
            return sqlUtil.processPage(invocation);
        } else {
            if (autoDialect) {
                initSqlUtil(invocation);
            }
            return sqlUtil.processPage(invocation);
        }
    }
    // 这个方法判断插件是否是Executor,如果是,就返回他自己定义的这个插件,如果不是,那么就返回原来的插件
	public Object plugin(Object target) {
        if (target instanceof Executor) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }
    // 设置一些属性
    public void setProperties(Properties p) {
        checkVersion();
        //多数据源时,获取jdbcurl后是否关闭数据源
        String closeConn = p.getProperty("closeConn");
        //解决#97
        if(StringUtil.isNotEmpty(closeConn)){
            this.closeConn = Boolean.parseBoolean(closeConn);
        }
        //初始化SqlUtil的PARAMS
        SqlUtil.setParams(p.getProperty("params"));
        //数据库方言
        String dialect = p.getProperty("dialect");
        String runtimeDialect = p.getProperty("autoRuntimeDialect");
        if (StringUtil.isNotEmpty(runtimeDialect) && runtimeDialect.equalsIgnoreCase("TRUE")) {
            this.autoRuntimeDialect = true;
            this.autoDialect = false;
            this.properties = p;
        } else if (StringUtil.isEmpty(dialect)) {
            autoDialect = true;
            this.properties = p;
        } else {
            autoDialect = false;
            sqlUtil = new SqlUtil(dialect);
            sqlUtil.setProperties(p);
        }
    }

我们在使用分页插件的时候,一开始我们调用PageHelper.startPage(page,pageSize)静态方法。
一直追溯到后面,他最后调用的方法如下:

	public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
        Page<E> page = new Page<E>(pageNum, pageSize, count);
        page.setReasonable(reasonable);
        page.setPageSizeZero(pageSizeZero);
        //当已经执行过orderBy的时候
        Page<E> oldPage = SqlUtil.getLocalPage();
        if (oldPage != null && oldPage.isOrderByOnly()) {
            page.setOrderBy(oldPage.getOrderBy());
        }
        SqlUtil.setLocalPage(page);
        return page;
    }

其中:SqlUtil.setLocalPage(page);


	private static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
	 
	public static void setLocalPage(Page page) {
        LOCAL_PAGE.set(page);
    }

从这里可以看出,我们最开始设置的参数,他封装成Page对象,然后放入了ThreadLocal中,在修改sql语句填入参数的时候,就从这里获取到我们设置的参数。
然后在看intercept方法:

	public Object intercept(Invocation invocation) throws Throwable {
        if (autoRuntimeDialect) {
            SqlUtil sqlUtil = getSqlUtil(invocation);
            return sqlUtil.processPage(invocation);
        } else {
            if (autoDialect) {
                initSqlUtil(invocation);
            }
            return sqlUtil.processPage(invocation);
        }
    }

sqlUtil.processPage(invocation);如下

public Object processPage(Invocation invocation) throws Throwable {
        try {
            Object result = _processPage(invocation);
            return result;
        } finally {
            clearLocalPage();
        }
    }

如果抛出异常,那么将ThreadLocal里面存放的数据清除。

	public static void clearLocalPage() {
        LOCAL_PAGE.remove();
    }

回到_processPage方法:

private Object _processPage(Invocation invocation) throws Throwable {
        final Object[] args = invocation.getArgs();
        Page page = null;
        //支持方法参数时,会先尝试获取Page
        if (supportMethodsArguments) {
            page = getPage(args);
        }
        //分页信息
        RowBounds rowBounds = (RowBounds) args[2];
        //支持方法参数时,如果page == null就说明没有分页条件,不需要分页查询
        if ((supportMethodsArguments && page == null)
                //当不支持分页参数时,判断LocalPage和RowBounds判断是否需要分页
                || (!supportMethodsArguments && SqlUtil.getLocalPage() == null && rowBounds == RowBounds.DEFAULT)) {
            return invocation.proceed();
        } else {
            //不支持分页参数时,page==null,这里需要获取
            if (!supportMethodsArguments && page == null) {
                page = getPage(args);
            }
            return doProcessPage(invocation, page, args);
        }
    }

然后进入doProcessPage方法

private Page doProcessPage(Invocation invocation, Page page, Object[] args) throws Throwable {
        //保存RowBounds状态
        RowBounds rowBounds = (RowBounds) args[2];
        //获取原始的ms
        MappedStatement ms = (MappedStatement) args[0];
        //判断并处理为PageSqlSource
        if (!isPageSqlSource(ms)) {
            processMappedStatement(ms);
        }
        //设置当前的parser,后面每次使用前都会set,ThreadLocal的值不会产生不良影响
        ((PageSqlSource)ms.getSqlSource()).setParser(parser);
        try {
            //忽略RowBounds-否则会进行Mybatis自带的内存分页
            args[2] = RowBounds.DEFAULT;
            //如果只进行排序 或 pageSizeZero的判断
            if (isQueryOnly(page)) {
                return doQueryOnly(page, invocation);
            }

            //简单的通过total的值来判断是否进行count查询
            if (page.isCount()) {
                page.setCountSignal(Boolean.TRUE);
                //替换MS
                args[0] = msCountMap.get(ms.getId());
                //查询总数
                Object result = invocation.proceed();
                //还原ms
                args[0] = ms;
                //设置总数
                page.setTotal((Integer) ((List) result).get(0));
                if (page.getTotal() == 0) {
                    return page;
                }
            } else {
                page.setTotal(-1l);
            }
            //pageSize>0的时候执行分页查询,pageSize<=0的时候不执行相当于可能只返回了一个count
            if (page.getPageSize() > 0 &&
                    ((rowBounds == RowBounds.DEFAULT && page.getPageNum() > 0)
                            || rowBounds != RowBounds.DEFAULT)) {
                //将参数中的MappedStatement替换为新的qs
                page.setCountSignal(null);
                BoundSql boundSql = ms.getBoundSql(args[1]);
                args[1] = parser.setPageParameter(ms, args[1], boundSql, page);
                page.setCountSignal(Boolean.FALSE);
                //执行分页查询
                Object result = invocation.proceed();
                //得到处理结果
                page.addAll((List) result);
            }
        } finally {
            ((PageSqlSource)ms.getSqlSource()).removeParser();
        }

        //返回结果
        return page;
    }

其中 判断并处理为PageSqlSource,进行了判断然对sql进行处理(修改SqlSource):
并且讲ms放入private static final Map<String, MappedStatement> msCountMap = new ConcurrentHashMap<String, MappedStatement>();中。然后提供给
//替换MS
args[0] = msCountMap.get(ms.getId());
这里来获取。

public void processMappedStatement(MappedStatement ms) throws Throwable {
        SqlSource sqlSource = ms.getSqlSource();
        MetaObject msObject = SystemMetaObject.forObject(ms);
        SqlSource pageSqlSource;
        if (sqlSource instanceof StaticSqlSource) {
            pageSqlSource = new PageStaticSqlSource((StaticSqlSource) sqlSource);
        } else if (sqlSource instanceof RawSqlSource) {
            pageSqlSource = new PageRawSqlSource((RawSqlSource) sqlSource);
        } else if (sqlSource instanceof ProviderSqlSource) {
            pageSqlSource = new PageProviderSqlSource((ProviderSqlSource) sqlSource);
        } else if (sqlSource instanceof DynamicSqlSource) {
            pageSqlSource = new PageDynamicSqlSource((DynamicSqlSource) sqlSource);
        } else {
            throw new RuntimeException("无法处理该类型[" + sqlSource.getClass() + "]的SqlSource");
        }
        msObject.setValue("sqlSource", pageSqlSource);
        //由于count查询需要修改返回值,因此这里要创建一个Count查询的MS
        msCountMap.put(ms.getId(), MSUtils.newCountMappedStatement(ms));
    }

args[1] = parser.setPageParameter(ms, args[1], boundSql, page);
如果调用的是mysql的,那么就会执行

public class MysqlParser extends AbstractParser {
    @Override
    public String getPageSql(String sql) {
        StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
        sqlBuilder.append(sql);
        sqlBuilder.append(" limit ?,?");
        return sqlBuilder.toString();
    }

    @Override
    public Map<String, Object> setPageParameter(MappedStatement ms, Object parameterObject, BoundSql boundSql, Page<?> page) {
        Map<String, Object> paramMap = super.setPageParameter(ms, parameterObject, boundSql, page);
        paramMap.put(PAGEPARAMETER_FIRST, page.getStartRow());
        paramMap.put(PAGEPARAMETER_SECOND, page.getPageSize());
        return paramMap;
    }
}

而这个解析起在获取pageSQL的时候,会调用上面的getPageSql方法,能够看到他在原始的基础上面加上了 limit ?,?
并且setPageParameter设置参数的时候,将我们最初设定的值给设置进去了。
而getPageSql方法是被getBoundSql方法调用的,getBoundSql是mybatis的MappedStatement类的方法,所以分页插件将其对象替换了,然后在getBoundSql加上了后缀,最后实现sql加了分页。

这两个方法,result 是一个List,因为查询出来的都是list,
然后page。addAll将查询出来的数据放入page对象中,page是继承了ArrayList的。

//执行分页查询
Object result = invocation.proceed();
//得到处理结果
page.addAll((List) result);

猜你喜欢

转载自blog.csdn.net/qq_42154259/article/details/107009072