《深入学习MyBatis系列之MyBatis应用分析和最佳实践12_SSM框架_集成MyBatis的插件使用和原理分析》

《深入学习MyBatis系列之MyBatis应用分析和最佳实践12_SSM框架_集成MyBatis的插件使用和原理分析》

大龄程序猿的知识分享,边学边记录边分享.

望觉得本文对您有意义的一键三连,点赞、收藏、评论.

您的支持是对我的坚持最大的鼓励,可以交友互相学习交流心得.

坐标: 浙江杭州

Q Q: 873373549

文章目录

一、本文目标

  • 已经掌握了基本SSM框架的搭建
  • 讲解SSM框架如何应用集成MyBatis插件
  • 分析插件的工作原理
  • 掌握自定义插件的编写方法

二、Spring SpringMVC MyBatis 集成插件步骤

分页插件 为例子

2.1 添加插件依赖

在pom.xml中添加插件依赖,如下:

<!--   分页插件的依赖     -->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.0.0</version>
</dependency>

2.2 插件配置

mybatis-config.xml中添加如下配置:

<!--  插件集合配置  -->
    <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>

2.3 使用插件

BlogController编写一个分页控制层方法,引入插件查询,如下:

/**
     * blog 分页
     *
     * @return
     */
    @GetMapping("/blogPageList")
    public String blogPageList(@Param(value = "pageNum") Integer pageNum, @Param(value = "pageSize") Integer pageSize, Model model) {
        PageHelper.startPage(pageNum, pageSize);
        List<Blog> blogList = blogService.findAll();
        model.addAttribute("blogList", blogList);
        log.info("blogList:{}", blogList);
        //连续显示的页数是10页
        //包装查出来的结果,只需要将pageInfo交给页面,封装了详细的分页信息
        //包括查询出来的数据
        PageInfo<Blog> pageInfo = new PageInfo<>(blogList, pageSize);
        model.addAttribute("pageInfo", pageInfo);
        return "blogPageList";
    }

2.4 编写分页的页面

为了使用PageContext需要引入一个新的jar

        <!-- https://mvnrepository.com/artifact/javax.servlet.jsp/javax.servlet.jsp-api -->
        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>javax.servlet.jsp-api</artifactId>
            <version>2.3.3</version>
            <scope>provided</scope>
        </dependency>

然后编写JSP页面如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="utf-8" %>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%
    pageContext.setAttribute("APP_PATH", request.getContextPath());
