MyBatis 插件开发

6/9/2022 MyBatis

摘要

JDK:1.8.0_202
MyBatis Version:3.5.9

# 一:前言

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

  • Executor(update、query、flushStatements、commit、rollback、getTransaction、close、isClosed),org.apache.ibatis.executor 包下
  • ParameterHandler(getParameterObject、setParameters),org.apache.ibatis.executor.parameter 包下
  • ResultSetHandler(handleResultSets、handleCursorResultSets、handleOutputParameters),org.apache.ibatis.executor.resultset 包下
  • StatementHandler(prepare、parameterize、batch、update、query),org.apache.ibatis.executor.statement 包下

这4个接口及其包含的方法的细节可以通过查看每个方法的定义来了解。如果不仅仅是想调用监控方法,那么应该很好地了解正在重写的方法的行为。因为在试图修改或重写已有方法行为的时候,很可能会破坏 MyBatis 的核心模块。这些都是底层的类和方法,所以使用插件的时候要特别当心。

# 二:拦截器

# 2.1 拦截器接口

MyBatis 插件可以用来实现拦截器接口 Interceptor,在实现类中对拦截对象和方法进行处理。

package org.apache.ibatis.plugin;

public interface Interceptor {

    Object intercept(Invocation invocation) throws Throwable;

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

    default void setProperties(Properties properties) {
    	// NOP
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

intercept

MyBatis 运行时要执行的拦截方法。通过该方法的参数 invocation 可以得到很多有用的信息,该参数的常用方法如下。

public Object intercept(Invocation invocation) throws Throwable {
	// 获取当前被拦截的对象
	Object target = invocation.getTarget();
	// 获取当前被拦截的方法
	Method method = invocation.getMethod();
	// 获取被拦截方法中的参数
	Object[] args = invocation.getArgs();
	// 执行被拦截对象真正的方法
	// proceed() 实际上执行了 method.invoke(target, args) 方法
	Object result = invocation.proceed();
	return result;
}
1
2
3
4
5
6
7
8
9
10
11
12

当配置多个拦截器时,MyBatis 会遍历所有拦截器,按顺序执行拦截器的 plugin 方法,被拦截的对象就会被层层代理。在执行拦截对象的方法时,会一层层地调用拦截器,拦截器通过 invocation.proceed() 调用下一层的方法,直到真正的方法被执行。方法执行的结果会从最里面开始向外一层层返回,所以如果存在按顺序配置的 A、B、C 三个签名相同的拦截器,MyBaits 会按照 C>B>A>target.proceed()>A>B>C 的顺序执行。如果A、B、C签名不同,就会按照 MyBatis 拦截对象的逻辑执行。

plugin

这个方法的参数target就是拦截器要拦截的对象,该方法会在创建被拦截的接口实现类时被调用。该方法的实现很简单,只需要调用 MyBatis 提供的 Plugin(org.apache.ibatis.plugin.Plugin)类的wrap静态方法就可以通过Java的动态代理拦截目标对象。这个接口方法通常的实现代码如下

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

Plugin.wrap 方法会自动判断拦截器的签名和被拦截对象的接口是否匹配,只有匹配的情况下才会使用动态代理拦截目标对象,因此在上面的实现方法中不必做额外的逻辑判断。

setProperties

这个方法用来传递插件的参数,可以通过参数来改变插件的行为。

在 mybatis-config.xml 中,一般情况下,拦截器的配置如下:

<plugins>
    <plugin interceptor="top.qform.plugin.XXXInterceptor">
        <property name="prop1" value="value1"/>
        <property name="prop2" value="value2"/>
    </plugin>
</plugins>
1
2
3
4
5
6

如果需要参数,可以在 plugin 标签内通过 properties 标签进行配置,配置后的参数在拦截器初始化时会通过 setProperties 方法传递给拦截器。

# 2.2 拦截器签名

除了需要实现拦截器接口外,还需要给实现类配置以下的拦截器注解。

  • @Intercepts(org.apache.ibatis.plugin.Intercepts):拦截器注解
  • @Signature(org.apache.ibatis.plugin.Signature):签名注解

@Intercepts 注解中的属性是一个 @Signature(签名) 数组。

@Intercepts({
	@Signature(
		type = ResultSetHandler.class,
		method = "handleResultSets",
		args = {Statement.class}
	})
})
public class ResultSetInterceptor implements Interceptor
1
2
3
4
5
6
7
8

@Signature 注释包含的属性:

