美文网首页mybatis
MyBatis源码系列--5.MyBatis 插件原理与自定义插

MyBatis源码系列--5.MyBatis 插件原理与自定义插

作者: WEIJAVA | 来源:发表于2019-04-30 15:57 被阅读0次

MyBatis 通过提供插件机制,让我们可以根据自己的需要去增强 MyBatis 的功能

需要注意的是,如果没有完全理解 MyBatis 的运行原理和插件的工作方式,最好不要使用插件,因为它会改变系底层的工作逻辑,给系统带来很大的影响。

MyBatis 的插件可以在不修改原来的代码的情况下,通过拦截的方式,改变四大核心对象的行为(在上一篇已经知晓),比如处理参数,处理 SQL,处理结果

它内部用到两个设计模式

  • 代理模式
    比如它可以在不修改对象的代码的情况下,对对象的行为进行修改,比如说在原来的方法前面做
    一点事情,在原来的方法后面做一点事情
  • 责任链模式
    我们可以定义很多的插件,那么这种所有的插件会形成一个链路,然后层层拦截去处理所有插件

参考官网:http://www.mybatis.org/mybatis-3/zh/configuration.html#plugins

插件编写与注册

(基于 spring-mybatis)运行自定义的插件,需要 3 步,我们以 PageHelper 为例:

  • 第一步,编写自己的插件类
    1.实现 Interceptor 接口,这个是所有的插件必须实现的接口。
    2.添加@Intercepts({@Signature()}),指定拦截的对象和方法、方法参数 方法名称+参数类型,构成了方法的签名,决定了能够拦截到哪个方法。
    3.实现接口的 3 个方法
// 用于覆盖被拦截对象的原有方法(在调用代理对象 Plugin 的 invoke()方法时被调用)
Object intercept(Invocation invocation) throws Throwable;
// target 是被拦截对象,这个方法的作用是给被拦截对象生成一个代理对象,并返回它
Object plugin(Object target);
// 设置参数
void setProperties(Properties properties);
  • 第二步,插件注册,在 mybatis-config.xml 中注册插件
 <!--分页插件的注册-->
    <plugins>
        <plugin interceptor="com.github.pagehelper.PageInterceptor">
            <!-- 4.0.0以后版本可以不设置该参数 ,可以自动识别
            <property name="dialect" value="mysql"/>  -->
            <!-- 该参数默认为false -->
            <!-- 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用 -->
            <!-- 和startPage中的pageNum效果一样-->
            <property name="offsetAsPageNum" value="true"/>
            <!-- 该参数默认为false -->
            <!-- 设置为true时,使用RowBounds分页会进行count查询 -->
            <property name="rowBoundsWithCount" value="true"/>
            <!-- 设置为true时,如果pageSize=0或者RowBounds.limit = 0就会查询出全部的结果 -->
            <!-- (相当于没有执行分页查询,但是返回结果仍然是Page类型)-->
            <property name="pageSizeZero" value="true"/>
            <!-- 3.3.0版本可用 - 分页参数合理化,默认false禁用 -->
            <!-- 启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页 -->
            <!-- 禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据 -->
            <property name="reasonable" value="true"/>
            <!-- 3.5.0版本可用 - 为了支持startPage(Object params)方法 -->
            <!-- 增加了一个`params`参数来配置参数映射,用于从Map或ServletRequest中取值 -->
            <!-- 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,orderBy,不配置映射的用默认值 -->
            <!-- 不理解该含义的前提下,不要随便复制该配置 -->
            <property name="params" value="pageNum=start;pageSize=limit;"/>
            <!-- 支持通过Mapper接口参数来传递分页参数 -->
            <property name="supportMethodsArguments" value="true"/>
            <!-- always总是返回PageInfo类型,check检查返回类型是否为PageInfo,none返回Page -->
            <property name="returnPageInfo" value="check"/>
        </plugin>
    </plugins>
  • 第三步,插件登记
    MyBatis 启 动 时 扫 描 <plugins> 标 签 , 注 册 到 Configuration 对 象 的InterceptorChain 中,property 里面的参数,会调用 setProperties()方法处理。

