1.引言
说起mybatis框架的插件,大家都非常熟悉分页插件PageHelper,当我们持久层框架选型mybatis后,实现分页是非常容易的事情,只需要3个小步骤
- 导入插件依赖
- 增加插件配置
- 代码使用(只需要一行代码)
比如说,我在springboot项目中,需要PageHelper插件,这么实现一下就可以了。
导入依赖:
xml
<!--pagehelper-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.3</version>
</dependency>
增加配置:
yaml
pagehelper:
helper-dialect: mysql
reasonable: true
support-methods-arguments: true
params: count=countSql
示例代码:
java
/**
* 分页查询账户列表
* @param pageNum 页码
* @param pageSize 页大小
* @return
*/
@Override
public List<Account> findAll(Integer pageNum, Integer pageSize) {
// 设置分页
PageHelper.startPage(pageNum,pageSize);
// 查询数据
return accountMapper.findAll();
}
查询第一页,页面大小2:http://127.0.0.1:8080/account/list?pageNum=1&pageSize=2

通过案例,看到PageHelper分页插件,真是太方便!
那么,今天我们就来探讨一下,mybatis框架的插件机制原理,以及自定义一个统计sql语句执行耗时的插件。继续一文搞懂系列,来吧。
2.案例
2.1.需求分析
假设在项目中,框架选型springboot + mybatis,我们现在有一个需求,要统计sql语句的执行耗时,这个需求到了你的手里,你该如何实现?
你可能想到的方案
- 通过过滤器Servlet Filter来实现,粒度太粗,Filter拦截的是请求级范围,做不到精准统计sql语句的执行时间,这个方案不合适
- 通过spring拦截器来实现,虽然比起Filter拦截请求,Interceptor可以拦截到controller方法级别,还是不能实现sql语句执行耗时的精准统计,这个方案不合适
- 还有一个实现方案:spring的aop,这个方案可行
但是,我们仔细想想,还有更合适的方案吗?既然是统计sql语句的执行耗时,是数据库层面的事情,放到应用层框架中,那就是持久层框架的事情。
那么mybatis框架有没有提供这样的能力呢?答案是有的,插件就可以解决我们的问题。
于是,你决定编写一个mybatis框架的插件,来实现这个需求,很完美!
2.2.代码实现
下面的案例,方便你理解整个mybatis框架插件的使用方式,我在一个只有mybatis框架的环境中做演示。
当然,最后我会告诉你,springboot整合mybatis框架的环境中,如何使用插件,事实上,整合环境中,比独立使用mybatis框架要更容易!
2.2.1.扩展插件Interceptor
mybatis框架,专门为插件提供了一个接口:Interceptor,我们只要实现该接口即可。
java
/**
* 统计sql执行耗时
*
* @author yanghouhua
* @since 1.0.0
*/
@Slf4j
@Intercepts(
{
@Signature(type=StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
@Signature(type=StatementHandler.class, method = "update", args = {Statement.class}),
@Signature(type=StatementHandler.class, method = "batch", args = {Statement.class})
}
)
public class CountSqlTimeInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 开始时间
long start = System.currentTimeMillis();
// 获取Jdbc封装的StatementHandler
Object target = invocation.getTarget();
StatementHandler statementHandler = (StatementHandler)target;
// 执行
try {
return invocation.proceed();
} finally {
long end = System.currentTimeMillis();
BoundSql boundSql = statementHandler.getBoundSql();
String sql = boundSql.getSql();
log.info("执行SQL:{},共耗时:{}毫秒.", sql, (end - start));
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
log.info("插件配置信息{}", properties);
}
}
2.2.2.配置插件
在mybatis主配置文件中,增加插件配置,我的案例是:sqlMapConfig.xml
xml
<plugins>
<plugin interceptor="com.quanbu.plugin.CountSqlTimeInterceptor">
<property name="xxxProperty" value="xxxValue"/>
</plugin>
</plugins>
2.2.3.执行效果
下面是一段查询账户列表数据代码,非常简单!
java
@Test
public void findAllTest() throws Exception{
// 初始化环境,拿到sqlSession
InputStream inputStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
// 拿到接口代理对象
AccountMapper mapper = sqlSession.getMapper(AccountMapper.class);
List<Account> list = mapper.findAll();
list.forEach(o ->{
System.out.println(o);
});
// 释放资源
sqlSession.close();
}
执行结果:

