MyBatis实现自定义MyBatis插件详解

news/2024/12/23 21:12:14 标签: mybatis, java

MyBatis实现自定义MyBatis插件详解

  • 初识插件
  • 拦截对象
  • 拦截实现
  • 加载流程
    • xml配置插件
    • XMLConfigBuilder加载插件
    • 创建插件对象
  • 例子
    • 确定拦截对象
    • 实现拦截接口
    • 配置插件
    • 测试

MyBatis的一个重要的特点就是插件机制,使得MyBatis的具备较强的扩展性,我们可以根据MyBatis的插件机制实现自己的个性化业务需求。

初识插件

我们在执行查询的时候,如果sql没有加上分页条件,数据量过大的话会造成内存溢出,因此我们可以通过MyBatis提供的插件机制来拦截sql,并进行sql改写。MyBatis的插件是通过动态代理来实现的,并且会形成一个插件链。原理类似于拦截器,拦截我们需要处理的对象,进行自定义逻辑后,返回一个代理对象,进行下一个拦截器的处理。

我们先来看下一个简单插件的模板,首先要实现一个Interceptor接口,并实现三个方法。并加上@Intercepts注解。接下来我们以分页插件为例将对每个细节进行讲解。

java">/**
 * @ClassName : PagePlugin
 * @Description : 分页插件
 * @Date: 2020/12/29
 */
@Intercepts({})
public class PagePlugin implements Interceptor {
    
    private Properties properties;
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        this.properties = properties;
    }
}

拦截对象

在进行插件创建的时候,需要指定拦截对象。@Intercepts注解指定需要拦截的方法签名,内容是个Signature类型的数组,而Signature就是对拦截对象的描述。

java">@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
  /**
   * Returns method signatures to intercept.
   *
   * @return method signatures
   */
  Signature[] value();
}

Signature 需要指定拦截对象中方法的信息的描述。

java">@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
  /**
   * 对象类型
   */
  Class<?> type();

  /**
   * 方法名
   */
  String method();

  /**
   * 参数类型
   */
  Class<?>[] args();
}

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

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

现在我们需要对sql进行改写,因此可以需要拦截Executor的query方法进行拦截

java">@Intercepts({@Signature(type = Executor.class, 
                        method = "query", 
                        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})

拦截实现

每个插件除了指定拦截的方法后,还需要实现Interceptor接口。Interceptor接口有以下三个方法。其中intercept是我们必须要实现的方法,在这里面我们需要实现自定义逻辑。其它两个方法给出了默认实现。

java">public interface Interceptor {

  /**
   * 进行拦截处理
   * @param invocation
   * @return
   * @throws Throwable
   */
  Object intercept(Invocation invocation) throws Throwable;

  /**
   * 返回代理对象
   * @param target
   * @return
   */
  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

  /**
   * 设置配置属性
   * @param properties
   */
  default void setProperties(Properties properties) {
    // NOP
  }

}

因此我们实现intercept方法即可,因为我们要改写查询sql语句,因此需要拦截Executor的query方法,然后修改RowBounds参数中的limit,如果limit大于1000,我们强制设置为1000。

java">@Slf4j
@Intercepts({@Signature(type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class , ResultHandler.class})})
public class PagePlugin implements Interceptor {

    private Properties properties;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        RowBounds rowBounds = (RowBounds)args[2];
        log.info("执行前, rowBounds = [{}]", JSONUtil.toJsonStr(rowBounds));
        if(rowBounds != null){
            if(rowBounds.getLimit() > 1000){
                Field field = rowBounds.getClass().getDeclaredField("limit");
                field.setAccessible(true);
                field.set(rowBounds, 1000);
            }
        }else{
            rowBounds = new RowBounds(0 ,100);
            args[2] = rowBounds;
        }
        log.info("执行后, rowBounds = [{}]", JSONUtil.toJsonStr(rowBounds));
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        this.properties = properties;
    }
}

加载流程

以上我们已经实现了一个简单的插件,在执行查询的时候对query方法进行拦截,并且修改分页参数。但是我们现在还没有进行插件配置,只有配置了插件,MyBatis才能启动过程中加载插件。

xml配置插件

mybatis-config.xml中添加plugins标签,并且配置我们上面实现的plugin

<plugins>
	<plugin interceptor="com.example.demo.mybatis.PagePlugin">
	</plugin>
</plugins>

XMLConfigBuilder加载插件

在启动流程中加载插件中使用到SqlSessionFactoryBuilder的build方法,其中XMLConfigBuilder这个解析器中的parse()方法就会读取plugins标签下的插件,并加载Configuration中的InterceptorChain中。

