引用一段官网的译文
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 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心。
自定义插件的实现步骤大致如下:
实现Mybatis框架的Interceptor接口
全局配置文件mybatis-config.xml中配置自定义插件即可
下面通过一个示例代码来演示一下具体流程
我这里演示一下,自定义SQl语句的拦截方法, 在SQL语句的执行完毕之后, 修改返回的集合中每一个输出对象的某一个具体属性(这里只是演示怎么用, 演示代码场景无法通用,不能用于生产请知,如果有兴趣可以自行研究拓展 )
自定义拦截器MyInterceptor
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*/@Overridepublic Object intercept(Invocation invocation) throws Throwable {System.out.println("method will be invoked......");List list = (List)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*/@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}/*** 自定义插件属性参数配置* @param properties* @return* @throws Throwable* @author kkarma* @date 2023-01-17*/@Overridepublic 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文件中各个标签元素的声明顺序
在全局配置文件中引入自定义插件
以上就是自定义插件的所有实现步骤, 是不是很简单, 下面我们测试一下:
我们写一个单元测试,查询数据库某个单表中的所有数据,看看我们的自定义插件是否生效
@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 libBooks = mapper.selectAllBook();libBooks.forEach(System.out::println);System.out.println("------------------------------------------");sqlSession.close();} catch (IOException e) {e.printStackTrace();}
}
说明我们的自定义插件生效了, 从这里可以看出自定义插件的步骤还是比较简单的,接下来我们通过Mybatis的源码,分析下插件的实现原理究竟是怎么回事。
上面的实例示例代码中我们在Mybatis-config.xml中声明了我们自定义的Interceptor,那么在全局配置文件的解析类中必然存在专门的方发负责解析处理我们自定义插件模块的方法,
在XMLConfigBuilder
类的parseConfiguration()
方法中调用pluginEelment
pluginEelment
方法用来解析全局配置文件中的plugins标签,然后对应的创建Interceptor对象,并且封装对应的属性信息。最后调用了Configuration
对象中addInterceptor(interceptorInstance)
完成拦截器注册
configuration.addInterceptor(interceptorInstance)
方法如下:
Mybatis中的InterceptorChain
开篇我们就已经进行了说明:
MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
之前我们在说Executor
、StatementHandler
、ParameterHandler
、ResultSetHandler
对象创建的过程时简单提过关于拦截器拦截这些对象来实现功能扩展, 下面来看看,上面四个核心类在创建失败都会调用
Configuration
类中的如下代码:
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()方法时,我们的拦截器就开始工作了
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;}
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 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;}
整个过程就是使用责任链模式结合
拦截器链中保存了多个拦截器,会遍历所有的拦截器,调用Interceptor.plugin(Object target)方法
Interceptor.plugin(Object target)方法会调用Plugin类的wrap()方法返回拦截目标对象的动态代理对象
Plugin类实现了InvocationHandler接口, 所以最后代理对象的invoke()方法会被调用
Plugin类的invoke()方法中调用了自定义拦截器的intercept(Invocation invocation)方法,这里就会在原来需要执行的方法之前和之后调用我们的拦截处理逻辑对方法的输入和输出进行扩展处理
如果我们有多个自定义的拦截器,那么他的拦截执行流程是怎么样的呢?
这里我增加了一个拦截器插件, 拦截的还是Executor.class
的query
方法, 通过具体的代码示例来验证一下我们的猜想。
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*/@Overridepublic Object intercept(Invocation invocation) throws Throwable {System.out.println("拦截方法执行之前记录日志...");List list = (List)invocation.proceed();System.out.println("拦截方法执行之后再次记录日志...");return list;}/*** 是否触发intercept方法* @param target* @return* @throws Throwable* @author kkarma* @date 2023-01-17*/@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}/*** 自定义插件属性参数配置* @param properties* @return* @throws Throwable* @author kkarma* @date 2023-01-17*/@Overridepublic 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中注册插件顺序如下:
执行调用后控制台打印结果如下图:
首先我们在配置文件中注册的顺序是MyInterceptor -> MyLogInterceptor
从配置文件中解析创建的Interceptors对象肯定是按照定义的顺序解析出来的, 所以这里再进行动态代理创建的时候也是按照注册的顺序去创建的 MyInterceptor -> MyLogInterceptor
继续执行,发现MyInteceptor的intercept()方法被执行了,然后query()方法被执行,在依次从内往外返回。
从打印日志我们可以看出执行调用的顺序
和注册和创建动态代理对象
的顺序刚好相反。
InterceptorChain
的List是按照插件在配置文件中定义的顺序从上往下的顺序解析、添加的。InterceptorChain
的List的顺序代理相反
。注册创建的过程(包装礼物的过程):
就是我们现在有一堆好看的太空沙(目标对象)想送给别的小朋友, 不能直接送给人吧,
于是你找来了一个瓶子(MyInteceptor)把太空沙装到里面,
但是这个瓶子也很难看, 于是你又骂了一个精致的礼物盒(MyLogInteceptor)做了一下包装,现在好看了,你把它送给你的朋友了。
调用执行的过程(拆礼物的过程):
你的朋友收到了礼物, 她会先把礼物盒拆开(MyLogInteceptor.intercept())被调用,会发现太空沙被用瓶子转起来了,
于是他又把瓶子(MyInteceptor.intercept())打开了,把太空沙(目标对象)倒出来了, 现在她可以开心的玩耍了。
StatementHandler、ParameterHandler、ResultSetHandler的拦截器与Executor的处理流程相同, 这里就不多加赘述了, 感兴趣可以自行研究。