MyBatis插件原理—分页插件PageHelper

导读:本篇文章讲解 MyBatis插件原理—分页插件PageHelper,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

使用方法

1.添加Maven依赖

<dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper</artifactId>
            <version>5.0.0</version>
</dependency>

2.在 Mybatis-Config.xml中配置PageHelper插件参数

<plugins>
        <plugin interceptor="com.github.pagehelper.PageInterceptor">
            <!-- 4.0.0以后版本可以不设置该参数 ,可以自动识别
            <property name="dialect" value="mysql"/>  -->
            <!-- 该参数默认为false -->
            <!-- 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用 -->
            <!-- 和startPage中的pageNum效果一样-->
            <property name="offsetAsPageNum" value="true"/>
            <!-- 该参数默认为false -->
            <!-- 设置为true时,使用RowBounds分页会进行count查询 -->
            <property name="rowBoundsWithCount" value="true"/>
            <!-- 设置为true时,如果pageSize=0或者RowBounds.limit = 0就会查询出全部的结果 -->
            <!-- (相当于没有执行分页查询,但是返回结果仍然是Page类型)-->
            <property name="pageSizeZero" value="true"/>
            <!-- 3.3.0版本可用 - 分页参数合理化,默认false禁用 -->
            <!-- 启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页 -->
            <!-- 禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据 -->
            <property name="reasonable" value="true"/>
            <!-- 3.5.0版本可用 - 为了支持startPage(Object params)方法 -->
            <!-- 增加了一个`params`参数来配置参数映射,用于从Map或ServletRequest中取值 -->
            <!-- 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,orderBy,不配置映射的用默认值 -->
            <!-- 不理解该含义的前提下,不要随便复制该配置 -->
            <property name="params" value="pageNum=start;pageSize=limit;"/>
            <!-- 支持通过Mapper接口参数来传递分页参数 -->
            <property name="supportMethodsArguments" value="true"/>
            <!-- always总是返回PageInfo类型,check检查返回类型是否为PageInfo,none返回Page -->
            <property name="returnPageInfo" value="check"/>
        </plugin>
    </plugins>

注意:plugins插件的配置在 settings之后 在environments之前,否则会导致配置文件无法解析

3.代码

		PageHelper.startPage(pn, 10); //pageNumber, pageSize,第几页,每页几条
        List<Employee> emps = employeeService.getAll();
        PageInfo page = new PageInfo(emps, 10);
        return Msg.success().add("pageInfo", page);

原理

从配置文件可以看出,pageHelper分页插件的核心类就是com.github.pagehelper.PageInterceptor

package com.github.pagehelper;

import com.github.pagehelper.cache.Cache;
import com.github.pagehelper.cache.CacheFactory;
import com.github.pagehelper.util.MSUtils;
import com.github.pagehelper.util.StringUtil;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;

/**
 * Mybatis - 通用分页拦截器<br/>
 * 项目地址 : http://git.oschina.net/free/Mybatis_PageHelper
 *
 * @author liuzh/abel533/isea533
 * @version 5.0.0
 */
@SuppressWarnings({"rawtypes", "unchecked"})
@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 {
    //缓存count查询的ms
    protected Cache<CacheKey, MappedStatement> msCountMap = null;
    private Dialect dialect;
    private String default_dialect_class = "com.github.pagehelper.PageHelper";
    private Field additionalParametersField;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        ...
    }
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    @Override
    public void setProperties(Properties properties) {
       ...
    }

}

主要是实现了org.apache.ibatis.plugin.Interceptor接口。其中主要由三个方法

  • intercept 执行拦截内容的地方,拦截目标对象的目标方法的执行
  • plugin 决定是否触发intercept()方法,包装目标对象,包装就是为目标对象创建一个代理对象
  • setProperties 给自定义的拦截器传递xml配置的属性参数。将插件注册时的property属性设置进来
package org.apache.ibatis.plugin;

import java.util.Properties;

/**
 * @author Clinton Begin
 */
public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;
  
  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }
  
  default void setProperties(Properties properties) {
  }
}

还需要注意的是PageInterceptor的@Intercepts注解,其指定了拦截的对象和方法。在默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, 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}),
    }
)

了解完了PageHelper的核心类,我们再来看看它是如何运行的。

1.加载

首先,在myBatis生成sqlSessionFactory会话工厂时,加载了mybatis-config.xml配置文件中plugin节点

XMLConfigBuilder.java

private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        String interceptor = child.getStringAttribute("interceptor");
        Properties properties = child.getChildrenAsProperties();
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
        interceptorInstance.setProperties(properties);
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }

循环读取所有的interceptor节点,并强转成Interceptor对象。塞到一个InterceptorChain容器中进行管理

InterceptorChain.java

public class InterceptorChain {
  private final List<Interceptor> interceptors = new ArrayList<>();
  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }

通过类名就可以看出,mybaits管理所有的插件使用了责任链的设计模式
通过代码使用分页看出,它没有修改原有的代码,那它是怎么做到改变和增强对象的行为的呢?另外,如果我有多个插件,拦截同一个类时,它是如何做到层层拦截的呢?
大胆猜测一下,通过代理模式实现改变和增强对象;通过责任链模式实现层层拦截。
现在,我们进入代码看一看是不是和我们猜测的一样。

2.拦截

在@Intercepts注解中看到分页插件拦截的对象是Execut下执行器,而mybatis的执行器是在创建会话sqlSession创建的

Configuration.java

  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 {
      // 默认 SimpleExecutor
      executor = new SimpleExecutor(this, transaction);
    }
    // 二级缓存开关,settings 中的 cacheEnabled 默认是 true
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    // 植入插件的逻辑,至此,四大对象已经全部拦截完毕
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

InterceptorChain.java

public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }

如猜测的那样,果然使用责任链模式管理所有plugin。
按照猜测,interceptor.plugin(target)方法就是用来创建代理对象的,点进去看看

Interceptor.java

default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

Plugin.java

 public static Object wrap(Object target, Interceptor interceptor) {
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

确实如猜测的那样,通过Plugin封装一个wrap方法,生成一个jdk动态代理类。
在Proxy.newProxyInstance方法中,共有三个参数:

  • ClassLoader loader 类加载器
  • Class<?>[] interfaces 被代理类的所有接口信息
  • InvocationHandler h 处理类,实现InvocationHandler接口中invoke方法。

所以接下来我们要关注的就是Plugin类中的invoke()方法。

 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)) {
      	//走插件流程
        return interceptor.intercept(new Invocation(target, method, args));
      }
      //继续走原来的流程
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }

这样,我们就回到了一开始拦截器intercept()的方法。在这里,它封装了一个Invocation类,也可以理解为被代理类的封装。

public class Invocation {

  private final Object target;
  private final Method method;
  private final Object[] args;

  public Invocation(Object target, Method method, Object[] args) {
    this.target = target;
    this.method = method;
    this.args = args;
  }
  public Object proceed() throws InvocationTargetException, IllegalAccessException {
  	//被代理类本来的方法
    return method.invoke(target, args);
  }
}

也就是说,我们只需要执行proceed()方法,就相当于执行了被代理类本来的方法。这样,我们在自己写插件类的时候可以在处理完成后调用proceed()方法就可以了。

总结

Mybatis插件关键对象:

  • Interceptor接口:自定义拦截器(实现类)
  • InterceptorChain:存放插件的容器
  • Plugin:h对象,提供创建代理类的方法
  • Invocation:对被代理类的封装

插件的工作流程图:

在这里插入图片描述
当有多个插件时:
在这里插入图片描述

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/12932.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!