1. Plug-in overview
Quoting a translation from the official website
MyBatis allows you to intercept calls at a certain point during the execution of a mapped statement. By default, MyBatis allows the use of plug-ins to intercept method calls including:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
The details of the methods in these classes can be discovered by looking at the signature of each method, or by looking directly at the source code in the MyBatis distribution.
If you want to do more than monitor method invocations, you'd better have a good understanding of the behavior of the method you want to override. Because when trying to modify or override the behavior of existing methods, it is likely to break the core module of MyBatis. These are lower-level classes and methods, so be careful when using plug-ins.
2. Custom plug-in implementation example
2.1 Custom plug-in implementation steps
The steps to implement a custom plug-in are roughly as follows:
-
Implement the Interceptor interface of the Mybatis framework
-
Just configure the custom plug-in in the global configuration file mybatis-config.xml
The following is a sample code to demonstrate the specific process.
2.2 Custom plug-in sample code
Let me demonstrate here the interception method of a customized SQl statement. After the execution of the SQL statement is completed, a specific attribute of each output object in the returned collection is modified (this is just to demonstrate how to use it. The demonstration code scenario cannot be universal and cannot be used. Please inform me about production, if you are interested, you can research and develop by yourself)
Custom interceptorMyInterceptor
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;
}
}
Modify the global configuration file mybatis-config.xml
这里注意Mybatis-config.xml文件中各个标签元素的声明顺序
Introduce custom plug-ins in the global configuration file
<plugins>
<plugin interceptor="com.kkarma.plugins.MyInterceptor">
<property name="author" value="kkarma"/>
</plugin>
</plugins>
The above are all the implementation steps of the custom plug-in. Is it very simple? Let’s test it:
2.3 Custom plug-in sample code testing
Let's write a unit test to query all the data in a single table in the database to see if our custom plug-in takes effect.
@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();
}
}
This shows that our custom plug-in has taken effect. From here, we can see that the steps to customize the plug-in are relatively simple. Next, we will analyze the implementation principle of the plug-in through the source code of Mybatis.
3 Analysis of plug-in implementation principles
3.1 How plug-ins are parsed and introduced
In the above example code, we declared our custom Interceptor in Mybatis-config.xml, then there must be a dedicated party in the parsing class of the global configuration file responsible for parsing and processing our custom plug-in module method,
is called in method of XMLConfigBuilder
classparseConfiguration()
pluginEelment
pluginEelment
The method is used to parse the plugins tag in the global configuration file, then create a corresponding Interceptor object and encapsulate the corresponding attribute information. Finally, the Configuration
objectaddInterceptor(interceptorInstance)
was called to complete the interceptor registration
configuration.addInterceptor(interceptorInstance)
as follows:
Mybatis ofInterceptorChain
3.2 Who is the plug-in intended for?
We have explained it at the beginning:
MyBatis allows you to intercept calls at a certain point during the execution of a mapped statement. By default, MyBatis allows the use of plug-ins to intercept method calls including:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
Before we were talking about Executor
, StatementHandler
, ParameterHandler
, ResultSetHandler
object creation During the process, we briefly mentioned that interceptors intercept these objects to implement functional expansion. Let's take a look. The above four core classes will call the following code in the
Configuration
class if the creation fails:
- Executor object creation
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;
}
Our interceptor starts working when we call the Executor.query() method
- StatementHandler object creation
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 object creation
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 object creation
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 Processing process of plug-in interception
3.1.1 Interception processing process of a single plug-in
The whole process is combined using the chain of responsibility model
Multiple interceptors are stored in the interceptor chain, and all interceptors will be traversed and the Interceptor.plugin(Object target) method will be called.
The Interceptor.plugin(Object target) method will call the wrap() method of the Plugin class to return the dynamic proxy object of the interception target object.
The Plugin class implements the InvocationHandler interface, so the invoke() method of the proxy object will be called in the end.
The invoke() method of the Plugin class calls the intercept (Invocation invocation) method of the custom interceptor. Here, our interception processing logic will be called before and after the original method that needs to be executed to extend the input and output of the method.
3.1.2 Interception processing process of multiple plug-ins
If we have multiple custom interceptors, what is its interception execution process?
Here I have added an interceptor plug-in, which intercepts the method. Let us verify our conjecture through specific code examples. 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
*/
@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;
}
}
The order of registering plug-ins in mybatis-config.xml is as follows:
<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>
After executing the call, the console prints the result as shown below:
3.1.2.1 Interceptor registration process
First, the order we register in the configuration file is MyInterceptor -> MyLogInterceptor
3.1.2.2 Creation process of proxy object
The Interceptors objects created by parsing from the configuration file must be parsed in the order of definition, so when dynamic agents are created here, they are also created in the order of registration. MyInterceptor -> MyLogInterceptor
3.1.2.3 Calling process of dynamic proxy object
Continue execution and find that the intercept() method of MyInteceptor is executed, and then the query() method is executed, returning from the inside to the outside in sequence.
From the print log, we can see that the order of 执行调用的顺序
and 注册和创建动态代理对象
is exactly the opposite.
3.1.2.4 Conclusion
- Register multiple plug-ins to
InterceptorChain
The List is parsed and added from top to bottom in the order defined by the plug-ins in the configuration file. - When creating a proxy object, it is also proxied in the order of the List of
InterceptorChain
- The process of calling and executing is exactly the same as the process of registration and creation
相反
.
The registration creation process (the process of wrapping gifts):
It’s just that we now have a bunch of beautiful space sand (target object) that we want to give to other children. We can’t give it away directly.
So you found a bottle (MyInteceptor) and filled it with space sand.
But the bottle is also ugly, so you package it in an exquisite gift box (MyLogInteceptor). Now it looks better, and you give it to your friend.
Call the execution process (the process of opening gifts):
When your friend receives a gift, she will open the gift box first (MyLogInteceptor.intercept()) is called, and she will find that the space sand is being rotated in a bottle.
So he opened the bottle (MyInteceptor.intercept()) again and poured out the space sand (target object). Now she can play happily.
The interceptors of StatementHandler, ParameterHandler, and ResultSetHandler have the same processing flow as the Executor. I won’t go into details here. If you are interested, you can study it yourself.