MyBatis source code analysis - plug-in related

In this article, I will introduce the source code of the plug-in. The MyBatis plug-in is a good function, similar to Spring's Aop, so that our users can operate SQL flexibly at specific points of the process.

Allow me to quote an online introduction because I think it is well summarized: MyBatis allows us to intercept and call a certain point during the execution of mapped statements in the form of plug-ins. Generally speaking, MyBatis plug-ins should be called interceptors. Generally speaking, plug-ins are well understood, so this article will not be very long, Therefore, in order to expand the length of the article, finally, we will use the paging plug-in of MyBatis plus as an example to explain how to cut in.

1. Basic use

It is generally used as follows:

@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,
                RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})})
@Component
public class TestExecutorInterceptor implements Interceptor{
  
}

MyBatis allows the plug-in to intercept the methods of the following four objects.

  • update, query, flushStatements, commit, rollback, getTransaction, close, isClosed methods of the Executor
  • Getparameterobject and setparameters methods of ParameterHandler
  • handleResultSets, handleOutputParameters methods of ResultSetHandler
  • prepare, parameterize, batch, update, query methods of StatementHandler

If you want to know why these are, you can click the pluginAll method of InterceptorChain to find that they are called by the above classes. We can find that there is an Interceptor chain in which interceptors maintain the list of Interceptor implementation classes. Therefore, when we need to use the plug-in function, we need to create a new Interceptor implementation class and implement the intercept method, and then declare it as a Bean so that it can be added to interceptors. When the methods related to the above four classes are called, Generate a proxy class and call. Let's start with the basic usage and principle of the plug-in.

2. Start with the Executor

According to the method we mentioned above, I'll take the Executor as an example. Then, clicking on the newExecutor method is also the method I covered in the previous article

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);
  }
  // The above has been explained in the previous article. You can have a look if you are interested,
  // Call the plug-in here with the executor as a parameter.
  executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}

Let's look at pluginAll.

public Object pluginAll(Object target) {
  // Traverse interceptors, including the ones we created.
  for (Interceptor interceptor : interceptors) {
    target = interceptor.plugin(target);
  }
  return target;
}

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

pluginAll is very simple. It just traverses the interceptors and calls the Interceptor's default plugin method. Of course, this is just a layer of skin, and it calls the wrap method of plugin. So let's look at plugin's wrap method.

public static Object wrap(Object target, Interceptor interceptor) {
  // key is the type in the Signature, that is, class, and value is the method set in the Signature
  Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
  // Get the class of target. This target refers to the class we want to proxy, not the class we write that inherits from Interceptor. Here, it refers to Executor
  Class<?> type = target.getClass();
  // Get all the interfaces implemented by the target class, and it is in the signatureMap key.
  Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
  if (interfaces.length > 0) {
    // Generate a proxy class for the target class through Java's native proxy.
    return Proxy.newProxyInstance(
        type.getClassLoader(),
        interfaces,
        new Plugin(target, interceptor, signatureMap));
  }
  return target;
}

2.1 obtain intercept annotations and other information

private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
  // Get the Intercepts annotation information on the implementation class
  Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
  // issue #251
  // If it doesn't exist, throw an exception
  if (interceptsAnnotation == null) {
    throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
  }
  // Gets the Signature annotation array on the annotation
  Signature[] sigs = interceptsAnnotation.value();
  // map of classes and methods on the Signature annotation
  Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
 	// Traverse and set
  for (Signature sig : sigs) {
    Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
    try {
      Method method = sig.type().getMethod(sig.method(), sig.args());
      methods.add(method);
    } catch (NoSuchMethodException e) {
      throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
    }
  }
  return signatureMap;
}

This method is easy to understand. It is to obtain the signature map.

Take another look at getAllInterfaces

private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
  Set<Class<?>> interfaces = new HashSet<>();
  while (type != null) {
    // Traverse all interfaces implemented by the proxy class, and if the signatureMap contains the interface name, add it to the interfaces.
    for (Class<?> c : type.getInterfaces()) {
      if (signatureMap.containsKey(c)) {
        interfaces.add(c);
      }
    }
    type = type.getSuperclass();
  }
  return interfaces.toArray(new Class<?>[interfaces.size()]);
}

This method is not difficult to understand. As for why we use this method, take the Executor as an example. We can see it in the newExecutor. We pass the parameter cacheingexecution, that is, the implementation class of the Executor, but we declare the Executor in the initial TestExecutorInterceptor, Therefore, this can avoid that the caching Executor that should have been proxied is not proxied.

2.2 agency method