%>
<html>
<head>
    <title>Blog列表页面</title>
    <script type="text/javascript" src="jquery/jquery-2.0.3.min.js"></script>
    <link href="css/bootstrap.min.css" rel="stylesheet">
    <script src="js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-12">
            <h1>This is an BlogList Html</h1>
        </div>
    </div>
    <div class="row">
        <div class="col-md-12">
            <table class="table table-hover">
                <tr>
                    <th>#</th>
                    <th>name</th>
                    <th>authorId</th>
                    <th>操作</th>
                </tr>
                <c:forEach items="${pageInfo.list}" var="blog">
                    <tr>
                        <th>${blog.bid}</th>
                        <th>${blog.name}</th>
                        <th>${blog.authorId}</th>
                        <th>
                            <button type="button" class="btn btn-primary btn-sm"><span
                                    class="glyphicon glyphicon-pencil"
                                    aria-hidden="true"></span>编辑
                            </button>
                            <button type="button" class="btn btn-danger btn-sm"><span class="glyphicon glyphicon-trash"
                                                                                      aria-hidden="true">删除</span>
                            </button>
                        </th>
                    </tr>
                </c:forEach>
            </table>
        </div>

        <div class="row">
            <%-- 分页信息--%>
            <div class="col-md-6">
                当前${pageInfo.pageNum}页,每页${pageInfo.pageSize}条记录,总共有${pageInfo.pages}页,总共有${pageInfo.total}条记录
            </div>
            <div class="col-md-6">
                <nav aria-label="Page navigation">
                    <ul class="pagination">
                        <li>
                            <a href="${APP_PATH}/blogPageList?pageNum=1&pageSize=${pageInfo.pageSize}">
                                首页
                            </a>
                        </li>
                        <c:if test="${pageInfo.hasPreviousPage}">
                            <li>
                                <a href="${APP_PATH}/blogPageList?pageNum=${pageInfo.pageNum-1}&pageSize=${pageInfo.pageSize}"
                                   aria-label="Previous">
                                    <span aria-hidden="true">&laquo;</span>
                                </a>
                            </li>
                        </c:if>
                        <c:forEach items="${pageInfo.navigatepageNums}" var="page_Number">
                            <c:if test="${page_Number==pageInfo.pageNum}">
                                <li class="active"><a href="#">${page_Number}</a></li>
                            </c:if>
                            <c:if test="${page_Number!=pageInfo.pageNum}">
                                <li>
                                    <a href="${APP_PATH}/blogPageList?pageNum=${page_Number}&pageSize=${pageInfo.pageSize}">${page_Number}</a>
                                </li>
                            </c:if>
                        </c:forEach>

                        <c:if test="${pageInfo.hasNextPage}">
                            <li>
                                <a href="${APP_PATH}/blogPageList?pageNum=${pageInfo.pageNum+1}&pageSize=${pageInfo.pageSize}"
                                   aria-label="Next">
                                    <span aria-hidden="true">&raquo;</span>
                                </a>
                            </li>
                        </c:if>
                        <li>
                            <a href="${APP_PATH}/blogPageList?pageNum=${pageInfo.pages}&pageSize=${pageInfo.pageSize}">
                                末页
                            </a>
                        </li>
                    </ul>
                </nav>

            </div>

        </div>
    </div>
</div>
</body>
</html>

2.5 服务启动,测试

访问地址: http://localhost:8080/blogPageList?pageNum=6&pageSize=2

《深入学习MyBatis系列之MyBatis应用分析和最佳实践12_SSM框架_集成MyBatis的插件使用和原理分析》

访问地址: http://localhost:8080/blogPageList?pageNum=1&pageSize=5

《深入学习MyBatis系列之MyBatis应用分析和最佳实践12_SSM框架_集成MyBatis的插件使用和原理分析》

2.6 总结

  • 1、引入插件的依赖
  • 2、mybatis-config.xml中加入插件配置
  • 3、在业务层代码中使用插件PageHelper.start(pageNum,pageSize)进行分页,然后PageInfo对查询的列表进行分页处理
  • 4、运行测试

三、MyBatis的插件配置源码解析

问:在插件的使用中,我们都会在mybatis-config.xml中配置一个插件,那么这个插件配置是如何被MyBatis解析的呢?

答:其实我们的MyBatis的全局配置文件在MyBatis启动的时候会解析<plugins></plugins>标签,然后注册到Configuration对象中的InterceptorChain中,而Properties中的参数会调用setProperties()进行设置,源码如下:

  private void parseConfiguration(XNode root) {
    try {
      ...
      pluginElement(root.evalNode("plugins"));
      ...
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }
  ||
  ||
  \/
  private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        //对应 <plugin interceptor="com.github.pagehelper.PageInterceptor"> 这个标签 取她的属性中的 插件拦截器
        String interceptor = child.getStringAttribute("interceptor");
        Properties properties = child.getChildrenAsProperties();
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
        interceptorInstance.setProperties(properties);
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }
  ||
  ||
  \/
   public void addInterceptor(Interceptor interceptor) {
    interceptorChain.addInterceptor(interceptor);
  }

最终我们发现,所有的插件拦截器都存放在了Configuration对象中的InterceptorChain中.那我们再看下InterceptorChain是什么呢?

protected final InterceptorChain interceptorChain = new InterceptorChain();