  • type:设置拦截的接口,可选值 Executor、ParameterHandler、ResultSetHandler、StatementHandler 四个接口
  • method:设置拦截接口中的方法名,可选值 Executor、ParameterHandler、ResultSetHandler、StatementHandler 4个接口对应的方法,需要和接口匹配。
  • args:设置拦截方法的参数类型数组,通过方法名和参数类型可以确定唯一一个方法。

由于MyBatis代码具体实现的原因,可以被拦截的4个接口中的方法并不是都可以被拦截的。下面将针对这4种接口,将可以被拦截的方法以及方法被调用的位置和对应的拦截器签名依次列举出来,

# 2.3 Executor接口

package org.apache.ibatis.executor;

public interface Executor {

  ResultHandler NO_RESULT_HANDLER = null;

  int update(MappedStatement ms, Object parameter) throws SQLException;

  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;

  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

  <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;

  List<BatchResult> flushStatements() throws SQLException;

  void commit(boolean required) throws SQLException;

  void rollback(boolean required) throws SQLException;

  CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);

  boolean isCached(MappedStatement ms, CacheKey key);

  void clearLocalCache();

  void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);

  Transaction getTransaction();

  void close(boolean forceRollback);

  boolean isClosed();

  void setExecutorWrapper(Executor executor);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
  • int update(MappedStatement ms, Object parameter) throws SQLException:该方法会在所有的 INSERT、UPDATE、DELETE 执行时被调用,因此如果想要拦截这3类操作,可以拦截该方法。接口方法对应的签名如下。
@Signature(
	type = Executor.class,
	method = "update",
	args = {MappedStatement.class, Object.class})
1
2
3
4
  • <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException:该方法会在所有 SELECT 查询方法执行时被调用。通过这个接口参数可以获取很多有用的信息,因此这是最常被拦截的一个方法。使用该方法需要注意的是,虽然接口中还有一个参数更多的同名接口,但由于 MyBatis 的设计原因,这个参数多的接口不能被拦截。接口方法对应的签名如下。
@Signature(
	type = Executor.class,
	method = "query",
	args = {MappedStatement.class, Object.class,
			RowBounds.class, ResultHandler.class})
1
2
3
4
5
  • <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException:该方法只有在查询的返回值类型为 Cursor 时被调用。接口方法对应的签名如下。
@Signature(
	type = Executor.class,
	method = "queryCursor",
	args = {MappedStatement.class, Object.class, RowBounds.class})
1
2
3
4
  • List<BatchResult> flushStatements() throws SQLException:该方法只在通过 SqlSession 方法调用 flushStatements 方法或执行的接口方法中带有 @Flush 注解时才被调用,接口方法对应的签名如下。
@Signature(
	type = Executor.class,
	method = "flushStatements",
	args = {})
1
2
3
4
  • void commit(boolean required) throws SQLException:该方法只在通过SqlSession方法调用commit方法时才被调用,接口方法对应的签名如下。
@Signature(
	type = Executor.class,
	method = "commit",
	args = {boolean.class})
1
2
3
4
  • void rollback(boolean required) throws SQLException:该方法只在通过SqlSession方法调用rollback方法时才被调用,接口方法对应的签名如下。
@Signature(
	type = Executor.class,
	method = "rollback",
	args = {boolean.class})
1
2
3
4
  • Transaction getTransaction():该方法只在通过SqlSession方法获取数据库连接时才被调用,接口方法对应的签名如下。
@Signature(
	type = Executor.class,
	method = "getTransaction",
	args = {})
1
2
3
4
  • void close(boolean forceRollback):该方法只在延迟加载获取新的Executor后才会被执行,接口方法对应的签名如下。
@Signature(
	type = Executor.class,
	method = "close",
	args = {boolean.class})
1
2
3
4
  • boolean isClosed():该方法只在延迟加载执行查询方法前被执行,接口方法对应的签名如下。
@Signature(
	type = Executor.class,
	method = "isClosed",
	args = {})