Plugin implements the InvocationHandler interface, so it will call the invoke method to intercept the calls of all methods of the proxy class. See invoke:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    // It is to judge whether the intercepted method is the method we declare to intercept.
    Set<Method> methods = signatureMap.get(method.getDeclaringClass());
    if (methods != null && methods.contains(method)) {
      // If yes, execute the logic of the plug-in
      return interceptor.intercept(new Invocation(target, method, args));
    }
    // If not, go straight to the original logic.
    return method.invoke(target, args);
  } catch (Exception e) {
    throw ExceptionUtil.unwrapThrowable(e);
  }
}

There are few invoke codes, mainly to judge whether the intercepting method is the method we declare to cut in. If so, we will go to the intercept method under our custom class.

In addition, it can be noted that there are more than one class implementing Interceptor. InterceptorChain calls pluginAll to traverse, so the finally generated class is actually wrapped layer by layer, so it will be called slowly from the outermost layer to the inner layer at that time.

3. Analyze the source code of mybatis plus paging plug-in

Mybatis plus is a very easy-to-use plug-in (this plug-in is not the other plug-in). Why not say it is very easy to use, because I personally think they have a little opinion that they only do enhancement without change on the official website. It doesn't feel like what they say, but a little intrusive. Of course, this is my family's opinion, and its other functions are very convenient.

First look at the example code of the official website.

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
    return interceptor;
}

The example code of the official website is relatively simple, just three lines

  1. Create a new MybatisPlusInterceptor
  2. Add a PaginationInnerInterceptor to its internal list. This class implements InnerInterceptor
  3. Use this MybatisPlusInterceptor as a bean
@SuppressWarnings({"rawtypes"})
@Intercepts(
    {
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
    }
)
// The query and update of the Executor and the prepare of the StatementHandler are intercepted
public class MybatisPlusInterceptor implements Interceptor {

  	// An InnerInterceptor list is maintained internally.
    @Setter
    private List<InnerInterceptor> interceptors = new ArrayList<>();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object target = invocation.getTarget();
        Object[] args = invocation.getArgs();
      	// If the target class is Executor
        if (target instanceof Executor) {
            final Executor executor = (Executor) target;
          	// Get the second of the method parameters, that is, all the parameters
            Object parameter = args[1];
          	// Obviously, the update method of the Executor has only two parameters
            boolean isUpdate = args.length == 2;
            MappedStatement ms = (MappedStatement) args[0];
          	// If it is a query
            if (!isUpdate && ms.getSqlCommandType() == SqlCommandType.SELECT) {
                RowBounds rowBounds = (RowBounds) args[2];
                ResultHandler resultHandler = (ResultHandler) args[3];
                BoundSql boundSql;
              	// If you call a query method with only four parameters, get BoundSql
                if (args.length == 4) {
                    boundSql = ms.getBoundSql(parameter);
                } else {
                    // It's almost impossible to get into this unless you call query[args[6]] using the Executor's proxy object
                    boundSql = (BoundSql) args[5];
                }
              	// Traverse the class implemented from InnerInterceptor
                for (InnerInterceptor query : interceptors) {
                  	// Judge whether the query operation can be performed
                  	// PaginationInnerInterceptor rewrites it and counts it. If the count is 0, it returns false and will not execute SQL
                  	// Although the internal logic still executes SQL, it just says
                    if (!query.willDoQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql)) {
                        return Collections.emptyList();
                    }
                  	// Perform the operation of querying money
                    query.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                }
                CacheKey cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
                return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
            } else if (isUpdate) {
              	// If it is an update
                for (InnerInterceptor update : interceptors) {
                    if (!update.willDoUpdate(executor, ms, parameter)) {
                        return -1;
                    }
                    update.beforeUpdate(executor, ms, parameter);
                }
            }
        } else {
            // StatementHandler
            final StatementHandler sh = (StatementHandler) target;
            Connection connections = (Connection) args[0];
            Integer transactionTimeout = (Integer) args[1];
            for (InnerInterceptor innerInterceptor : interceptors) {
                innerInterceptor.beforePrepare(sh, connections, transactionTimeout);
            }
        }
        return invocation.proceed();
    }
  
  // Irrelevant methods are omitted
}

All that should be said is in the comments. Why do I say it is strongly intrusive is that it encapsulates all the parameters of our query conditions into one parameter. In this way, if you want to change the incoming parameters of a specific class without modifying the existing business, there will be a great limit. Of course, there is nothing you can do, but it is more troublesome.

above!

Keywords: Java Mybatis Spring batch

Added by jtgraphic on Wed, 15 Dec 2021 22:22:13 +0200