public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<>();

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

  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }

  public List<Interceptor> getInterceptors() {
    return Collections.unmodifiableList(interceptors);
  }

}

它其实就是插件的集合列表List,也就是说在MyBatis的启动阶段,就将N多插件已经存入Configuration的这个List对象中保存好了,后续待用。

四、MyBatis的插件使用猜想

我们都知道MyBatis的分页插件,在不修改原jar包中的代码,就可以改变核心对象的行为,比如: 在前面处理参数,在中间处理SQL,在最后处理结果集。那么它是如何实现的呢?我们可以有以下猜想:

4.1 不修改代码怎么增强功能呢?

这个问题,一读完就很容易让人联想到一个设计模式–代理模式 ,它就是可以在不修改原有代码的基础上,前后增强功能,所以很明显,MyBatis的插件就是用的代理模式

4.2 多个插件怎么拦截呢?

其实这个问题,在插件的配置解析的时候已经明显的给出了答案了,InterceptorChain,这个单词的释义就是拦截器链,所以我们定义了很多插件,那么这些插件就是形成一个插件的链路,执行完一个插件的逻辑之后在继续执行下一个插件的逻辑,以此类推进行。

所以这个猜想就是: 多个插件是层层拦截的。这里用到了设计模式—责任链模式

4.3 在MyBatis中,什么对象是可以被拦截的呢?

其实这个问题,在以往的MyBatis的配置讲解中也说过了,可以看下官网的说法 如下:

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

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

所以,总结如上的描述为下面的表格:

对象 描述 可拦截的方法 作用
Executor 上层的对象,SQL执行全过程,包括组装SQL参数、执行SQL、组装结果集返回
update() 执行insert、update、delete操作
query() 执行query操作
flushStatements() 在commit的时候自动调用,SimpleExecutor,ReuseExecutor,BatchExecutor处理不同
commit() 提交事务
rollBack() 事务回滚
getTransaction() 获取事务
close() 结束关闭事务
isClosed() 判断事务是否关闭
StatementHandler 执行SQL的过程,最常用的拦截对象
prepare() SQL预编译阶段
parametrize() 设置参数
batch() 批处理操作
update() 增删改操作
query() 查询操作
ParameterHandler SQL参数组装的过程
getParameterObject() 获取参数
setParameters() 设置参数
ResultSetHandler 结果集的组装
handleResultSets() 处理结果集
handleOutputParameters() 处理存储过程出参

4.4 当插件的代理对象存在被缓存装饰的时候,是先装饰再代理还是先代理再装饰呢?

这个问题主要涉及的代理对象是Executor,因为它可能会被二级缓存装饰,那我们可以去找到SQL执行的过程中找到DefaultSqlSession.openSessionFromDataSource()看下源码,如下:

//这一步开始进行创建Executor,那么我们进入这个方法
final Executor executor = configuration.newExecutor(tx, execType);
||
||
\/
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 {
      executor = new SimpleExecutor(this, transaction);
    }
    //从这里可以看到,它是先装饰了简单的基本执行器 然后在判断是否存在缓存装饰器 ,若存在 则先装饰缓存装饰执行器
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    //二级缓存装饰完毕之后,再进入插件的处理进行代理的。
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

看了源码和我本人在上面的注释之后,相信同学们可以看到:它是先创建基本执行器,然后二级缓存执行器,最后插件的代理,所以我们拦截的是CachingExecutor.所以是:先装饰再代理

五、MyBatis的插件原理分析

5.1 引言

看了上述的各种对MyBatis的插件的分析、猜想和使用,我们已经知道了一件事情,那就是:MyBatis的插件核心思想是:代理模式和责任链模式

那么既然是用到了代理模式 ,就有如下问题等待我们去解决:

  • 1.代理类怎么创建的?怎么样创建代理的?使用的是JDK动态代理还是CGLIB动态代理呢?
  • 2.代理类是在什么时候创建的?是在解析配置的时候呢还是获取会话的时候呢还是在调用的时候呢?
  • 3.核心对象被代理之后是怎么调用呢?执行流程如何呢?怎么一次执行多个插件的逻辑呢?在执行完插件的逻辑之后又是如何执行原来的逻辑的呢?