java">// SqlSessionFactoryBuilder
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
	SqlSessionFactory var5;
	try {
		XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
		var5 = this.build(parser.parse());
	} catch (Exception var14) {
		throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
	} finally {
		ErrorContext.instance().reset();

		try {
			inputStream.close();
		} catch (IOException var13) {
		}

	}

	return var5;
}

可见XMLConfigBuilder这个parse()方法就是解析xml中配置的各个标签。

java">// XMLConfigBuilder
public Configuration parse() {
	if (parsed) {
	  throw new BuilderException("Each XMLConfigBuilder can only be used once.");
	}
	parsed = true;
	parseConfiguration(parser.evalNode("/configuration"));
	return configuration;
}

private void parseConfiguration(XNode root) {
	try {
	  // issue #117 read properties first
	  // 解析properties节点
	  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);
	}
}
XMLConfigBuilder 的pluginElement就是遍历plugins下的plugin加载到interceptorChain中。

// XMLConfigBuilder
private void pluginElement(XNode parent) throws Exception {
	if (parent != null) {
	  // 遍历每个plugin插件
	  for (XNode child : parent.getChildren()) {
		// 读取插件的实现类
		String interceptor = child.getStringAttribute("interceptor");
		// 读取插件配置信息
		Properties properties = child.getChildrenAsProperties();
		// 创建interceptor对象
		Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
		interceptorInstance.setProperties(properties);
		// 加载到interceptorChain链中
		configuration.addInterceptor(interceptorInstance);
	  }
	}
}

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

java">public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<>();

  // 生成代理对象
  public Object pluginAll(Object target) {
    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);
  }

}

创建插件对象

因为我们需要对拦截对象进行拦截,并进行一层包装返回一个代理类,那是什么时候进行处理的呢?以Executor为例,在创建Executor对象的时候,会有以下代码。

java">// Configuration
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对象后,就会调用interceptorChain.pluginAll()方法,实际调用的是每个Interceptor的plugin()方法。plugin()就是对目标对象的一个代理,并且生成一个代理对象返回。而Plugin.wrap()就是进行包装的操作。

java">// Interceptor
/**
* 返回代理对象
* @param target
* @return
*/
default Object plugin(Object target) {
	return Plugin.wrap(target, this);
}

Plugin的wrap()主要进行了以下步骤:

  • 获取拦截器拦截的方法,以拦截对象为key,拦截方法集合为value
  • 获取目标对象的class对,比如Executor对象
  • 如果拦截器中拦截的对象包含目标对象实现的接口,则返回拦截的接口
  • 创建代理类Plugin对象,Plugin实现了InvocationHandler接口,最终对目标对象的调用都会调用Plugin的invocate方法。
java">// Plugin
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;
}

例子

我们已经了解MyBatis插件的配置,创建,实现流程,接下来就以一开始我们提出的例子来介绍实现一个插件应该做哪些。

确定拦截对象

因为我们要对查询sql分页参数进行改写,因此可以拦截Executor的query方法,并进行分页参数的改写

java">@Intercepts({@Signature(type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class , ResultHandler.class})})

实现拦截接口

实现Interceptor接口,并且实现intercept实现我们的拦截逻辑

java">@Slf4j
@Intercepts({@Signature(type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class , ResultHandler.class})})
public class PagePlugin implements Interceptor {

    private Properties properties;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        RowBounds rowBounds = (RowBounds)args[2];
        log.info("执行前, rowBounds = [{}]", JSONUtil.toJsonStr(rowBounds));
        if(rowBounds != null){
            if(rowBounds.getLimit() > 1000){
                Field field = rowBounds.getClass().getDeclaredField("limit");
                field.setAccessible(true);
                field.set(rowBounds, 1000);
            }
        }else{
            rowBounds = new RowBounds(0 ,100);
            args[2] = rowBounds;
        }
        log.info("执行后, rowBounds = [{}]", JSONUtil.toJsonStr(rowBounds));
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        this.properties = properties;
    }
}

配置插件

mybatis-config.xml中配置以下插件

<plugins>
	<plugin interceptor="com.example.demo.mybatis.PagePlugin">
	</plugin>
</plugins>

测试

TTestUserMapper.java 新增selectByPage方法

java">List<TTestUser> selectByPage(@Param("offset") Integer offset, @Param("pageSize") Integer pageSize);

mapper/TTestUserMapper.xml 新增对应的sql

<select id="selectByPage" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from t_test_user
<if test="offset != null">
  limit #{offset}, #{pageSize}
</if>
</select>

最终测试代码,我们没有在查询的时候指定分页参数。

