MyBatis源码(4)-插件机制


一、介绍

一般开源框架都会提供扩展点,让开发者自行扩展,从而完成逻辑的增强。

通过Mybatis插件可以实现对框架的扩展,来实现自定义功能,并且对于用户是无感知的。

基于插件机制可以实现了很多有用的功能,比如说分页,字段加密,监控等功能,这种通用的功能,就如同AOP一样,横切在数据操作上

Mybatis插件本质上来说就是一个拦截器,它体现了JDK动态代理和责任链设计模式的综合运用

Mybatis中针对四大组件提供了扩展机制,这四个组件分别是:

image-20221206214319021

Mybatis中所允许拦截的方法如下:

  • Executor 【SQL执行器】【update,query,commit,rollback】
  • StatementHandler 【Sql语法构建器对象】【prepare,parameterize,batch,update,query等】
  • ParameterHandler 【参数处理器】【getParameterObject,setParameters等】
  • ResultSetHandler 【结果集处理器】【handleResultSets,handleOuputParameters等】

能干什么

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

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

二、拦截器相关的类

拦截器拦截哪个类的哪个方法就由下面这个注解提供拦截信息

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {  
    Signature[] value();
}

由于一个拦截器可以同时拦截多个对象的多个方法,所以就使用了Signture数组,该注解定义了拦截的完整信息

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
    // 拦截的类
    Class<?> type();
    // 拦截的方法
    String method();
    // 拦截方法的参数    
    Class<?>[] args();

} 

已经知道了该拦截哪些对象的哪些方法,拦截后要干什么就需要实现Intercetor#intercept方法,在这个方法里面实现拦截后的处理逻辑

public interface Interceptor {
    /**
   * 真正方法被拦截执行的逻辑
   *
   * @param invocation 主要目的是将多个参数进行封装
   */
    Object intercept(Invocation invocation) throws Throwable;

    // 生成目标对象的代理对象
    default Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    // 可以拦截器设置一些属性
    default void setProperties(Properties properties) {
        // NOP
    }
}

三、自定义插件

需求:把Mybatis所有执行的sql都记录下来

步骤

① 创建Interceptor的实现类,重写方法

② 使用@Intercepts注解完成插件签名 说明插件的拦截四大对象之一的哪一个对象的哪一个方法

③ 将写好的插件注册到全局配置文件中

1、Interceptor的实现类

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.*;

import java.sql.Connection;
import java.util.Properties;

/**
 * 使用@Intercepts注解完成插件签名 说明插件的拦截四大对象之一的哪一个对象的哪一个方法
 */
@Intercepts({
        @Signature(type = StatementHandler.class,
                method = "prepare",
                args = {Connection.class, Integer.class})
})
public class MyPlugin implements Interceptor {


    /**
     * 对目标方法的增强,每次执行签名指定方法时,都会调用
     * @param invocation 目标对象
     * @return 方法执行后返回值
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //前置增强逻辑
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = statementHandler.getBoundSql();
        String sql = boundSql.getSql();
        System.out.println("mybatis intercept sql:" + sql);

        //执行原方法
        Object returnObject = invocation.proceed();

        //后置增强...

        return returnObject;
    }

    /**
     *  包装目标对象 为目标对象创建代理对象
     *  依次传入 Executor,ParameterHandler,ResultSetHandler,StatementHandler四大对象
     *
     *    @Param target为要拦截的对象
     *    @Return 代理对象
     */
    @Override
    public Object plugin(Object target) {
        System.out.println("将要包装的目标对象:"+target);
        //判断对象类型是否在 @Intercepts 中配置, 如果需要扩展则生成代理类并返回
        return Plugin.wrap(target,this);
    }


    /**
     * 获取配置插件时设置的属性
     * 插件初始化的时候调用,也只调用一次,插件配置的属性从这里设置进来
     * @param properties
     */
    @Override
    public void setProperties(Properties properties) {
        System.out.println("插件配置的初始化参数:"+properties );
    }
}

2、注册插件

(1)原生配置

注意:标签顺序(必须按照下面的顺序书写标签)