当我们搞清楚了上述的3个大问题和每个大问题中的小问题之后,我们就对插件的原理有个非常清晰的了解了。下面我们就一一的去剖析它的原理如下:

5.2 代理类什么时候创建?

这个问题其实在猜想的时候已经被找到了,在我们每次请求的openSqlSession()方法中的源码DefaultSqlSession.openSessionFromDataSource()中会对配置进行一个处理如下:·

final Executor executor = configuration.newExecutor(tx, execType);
==>
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 {
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    //这一段就是我们插件的处理
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

可以发现executor = (Executor) interceptorChain.pluginAll(executor);这一段源码就是我们所有的插件的代理处理的源码地址了

所以,我们得出这个问题的结论:Executor的代理类是在我们的SqlSessionFactory创建会话的时候创建代理的。

5.3 代理类是怎么创建的?

我们继续看下源码:

  public Object pluginAll(Object target) {
  //批量生成插件的代理类
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }
  
  ==> interceptor.plugin(target)的源码如下:
    default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }
  ==> 
  //这里就是代理的过程了  看源码是使用的JDK动态代理
    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;
  }

这里可以看到,先调用了pluginAll(Object target)方法,然后调用Plugin.wrap(target, this);,这个方法就是代理的具体过程方法,我们也看到源码了,它其实就是
使用了触发管理插件类Plugin,然后实现了InvocationHandler接口,然后进行了包装处理,最后生成代理类:Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap))

总结: 使用JDK动态代理来生成的代理类的,通过Plugin类实现了InvocationHandler接口来创建代理类的。

5.4 生成代理类之后,是如何执行调用的呢?

从上述的分析过程,我们知道了Plugin类实现了InvocationHandler,所以在调用的时候一定会进入这个类的invoke(),所以我们看下这个源码如下:

  @Override
  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);
    }
  }

可以看到这里做了一个判断:if (methods != null && methods.contains(method)) 只有当被拦截的方法不为空的时候执行return interceptor.intercept(new Invocation(target, method, args));,这个就是插件的逻辑执行了。

这里我们注意到了,插件逻辑执行的是依旧传入了被拦截方法、被拦截方法的参数等等被拦截的信息,其实这是为了在多个插件执行完毕之后。再执行method.invoke(target, args);这就是愿逻辑的执行代码了。

总结: 生成代理类之后,是通过Plugininvoke()不断被调用来实现多个插件的拦截执行的,当插件执行完毕之后,执行原有逻辑的方法。

5.5 插件原理总结

插件的执行流程图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xcNEK1L2-1628576720489)(image/plugin-executor.png)]

5.6 插件的执行顺序

首先我们配置插件有一定的顺序,是按照标签来排的,但是插件的执行是按照你配置的顺序的反顺序来执行的。这点需要注意。

因为配置插件,它在解析插件的时候,interceptorChain是添加是按照插件从上往下的顺序进行添加进去的,而且创建生成代理类就是按照这个list的顺序来创建的,所以这个代理类的调用的时候肯定是最后进入的代理类先开始的。

5.7 插件用到的核心对象总结

核心对象 作用
Interceptor 自定义插件需要实现的插件接库,实现4个方法
InterceptorChain 配置的插件配置解析之后会保存在Configuration的interceptorChain中
Plugin 触发管理类,还可以用来创建代理类
Invocation 对被代理类进行包装,可以调用proceed()方法调用到被拦截的方法

六、分页插件原理分析

6.1 引言

我们已经知道了我们先是配置了PageInterceptor插件,然后在方法中使用了PageHelper.start(pageNum,pageSize)就自动实现了翻页,MyBatis 什么都不需要处理,就实现了分页功能。