代理和拦截是怎么实现的

我们先来看看4个问题

  • 1.四大对象什么时候被代理?代理对象是什么时候创建的?
    Executor 是 openSession() 的 时 候 创 建 的 ;
    StatementHandler 是SimpleExecutor.doQuery()创建的;
    里面包含了处理参数的 ParameterHandler 和处理 结果集的 ResultSetHandler 的创建,
    创建之后即调用 InterceptorChain.pluginAll(),返回层层代理后的对象。

  • 2.多个插件的情况下,代理对象能不能再被其他对象所代理?代理顺序和调用顺序的关系?
    可以被代理,顺序如下图:


    image.png
  • 3.谁来创建代理对象?
    Plugin类,在重写的plugin() 方法里面可以直接调用return Plugin.wrap(target, this);返回代理对象

  • 4.被代理后,调用的是什么方法?怎么调用到原被代理对象的方法?
    因为代理类是 Plugin,所以最后调用的是 Plugin 的 invoke()方法。它先调用了定义的拦截器的 intercept()方法。可以通过 invocation.proceed()调用到被代理对象被拦截的方法

带这4个问题和答案,以PageInterceptor为例,跟进代码来核实一下
首先mybatis-config.xml配置插件

   <plugins>
        <plugin interceptor="com.github.pagehelper.PageInterceptor">
        ...

打开PageInterceptor 的源码


@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 {
     ...

    // 用于覆盖被拦截对象的原有方法(在调用代理对象 Plugin 的 invoke()方法时被调用)
    public Object intercept(Invocation invocation) throws Throwable {
        ...
        //这个内部逻辑大概就是重新组织sql,把sql加上分页的语句,
       //根据方言不同,生成不同数据库的分页sql,
       //这个分页的值(比如pageNum,pageSize),
       //是利用PageHelper.startPage(1, 10); 来设置的
     //内部使用了ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal();
    //因为使用了ThreadLocal,所以直接从这里可以获取到分页值,重组sql语句即可
    }

    //target 是被拦截对象,这个方法的作用是给被拦截对象生成一个代理对象,并返回它
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    //这个方法就是获取配置文件中的配置项,并设置参数
    public void setProperties(Properties properties) {
         ...        
        this.msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties);
        String dialectClass = properties.getProperty("dialect");
         ... 
}

我们先来看Plugin.wrap(target, this); 方法

    public static Object wrap(Object target, Interceptor interceptor) {
        Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
        Class<?> type = target.getClass();
        Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
        return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)) : target;
    }

可知,最终返回一个jdk的动态代理,代理对象就是Plugin,验证了第三个问题是正确的
进入Plugin类

public class Plugin implements InvocationHandler {
    private final Object target;
    private final Interceptor interceptor;
    private final Map<Class<?>, Set<Method>> signatureMap;

    private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
        this.target = target;
        this.interceptor = interceptor;
        this.signatureMap = signatureMap;
    }

    public static Object wrap(Object target, Interceptor interceptor) {
        Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
        Class<?> type = target.getClass();
        Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
        return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)) : target;
    }

   //最关键的invoke方法
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            Set<Method> methods = (Set)this.signatureMap.get(method.getDeclaringClass());
            return methods != null && methods.contains(method) ? this.interceptor.intercept(new Invocation(this.target, method, args)) : method.invoke(this.target, args);
        } catch (Exception var5) {
            throw ExceptionUtil.unwrapThrowable(var5);
        }
    }

看到invoke方法,可以知道,最终还是会调用具体插件的intercept方法,验证了第四个问题是正确的

插件调用时序图.jpg

总结一下4个对象


image.png

——学自咕泡学院

相关文章

网友评论

    本文标题:MyBatis源码系列--5.MyBatis 插件原理与自定义插

    本文链接:https://www.haomeiwen.com/subject/tidvnqtx.html