元素类型为 "configuration" 的内容必须匹配 "(properties?,settings?,typeAliases?,typeHandlers?,objectFactory?,objectWrapperFactory?,reflectorFactory?,plugins?,environments?,databaseIdProvider?,mappers?)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <plugins>
        <!-- 可自定义一个或多个拦截器 -->
        <!-- MyPlugin是一个实现了Interceptor接口的类 -->
        <plugin interceptor="org.apache.ibatis.mytest.MyPlugin">
            <!-- 拦截器可接收到这里配置的属性 -->
            <property name="dialect" value="mysql" />
        </plugin>
    </plugins>
</configuration>

(2)springboot配置

@Configuration
public class MybatisConfiguration {
    @Bean
    public MyPlugin myPlugin() {
        return new  MyPlugin();
    }
}

四、插件原理

就是使用JDK动态代理的方式,对这四个对象进行包装增强。具体的做法是,创建一个类实现Mybatis的拦截器接口,并且加入到拦截器链中,在创建核心对象的时候,不直接返回,而是遍历拦截器链,把每一个拦截器都作用于核心对象中。这么一来,Mybatis创建的核心对象其实都是代理对象,都是被包装过的。

1、代理对象生成

(1)Executor

Executor代理对象(Configuration#newExecutor)

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 = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

(2)ParameterHandler

ParameterHandler代理对象(Configuration#newParameterHandler)

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    // 生成ParameterHandler代理对象逻辑 
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
}

(3)ResultSetHandler

ResultSetHandler代理对象(Configuration#newResultSetHandler)

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) interceptorChain.pluginAll(resultSetHandler);
    return resultSetHandler;
}

(4)StatementHandler

StatementHandler代理对象(Configuration#newStatementHandler)

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) interceptorChain.pluginAll(statementHandler);
    return statementHandler;

}

2、拦截器链

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

通过查看源码会发现,所有代理对象的生成都是通过InterceptorChain#pluginAll方法来创建的,进一步查看pluginAll方法

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

}

InterceptorChain#pluginAll内部通过遍历Interceptor#plugin方法来创建代理对象,并将生成的代理对象又赋值给target,如果存在多个拦截器的话,生成的代理对象会被另一个代理对象所代理,从而形成一个代理链,执行的时候,依次执行所有拦截器的拦截逻辑代码,我们再跟进去

default Object plugin(Object target) {
    return Plugin.wrap(target, this);
}

Interceptor#plugin方法最终将目标对象和当前的拦截器交给Plugin.wrap方法来创建代理对象。该方法是默认方法,是Mybatis框架提供的一个典型plugin方法的实现。让我们看看在Plugin#wrap方法中是如何实现代理对象的

public static Object wrap(Object target, Interceptor interceptor) {
    // 1.解析该拦截器所拦截的所有接口及对应拦截接口的方法
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    // 2.获取目标对象实现的所有被拦截的接口
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    // 3.目标对象有实现被拦截的接口,生成代理对象并返回
    if (interfaces.length > 0) {
        // 通过JDK动态代理的方式实现
        return Proxy.newProxyInstance(
            type.getClassLoader(),
            interfaces,
            new Plugin(target, interceptor, signatureMap));
    }
    // 目标对象没有实现被拦截的接口,直接返回原对象
    return target;

}

最终我们看到其实是通过JDK提供的Proxy.newProxyInstance方法来生成代理对象

以上代理对象生成过程的时序图如下:

image-20221207215849800

3、拦截器执行顺序

配置在最前面的拦截器最先被代理,但是执行的时候却是最外层的先执行

具体点:

假如依次定义了三个插件:插件1插件2 和 插件3

那么List中就会按顺序存储:插件1插件2插件3

而解析的时候是遍历list,所以解析的时候也是按照:插件1 ,插件2,插件3的顺序。

但是执行的时候就要反过来了,执行的时候是按照:插件3插件2插件1的顺序进行执行

image-20221207215931930

当 Executor 的某个方法被调用的时候,插件逻辑会先行执行。执行顺序由外而内,比如上图的执行顺序为 plugin3 → plugin2 → Plugin1 → Executor


  目录