本篇博客将主要讲解 mybatis 插件的主要流程,其中主要包括动态代理和责任链的使用;

一、mybatis 拦截器主体结构

在编写 mybatis 插件的时候,首先要实现 Interceptor 接口,然后在 mybatis-conf.xml 中添加插件,

<configuration>
<plugins>
<plugin interceptor="***.interceptor1"/>
<plugin interceptor="***.interceptor2"/>
</plugins>
</configuration>

这里需要注意的是,添加的插件是有顺序的,因为在解析的时候是依次放入 ArrayList 里面,而调用的时候其顺序为:2 > 1 > target > 1 > 2;(插件的顺序可能会影响执行的流程)更加细致的讲解可以参考 QueryInterceptor 规范 ;

然后当插件初始化完成之后,添加插件的流程如下:

首先要注意的是,mybatis 插件的拦截目标有四个,Executor、StatementHandler、ParameterHandler、ResultSetHandler:

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
} 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) interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
} 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) interceptorChain.pluginAll(statementHandler);
return statementHandler;
} 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;
}

这里使用的时候都是用动态代理将多个插件用责任链的方式添加的,最后返回的是一个代理对象; 其责任链的添加过程如下:

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

最终动态代理生成和调用的过程都在 Plugin 类中:

public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); // 获取签名Map
Class<?> type = target.getClass(); // 拦截目标 (ParameterHandler|ResultSetHandler|StatementHandler|Executor)
Class<?>[] interfaces = getAllInterfaces(type, signatureMap); // 获取目标接口
if (interfaces.length > 0) {
return Proxy.newProxyInstance( // 生成代理
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}

这里所说的签名是指在编写插件的时候,指定的目标接口和方法,例如:

@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class ExamplePlugin implements Interceptor {
public Object intercept(Invocation invocation) throws Throwable {
...
}
}

这里就指定了拦截 Executor 的具有相应方法的 update、query 方法;注解的代码很简单,大家可以自行查看;然后通过 getSignatureMap 方法反射取出对应的 Method 对象,在通过 getAllInterfaces 方法判断,目标对象是否有对应的方法,有就生成代理对象,没有就直接反对目标对象;

在调用的时候:

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

二、PageHelper 拦截器分析

mybatis 插件我们平时使用最多的就是分页插件了,这里以 PageHelper 为例,其使用方法可以查看相应的文档 如何使用分页插件,因为官方文档讲解的很详细了,我这里就简单补充分页插件需要做哪几件事情;

使用:

PageHelper.startPage(1, 2);
List<User> list = userMapper1.getAll();

PageHelper 还有很多中使用方式,这是最常用的一种,他其实就是在 ThreadLocal 中设置了 Page 对象,能取到就代表需要分页,在分页完成后在移除,这样就不会导致其他方法分页;(PageHelper 使用的其他方法,也是围绕 Page 对象的设置进行的)

protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
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;
}

主要实现:

@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
})
public class PageInterceptor implements Interceptor { @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(); List resultList;
//调用方法判断是否需要进行分页,如果不需要,直接返回结果
if (!dialect.skip(ms, parameter, rowBounds)) {
//判断是否需要进行 count 查询
if (dialect.beforeCount(ms, parameter, rowBounds)) {
//查询总数
Long count = count(executor, ms, parameter, rowBounds, resultHandler, 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();
}
}
}
}
  • 首先可以看到拦截的是 Executor 的两个 query 方法(这里的两个方法具体拦截到哪一个受插件顺序影响,最终影响到 cacheKey 和 boundSql 的初始化);
  • 然后使用 checkDialectExists 判断是否支持对应的数据库;
  • 在分页之前需要查询总数,这里会生成相应的 sql 语句以及对应的 MappedStatement 对象,并缓存;
  • 然后拼接分页查询语句,并生成相应的 MappedStatement 对象,同时缓存;
  • 最后查询,查询完成后使用 dialect.afterPage 移除 Page对象

