Mybatis框架源码笔记(八)之Plugin插件原理解析

1、插件概述

引用一段官网的译文

MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。
如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心。

2、自定义插件实现示例

2.1 自定义插件实现步骤

自定义插件的实现步骤大致如下:

  • 实现Mybatis框架的Interceptor接口

  • 全局配置文件mybatis-config.xml中配置自定义插件即可

下面通过一个示例代码来演示一下具体流程

2.2 自定义插件示例代码

我这里演示一下,自定义SQl语句的拦截方法, 在SQL语句的执行完毕之后, 修改返回的集合中每一个输出对象的某一个具体属性(这里只是演示怎么用, 演示代码场景无法通用,不能用于生产请知,如果有兴趣可以自行研究拓展 )

自定义拦截器MyInterceptor
Mybatis框架源码笔记(八)之Plugin插件原理解析_第1张图片

package com.kkarma.plugins;

import com.kkarma.pojo.LibBook;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.util.List;
import java.util.Properties;


/**
 * @author kkarma
 * @date 2023/1/17
 */
@Intercepts({
        @Signature(type = Executor.class, method = "query",
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
})
public class MyInterceptor implements Interceptor {

    private String author;

    /**
     * 执行拦截逻辑的方法
     * @param invocation
     * @return
     * @throws Throwable
     * @author kkarma
     * @date 2023-01-17
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("method will be invoked......");
        List<LibBook> list = (List<LibBook>)invocation.proceed();
        list.stream().forEach(v -> v.setBookName(v.getBookName() + "----mod"));
        System.out.println("method has been invoked......");
        return list;
    }

    /**
     * 是否触发intercept方法
     * @param target
     * @return
     * @throws Throwable
     * @author kkarma
     * @date 2023-01-17
     */
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    /**
     * 自定义插件属性参数配置
     * @param properties
     * @return
     * @throws Throwable
     * @author kkarma
     * @date 2023-01-17
     */
    @Override
    public void setProperties(Properties properties) {
        Object author = properties.get("author");
        System.out.println(author);
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }
}

修改全局配置文件mybatis-config.xml

这里注意Mybatis-config.xml文件中各个标签元素的声明顺序
Mybatis框架源码笔记(八)之Plugin插件原理解析_第2张图片

在全局配置文件中引入自定义插件
Mybatis框架源码笔记(八)之Plugin插件原理解析_第3张图片

    <plugins>
        <plugin interceptor="com.kkarma.plugins.MyInterceptor">
            <property name="author" value="kkarma"/>
        plugin>
    plugins>

以上就是自定义插件的所有实现步骤, 是不是很简单, 下面我们测试一下:

2.3 自定义插件示例代码测试

我们写一个单元测试,查询数据库某个单表中的所有数据,看看我们的自定义插件是否生效

@Test
public void testInterceptor() {
	SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
    try (InputStream ins = Resources.getResourceAsStream("mybatis-config.xml")) {
        SqlSessionFactory factory = factoryBuilder.build(ins);
        SqlSession sqlSession = factory.openSession(true);
        SqlSession sqlSession1 = factory.openSession(true);
        LibBookMapper mapper = sqlSession.getMapper(LibBookMapper.class);
        List<LibBook> libBooks = mapper.selectAllBook();
        libBooks.forEach(System.out::println);
        System.out.println("------------------------------------------");
        sqlSession.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

在这里插入图片描述
在这里插入图片描述
说明我们的自定义插件生效了, 从这里可以看出自定义插件的步骤还是比较简单的,接下来我们通过Mybatis的源码,分析下插件的实现原理究竟是怎么回事。

3 插件实现原理剖析

3.1 插件是如何解析引入的

上面的实例示例代码中我们在Mybatis-config.xml中声明了我们自定义的Interceptor,那么在全局配置文件的解析类中必然存在专门的方发负责解析处理我们自定义插件模块的方法,
Mybatis框架源码笔记(八)之Plugin插件原理解析_第4张图片
XMLConfigBuilder类的parseConfiguration()方法中调用pluginEelment
Mybatis框架源码笔记(八)之Plugin插件原理解析_第5张图片

pluginEelment方法用来解析全局配置文件中的plugins标签,然后对应的创建Interceptor对象,并且封装对应的属性信息。最后调用了Configuration对象中addInterceptor(interceptorInstance)完成拦截器注册Mybatis框架源码笔记(八)之Plugin插件原理解析_第6张图片
configuration.addInterceptor(interceptorInstance)方法如下:
Mybatis框架源码笔记(八)之Plugin插件原理解析_第7张图片
Mybatis框架源码笔记(八)之Plugin插件原理解析_第8张图片
Mybatis中的InterceptorChain
Mybatis框架源码笔记(八)之Plugin插件原理解析_第9张图片

3.2 插件的作用对象是谁?

开篇我们就已经进行了说明:

MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

之前我们在说ExecutorStatementHandler ParameterHandlerResultSetHandler 对象创建的过程时简单提过关于拦截器拦截这些对象来实现功能扩展, 下面来看看,上面四个核心类在创建失败都会调用
Configuration类中的如下代码:

  • Executor 对象创建
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : 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;
  }

在我们调用Executor.query()方法时,我们的拦截器就开始工作了
Mybatis框架源码笔记(八)之Plugin插件原理解析_第10张图片
Mybatis框架源码笔记(八)之Plugin插件原理解析_第11张图片

  • StatementHandler 对象创建
  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;
  }
  • ParameterHandler 对象创建
  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;
  }
  • ResultSetHandler 对象创建
  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;
  }

