转载:https://www.cnblogs.com/wuzhenzhao/p/11120848.html

  MyBatis 通过提供插件机制,让我们可以根据自己的需要去增强MyBatis 的功能。需要注意的是,如果没有完全理解MyBatis 的运行原理和插件的工作方式,最好不要使用插件,因为它会改变系底层的工作逻辑,给系统带来很大的影响。

  MyBatis 的插件可以在不修改原来的代码的情况下,通过拦截的方式,改变四大核心对象的行为,比如处理参数,处理SQL,处理结果。

第一个问题:

  不修改对象的代码,怎么对对象的行为进行修改,比如说在原来的方法前面做一点事情,在原来的方法后面做一点事情?

  答案:大家很容易能想到用代理模式,这个也确实是MyBatis 插件的原理。

第二个问题:

  我们可以定义很多的插件,那么这种所有的插件会形成一个链路,比如我们提交一个休假申请,先是项目经理审批,然后是部门经理审批,再是HR 审批,再到总经理审批,怎么实现层层的拦截?

  答案:插件是层层拦截的,我们又需要用到另一种设计模式——责任链模式。

  在之前的源码中我们也发现了,mybatis内部对于插件的处理确实使用的代理模式,既然是代理模式,我们应该了解MyBatis 允许哪些对象的哪些方法允许被拦截,并不是每一个运行的节点都是可以被修改的。只有清楚了这些对象的方法的作用,当我们自己编写插件的时候才知道从哪里去拦截。在MyBatis 官网有答案,我们来看一下:http://www.mybatis.org/mybatis-3/zh/configuration.html#plugins

  Executor 会拦截到CachingExcecutor 或者BaseExecutor。因为创建Executor 时是先创建CachingExcecutor,再包装拦截。从代码顺序上能看到。我们可以通过mybatis的分页插件来看看整个插件从包装拦截器链到执行拦截器链的过程。

  在查看插件原理的前提上,我们需要来看看官网对于自定义插件是怎么来做的,官网上有介绍:通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。这里本人踩了一个坑,在Springboot中集成,同时引入了pagehelper-spring-boot-starter 导致RowBounds参数的值被刷掉了,也就是走到了我的拦截其中没有被设置值,这里需要注意,拦截器出了问题,可以Debug看一下Configuration配置类中拦截器链的包装情况。

  1. @Intercepts({//需要拦截的方法
  2. @Signature(type = Executor.class,method = "query",
  3. args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
  4. ), @Signature(type = Executor.class,method = "query",
  5. args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}
  6. )})
  7. public class MyPageInterceptor implements Interceptor {
  8.  
  9. // 用于覆盖被拦截对象的原有方法(在调用代理对象Plugin 的invoke()方法时被调用)
  10. @Override
  11. public Object intercept(Invocation invocation) throws Throwable {
  12. System.out.println("将逻辑分页改为物理分页");
  13. Object[] args = invocation.getArgs();
  14. MappedStatement ms = (MappedStatement) args[0]; // MappedStatement
  15. BoundSql boundSql = ms.getBoundSql(args[1]); // Object parameter
  16. RowBounds rb = (RowBounds) args[2]; // RowBounds
  17. // RowBounds为空,无需分页
  18. if (rb == RowBounds.DEFAULT) {
  19. return invocation.proceed();
  20. }// 在SQL后加上limit语句
  21. String sql = boundSql.getSql();
  22. String limit = String.format("LIMIT %d,%d", rb.getOffset(), rb.getLimit());
  23. sql = sql + " " + limit;
  24.  
  25. // 自定义sqlSource
  26. SqlSource sqlSource = new StaticSqlSource(ms.getConfiguration(), sql, boundSql.getParameterMappings());
  27.  
  28. // 修改原来的sqlSource
  29. Field field = MappedStatement.class.getDeclaredField("sqlSource");
  30. field.setAccessible(true);
  31. field.set(ms, sqlSource);
  32.  
  33. // 执行被拦截方法
  34. return invocation.proceed();
  35. }
  36.  
  37. // target 是被拦截对象,这个方法的作用是给被拦截对象生成一个代理对象,并返回它
  38. @Override
  39. public Object plugin(Object target) {
  40. return Plugin.wrap(target, this);
  41. }
  42.  
  43. // 设置参数
  44. @Override
  45. public void setProperties(Properties properties) {
  46. }
  47. }

  插件注册,在mybatis-config.xml 中注册插件:

  1. <plugins>
  2.   <plugin interceptor="com.github.pagehelper.PageInterceptor">
  3.     <property name="offsetAsPageNum" value="true"/>
  4.       ……后面全部省略……
  5.   </plugin>
  6. </plugins>

  拦截签名跟参数的顺序有严格要求,如果按照顺序找不到对应方法会抛出异常:

  1. org.apache.ibatis.exceptions.PersistenceException:
  2. ### Error opening session. Cause: org.apache.ibatis.plugin.PluginException: Could not find method on interface org.apache.ibatis.executor.Executor named query

  MyBatis 启动时扫描<plugins> 标签, 注册到Configuration 对象的 InterceptorChain 中。property 里面的参数,会调用setProperties()方法处理。

