Save Load
GitHub 切换暗/亮/自动模式 切换暗/亮/自动模式 切换暗/亮/自动模式 返回首页

Mybatis 插件

Mybatis 插件

Mybatis 插件可以在 DAO 层进行拦截,如打印执行的 SQL 语句日志,做一些权限控制,分页等功能;

Mybatis 插件典型适用场景

  • 分页功能

mybatis 的分页默认是基于内存分页的(查出所有,再截取),数据量大的情况下效率较低,不过使用mybatis插件可以改变该行为,只需要拦截 StatementHandler 类的 prepare方法,改变要执行的SQL语句为分页语句即可;

  • 公共字段统一赋值

一般业务系统都会有创建者,创建时间,修改者,修改时间四个字段,对于这四个字段的赋值,实际上可以在DAO层统一拦截处理,可以用mybatis插件拦截Executor类的update方法,对相关参数进行统一赋值即可;

  • 性能监控

对于SQL语句执行的性能监控,可以通过拦截 Executor 类的 update, query 等方法,用日志记录每个方法执行的时间;

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 执行流程,如下代码所示:

image

以上代码主要完成以下功能:

  • 读取mybatis的xml配置文件信息
  • 通过SqlSessionFactoryBuilder创建SqlSessionFactory对象
  • 通过SqlSessionFactory获取SqlSession对象
  • 执行SqlSession对象的selectList方法,查询结果
  • 关闭SqlSession

如下是时序图,在整个时序图中,涉及到mybatis插件部分已标红,基本上就是体现在上文中提到的四个类上,对这些类上的方法进行拦截。

image

Mybatis 插件实现机制

先来看下mybatis是如何加载插件配置的,对应的xml配置信息如下:

image

对应的解析代码如下,主要做以下工作:

  1. 根据解析到的类信息创建 Interceptor 对象;
  2. 调用 setProperties 方法设置属性变量;
  3. 添加到 Configuration 的 interceptorChain 拦截器链中;

image

以上逻辑对应的时序图如下:

image

代理对象的生成

Mybatis 插件的实现机制主要是基于动态代理实现的,其中最为关键的就是代理对象的生成,所以有必要来了解下这些代理对象是如何生成的。

Executor 代理对象

image

ParameterHandler代理对象

image

ResultSetHandler代理对象

image

StatementHandler代理对象

image

观察源码,发现这些可拦截的类对应的对象生成都是通过InterceptorChain的pluginAll方法来创建的,进一步观察pluginAll方法,如下:

image

遍历所有拦截器,调用拦截器的plugin方法生成代理对象,注意生成代理对象重新赋值给target,所以如果有多个拦截器的话,生成的代理对象会被另一个代理对象代理,从而形成一个代理链条,执行的时候,依次执行所有拦截器的拦截逻辑代码;

接下来看一下我们在编写拦截器的时候,一个典型的plugin方法实现方式,如下:

image

再进一步查看wrap方法,如下:

典型的动态代理实现,调用的是Proxy.newProxyInstance方法来生成代理对象。

image

以上逻辑对应的时序图如下,这里我们假设声明了两个拦截器,那么在创建target代理对象的时候,最终返回的代理对象proxy2,实际上代理了proxy1,而proxy1又代理了target,:

image

拦截逻辑的执行

由于真正去执行 Executor、ParameterHandler、ResultSetHandler和StatementHandler 类中的方法的对象是代理对象(建议将代理对象转为 class 文件,反编译查看其结构,帮助理解),所以在执行方法时,首先调用的是 Plugin 类(实现了InvocationHandler 接口)的 invoke 方法,如下: 首先根据执行方法所属类获取拦截器中声明需要拦截的方法集合; 判断当前方法需不需要执行拦截逻辑,需要的话,执行拦截逻辑方法(即 Interceptor 接口的 intercept 方法实现),不需要则直接执行原方法。

image

可以关注下Interceptor接口的intercept方法实现,一般需要用户自定义实现逻辑,其中有一个重要参数,即Invocation类,通过改参数我们可以获取执行对象,执行方法,以及执行方法上的参数,从而进行各种业务逻辑实现,一般在该方法的最后一句代码都是invocation.proceed()(内部执行method.invoke方法),否则将无法执行下一个拦截器的intercept方法。

以上逻辑对应的时序图如下,这里我们以执行executor对象的query方法为例,且假设有两个拦截器存在:

image

Mybatis 插件开发例子

这里以分页插件为例,来了解下一般mybatis插件的编写规则,如下所示:

主要需要实现三个方法

  1. intercept:在此实现自己的拦截逻辑,可从 Invocation 参数中拿到执行方法的对象,方法,方法参数,从而实现各种业务逻辑, 如下代码所示,从 invocation 中获取的 statementHandler 对象即为被代理对象,基于该对象,我们获取到了执行的原始SQL语句,以及 prepare 方法上的分页参数,并更改SQL语句为新的分页语句,最后调用 invocation.proceed()返回结果。
  2. plugin:生成代理对象;
  3. setProperties:设置一些属性变量;

image

小结

简单的说,mybatis 插件就是对 ParameterHandler、ResultSetHandler、StatementHandler、Executor 这四个接口上的方法进行拦截,利用JDK动态代理机制,为这些接口的实现类创建代理对象,在执行方法时,先去执行代理对象的方法,从而执行自己编写的拦截逻辑,所以真正要用好 mybatis 插件,主要还是要熟悉这四个接口的方法以及这些方法上的参数的含义;

另外,如果配置了多个拦截器的话,会出现层层代理的情况,即代理对象代理了另外一个代理对象,形成一个代理链条,执行的时候,也是层层执行;

关于mybatis插件涉及到的设计模式和软件思想如下:

  1. 设计模式:代理模式、责任链模式;
  2. 软件思想:AOP编程思想,降低模块间的耦合度,使业务模块更加独立;

一些注意事项:

  1. 不要定义过多的插件,代理嵌套过多,执行方法的时候,比较耗性能;
  2. 拦截器实现类的 intercept 方法里最后不要忘了执行 invocation.proceed() 方法,否则多个拦截器情况下,执行链条会断掉;

番外:本地实践

import com.yestae.framework.search.aop.core.CoreMapperAnnotationProcessor;
import org.aopalliance.intercept.Interceptor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
 * 拦截@Mapper注解的接口的所有方法
 *
 * @author: xiexindong
 * @date: 2020-08-13 20:41
 */
@Aspect
@Component
@Order(CoreMapperMethodInterceptor.ORDER)
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class CoreMapperMethodInterceptor implements Interceptor, Ordered {
    public static final int ORDER = 0;
    @Autowired
    private CoreMapperAnnotationProcessor coreMapperAnnotationProcessor;

    @Override
    public int getOrder() {
        return CoreMapperMethodInterceptor.ORDER;
    }

    /**
     * 拦截处理所有mapper方法
     *
     * @param proceedingJoinPoint
     * @return
     * @throws Throwable
     */
    @Around("@within(org.apache.ibatis.annotations.Mapper)")
    public Object invokeMapperAllMethod(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        return coreMapperAnnotationProcessor.process(proceedingJoinPoint);
    }
}