[mybatis source code analysis] principle of mybatis plug-in

Posted by magic003 on Mon, 07 Feb 2022 04:51:14 +0100

MyBatis provides a function of plug-in. Although it is called plug-in, it is actually an interceptor function. So what does the interceptor intercept in MyBatis?
We enter Official website have a look:
MyBatis allows you to intercept calls at some point during the execution of mapped statements. By default, MyBatis allows the use of plug-ins to intercept method calls, including:
1.Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
2.ParameterHandler (getParameterObject, setParameters)
3.ResultSetHandler (handleResultSets, handleOutputParameters)
4.StatementHandler (prepare, parameterize, batch, update, query)
We have seen some methods that can intercept the Executor interface, such as update, query, commit, rollback and other methods, as well as some methods of other interfaces.
The general summary is as follows:
1. Method of intercepting actuator
2. Processing of interception parameters
3. Processing of interception result set
4. Processing of intercepting Sql syntax construction

Use of plug-ins

Introduction and configuration of interceptor

First, let's take a look at the interface definition of MyBatis Interceptor:

public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  Object plugin(Object target);

  void setProperties(Properties properties);

}

Relatively simple, there are only three methods. MyBatis does not have an implementation class of interceptor interface by default. Developers can implement interceptors that meet their own needs.
The following is an example of an interceptor on the MyBatis official website:

@Intercepts({@Signature(
  type= Executor.class,
  method = "update",
  args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
  public Object intercept(Invocation invocation) throws Throwable {
    return invocation.proceed();
  }
  public Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }
  public void setProperties(Properties properties) {
  }
}

Global xml configuration:

<plugins>
    <plugin interceptor="org.format.mybatis.cache.interceptor.ExamplePlugin"></plugin>
</plugins>

This interceptor intercepts the update method of the executor interface (in fact, the addition, deletion and modification of SqlSession). All update methods executing the executor will be intercepted by this interceptor.

Source code analysis

Let's analyze the source code behind this code.
Start with source - > configuration file:
XMLConfigBuilder resolves the pluginElement private method of MyBatis global configuration file:

private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        String interceptor = child.getStringAttribute("interceptor");
        Properties properties = child.getChildrenAsProperties();
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
        interceptorInstance.setProperties(properties);
        configuration.addInterceptor(interceptorInstance);
      }
    }
}

The specific parsing code is actually relatively simple and will not be pasted. It is mainly to instantiate the class represented by the interceptor attribute in the plugin node through reflection. Then call the global configuration class Configuration's addInterceptor method.

public void addInterceptor(Interceptor interceptor) {
       interceptorChain.addInterceptor(interceptor);
}

This interceptorchain is the internal attribute of Configuration. The type is interceptorchain, which is an interceptor chain. Let's see its definition:

public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<Interceptor>();

  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }

  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }

  public List<Interceptor> getInterceptors() {
    return Collections.unmodifiableList(interceptors);
  }

}

Now that we understand the analysis of interceptor configuration and the ownership of interceptors, let's look back and see why interceptors intercept these methods (some methods of Executor, ParameterHandler, ResultSetHandler and 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 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 Executor newExecutor(Transaction transaction, ExecutorType executorType, boolean autoCommit) {
    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, autoCommit);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

The above four methods are all Configuration methods. These methods will be executed in an operation (add, delete, modify, query) of MyBatis. The order of execution is Executor, ParameterHandler, ResultSetHandler, Statementhandler (ParameterHandler and ResultSetHandler are created when the constructors of statementhandler [three available implementation classes callablestatementhandler, preparedstatementhandler and simplestationhandler] are created [the constructors of these three implementation classes actually call the constructors of the parent class BaseStatementHandler]).
These 4 methods, after instantiating the corresponding objects, will call the pluginAll method of interceptorChain. The pluginAll of InterceptorChain has just been introduced, that is, traversing all the interceptors, then calling the plugin method of each interceptor. Note: the return value of the plugin method of the interceptor will be directly assigned to the original object
Because StatementHandler can be intercepted, this interface mainly deals with the construction of sql syntax. Therefore, for example, the function of paging can be implemented by interceptor. You only need to process the sql in the StatementHandler interface implementation class in the plugin method of interceptor, which can be implemented by reflection.
MyBatis also provides @ Intercepts and @ Signature annotations on interceptors. The example of the official website uses these two annotations, including the use of Plugin class:

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