6.2 如何拦截实现翻页?

那么PageHelper.start(pageNum,pageSize)是如何实现拦截翻页的呢?

答: 首先我们看下拦截器PageInterceptor的源码如下:

@Override
    public Object intercept(Invocation invocation) throws Throwable {
        try {
            Object[] args = invocation.getArgs();
            MappedStatement ms = (MappedStatement) args[0];
            Object parameter = args[1];
            RowBounds rowBounds = (RowBounds) args[2];
            ResultHandler resultHandler = (ResultHandler) args[3];
            Executor executor = (Executor) invocation.getTarget();
            CacheKey cacheKey;
            BoundSql boundSql;
            //由于逻辑关系,只会进入一次
            if (args.length == 4) {
                //4 个参数时
                boundSql = ms.getBoundSql(parameter);
                cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
            } else {
                //6 个参数时
                cacheKey = (CacheKey) args[4];
                boundSql = (BoundSql) args[5];
            }
            checkDialectExists();
            //对 boundSql 的拦截处理
            if (dialect instanceof BoundSqlInterceptor.Chain) {
                boundSql = ((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.ORIGINAL, boundSql, cacheKey);
            }
            List resultList;
            //调用方法判断是否需要进行分页,如果不需要,直接返回结果
            if (!dialect.skip(ms, parameter, rowBounds)) {
                //判断是否需要进行 count 查询
                if (dialect.beforeCount(ms, parameter, rowBounds)) {
                    //查询总数
                    Long count = count(executor, ms, parameter, rowBounds, null, boundSql);
                    //处理查询总数,返回 true 时继续分页查询,false 时直接返回
                    if (!dialect.afterCount(count, parameter, rowBounds)) {
                        //当查询总数为 0 时,直接返回空的结果
                        return dialect.afterPage(new ArrayList(), parameter, rowBounds);
                    }
                }
                resultList = ExecutorUtil.pageQuery(dialect, executor,
                        ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
            } else {
                //rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
                resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
            }
            return dialect.afterPage(resultList, parameter, rowBounds);
        } finally {
            if(dialect != null){
                dialect.afterAll();
            }
        }
    }

核心看Interceptor()方法,这里就是拦截的插件逻辑了,我们仔细看下逻辑,它是先处理逻辑分页,然后再判断是否需要物理分页,若需要会先去查询数据的数量Long count = count(executor, ms, parameter, rowBounds, null, boundSql);

然后更改查询的语句加上limit语句进行分页处理,方法源码如下:

resultList = ExecutorUtil.pageQuery(dialect, executor,
                        ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
==>
//判断是否需要进行分页查询
        if (dialect.beforePage(ms, parameter, rowBounds)) {
            //生成分页的缓存 key
            CacheKey pageKey = cacheKey;
            //处理参数对象
            parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
            //调用方言获取分页 sql  这里的核心方法 
            String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
            BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);

            Map<String, Object> additionalParameters = getAdditionalParameter(boundSql);
            //设置动态参数
            for (String key : additionalParameters.keySet()) {
                pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
            }
            //对 boundSql 的拦截处理
            if (dialect instanceof BoundSqlInterceptor.Chain) {
                pageBoundSql = ((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.PAGE_SQL, pageBoundSql, pageKey);
            }
            //执行分页查询
            return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
        } else {
            //不执行分页的情况下,也不执行内存分页
            return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
        }
==> 
/**
     * 生成分页查询 sql
     *
     * @param ms              MappedStatement
     * @param boundSql        绑定 SQL 对象
     * @param parameterObject 方法参数
     * @param rowBounds       分页参数
     * @param pageKey         分页缓存 key
     * @return
     */
    String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey);
==> 这里就是会有很多实现 主要是根据不同的插件来的 我们看下PageHelper的
    @Override
    public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
        String sql = boundSql.getSql();
        Page page = getLocalPage();
        //支持 order by
        String orderBy = page.getOrderBy();
        if (StringUtil.isNotEmpty(orderBy)) {
            pageKey.update(orderBy);
            sql = OrderByParser.converToOrderBySql(sql, orderBy);
        }
        if (page.isOrderByOnly()) {
            return sql;
        }
        return getPageSql(sql, page, pageKey);
    }
 ==> 继续进入 getPageSql(sql, page, pageKey) 方法 
     public abstract String getPageSql(String sql, Page page, CacheKey pageKey);
//这是个抽象方法钩子   这里会有很多实现类 我们选择Mysql的  
    @Override
    public String getPageSql(String sql, Page page, CacheKey pageKey) {
        StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
        sqlBuilder.append(sql);
        if (page.getStartRow() == 0) {
            sqlBuilder.append("\n LIMIT ? ");
        } else {
            sqlBuilder.append("\n LIMIT ?, ? ");
        }
        return sqlBuilder.toString();
    }
    //这里就很明显了, 就是在语句最后加上 limit 语句 

所以,经过我们的一层一层的分析,源码追踪,我们知道了物理分页的本质就是: 改变SQL语句加上limit分页

6.3 如何保存分页信息的?

这个问题就要回到PageHelper.start()方法了,我们发现PageHelper中没有该方法,所以找它的父类,发现父类中有这个方法,所以我们跟踪下源码下:

PageMethod.java

    /**
     * 开始分页
     *
     * @param pageNum  页码
     * @param pageSize 每页显示数量
     */
    public static <E> Page<E> startPage(int pageNum, int pageSize) {
        return startPage(pageNum, pageSize, DEFAULT_COUNT);
    }
    ==>
    /**
     * 开始分页
     *
     * @param pageNum  页码
     * @param pageSize 每页显示数量
     * @param count    是否进行count查询
     */
    public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count) {
        return startPage(pageNum, pageSize, count, null, null);
    }
    ==>
    /**
     * 开始分页
     *
     * @param pageNum      页码
     * @param pageSize     每页显示数量
     * @param count        是否进行count查询
     * @param reasonable   分页合理化,null时用默认配置
     * @param pageSizeZero true且pageSize=0时返回全部结果,false时分页,null时用默认配置
     */
    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 = getLocalPage();
        if (oldPage != null && oldPage.isOrderByOnly()) {
            page.setOrderBy(oldPage.getOrderBy());
        }
        setLocalPage(page);
        return page;
    }
    ==>
        protected static void setLocalPage(Page page) {
        LOCAL_PAGE.set(page);
    }
   

