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

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 中注册插件
 
    
        
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
            
        
    
  • 第三步,插件登记
    MyBatis 启 动 时 扫 描 标 签 , 注 册 到 Configuration 对 象 的InterceptorChain 中,property 里面的参数,会调用 setProperties()方法处理。

代理和拦截是怎么实现的

我们先来看看4个问题

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

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


    MyBatis源码系列--5.MyBatis 插件原理与自定义插件_第1张图片
    image.png
  • 3.谁来创建代理对象?
    Plugin类,在重写的plugin() 方法里面可以直接调用return Plugin.wrap(target, this);返回代理对象

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

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

   
        
        ...

打开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 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, Set> 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, Set> signatureMap;

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

    public static Object wrap(Object target, Interceptor interceptor) {
        Map, Set> 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 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方法,验证了第四个问题是正确的

MyBatis源码系列--5.MyBatis 插件原理与自定义插件_第2张图片
插件调用时序图.jpg

总结一下4个对象


image.png

——学自咕泡学院

你可能感兴趣的:(MyBatis源码系列--5.MyBatis 插件原理与自定义插件)