3.1 插件拦截的处理过程

3.1.1 单个插件的拦截处理过程

整个过程就是使用责任链模式结合

拦截器链中保存了多个拦截器,会遍历所有的拦截器,调用Interceptor.plugin(Object target)方法

Interceptor.plugin(Object target)方法会调用Plugin类的wrap()方法返回拦截目标对象的动态代理对象

Plugin类实现了InvocationHandler接口, 所以最后代理对象的invoke()方法会被调用

Plugin类的invoke()方法中调用了自定义拦截器的intercept(Invocation invocation)方法,这里就会在原来需要执行的方法之前和之后调用我们的拦截处理逻辑对方法的输入和输出进行扩展处理
Mybatis框架源码笔记(八)之Plugin插件原理解析_第12张图片
Mybatis框架源码笔记(八)之Plugin插件原理解析_第13张图片
Mybatis框架源码笔记(八)之Plugin插件原理解析_第14张图片

3.1.2 多个插件的拦截处理流程

如果我们有多个自定义的拦截器,那么他的拦截执行流程是怎么样的呢?

这里我增加了一个拦截器插件, 拦截的还是Executor.classquery方法, 通过具体的代码示例来验证一下我们的猜想。

package com.kkarma.plugins;

import com.kkarma.pojo.LibBook;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.util.List;
import java.util.Properties;


/**
 * @author kkarma
 * @date 2023/1/17
 */
@Intercepts({
        @Signature(type = Executor.class, method = "query",
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
})
public class MyLogInterceptor implements Interceptor {

    private String logger;

    /**
     * 执行拦截逻辑的方法
     * @param invocation
     * @return
     * @throws Throwable
     * @author kkarma
     * @date 2023-01-17
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("拦截方法执行之前记录日志...");
        List<LibBook> list = (List<LibBook>)invocation.proceed();
        System.out.println("拦截方法执行之后再次记录日志...");
        return list;
    }

    /**
     * 是否触发intercept方法
     * @param target
     * @return
     * @throws Throwable
     * @author kkarma
     * @date 2023-01-17
     */
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    /**
     * 自定义插件属性参数配置
     * @param properties
     * @return
     * @throws Throwable
     * @author kkarma
     * @date 2023-01-17
     */
    @Override
    public void setProperties(Properties properties) {
        Object logger = properties.get("logger");
        System.out.println(logger);
    }

    public String getLogger() {
        return logger;
    }

    public void setLogger(String logger) {
        this.logger = logger;
    }
}

mybatis-config.xml中注册插件顺序如下:

    <plugins>
        <plugin interceptor="com.kkarma.plugins.MyInterceptor">
            <property name="author" value="kkarma"/>
        plugin>
        <plugin interceptor="com.kkarma.plugins.MyLogInterceptor">
            <property name="logger" value="myLogger"/>
        plugin>
    plugins>

执行调用后控制台打印结果如下图:
Mybatis框架源码笔记(八)之Plugin插件原理解析_第15张图片

3.1.2.1 拦截器注册的过程

首先我们在配置文件中注册的顺序是MyInterceptor -> MyLogInterceptor
Mybatis框架源码笔记(八)之Plugin插件原理解析_第16张图片

3.1.2.2 代理对象的创建过程

从配置文件中解析创建的Interceptors对象肯定是按照定义的顺序解析出来的, 所以这里再进行动态代理创建的时候也是按照注册的顺序去创建的 MyInterceptor -> MyLogInterceptor
Mybatis框架源码笔记(八)之Plugin插件原理解析_第17张图片

3.1.2.3 动态代理对象的调用过程

Mybatis框架源码笔记(八)之Plugin插件原理解析_第18张图片
继续执行,发现MyInteceptor的intercept()方法被执行了,然后query()方法被执行,在依次从内往外返回。
Mybatis框架源码笔记(八)之Plugin插件原理解析_第19张图片
从打印日志我们可以看出执行调用的顺序注册和创建动态代理对象的顺序刚好相反。
Mybatis框架源码笔记(八)之Plugin插件原理解析_第20张图片

3.1.2.4 结论

  • 将多个插件注册到InterceptorChain的List是按照插件在配置文件中定义的顺序从上往下的顺序解析、添加的。
  • 创建代理对象的时候也是按照InterceptorChain的List的顺序代理
  • 调用执行的过程和注册创建的过程刚好相反

注册创建的过程(包装礼物的过程):

就是我们现在有一堆好看的太空沙(目标对象)想送给别的小朋友, 不能直接送给人吧,

于是你找来了一个瓶子(MyInteceptor)把太空沙装到里面,

但是这个瓶子也很难看, 于是你又骂了一个精致的礼物盒(MyLogInteceptor)做了一下包装,现在好看了,你把它送给你的朋友了。

调用执行的过程(拆礼物的过程):

你的朋友收到了礼物, 她会先把礼物盒拆开(MyLogInteceptor.intercept())被调用,会发现太空沙被用瓶子转起来了,

于是他又把瓶子(MyInteceptor.intercept())打开了,把太空沙(目标对象)倒出来了, 现在她可以开心的玩耍了。

Mybatis框架源码笔记(八)之Plugin插件原理解析_第21张图片
StatementHandler、ParameterHandler、ResultSetHandler的拦截器与Executor的处理流程相同, 这里就不多加赘述了, 感兴趣可以自行研究。

你可能感兴趣的:(Mybatis,Spring全家桶,Java,mybatis,java,mysql)