foreword
In this article, we will introduce the development of the MyBatis plug-in. This also comes from my previous interview experience. The interviewer asked me how to count the slow SQL of the Dao layer. My answer at that time was to use Spring's AOP mechanism to intercept all the Dao layer. method, but the interviewer asked again, this is not actually the execution time of SQL, there are other code time in it, and asked me if I have any other ideas? I thought about it and said no, the interviewer then asked, have you ever been in contact with the development of MyBatis plugins? I said no contact. But it was also given to me later. I think this question is a valuable question, so I also put it in my study plan.
Before reading this article, it is recommended to read:
- "Agent Mode - Introduction to AOP"
- "Pretending to be Xiaobai's Re-learning MyBatis (1)"
If someone asks where to find the above two articles, you can go to the Nuggets or think about it. There is no official account yet. It is expected that the articles on the three platforms will be unified in the middle of the year.
Overview
If you read the official documents, MyBatis does not give a specific definition of the plug-in, but it is basically an interceptor. MyBatis's plug-ins are some interceptors that can intercept some MyBats core component methods and enhance functions. The official documentation lists four pointcuts that can be enhanced:
- Executor
The core component that executes SQL. Intercepting the Executor means interfering with or enhancing the underlying CRUD operations performed
- ParameterHandler
Intercepting the ParameterHandler means interfering with SQL parameter injection and reading.
- ResultSetHandler
Intercept the ParameterHandler to interfere/enhance the action of encapsulating the result set
- StatementHandler
Intercepting StatementHandler means interfering/enhancing the creation and execution of Statement
Of course, it still starts with HelloWorld
To be a plug-in of MyBatis, you must first implement the Interceptor interface of MyBatis. Be careful not to mislead the class. Interceptor is very popular. This class is located under org.apache.ibatis.plugin.Interceptor. To implement this interface, MyBatis will use the implementation class as an interceptor of MyBatis. Which methods should be intercepted and how should I specify it? It is implemented through the @Intercepts annotation. The following is an example of use:
@Intercepts(@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
public class MyBatisPluginDemo implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("into invocation ..........");
System.out.println(invocation.getTarget());
System.out.println(invocation.getMethod().getName());
System.out.println(Arrays.toString(invocation.getArgs()));
return invocation.proceed();
}
}
@Intercepts可以填多个@Signature,@Signature是方法签名,type用于定位类,method定位方法名,args用于指定方法的参数类型。三者加在一起就可以定位到具体的方法。注意写完还需要将此插件注册到MyBatis的配置文件中,让MyBatis加载该插件。
Note that this label must be placed above the environments, MyBatis strictly limits the order of labels.
<plugins>
<plugin interceptor="org.example.mybatis.MyBatisPluginDemo"></plugin>
</plugins>
Let's take a look at the execution results:
Performance analysis plug-in to go
Who will stop? At present, there are only Executor and StatementHandler for us to choose from. We are looking at the time-consuming of SQL. Executor is still a little far from SQL execution. It only goes to SQL execution layer by layer. The execution process of tags in MyBatis is in "MyBatis Source Code Study Notes" (1) The First Encounter" has already been described, and I won't repeat it here. At present, StatementHandler is the closest to SQL, and its implementation class goes directly to JDBC, so we intercept StatementHandler, and some inserts are inserted a lot. value, do we want to intercept, of course, we have to intercept, our plug-in method is as follows:
@Intercepts({@Signature(type = StatementHandler.class, method = "query",
args = {Statement.class, ResultHandler.class}), @Signature(type = StatementHandler.class,method = "update" ,args = Statement.class )})
public class MyBatisSlowSqlPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("-----开始进入性能分析插件中----");
long startTime = System.currentTimeMillis();
Object result = invocation.proceed();
long endTime = System.currentTimeMillis();
// query方法入参是statement,所以我们可以将其转为Statement
if (endTime - startTime > 1000){
}
return result;
}
}
How to get the corresponding SQL? Let's go to StatementHandler to see:
We still have to get it through the input parameter of Statement. Let's try it out. You will find that when the log level is DEBUG, SQL will be output, like the following:
If the log level is DEBUG the output will be as follows:
Why is this? If you have read "MyBatis Source Code Study Notes (1) First Encounter", you may think that the log module in the MyBatis architecture will use an agent in order to access the log framework, so this must be the agent class, we call Breakpoint to test our idea:
Proxy Analysis
My original idea was the proxy class of PreparedStatementLogger. After thinking about it carefully, I felt that it was wrong. I felt that I still did not understand the proxy mode very well, so I read the previous article "Proxy Mode - AOP Introduction". The dynamic proxy mode The goal:
- We have a bunch of classes, and we want to enhance them without changing them, and we also want to focus only on writing the code that enhances the target object.
- We also want these classes to be written by programs, not by programmers, because there are too many.
What we do in "Agent Pattern - AOP Introduction" is a very simple proxy:
public interface IRentHouse {
void rentHouse();
void study();
}
public class RentHouse implements IRentHouse{
@Override
public void rentHouse() {
System.out.println("sayHello.....");
}
@Override
public void study() {
System.out.println("say Study");
}
}
Our current requirement is to enhance the methods in IRentHouse. Using static proxy is to make another implementation class for IRentHouse, which is equivalent to wrapping another layer on RentHouse. But if I have a lot of classes that I want to enhance, wrapping them in this way is actually very intrusive to the code. For this situation, our final choice is dynamic proxy, which generates the proxy class of the interface implementation class at runtime. The method we finally generate the proxy object is:
/**
* @param target 为需要增强的类
* @return 返回的对象在调用接口中的任意方法都会走到Lambda回调中。
*/
private static Object getProxy(Object target){
Object proxy = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), (proxy1, method, args) -> {
System.out.println("方法开始执行..........");
Object obj = method.invoke(target, args);
System.out.println("方法执行结束..........");
return obj;
});
return proxy;
}
Next, let's take a look at how MyBatis is packaged. Let's start with PreparedStatementLogger:
InvocationHandler is the interface of dynamic proxy, BaseJdbcLogger will not pay attention to it. It is noteworthy that:
public static PreparedStatement newInstance(PreparedStatement stmt, Log statementLog, int queryStack) {
InvocationHandler handler = new PreparedStatementLogger(stmt, statementLog, queryStack);
ClassLoader cl = PreparedStatement.class.getClassLoader();
return (PreparedStatement) Proxy.newProxyInstance(cl, new Class[]{PreparedStatement.class, CallableStatement.class}, handler);
}
Some students may ask why newProxyInstance gave two parameters, because CallableStatement inherits PreparedStatement. This is one layer. In fact, another layer can be pointed out. In the callback of ConnectionLogger (ConnectionLogger also implements InvocationHandler, so this is also a proxy callback class), the instantiation of ConnectionLogger is completed in the class BaseExecutor, if you still If you can recall that JDBC generates SQL, the process at that time is actually like this:
public static boolean execute(String sql, Object... param) throws Exception {
boolean result = true;
Connection connection = null;
PreparedStatement preparedStatement = null;
try {
//获取数据库连接
connection = getConnection();
connection.setAutoCommit(false);
preparedStatement = connection.prepareStatement(sql);
// 设置参数
for (int i = 0; i < param.length; i++) {
preparedStatement.setObject(i, param[i]);
preparedStatement.addBatch();
}
preparedStatement.executeBatch();
//提交事务
connection.commit();
} catch (SQLException e) {
e.printStackTrace();
if (connection != null) {
try {
connection.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
// 日志记录事务回滚失败
result = false;
return result;
}
}
result = false;
} finally {
close(preparedStatement, connection);
}
return result;
}
Let's take a look, ConnectionLogger is the proxy for reading Connection, but there are many methods in the Connection interface, so ConnectionLogger makes a judgment when calling back:
@Override
public Object invoke(Object proxy, Method method, Object[] params)
throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, params);
}
if ("prepareStatement".equals(method.getName()) || "prepareCall".equals(method.getName())) {
if (isDebugEnabled()) {
debug(" Preparing: " + removeExtraWhitespace((String) params[0]), true);
}
// Connection 的prepareStatement方法、prepareCall会产生PreparedStatement
PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
// 然后PreparedStatementLogger产生的还是stmt的代理类
// 我们在plugin中拿到的就是
stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
return stmt;
} else if ("createStatement".equals(method.getName())) {
Statement stmt = (Statement) method.invoke(connection, params);
stmt = StatementLogger.newInstance(stmt, statementLog, queryStack);
return stmt;
} else {
return method.invoke(connection, params);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
PreparedStatementLogger is a callback class. This PreparedStatementLogger has a corresponding Statement, and we can get the corresponding SQL through the Statement. What is the relationship between the callback class and the proxy class? Let's take a look at the general structure of the Proxy class:
So my initial thought is that the class generated by JDK for us has a callback class instance. This object will have an InvocationHandler member variable, but if you use getClass().getDeclaredField("h") to get it and find that you can't get it, then the proxy class will be Without this callback class instance, let's study the method getProxyClass0:
private static Class<?> getProxyClass0(ClassLoader loader,
Class<?>... interfaces) {
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}
// If the proxy class defined by the given loader implementing
// the given interfaces exists, this will simply return the cached copy;
// otherwise, it will create the proxy class via the ProxyClassFactory
// proxyClassCache 是 new WeakCache<>(new KeyFactory(), new ProxyClassFactory()) 的实例
// 最终会调用ProxyClassFactory的apply方法。
// 在ProxyClassFactory的apply方法中有 ProxyGenerator.generateProxyClass()
// 答案就在其中,最后调用的是ProxyGenerator的generateClassFile方法
// 中产生代理类时,让代理类继承Proxy类。
return proxyClassCache.get(loader, interfaces);
}
So the case is solved, the InvocationHandler in Proxy is protected, so we should take the variable as follows:
@Intercepts({@Signature(type = StatementHandler.class, method = "query",
args = {Statement.class, ResultHandler.class}), @Signature(type = StatementHandler.class,method = "update" ,args = Statement.class )})
public class MyBatisSlowSqlPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("-----开始进入性能分析插件中----");
long startTime = System.currentTimeMillis();
Object result = invocation.proceed();
long endTime = System.currentTimeMillis();
// query方法入参是statement,所以我们可以将其转为Statement
Statement statement = (Statement)invocation.getArgs()[0];
if (Proxy.isProxyClass(statement.getClass())){
Class<?> statementClass = statement.getClass().getSuperclass();
Field targetField = statementClass.getDeclaredField("h");
targetField.setAccessible(true);
PreparedStatementLogger loggerStatement = (PreparedStatementLogger) targetField.get(statement);
PreparedStatement preparedStatement = loggerStatement.getPreparedStatement();
if (endTime - startTime > 1){
System.out.println(preparedStatement.toString());
}
}else {
if (endTime - startTime > 1){
System.out.println(statement.toString());
}
}
return result;
}
}
The final output is as follows:
But this plugin is not so perfect, it is the slow SQL query time, we are now dead
Both of these problems can be solved in MyBatis. We can look at the interface of Interceptor:
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
}
}
setProperties is used to get the value from the configuration file, plugin adds the current plugin, intercept is the real enhancement method. The above two problems have been solved:
- hardcode
First configure in the configuration file
<plugins>
<plugin interceptor="org.example.mybatis.MyBatisSlowSqlPlugin">
<property name = "maxTolerate" value = "10"/>
</plugin>
</plugins>
Then rewrite:
@Override
public void setProperties(Properties properties) {
//maxTolerate 是MyBatisSlowSqlPlugin的成员变量
this.maxTolerate = Long.parseLong(properties.getProperty("maxTolerate"));
}
Recall from JDBC that we actually implement SQl in two ways:
- prepareStatement method in Connection
- createStatement in Connection
In MyBatis, these two methods correspond to different StatementTypes. The PreparedStatementLogger above corresponds to the prepareStatement method in Connection. If you declare the statement as Statement in MyBatis, then our SQL monitoring statement will go wrong, so here we also need to Adapt the Statement statement type separately.
@Intercepts({@Signature(type = StatementHandler.class, method = "query",
args = {Statement.class, ResultHandler.class}), @Signature(type = StatementHandler.class,method = "update" ,args = Statement.class )})
public class MyBatisSlowSqlPlugin implements Interceptor {
private long maxTolerate;
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("-----开始进入性能分析插件中----");
long startTime = System.currentTimeMillis();
Object result = invocation.proceed();
SystemMetaObject
long endTime = System.currentTimeMillis();
// query方法入参是statement,所以我们可以将其转为Statement
Statement statement = (Statement)invocation.getArgs()[0];
if (Proxy.isProxyClass(statement.getClass())){
Class<?> statementClass = statement.getClass().getSuperclass();
Field targetField = statementClass.getDeclaredField("h");
targetField.setAccessible(true);
Object object = targetField.get(statement);
if (object instanceof PreparedStatementLogger) {
PreparedStatementLogger loggerStatement = (PreparedStatementLogger) targetField.get(statement);
PreparedStatement preparedStatement = loggerStatement.getPreparedStatement();
if (endTime - startTime > maxTolerate){
System.out.println(preparedStatement.toString());
}
}else {
// target 是对应的语句处理器
// 为什么不反射拿? Statement 对应的实现类未重写toString方法
// 但是在RoutingStatementHandler 中提供了getBoundSql方法
RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget();
BoundSql boundSql = handler.getBoundSql();
if (endTime - startTime > maxTolerate){
System.out.println(boundSql);
}
}
}else {
if (endTime - startTime > maxTolerate){
System.out.println(statement.toString());
}
}
return result;
}
@Override
public void setProperties(Properties properties) {
this.maxTolerate = Long.parseLong(properties.getProperty("maxTolerate"));
}
}
In fact, the reflection tool class is written in MyBatis, which is SystemMetaObject. The usage example is as follows:
@Intercepts({@Signature(type = StatementHandler.class, method = "query",
args = {Statement.class, ResultHandler.class}), @Signature(type = StatementHandler.class,method = "update" ,args = Statement.class )})
public class MyBatisSlowSqlPlugin implements Interceptor {
private long maxTolerate;
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("-----开始进入性能分析插件中----");
long startTime = System.currentTimeMillis();
Object result = invocation.proceed();
long endTime = System.currentTimeMillis();
// query方法入参是statement,所以我们可以将其转为Statement
Statement statement = (Statement)invocation.getArgs()[0];
MetaObject metaObject = SystemMetaObject.forObject(statement);
if (Proxy.isProxyClass(statement.getClass())){
Object object = metaObject.getValue("h");
if (object instanceof PreparedStatementLogger) {
PreparedStatementLogger loggerStatement = (PreparedStatementLogger) object;
PreparedStatement preparedStatement = loggerStatement.getPreparedStatement();
if (endTime - startTime > maxTolerate){
System.out.println(preparedStatement.toString());
}
}else {
// target 是对应的语句处理器
// 为什么不反射拿? Statement 对应的实现类未重写toString方法
// 但是在RoutingStatementHandler 中提供了getBoundSql方法
RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget();
BoundSql boundSql = handler.getBoundSql();
if (endTime - startTime > maxTolerate){
System.out.println(boundSql);
}
}
}else {
if (endTime - startTime > maxTolerate){
System.out.println(statement.toString());
}
}
return result;
}
@Override
public void setProperties(Properties properties) {
this.maxTolerate = Long.parseLong(properties.getProperty("maxTolerate"));
}
}
Then I have multiple plugins, how to specify the order? Specify it in the configuration file and execute it from top to bottom
<plugins>
<plugin interceptor="org.example.mybatis.MyBatisSlowSqlPlugin01">
<property name = "maxTolerate" value = "10"/>
</plugin>
<plugin interceptor="org.example.mybatis.MyBatisSlowSqlPlugin02">
<property name = "maxTolerate" value = "10"/>
</plugin>
</plugins>
As configured above, the execution order is MyBatisSlowSqlPlugin01, MyBatisSlowSqlPlugin02. What is the execution order of several methods of the plugin?
write at the end
I was deeply moved. It was originally expected to be written in two hours, and then I wrote it for an afternoon.
References
- Example of mybatis interceptor plug-in - get directly executable sql B station teaching video without placeholder https://www.bilibili.com/video/BV1Fh411p7K1?spm_id_from=333.337.search-card.all.click
- MyBatis Advanced https://www.bilibili.com/video/BV1Q4411579K?spm_id_from=333.337.search-card.all.click
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。