最终我们发现,我们是把pageNum和pageSize存入Page对象,然后存在LOCAL_PAGE中,那么LOCAL_PAGE是什么呢?

protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();

看了下源码,发现它就是一个本地线程变量,而在获取的时候直接使用PageHelper.getLocalPage()就获取了,这就是线程变量的存储,所以每次查询都有一个私有的线程变量,它里面存储着分页的信息。

6.4 总结

就这样,我们通过PageInterceptor以及PageHelper工具类实现了一个物理分页的效果。

七、自定义一个MyBatis插件

现在我们为了模拟一个场景,实现打印SQL的需求。

7.1 第一步,新建一个拦截器实现Interceptor接口

package com.gitee.qianpz.interceptor;

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.TypeHandlerRegistry;

import java.text.DateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Properties;


/**
 * 打印SQL的插件
 *
 * @author pengzhan.qian
 * @since 1.0.0
 */
//定义拦截的对象 因为是打印SQL 所以对增删改查操作拦截即可
@Intercepts({
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})}
)
@Slf4j
public class MyBatisPrintSqlInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        Object parameter = null;
        if (invocation.getArgs().length > 1) {
            parameter = invocation.getArgs()[1];
        }
        String sqlId = mappedStatement.getId();
        BoundSql boundSql = mappedStatement.getBoundSql(parameter);
        Configuration configuration = mappedStatement.getConfiguration();

        long startTime = System.currentTimeMillis();
        Object result = null;
        try {
            result = invocation.proceed();
        } finally {
            try {
                long sqlCostTime = System.currentTimeMillis() - startTime;
                String sql = getSql(configuration, boundSql, sqlId, sqlCostTime, result);
                System.out.println("自定义SQL:" + sql);
            } catch (Exception ignored) {

            }
        }
        return result;
    }

    public static String getSql(Configuration configuration, BoundSql boundSql, String sqlId, long time, Object returnValue) {
        String sql = showSql(configuration, boundSql);
        StringBuilder str = new StringBuilder(100);
        str.append(sqlId);
        str.append("-->[");
        str.append(sql);
        str.append("]-->");
        str.append(time);
        str.append("ms-->");
        str.append("returnValue=").append(returnValue);
        return str.toString();
    }

    private static String getParameterValue(Object obj) {
        String value = null;
        if (obj instanceof String) {
            value = "'" + obj.toString() + "'";
        } else if (obj instanceof Date) {
            DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
            value = "'" + formatter.format(new Date()) + "'";
        } else {
            if (obj != null) {
                value = obj.toString();
            } else {
                value = "";
            }

        }
        return value;
    }

    public static String showSql(Configuration configuration, BoundSql boundSql) {
        Object parameterObject = boundSql.getParameterObject();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
        if (parameterMappings.size() > 0 && parameterObject != null) {
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                sql = sql.replaceFirst("\\?", getParameterValue(parameterObject));

            } else {
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                for (ParameterMapping parameterMapping : parameterMappings) {
                    String propertyName = parameterMapping.getProperty();
                    if (metaObject.hasGetter(propertyName)) {
                        Object obj = metaObject.getValue(propertyName);
                        sql = sql.replaceFirst("\\?", getParameterValue(obj));
                    } else if (boundSql.hasAdditionalParameter(propertyName)) {
                        Object obj = boundSql.getAdditionalParameter(propertyName);
                        sql = sql.replaceFirst("\\?", getParameterValue(obj));
                    }
                }
            }
        }
        return sql;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
    }
}

