从 PageHelper 到 MyBatis Plugin
在很多业务场景下我们需要去拦截
SQL
,达到不入侵原有代码业务处理一些东西,比如:历史记录、分页操作、数据权限过滤操作、SQL
执行时间性能监控等等,这里我们就可以用到 MyBatis
的插件 Plugin
。下面我们来了解一下 Plugin
到底是如何工作的。
一、背景
使用过 MyBatis
框架的朋友们肯定都听说过 PageHelper
这个分页神器吧,其实 PageHelper
的底层实现就是依靠 plugin
。下面我们来看一下 PageHelper
是如何利用 plugin
实现分页的。
二、MyBatis 执行概要图
首先我们先看一下 MyBatis
的执行流程图,对其执行流程有一个大体的认识。
三、MyBatis 核心对象介绍
从 MyBatis
代码实现的角度来看,MyBatis
的主要的核心部件有以下几个:
-
Configuration
:初始化基础配置,比如 MyBatis
的别名等,一些重要的类型对象,如,插件,映射器,ObjectFactory
和 typeHandler
对象,MyBatis
所有的配置信息都维持在 Configuration
对象之中。 -
SqlSessionFactory
:SqlSession
工厂,用于生产 SqlSession
。 -
SqlSession
: 作为 MyBatis
工作的主要顶层 API
,表示和数据库交互的会话,完成必要数据库增删改查功能 -
Executor
:MyBatis
执行器,是 MyBatis
调度的核心,负责 SQL
语句的生成和查询缓存的维护 -
StatementHandler
:封装了 JDBC Statement
操作,负责对 JDBC Statement
的操作,如设置参数、将 Statement
结果集转换成List集合。 -
ParameterHandler
:负责对用户传递的参数转换成 JDBC Statement
所需要的参数, -
ResultSetHandler
:负责将 JDBC
返回的 ResultSet
结果集对象转换成 List
类型的集合; -
TypeHandler
:负责 java
数据类型和 jdbc
数据类型之间的映射和转换 -
MappedStatement
:MappedStatement
维护了一条
节点的封装, -
SqlSource
:负责根据用户传递的 parameterObject
,动态地生成 SQL
语句,将信息封装到 BoundSql
对象中,并返回 -
BoundSql
:表示动态生成的 SQL
语句以及相应的参数信息
说了这么多,怎么还没进入正题啊,别急,下面就开始讲解 Plugin
的实现原理。
四、Plugin 实现原理
MyBatis
支持对 Executor、StatementHandler、PameterHandler和ResultSetHandler
接口进行拦截,也就是说会对这4种对象进行代理。下面我们结合 PageHelper
来讲解 Plugin
是怎样实现的。
1、定义 Plugin
要使用自定义 Plugin
首先要实现 Interceptor
接口。可以通俗的理解为一个 Plugin
就是一个拦截器。
现在我们来看一下 PageHelper
是如何通过 Plugin
实现分页的。
代码太长不看系列: 其实这段代码最主要的逻辑就是在执行 Executor
方法的时候,拦截 query
也就是查询类型的 SQL
, 首先会判断它是否需要分页,如果需要分页就会根据查询参数在 SQL
末尾加上 limit pageNum, pageSize
来实现分页。
2、注册拦截器
- 通过
SqlSessionFactoryBean
去构建 Configuration
添加拦截器并构建获取 SqlSessionFactory
。
- 通过原始的
XMLConfigBuilder
构建 configuration
添加拦截器
上面是两种不同的形式构建 configuration
并添加拦截器 interceptor
,上面第二种一般是以前 XML
配置的情况,这里主要是解析配置文件的 plugin
节点,根据配置的 interceptor
属性实例化 Interceptor
对象,然后添加到 Configuration
对象中的 InterceptorChain
属性中。
如果定义多个拦截器就会它们链起来形成一个拦截器链,初始化配置文件的时候就把所有的拦截器添加到拦截器链中。
3、执行拦截器
从以下代码可以看出 MyBatis
在实例化 Executor、ParameterHandler、ResultSetHandler、StatementHandler
四大接口对象的时候调用 interceptorChain.pluginAll()
方法插入进去的。其实就是循环执行拦截器链所有的拦截器的 plugin()
方法, MyBatis
官方推荐的 plugin
方法是 Plugin.wrap()
方法,这个就会生成代理类。
4、Plugin 的动态代理
我们首先看一下Plugin.wrap()
方法,这个方法的作用是为实现Interceptor注解的接口实现类生成代理对象的。
Plugin
中的 getSignatureMap、 getAllInterfaces
两个辅助方法,来帮助判断是否为是否Interceptor注解的接口实现类。
我们来看一下代理类的 query
方法,其实就是调用了 Plugin.invoke()
方法。代理类屏蔽了 intercept
方法的调用。
最后 Plugin.invoke()
就是判断当前方法是否拦截,如果需要拦截则会调用 Interceptor.intercept()
对当前方法执行拦截逻辑。
总结
我们以 PageHelper
为切入点讲解了 MyBatis Plugin
的实现原理,其中 MyBatis 拦截器用到责任链模式+动态代理+反射机制。 通过上面的分析可以知道,所有可能被拦截的处理类都会生成一个代理类,如果有 N 个拦截器,就会有 N 个代理,层层生成动态代理是比较耗性能的。而且虽然能指定插件拦截的位置,但这个是在执行方法时利用反射动态判断的,初始化的时候就是简单的把拦截器插入到了所有可以拦截的地方。所以尽量不要编写不必要的拦截器,并且拦截器尽量不要写复杂的逻辑。