背景
关于 Mybatis 插件, 大部分人都知道, 也都使用过, 但很多时候, 我们仅仅是停留在表面上, 知道 Mybatis 插件可以在 DAO 层进行拦截, 如打印执行的 SQL 语句日志, 做一些权限控制, 分页等功能; 但对其内部实现机制, 涉及的软件设计模式, 编程思想往往没有深入的理解.
本篇案例将帮助读者对 Mybatis 插件的使用场景, 实现机制, 以及其中涉及的编程思想进行一个小结, 希望对以后的编程开发工作有所帮助.
注: 本案例以 mybatis 3.4.7-SNAPSHOT 版本为例.
PS: 文章是挺久之前写的, 当时花了一些心思, 存到电脑的 Word 里, 今天正好看到, 就是里面的源码都是图片, 哈哈哈, 凑合着看吧.
Mybatis 插件典型适用场景
分页功能
mybatis 的分页默认是基于内存分页的(查出所有, 再截取), 数据量大的情况下效率较低, 不过使用 mybatis 插件可以改变该行为, 只需要拦截 StatementHandler 类的 prepare 方法, 改变要执行的 SQL 语句为分页语句即可;
公共字段统一赋值
一般业务系统都会有创建者, 创建时间, 修改者, 修改时间四个字段, 对于这四个字段的赋值, 实际上可以在 DAO 层统一拦截处理, 可以用 mybatis 插件拦截 Executor 类的 update 方法, 对相关参数进行统一赋值即可;
性能监控
对于 SQL 语句执行的性能监控, 可以通过拦截 Executor 类的 update, query 等方法, 用日志记录每个方法执行的时间;
其它
其实 mybatis 扩展性还是很强的, 基于插件机制, 基本上可以控制 SQL 执行的各个阶段, 如执行阶段, 参数处理阶段, 语法构建阶段, 结果集处理阶段, 具体可以根据项目业务来实现对应业务逻辑.
Mybatis 插件介绍
什么是 Mybatis 插件
与其称为 Mybatis 插件, 不如叫 Mybatis 拦截器, 更加符合其功能定位, 实际上它就是一个拦截器, 应用代理模式, 在方法级别上进行拦截.
支持拦截的方法
执行器 Executor(update,query,commit,rollback 等方法);
参数处理器 ParameterHandler(getParameterObject,setParameters 方法);
结果集处理器 ResultSetHandler(handleResultSets,handleOutputParameters 等方法);
SQL 语法构建器 StatementHandler(prepare,parameterize,batch,update,query 等方法);
拦截阶段
那么这些类上的方法都是在什么阶段被拦截的呢? 为理解这个问题, 我们先看段简单的代码(摘自 mybatis 源码中的单元测试 SqlSessionTest 类), 来了解下典型的 mybatis 执行流程, 如下代码所示:
以上代码主要完成以下功能:
读取 mybatis 的 xml 配置文件信息
通过 SqlSessionFactoryBuilder 创建 SqlSessionFactory 对象
通过 SqlSessionFactory 获取 SqlSession 对象
执行 SqlSession 对象的 selectList 方法, 查询结果
关闭 SqlSession
如下是时序图, 在整个时序图中, 涉及到 mybatis 插件部分已标红, 基本上就是体现在上文中提到的四个类上, 对这些类上的方法进行拦截.
Mybatis 插件实现机制
插件配置信息的加载
先来看下 mybatis 是如何加载插件配置的, 对应的 xml 配置信息如下:
对应的解析代码如下, 主要做以下工作:
根据解析到的类信息创建 Interceptor 对象;
调用 setProperties 方法设置属性变量;
添加到 Configuration 的 interceptorChain 拦截器链中;
以上逻辑对应的时序图如下:
代理对象的生成
Mybatis 插件的实现机制主要是基于动态代理实现的, 其中最为关键的就是代理对象的生成, 所以有必要来了解下这些代理对象是如何生成的.
Executor 代理对象
ParameterHandler 代理对象
ResultSetHandler 代理对象
StatementHandler 代理对象
观察源码, 发现这些可拦截的类对应的对象生成都是通过 InterceptorChain 的 pluginAll 方法来创建的, 进一步观察 pluginAll 方法, 如下:
遍历所有拦截器, 调用拦截器的 plugin 方法生成代理对象, 注意生成代理对象重新赋值给 target, 所以如果有多个拦截器的话, 生成的代理对象会被另一个代理对象代理, 从而形成一个代理链条, 执行的时候, 依次执行所有拦截器的拦截逻辑代码;
接下来看一下我们在编写拦截器的时候, 一个典型的 plugin 方法实现方式, 如下:
再进一步查看 wrap 方法, 如下:
典型的动态代理实现, 调用的是 Proxy.newProxyInstance 方法来生成代理对象.
以上逻辑对应的时序图如下, 这里我们假设声明了两个拦截器, 那么在创建 target 代理对象的时候, 最终返回的代理对象 proxy2, 实际上代理了 proxy1, 而 proxy1 又代理了 target,:
拦截逻辑的执行
由于真正去执行 Executor,ParameterHandler,ResultSetHandler 和 StatementHandler 类中的方法的对象是代理对象 (建议将代理对象转为 class 文件, 反编译查看其结构, 帮助理解), 所以在执行方法时, 首先调用的是 Plugin 类(实现了 InvocationHandler 接口) 的 invoke 方法, 如下:
首先根据执行方法所属类获取拦截器中声明需要拦截的方法集合;
判断当前方法需不需要执行拦截逻辑, 需要的话, 执行拦截逻辑方法(即 Interceptor 接口的 intercept 方法实现), 不需要则直接执行原方法.
可以关注下 Interceptor 接口的 intercept 方法实现, 一般需要用户自定义实现逻辑, 其中有一个重要参数, 即 Invocation 类, 通过改参数我们可以获取执行对象, 执行方法, 以及执行方法上的参数, 从而进行各种业务逻辑实现, 一般在该方法的最后一句代码都是 invocation.proceed()(内部执行 method.invoke 方法), 否则将无法执行下一个拦截器的 intercept 方法.
以上逻辑对应的时序图如下, 这里我们以执行 executor 对象的 query 方法为例, 且假设有两个拦截器存在:
Mybatis 插件开发例子
这里以分页插件为例, 来了解下一般 mybatis 插件的编写规则, 如下所示:
主要需要实现三个方法
intercept: 在此实现自己的拦截逻辑, 可从 Invocation 参数中拿到执行方法的对象, 方法, 方法参数, 从而实现各种业务逻辑, 如下代码所示, 从 invocation 中获取的 statementHandler 对象即为被代理对象, 基于该对象, 我们获取到了执行的原始 SQL 语句, 以及 prepare 方法上的分页参数, 并更改 SQL 语句为新的分页语句, 最后调用 invocation.proceed()返回结果.
plugin: 生成代理对象;
setProperties: 设置一些属性变量;
小结
简单的说, mybatis 插件就是对 ParameterHandler,ResultSetHandler,StatementHandler,Executor 这四个接口上的方法进行拦截, 利用 JDK 动态代理机制, 为这些接口的实现类创建代理对象, 在执行方法时, 先去执行代理对象的方法, 从而执行自己编写的拦截逻辑, 所以真正要用好 mybatis 插件, 主要还是要熟悉这四个接口的方法以及这些方法上的参数的含义;
另外, 如果配置了多个拦截器的话, 会出现层层代理的情况, 即代理对象代理了另外一个代理对象, 形成一个代理链条, 执行的时候, 也是层层执行;
关于 mybatis 插件涉及到的设计模式和软件思想如下:
设计模式: 代理模式, 责任链模式;
软件思想: AOP 编程思想, 降低模块间的耦合度, 使业务模块更加独立;
一些注意事项:
不要定义过多的插件, 代理嵌套过多, 执行方法的时候, 比较耗性能;
拦截器实现类的 intercept 方法里最后不要忘了执行 invocation.proceed()方法, 否则多个拦截器情况下, 执行链条会断掉;
来源: https://www.cnblogs.com/chenpi/p/10498921.html