前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >MyBatis源码阅读(九) --- 插件原理

MyBatis源码阅读(九) --- 插件原理

作者头像
终有救赎
发布2024-01-30 09:02:12
930
发布2024-01-30 09:02:12
举报
文章被收录于专栏:多线程多线程
一、概述

插件功能也是Mybatis框架中的一个核心功能,Mybatis提供了自定义插件功能来帮我们扩展个性化业务需求。本篇文章我们将总结Mybatis的插件机制以及如何自定义一个插件。

我们在做查询操作的时候,如果数据量很大,不可能一次性返回所有数据,一般会采用分页查询的方式,那么在Mybatis的分页是如何做的呢?其实,要实现分页,就可以使用到Mybatis的插件功能,我们可以拦截到Mybatis将要执行的SQL语句,然后动态修改其中的参数,比如加入limit限制条数等。MyBatis的插件是通过动态代理来实现的,并且会形成一个interceptorChain插件链。

下面我们先通过一个简单的分页插件来详细分析Mybatis的插件机制。

二、自定义简单分页插件

在MyBatis中,我们只能对以下四种类型的对象进行拦截

  • ParameterHandler : 对sql参数进行处理;
  • ResultSetHandler : 对结果集对象进行处理;
  • StatementHandler : 对sql语句进行处理;
  • Executor : 执行器,执行增删改查;

这里我们要实现动态修改或者拼接SQL语句的limit条数限制,所以可以选择拦截Executor的query方法。大体步骤如下:

  • 1、使用@Intercepts和@Signature指定需要拦截的哪个类的哪个方法;
  • 2、Mybatis的插件需要执行拦截哪个类的哪个方法,使用@Intercepts注解,里面是方法的签名信息,使用@Signature定义拦截方法信息的描述;
  • 3、Mybatis的插件类需要实现Interceptor接口,重写其中的方法;
  • 4、重写具体的拦截逻辑intercept()方法;
  • 5、plugin()方法返回代理对象;
  • 6、在全局配置文件中配置自定义的插件对象;

具体代码如下:

代码语言:javascript
复制
package com.wsh.mybatis.mybatisdemo;
 
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
import java.lang.reflect.Field;
import java.util.Properties;
 
/**
 * Mybatis插件机制 - 自定义简单分页插件
 * 说明:
 * 1、使用@Intercepts和@Signature指定需要拦截的哪个类的哪个方法;
 * 2、Mybatis的插件需要执行拦截哪个类的哪个方法,使用@Intercepts注解,里面是方法的签名信息,使用@Signature定义拦截方法信息的描述;
 * 3、Mybatis的插件类需要实现Interceptor接口,重写其中的方法;
 * 4、重写具体的拦截逻辑intercept()方法;
 * 5、plugin()方法返回代理对象;
 */