mybatis 源码分析(五)Interceptor 详解的更多相关文章

  1. vuex 源码分析(五) action 详解

    action类似于mutation,不同的是Action提交的是mutation,而不是直接变更状态,而且action里可以包含任意异步操作,每个mutation的参数1是一个对象,可以包含如下六个属 ...

  2. 【集合框架】JDK1.8源码分析之ArrayList详解(一)

    [集合框架]JDK1.8源码分析之ArrayList详解(一) 一. 从ArrayList字表面推测 ArrayList类的命名是由Array和List单词组合而成,Array的中文意思是数组,Lis ...

  3. nginx源码分析线程池详解

    nginx源码分析线程池详解 一.前言     nginx是采用多进程模型,master和worker之间主要通过pipe管道的方式进行通信,多进程的优势就在于各个进程互不影响.但是经常会有人问道,n ...

  4. vuex 源码分析(六) 辅助函数 详解

    对于state.getter.mutation.action来说,如果每次使用的时候都用this.$store.state.this.$store.getter等引用,会比较麻烦,代码也重复和冗余,我 ...

  5. Golang源码分析之目录详解

    开源项目「go home」聚焦Go语言技术栈与面试题,以协助Gopher登上更大的舞台,欢迎go home~ 导读 学习Go语言源码的第一步就是了解先了解它的目录结构,你对它的源码目录了解多少呢? 目 ...

  6. Tomcat源码分析 | 一文详解生命周期机制Lifecycle

    目录 什么是Lifecycle? Lifecycle方法 LifecycleBase 增加.删除和获取监听器 init() start() stop() destroy() 模板方法 总结 前言 To ...

  7. Java 容器源码分析之集合类详解

    集合类说明及区别 Collection ├List │├LinkedList │├ArrayList │└Vector │ └Stack └Set Map ├Hashtable ├HashMap └W ...

  8. Cloudera Impala源码分析: SimpleScheduler调度策略详解包括作用、接口及实现等

    问题导读:1.Scheduler任务中Distributed Plan.Scan Range是什么?2.Scheduler基本接口有哪些?3.QuerySchedule这个类如何理解?4.Simple ...

  9. Mybatis源码分析之Cache二级缓存原理 (五)

    一:Cache类的介绍 讲解缓存之前我们需要先了解一下Cache接口以及实现MyBatis定义了一个org.apache.ibatis.cache.Cache接口作为其Cache提供者的SPI(Ser ...

  10. MyBatis源码分析-MyBatis初始化流程

    MyBatis 是支持定制化 SQL.存储过程以及高级映射的优秀的持久层框架.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.MyBatis 可以对配置和原生Map使用简 ...

随机推荐

  1. sql注入------基于时间延迟benchmark函数注入脚本

    #author:windy_2import requests urlx = 'http://127.0.0.1/?id= 1 and if((substr((select database()),' ...

  2. 【SVN】SVN使用教程总结

    SVN使用教程总结 SVN简介: 为什么要使用SVN? 程序员在编写程序的过程中,每个程序员都会生成很多不同的版本,这就需要程序员有效的管理代码,在需要的时候可以迅速,准确取出相应的版本. Subve ...

  3. C/C++指针函数和函数指针

    一.指针函数 当一个函数声明其返回值为一个指针时,实际上就是返回一个地址给调用函数,以用于需要指针或地址的表达式中. 格式: 类型说明符 * 函数名(参数) 当然了,由于返回的是一个地址,所以类型说明 ...

  4. 微信小程序踩坑日记2——实时访问数据库并渲染UI

    0. 引言 主要讲讲对于实时访问数据库并渲染UI我的解决方法. 一开始查到了随让小程序是单线程的,但是有一个基本上是封装的worker线程,相当于可以自己自定义(类似于Android开发里的handl ...

  5. 初学者的linux - 基本知识篇

    1.Linux系统结构 Linux是一套免费使用和自由传播的类Unix操作系统,它是一种倒树结构. “/”就是系统的顶级目录,称作根目录,“/bin,/root,/home,/etc.."这 ...

  6. Windows+Apache+Python+Django 踩坑记录

    摘要 使用Python进行Web项目开发:相对于主流三大Web端解决方案(Java/.NET/PHP) Python在某些方面具有一定的优势,相对 Java/.NET 有更轻量级的部署方案,相对PHP ...

  7. 记一次远程CMD开发过程

    开发初衷: 有些同学电脑老是要出问题,但又不是什么大问题,通常几句cmd就能搞定.之前解决方案有2:一是远程演示,我口述别人操作:一是我写个cmd脚本,但毕竟不在本机不好调试.(吐槽一下常用的远程控制 ...

  8. 前后端分离 之vue-cli 搭建项目mac 系统讲解

    前端项目搭建必备技术 webpack nodejs 搭建 vue-cli 的安装 以上技术自行了解安装 一:创建前端项目 采用vue-cli 脚手架 1:终端执行如下命令 vue init webpa ...

  9. java中map,set的简单使用

    package test2; import java.util.*; import static java.lang.System.out; public class test2 extends St ...

  10. Android开发进阶——自定义View的使用及其原理探索

    在Android开发中,系统提供给我们的UI控件是有限的,当我们需要使用一些特殊的控件的时候,只靠系统提供的控件,可能无法达到我们想要的效果,这时,就需要我们自定义一些控件,来完成我们想要的效果了.下 ...