Let's analyze the source code of these three "new combinations". First, let's look at the wrap method of Plugin class:

public static Object wrap(Object target, Interceptor interceptor) {
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
}

The Plugin class implements the InvocationHandler interface. Obviously, we see that a dynamic proxy class provided by JDK itself is returned here. Let's dissect other methods called by this method:
getSignatureMap method:

private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
    if (interceptsAnnotation == null) { // issue #251
      throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());      
    }
    Signature[] sigs = interceptsAnnotation.value();
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
    for (Signature sig : sigs) {
      Set<Method> methods = signatureMap.get(sig.type());
      if (methods == null) {
        methods = new HashSet<Method>();
        signatureMap.put(sig.type(), methods);
      }
      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;
}

getSignatureMap method explanation: first get the @ Interceptors annotation of the interceptor Class, then get the attribute @ Signature annotation Set of this annotation, and then traverse this Set. When traversing, take out the type attribute (Class type) of the @ Signature annotation, and then get the method with method attribute and args attribute according to this type. Since the @ Signature attribute of the @ Interceptors annotation is an attribute, it will eventually return a Map with type as key and value as Set.

@Intercepts({@Signature(
  type= Executor.class,
  method = "update",
  args = {MappedStatement.class,Object.class})})

For example, the @ Interceptors annotation will return a set with key as Executor and value as set (this set has only one element, that is, method instance, which is the update method of Executor interface, and this method has MappedStatement and Object type parameters). This method instance is obtained according to the method and args properties of @ Signature. If the args parameter does not correspond to the method method method of type, an exception will be thrown.
getAllInterfaces method:

private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
    Set<Class<?>> interfaces = new HashSet<Class<?>>();
    while (type != null) {
      for (Class<?> c : type.getInterfaces()) {
        if (signatureMap.containsKey(c)) {
          interfaces.add(c);
        }
      }
      type = type.getSuperclass();
    }
    return interfaces.toArray(new Class<?>[interfaces.size()]);
}

getAllInterfaces method explains: according to the target instance target (the target is the class that the MyBatis interceptor can intercept, executor, parameterhandler, resultsethandler and statementhandler) and its parent classes, return the interface array containing the target implementation in the signatureMap.
Therefore, the function of Plugin class is to get the attribute @ Signature array of this annotation according to the @ Interceptors annotation, and then use reflection to find the corresponding method according to the type, method and args attributes of each @ Signature annotation. Finally, the interface implemented by the called target object determines whether to return a proxy object to replace the original target object.
For example, in the example of MyBatis official website, when Configuration calls the newExecutor method, the update(MappedStatement ms, Object parameter) method of the Executor interface is intercepted by the interceptor. Therefore, the final return is a proxy class Plugin, not Executor. When a method is called in this way, if it is a proxy class, it will execute:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      if (methods != null && methods.contains(method)) {
        return interceptor.intercept(new Invocation(target, method, args));
      }
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
}

Yes, if the corresponding method is found to be proxied, the interceptor method of the interceptor interface will be executed.
This Invocation class is as follows:

public class Invocation {

  private Object target;
  private Method method;
  private Object[] args;

  public Invocation(Object target, Method method, Object[] args) {
    this.target = target;
    this.method = method;
    this.args = args;
  }

  public Object getTarget() {
    return target;
  }

  public Method getMethod() {
    return method;
  }

  public Object[] getArgs() {
    return args;
  }

  public Object proceed() throws InvocationTargetException, IllegalAccessException {
    return method.invoke(target, args);
  }

}

Its processed method is to call the original method (without proxy).

summary

Among the three methods provided by the MyBatis interceptor interface, the plugin method is used for the construction of some processors (handlers). The interceptor method is used to handle the execution of the proxy class. The setProperties method is used to set the interceptor properties.
In fact, we don't have to use @ Interceptors, @ Signature annotation and plugin class to deal with Interceptors directly. We can also discard these three classes and directly do corresponding operations within the plugin method according to the type of target instance.
Generally speaking, the MyBatis interceptor is still very simple. The interceptor itself does not need too many knowledge points, but learning the interceptor needs to be familiar with each interface in MyBatis, because the interceptor involves the knowledge points of each interface.

Topics: Java Mybatis