@Intercepts({@Signature(type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class CustomPagePlugin implements Interceptor {
 
    private static final Logger logger = LoggerFactory.getLogger(CustomPagePlugin.class);
 
    /**
     * 拦截目标对象的目标方法
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        RowBounds rowBounds = (RowBounds) args[2];
        logger.info("拦截前rowBounds对象offset=" + rowBounds.getOffset() + ", limit = " + rowBounds.getLimit());
        //修改RowBounds参数中的limit
        if (rowBounds != null) {
            if (rowBounds.getLimit() > 2) {
                Field field = rowBounds.getClass().getDeclaredField("limit");
                field.setAccessible(true);
                field.set(rowBounds, 2);
            }
        } else {
            rowBounds = new RowBounds(0, 2);
            args[2] = rowBounds;
        }
        logger.info("拦截后rowBounds对象offset=" + rowBounds.getOffset() + ", limit = " + rowBounds.getLimit());
        //执行目标方法,并返回执行后的结果
        return invocation.proceed();
    }
 
    /**
     * 包装目标对象,为目标对象返回一个代理对象
     */
    @Override
    public Object plugin(Object target) {
        //target: 需要包装的对象
        //this: 使用当前拦截器CustomPagePlugin进行拦截
        //返回为当前target创建的代理对象
        return Plugin.wrap(target, this);
    }
 
    /**
     * 将插件注册的时候的属性信息设置进来
     */
    @Override
    public void setProperties(Properties properties) {
 
    }
}

插件逻辑写完了,我们需要在配置文件中配置一下插件才能生效,在mybatis-config.xml中添加plugins标签,并且配置我们上面实现的插件的全限定类名:

代码语言:javascript
复制
<plugins>
    <plugin interceptor="com.wsh.mybatis.mybatisdemo.CustomPagePlugin">
    </plugin>
</plugins>
二、Mybatis插件加载时机

插件编写完了,主要实现intercept(Invocation invocation)方法。那么Mybatis的插件是在什么时候被加载的呢?

如果前面有了解过Mybatis的配置信息解析的话,这里不能猜到,插件肯定也是在一开始解析XML配置,就跟mapper接口、environment等信息的解析在一块,在XMLConfigBuilder的parse()解析的时候加载的。

代码语言:javascript
复制
//org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration
private void parseConfiguration(XNode root) {
  try {
    //issue #117 read properties first
    propertiesElement(root.evalNode("properties"));
    Properties settings = settingsAsProperties(root.evalNode("settings"));
    loadCustomVfs(settings);
    loadCustomLogImpl(settings);
    typeAliasesElement(root.evalNode("typeAliases"));
    pluginElement(root.evalNode("plugins"));
    objectFactoryElement(root.evalNode("objectFactory"));
    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
    reflectorFactoryElement(root.evalNode("reflectorFactory"));
    settingsElement(settings);
    // read it after objectFactory and objectWrapperFactory issue #631
    environmentsElement(root.evalNode("environments"));
    databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    typeHandlerElement(root.evalNode("typeHandlers"));
    mapperElement(root.evalNode("mappers"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  }
}

注意pluginElement(root.evalNode("plugins"))方法,这个就是具体解析插件的方法。

代码语言:javascript
复制
//org.apache.ibatis.builder.xml.XMLConfigBuilder#pluginElement
private void pluginElement(XNode parent) throws Exception {
  if (parent != null) {
    //遍历我们在mybatis-config.xml配置的所有插件
    for (XNode child : parent.getChildren()) {
       //获取到具体的插件全限定类名
       String interceptor = child.getStringAttribute("interceptor");
       //获取插件配置的属性信息
       Properties properties = child.getChildrenAsProperties();
      //通过classLoaderWrapper创建一个拦截器对象
      Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
      //设置属性
      interceptorInstance.setProperties(properties);
      //将拦截器添加到configuration中
      configuration.addInterceptor(interceptorInstance);
    }
  }
}
 
//org.apache.ibatis.session.Configuration#addInterceptor
public void addInterceptor(Interceptor interceptor) {
    //Configuration的一个成员属性:InterceptorChain interceptorChain = new InterceptorChain();
    interceptorChain.addInterceptor(interceptor);
}

下图是解析插件拦截器的过程:

图片.png
图片.png

下面我们来看一下InterceptorChain类,InterceptorChain 是一个interceptor集合,相当于是一层层包装,后一个插件就是对前一个插件的包装,并返回一个代理对象。

代码语言:javascript
复制
public class InterceptorChain {
 
  private final List<Interceptor> interceptors = new ArrayList<>();
 
  // 生成代理对象
  public Object pluginAll(Object target) {
    //循环遍历所有的插件,调用对应的plugin方法
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }
 
  // 将插件加到集合中
  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }
 
  //获取所有的插件
  public List<Interceptor> getInterceptors() {
    return Collections.unmodifiableList(interceptors);
  }
 
}

前面我们已经知道了Mybatis加载插件的流程,那么插件到底是在哪里发挥作用的。以Executor为例,在创建Executor对象的时候,注意看下面的代码:

代码语言:javascript
复制
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) interceptorChain.pluginAll(executor);
  return executor;
}
  • executor = (Executor) interceptorChain.pluginAll(executor);

插件其实就是在这里创建的,它会调用拦截器链的pluginAll方法,将自身对象executor传入进去,实际调用的是每个Interceptor的plugin()方法,plugin()就是对目标对象的一个代理,并且生成一个代理对象返回。我们来看看plugin()方法的源码:

代码语言:javascript
复制
public Object plugin(Object target) {
    return Plugin.wrap(target, this);
}
 
  //org.apache.ibatis.plugin.Plugin#wrap
  public static Object wrap(Object target, Interceptor interceptor) {
     // 获取拦截器需要拦截的方法签名信息,以拦截器对象为key,拦截方法为value
     Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
     // 获取目标对象的class对象
    Class<?> type = target.getClass();
    // 如果拦截器中拦截的对象包含目标对象实现的接口,则返回拦截的接口
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    // 如果对目标对象进行了拦截
    if (interfaces.length > 0) {
      // 创建代理类Plugin对象,使用到了动态代理
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }
四、插件测试

【a】编写mapper接口

代码语言:javascript
复制
List<User> getAllUser(@Param("offset") Integer offset, @Param("pageSize") Integer pageSize);

【b】编写mapper.xml

代码语言:javascript
复制
<select id="getAllUser" resultType="com.wsh.mybatis.mybatisdemo.entity.User">
  select * from  user
  <if test="offset != null">
    limit #{offset}, #{pageSize}
  </if>
</select>

【c】启动单元测试

代码语言:javascript
复制
List<User> allUser = userMapper.getAllUser(null, null);
System.out.println("查询到的总记录数:" + allUser.size());
for (User user : allUser) {
    System.out.println(user);
}

后台打印日志如下:

代码语言:javascript
复制
16:10:41.447 [main] INFO com.wsh.mybatis.mybatisdemo.CustomPagePlugin - 拦截前rowBounds对象offset=0, limit = 2147483647
16:10:41.448 [main] INFO com.wsh.mybatis.mybatisdemo.CustomPagePlugin - 拦截后rowBounds对象offset=0, limit = 2
Opening JDBC Connection
Created connection 1667689440.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@6366ebe0]
==>  Preparing: select * from user 
==> Parameters: 
<==    Columns: id, username
<==        Row: 1, 张三
<==        Row: 2, 李四
查询到的总记录数:2
User{id='1', username='张三'}
User{id='2', username='李四'}

我们可以看到,拦截后的SQL语句的limit已经被修改为2了,通过返回结果也可以看到,只返回了两条数据回来。

再来看看插件运行过程中几个关键步骤的各个属性的值:

如下是在openSession开启会话的时候,创建Executor执行器对象时,executor = (Executor) interceptorChain.pluginAll(executor)调用拦截器链的pluginAll方法时。

图片.png
图片.png

如下是在具体生成代理对象时:

图片.png
图片.png

当我们执行userMapper.getAllUser(null, null)方法的时候,会调用executor的query方法,这个query方法会被拦截器拦截,生成executor的一个代理对象,所以执行query方法的时候就会执行代理对象的invoke()方法,而invoke()方法里面会调用插件的intercept()方法,实现一些自定义业务逻辑扩展。

图片.png
图片.png
五、总结

本文通过一个简单的自定义分页插件的例子,总结了Mybatis的插件运行原理、加载时机和创建时机。Mybatis插件的功能非常强大,能拦截到SQL,添加分页参数实现分页功能,将具体查询的条件修改掉,达到偷梁换柱的功能等等。

注意:

如果多个插件同时拦截同一个目标对象的目标方法,会发生层层代理,举个例子,有两个自定义插件:

FirstPlugin和SecondPlugin【假设配置文件中配置的顺序是FirstPlugin、SecondPlugin】,具体执行的时候其实是先执行SecondPlugin的intercept()方法,然后再执行FirstPlugin的intercept()方法。

Mybatis注册插件时,创建动态代理对象的时候,是按照插件配置的顺序插件层层代理对象,执行插件的时候,则是按照逆向顺序执行。

如下图:

图片.png
图片.png

最后总结一下,自定义Mybatis插件的大体步骤:

  • 1、使用@Intercepts和@Signature指定需要拦截的哪个类的哪个方法;
  • 2、Mybatis的插件需要执行拦截哪个类的哪个方法,使用@Intercepts注解,里面是方法的签名信息,使用@Signature定义拦截方法信息的描述;
  • 3、Mybatis的插件类需要实现Interceptor接口,重写其中的方法;
  • 4、重写具体的拦截逻辑intercept()方法;
  • 5、plugin()方法返回代理对象;
  • 6、在全局配置文件中配置自定义的插件对象;

鉴于笔者水平有限,如果文章有什么错误或者需要补充的,希望小伙伴们指出来,希望这篇文章对大家有帮助。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2024-01-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、概述
  • 二、自定义简单分页插件
  • 二、Mybatis插件加载时机
  • 四、插件测试
  • 五、总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档