java">public static void main(String[] args) {
	try {
		// 1. 读取配置
		InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
		// 2. 创建SqlSessionFactory工厂
		SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
		// 3. 获取sqlSession
		SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.SIMPLE);
		// 4. 获取Mapper
		TTestUserMapper userMapper = sqlSession.getMapper(TTestUserMapper.class);
		// 5. 执行接口方法
		List<TTestUser> list2 = userMapper.selectByPage(null, null);
		System.out.println("list2="+list2.size());
		// 6. 提交事物
		sqlSession.commit();
		// 7. 关闭资源
		sqlSession.close();
		inputStream.close();
	} catch (Exception e){
		log.error(e.getMessage(), e);
	}
}

最终打印的日志如下,我们可以看到rowBounds已经被我们强制修改了只能查处1000条数据。

10:11:49.313 [main] INFO com.example.demo.mybatis.PagePlugin - 执行前, rowBounds = [{"offset":0,"limit":2147483647}]
10:11:58.015 [main] INFO com.example.demo.mybatis.PagePlugin - 执行后, rowBounds = [{"offset":0,"limit":1000}]
10:12:03.211 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
10:12:04.269 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 749981943.
10:12:04.270 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@2cb3d0f7]
10:12:04.283 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPage - ==>  Preparing: select id, member_id, real_name, nickname, date_create, date_update, deleted from t_test_user 
10:12:04.335 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPage - ==> Parameters: 
list2=1000

http://www.niftyadmin.cn/n/5797008.html

相关文章

JavaIO 在 Android 中的应用

主要是学习如何设计这样的 IO 系统&#xff0c;学习思想而不是代码本身。 1、装饰模式在 IO 中的应用 IO 嵌套其实使用到了装饰模式。装饰模式在 Android 中有大量的使用实例&#xff0c;比如 Context 体系&#xff1a; 可以看到 Context 还是基本上遵循了标准装饰模式的结构…

windows C#-编写复制构造函数

C # 记录为对象提供复制构造函数&#xff0c;但对于类&#xff0c;你必须自行编写。 编写适用于类层次结构中所有派生类型的复制构造函数可能很困难。 如果类不是 sealed&#xff0c;则强烈建议考虑创建 record class 类型的层次结构&#xff0c;以使用编译器合成的复制构造函…

【C语言之】二进制的四种位运算:取反、与、或、异或

【C语言之】二进制的四种位运算&#xff1a;取反、与、或、异或 1、按位取反运算&#xff08; bit not : ~ &#xff09; 对操作数的每一位执行逻辑取反操作&#xff0c;即将每一位的 0 变为 1&#xff0c;1 变为 0。取反运算符&#xff0c;按二进制位进行"取反"运…

数据结构---------二叉树前序遍历中序遍历后序遍历

以下是用C语言实现二叉树的前序遍历、中序遍历和后序遍历的代码示例&#xff0c;包括递归和非递归&#xff08;借助栈实现&#xff09;两种方式&#xff1a; 1. 二叉树节点结构体定义 #include <stdio.h> #include <stdlib.h>// 二叉树节点结构体 typedef struct…

Linux网络功能 - 服务和客户端程序CS架构和简单web服务示例

By: fulinux E-mail: fulinux@sina.com Blog: https://blog.csdn.net/fulinus 喜欢的盆友欢迎点赞和订阅! 你的喜欢就是我写作的动力! 目录 概述准备工作扫描服务端有那些开放端口创建客户端-服务器设置启动服务器和客户端进程双向发送数据保持服务器进程处于活动状态设置最小…

【优选算法---分治】快速排序三路划分(颜色分类、快速排序、数组第K大的元素、数组中最小的K个元素)

一、颜色分类 题目链接: 75. 颜色分类 - 力扣&#xff08;LeetCode&#xff09; 题目介绍&#xff1a; 给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums &#xff0c;原地 对它们进行排序&#xff0c;使得相同颜色的元素相邻&#xff0c;并按照红色、白色、蓝色顺序…

目标检测文献阅读-Faster R-CNN:通过区域建议网络实现实时目标检测(12.16-12.22)

目录 摘要 Abstract 1 引言 2 Fast R-CNN 2.1 RoI池化层 2.2 多任务损失 3 RPN 3.1 Anchors 3.2 损失函数 3.3 训练RPN 4 RPN和Fast R-CNN共享特征 总结 摘要 本周阅读的论文题目是《Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Netw…

MySQL 中的常见错误与排查

在 MySQL 数据库的日常运维中&#xff0c;管理员可能会遇到各种错误。无论是查询性能问题、连接异常、数据一致性问题&#xff0c;还是磁盘空间不足等&#xff0c;及时排查并解决这些问题是保证数据库稳定运行的关键。本文将列出 MySQL 中一些常见的错误及其排查方法。 一、连接…