从执行结果,我们看到通过自定义插件CountSqlTimeInterceptor,实现sql语句执行耗时的统计。有没有一点小激动?
代码实现很简单,关键是要搞清楚原理机制,下一小节再做原理分析,在这之前,我先告诉你,如果是springboot整合mybatis环境中,如何使用插件,两个小步骤即可
- 编写自定义插件,就是上面的CountSqlTimeInterceptor
- 将CountSqlTimeInterceptor交给spring容器管理,即在类上面加上注解:@Component,像这样
java
@Component
@Slf4j
@Intercepts(
{
@Signature(type=StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
@Signature(type=StatementHandler.class, method = "update", args = {Statement.class}),
@Signature(type=StatementHandler.class, method = "batch", args = {Statement.class})
}
)
public class CountSqlTimeInterceptor implements Interceptor {
....省略代码.....
}
是不是更简单、方便、直接!
2.3.原理分析
2.3.1.分析思路
插件的使用,相信你都知道了,毕竟是很容易的一个事情。那么我们来做原理分析吧。就从插件代码CountSqlTimeInterceptor开始。
在CountSqlTimeInterceptor代码上,需要重点关注的地方
- 类上面的注解:@Intercepts
- 方法:Intercept
- 方法:plugin
mybatis框架的插件机制,应用了责任链设计模式,这也是为什么在文章开篇,我会引出过滤器Filter,拦截器Interceptor的原因。它们都是责任链模式的应用,且它们都需要在应用中告诉拦截什么,在什么地方拦截
- 过滤器Filter,拦截请求
- 拦截器Interceptor,拦截controller方法
那么mybatis框架的插件,通过@Interceptor注解,告诉插件需要在哪里拦截,比如案例代码
java
@Intercepts(
{
@Signature(type=StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
@Signature(type=StatementHandler.class, method = "update", args = {Statement.class}),
@Signature(type=StatementHandler.class, method = "batch", args = {Statement.class})
}
)
通过@Intercepts注解,@Signature注解,指明该插件需要拦截StatementHandler对象中的query、update、batch方法。
那么StatementHandler是什么呢?这就要回到mybatis框架的使用上来,在mybatis框架中,提供应用层编程接口SqlSession,通过SqlSession在应用层完成数据库的操作。
但是它的底层,是要委托给Executor组件来执行,Executor组件具体再委托三个组件
- StatementHandler,负责执行sql语句,它就是Jdbc中Statement的封装
- ParameterHandler,负责参数处理
- ResultSetHandler,负责结果集处理,它就是Jdbc中ResultSet的封装
看一个图,你就明白了

到这里,你应该能明白插件类上注解声明的作用了。剩下
- 方法plugin,用于创建目标对象,即StatementHandler的代理对象,mybatis框架插件还应用了动态代理
- 方法Intercept,用于实现插件业务逻辑处理
2.3.2.关键源码
mybatis框架插件相关的关键源码,我们需要关注的有
下面我只贴与插件相关的关键源码,如果你感兴趣,可以自行翻阅完整源码,当然,我建议你这么去做,这是技术人应该要有的最纯粹的追求!
2.3.2.1.Interceptor
java
public interface Interceptor {
Object intercept(Invocation var1) throws Throwable;
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
default void setProperties(Properties properties) {
}
}
2.3.2.2.InterceptorChain
java
// 插件集合列表
private final List<Interceptor> interceptors = new ArrayList();
// 责任链模式应用,依次应用每个插件
public Object pluginAll(Object target) {
Interceptor interceptor;
for(Iterator var2 = this.interceptors.iterator(); var2.hasNext(); target = interceptor.plugin(target)) {
interceptor = (Interceptor)var2.next();
}
return target;
}
2.3.2.3.Plugin
java
// jdk动态代理InvocationHandler的实现
public class Plugin implements InvocationHandler {
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
// jdk动态代理,创建目标对象的代理对象
return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)) : target;
}
}
2.3.2.4.XMLConfigBuilder
java
// 解析插件元素
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
Iterator var2 = parent.getChildren().iterator();
while(var2.hasNext()) {
XNode child = (XNode)var2.next();
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
// 创建插件对象
Interceptor interceptorInstance = (Interceptor)this.resolveClass(interceptor).getDeclaredConstructor().newInstance();
interceptorInstance.setProperties(properties);
// 将插件对象注册到Configuration中,底层注册到了InterceptorChain中
this.configuration.addInterceptor(interceptorInstance);
}
}
}
2.3.2.5.Configuration
java
// 创建ParameterHandler
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
parameterHandler = (ParameterHandler)this.interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
// 创建ResultSetHandler
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) {
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
ResultSetHandler resultSetHandler = (ResultSetHandler)this.interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
}
// 创建StatementHandler
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
StatementHandler statementHandler = (StatementHandler)this.interceptorChain.pluginAll(statementHandler);
return statementHandler;
}