1
2
3
4

# 2.4 ParameterHandler接口

package org.apache.ibatis.executor.parameter;

public interface ParameterHandler {

  Object getParameterObject();

  void setParameters(PreparedStatement ps) throws SQLException;

}
1
2
3
4
5
6
7
8
9
  • Object getParameterObject():该方法只在执行存储过程处理出参的时候被调用。
@Signature(
	type = ParameterHandler.class,
	method = "getParameterObject",
	args = {})
1
2
3
4
  • void setParameters(PreparedStatement ps) throws SQLException:该方法在所有数据库方法设置SQL参数时被调用。接口方法对应的签名如下。
@Signature(
	type = ParameterHandler.class,
	method = "setParameters",
	args = {PreparedStatement.class})
1
2
3
4

# 2.5 ResultSetHandler接口

package org.apache.ibatis.executor.resultset;

public interface ResultSetHandler {

  <E> List<E> handleResultSets(Statement stmt) throws SQLException;

  <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;

  void handleOutputParameters(CallableStatement cs) throws SQLException;

}
1
2
3
4
5
6
7
8
9
10
11
  • <E> List<E> handleResultSets(Statement stmt) throws SQLException:该方法会在除存储过程及返回值类型为 Cursor\<T>(org.apache.ibatis.cursor.Cursor\<T>) 以外的查询方法中被调用。接口方法对应的签名如下。
@Signature(
	type = ResultSetHandler.class,
	method = "handleResultSets",
	args = {Statement.class})
1
2
3
4

对于拦截处理 MyBatis 的查询结果非常有用,并且由于这个接口被调用的位置在处理二级缓存之前,因此通过这种方式处理的结果可以执行二级缓存。

  • <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException:该方法是 3.4.0 版本中新增加的,只会在返回值类型为 Cursor<T> 的查询方法中被调用,接口方法对应的签名如下。
@Signature(
	type = ResultSetHandler.class,
	method = "handleCursorResultSets",
	args = {Statement.class})
1
2
3
4
  • void handleOutputParameters(CallableStatement cs) throws SQLException:该方法只在使用存储过程处理出参时被调用,接口方法对应的签名如下。
@Signature(
	type = ResultSetHandler.class,
	method = "handleCursorResultSets",
	args = {Statement.class})
1
2
3
4

# 2.6 StatementHandler接口

package org.apache.ibatis.executor.statement;

public interface StatementHandler {

  Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException;

  void parameterize(Statement statement) throws SQLException;

  void batch(Statement statement) throws SQLException;

  int update(Statement statement) throws SQLException;

  <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException;

  <E> Cursor<E> queryCursor(Statement statement) throws SQLException;

  BoundSql getBoundSql();

  ParameterHandler getParameterHandler();

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  • Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException:该方法会在数据库执行前被调用,优先于当前接口中的其他方法而被执行。接口方法对应的签名如下。
@Signature(
	type = StatementHandler.class,
	method = "prepare",
	args = {Connection.class, Integer.class})
1
2
3
4
  • void parameterize(Statement statement) throws SQLException:该方法在 prepare方法之后执行,用于处理参数信息,接口方法对应的签名如下。
@Signature(
	type = StatementHandler.class,
	method = "parameterize",
	args = {Statement.class})
1
2
3
4
  • void batch(Statement statement) throws SQLException:在全局设置配置 defaultExecutorType="BATCH" 时,执行数据操作才会调用该方法,接口方法对应的签名如下。
@Signature(
	type = StatementHandler.class,
	method = "batch",
	args = {Statement.class})
1
2
3
4
  • <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException:执行SELECT方法时调用,接口方法对应的签名如下。
@Signature(
	type = StatementHandler.class,
	method = "query",
	args = {Statement.class, ResultHandler.class})
1
2
3
4
  • <E> Cursor<E> queryCursor(Statement statement) throws SQLException:该方法是3.4.0版本中新增加的,只会在返回值类型为Cursor<T>的查询中被调用,接口方法对应的签名如下。
@Signature(
	type = StatementHandler.class,
	method = "queryCursor",
	args = {Statement.class})
1
2
3
4

# 三:

# 六:参考文献

最后更新: 6/15/2022, 2:47:51 PM