代理和拦截是怎么实现的?

  上面提到的可以被代理的四大对象都是什么时候被代理的呢?Executor 是openSession() 的时候创建的; StatementHandler 是SimpleExecutor.doQuery()创建的;里面包含了处理参数的ParameterHandler 和处理结果集的ResultSetHandler 的创建,创建之后即调用InterceptorChain.pluginAll(),返回层层代理后的对象。代理是由Plugin 类创建。在我们重写的 plugin() 方法里面可以直接调用returnPlugin.wrap(target, this);返回代理对象。

  当个插件的情况下,代理能不能被代理?代理顺序和调用顺序的关系? 可以被代理。

  因为代理类是Plugin,所以最后调用的是Plugin 的invoke()方法。它先调用了定义的拦截器的intercept()方法。可以通过invocation.proceed()调用到被代理对象被拦截的方法。

  调用流程时序图:

PageHelper 原理:

  先来看一下分页插件的简单用法:

  1. PageHelper.startPage(1, 3);
  2. List<Blog> blogs = blogMapper.selectBlogById2(blog);
  3. PageInfo page = new PageInfo(blogs, 3);

  对于插件机制我们上面已经介绍过了,在这里我们自然的会想到其所涉及的核心类 :PageInterceptor。拦截的是Executor 的两个query()方法,要实现分页插件的功能,肯定是要对我们写的sql进行改写,那么一定是在 intercept 方法中进行操作的,我们会发现这么一行代码:

  1. String pageSql = this.dialect.getPageSql(ms, boundSql, parameter, rowBounds, cacheKey);

  调用到 AbstractHelperDialect 中的  getPageSql 方法:

  1. public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
    // 获取sql
  2. String sql = boundSql.getSql();
    //获取分页参数对象
  3. Page page = this.getLocalPage();
  4. return this.getPageSql(sql, page, pageKey);
  5. }

  这里可以看到会去调用 this.getLocalPage(),我们来看看这个方法:

  1. public <T> Page<T> getLocalPage() {
  2.   return PageHelper.getLocalPage();
  3. }
  4. //线程独享
  5. protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal();
  6. public static <T> Page<T> getLocalPage() {
  7.   return (Page)LOCAL_PAGE.get();
  8. }

  可以发现这里是调用的是PageHelper的一个本地线程变量中的一个 Page对象,从其中获取我们所设置的  PageSize 与 PageNum,那么他是怎么设置值的呢?请看:

  1. PageHelper.startPage(1, 3);
  2.  
  3. public static <E> Page<E> startPage(int pageNum, int pageSize) {
  4. return startPage(pageNum, pageSize, true);
  5. }
  6.  
  7. public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
  8. Page<E> page = new Page(pageNum, pageSize, count);
  9. page.setReasonable(reasonable);
  10. page.setPageSizeZero(pageSizeZero);
  11. Page<E> oldPage = getLocalPage();
  12. if (oldPage != null && oldPage.isOrderByOnly()) {
  13. page.setOrderBy(oldPage.getOrderBy());
  14.      }
  15. //设置页数,行数信息
  16. setLocalPage(page);
  17. return page;
  18. }
  19.  
  20. protected static void setLocalPage(Page page) {
    //设置值
  21. LOCAL_PAGE.set(page);
  22. }

  在我们调用 PageHelper.startPage(1, 3); 的时候,系统会调用 LOCAL_PAGE.set(page) 进行设置,从而在分页插件中可以获取到这个本地变量对象中的参数进行 SQL 的改写,由于改写有很多实现,我们这里用的Mysql的实现:

  在这里我们会发现分页插件改写SQL的核心代码,这个代码就很清晰了,不必过多赘述:

  1. public String getPageSql(String sql, Page page, CacheKey pageKey) {
  2. StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
  3. sqlBuilder.append(sql);
  4. if (page.getStartRow() == 0) {
  5. sqlBuilder.append(" LIMIT ");
  6. sqlBuilder.append(page.getPageSize());
  7. } else {
  8. sqlBuilder.append(" LIMIT ");
  9. sqlBuilder.append(page.getStartRow());
  10. sqlBuilder.append(",");
  11. sqlBuilder.append(page.getPageSize());
  12. pageKey.update(page.getStartRow());
  13. }
  14.  
  15. pageKey.update(page.getPageSize());
  16. return sqlBuilder.toString();
  17. }

  PageHelper 就是这么一步一步的改写了我们的SQL 从而达到一个分页的效果。

  关键类总结:

mybatis(六)插件机制及分页插件原理的更多相关文章

  1. mybatis插件机制及分页插件原理

    MyBatis 插件原理与自定义插件: MyBatis 通过提供插件机制,让我们可以根据自己的需要去增强MyBatis 的功能.需要注意的是,如果没有完全理解MyBatis 的运行原理和插件的工作方式 ...

  2. Java框架之MyBatis 07-动态SQL-缓存机制-逆向工程-分页插件

    MyBatis 今天大年初一,你在学习!不学习做什么,斗地主...人都凑不齐.学习吧,学习使我快乐!除了诗和远方还有责任,我也想担当,我也想负责,可臣妾做不到啊,怎么办?你说怎么办,为啥人家能做到你做 ...

  3. Mybatis插件机制以及PageHelper插件的原理

    首先现在已经有很多Mybatis源码分析的文章,之所以重复造轮子,只是为了督促自己更好的理解源码. 1.先看一段PageHelper拦截器的配置,在mybatis的配置文件<configurat ...

  4. Omi框架学习之旅 - 插件机制之omi-finger 及原理说明

    以前那篇我写的alloyfinger源码解读那篇帖子,就说过这是一个很好用的手势库,hammer能做的,他都能做到, 而且源码只有350来行代码,很容易看懂. 那么怎么把这么好的库作为omi库的一个插 ...

  5. Mybatis拦截器介绍及分页插件

    1.1    目录 1.1 目录 1.2 前言 1.3 Interceptor接口 1.4 注册拦截器 1.5 Mybatis可拦截的方法 1.6 利用拦截器进行分页 1.2     前言 拦截器的一 ...

  6. Omi框架学习之旅 - 插件机制之omi-router及原理说明

    先来看看官网的介绍吧:https://github.com/AlloyTeam/omi/tree/master/plugins/omi-router 其实我推荐直接看官网的介绍.我所写的,主要给个人做 ...

  7. Omi框架学习之旅 - 插件机制之omi-touch 及原理说明

    这个插件也能做好多好多的事,比如上拉下拉加载数据,轮播,等一切和运动有关的特效. 具体看我的allowTouch这篇博客,掌握了其用法,在来看它是怎么和omi结合的.就会很简单. 当然使用起来也比较方 ...

  8. Omi框架学习之旅 - 插件机制之omi-transform及原理说明

    给omi-transform插件做个笔记,使用起来也很爽. transform.js这个库,一直想写一篇帖子的,可是,数学不好,三维矩阵和二位矩阵理解的不好,所以迟迟没写了, 这也是一个神库,反正我很 ...

  9. mybatis插件机制原理

    mybatis插件机制及分页插件原理 参考链接:mybatis插件机制及分页插件原理 如何编写一个自定义mybatis插件 参考链接:mybatis 自定义插件的使用

随机推荐

  1. 解决windows与虚拟机ubuntu互相ping不通的问题

    工作中经常用Ubuntu开发,而Ubuntu是安装在虚拟机中的,在弄网络开发的时候经常会用windows下的网络调试工具与Ubuntu中写好的网络程序进行通信,首先要保证windows与Ubuntu能 ...

  2. 转 Jmeter测试实践:文件上传接口

    Jmeter测试实践:文件上传接口   1.打开jmeter4.0,新建测试计划,添加线程组.根据实际情况配置线程属性. 2.添加HTTP请求. Basic部分修改如下: Advanced部分我做任何 ...

  3. mysql(视图 事务 索引 外键)

    视图   视图本质就是对查询的封装   创建视图(定义视图 起名以v_开头) create view v_students as select classes.name as c_name ,stud ...

  4. JavaScript小案例-阶乘!

    JavaScript小案例-阶乘! 阶乘:就是像台阶一样一阶一阶的,从高阶到低阶,依次乘下来!代码超少!容易理解! // factorial 阶乘 // 如果 function factorial(n ...

  5. ValueError: the environment variable is longer than 32767 characters On Windows, an environment variable string ("name=value" string) is limited to 32,767 characters

    https://github.com/python/cpython/blob/aa1b8a168d8b8dc1dfc426364b7b664501302958/Lib/test/test_os.py ...

  6. 后端API接口的错误信息返回规范

    前言 最近我司要制定开发规范.在讨论接口返回的时候,后端的同事询问我们前端,错误信息的返回,前端有什么意见? 所以做了一些调研给到后端的同事做参考. 错误信息返回 在使用API时无可避免地会因为各种情 ...

  7. CF1209A

    所谓染色,并使同颜色数都能被当前颜色中最小的数整除 也就是说,把能被某个数整除的所有数放在一起为一组,问共有几组 开始我想写个并查集但是很懒,看数据范围小的可怜,那我们写个暴力看看 因为每组的共因数都 ...

  8. 最短路-朴素版Dijkstra算法&堆优化版的Dijkstra

    朴素版Dijkstra 目标 找到从一个点到其他点的最短距离 思路 ①初始化距离dist数组,将起点dist距离设为0,其他点的距离设为无穷(就是很大的值) ②for循环遍历n次,每层循环里找出不在S ...

  9. Spark-读写HBase,SparkStreaming操作,Spark的HBase相关操作

    Spark-读写HBase,SparkStreaming操作,Spark的HBase相关操作 1.sparkstreaming实时写入Hbase(saveAsNewAPIHadoopDataset方法 ...

  10. python几个练习(素数、斐波那契数列)

    随机输入求素数: x = int(input("please enter the number:")) if x != 1: for i in range(2, x): if x ...