7.2 第二步,配置拦截器

        <!--  自定义SQL打印插件 -->
        <plugin interceptor="com.gitee.qianpz.interceptor.MyBatisPrintSqlInterceptor"/>

因为我这里不需要其他的属性配置,所以不需要配置properties

7.3 测试

http://localhost:8080/blogPageList?pageNum=1&pageSize=5

结果如下:
自定义SQL:com.gitee.qianpz.mapper.BlogMapper.findAll-->[select * from blog]-->1ms-->returnValue=[Blog{bid=1, name='博客01-修改', authorId=1}, Blog{bid=2, name='batch_insert_2', authorId=1}, Blog{bid=3, name='batch_insert_3', authorId=1}, Blog{bid=4, name='batch_insert_4', authorId=1}, Blog{bid=5, name='batch_insert_5', authorId=1}]

八、MyBatis插件的应用场景分析

作用 描述 实现方式
数据脱敏 手机号、身份证号码加密方式存入数据库,但是取出的时候给别人看的时候需要解密并且隐藏手机号码中间四位、身份证号的中间出生日期进行展示,自己看的时候直接解密查看 对Executor的query方法的时候拦截
对ResultHandler进行处理返回。
菜单权限控制 不同的用户登陆的时候查看权限表获得不同的结果,在前端展示不同的菜单 对Executor的query方法的时候拦截
在方法上添加注解,根据权限配置,以及用户登陆信息,在SQL上加上权限过滤条件
水平分表 单表数据过大,对单表进行拆分成N张表,表名字接后缀0~N-1 对query和update进行拦截
修改SQL语句中的表名字即可

九、源码地址

插件使用源码

上一篇:袋鼠云旗下新公司云掣科技启航,深耕云MSP业务助推企业数字化转型


下一篇:psp个人软件过程