03 mybatis interceptor mechanism

Keywords: Java Mybatis Attribute SQL xml

Catalog

Reprinted from Research on the principle of MyBatis interceptor

Introduction to MyBatis interceptor

MyBatis provides a plug-in function. Although it is called a 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 make 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 of other interfaces.

In general:

  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 interceptors

Introduction and configuration of interceptor

First, let's 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);

}

It's relatively simple. There are only three ways MyBatis does not have an implementation class of interceptor interface by default. Developers can implement interceptors that meet their needs.

Here is an example of an interceptor on MyBatis 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>

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

Source code analysis

Let's analyze the source code behind this code.

First, analyze from source - > configuration file:

XMLConfigBuilder resolves 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);
      }
    }
}

In fact, the specific parsing code is relatively simple, so it's not pasted. It's 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 an internal property of Configuration. Its 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 go back and see why interceptors intercept these methods (some methods of Executor, ParameterHandler, ResultSetHandler, 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 in the order of Executor, ParameterHandler, ResultSetHandler, statementhandler (where the creation of ParameterHandler and ResultSetHandler is to create statementhandler [three available implementation classes callablestatementhandler, preparedstatementhandler, simplestatem When enthandler], its constructor calls [the constructors of the 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. For example, the function of paging can be realized by interceptor. Only the sql in StatementHandler interface implementation class needs to be processed in the plugin method of interceptor, which can be realized by reflection.

MyBatis also provides @ Intercepts and @ Signature annotations for interceptors. The example on 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, look at the wrap method of the 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 @ Signature annotation Set of the annotation attribute, and then traverse the Set. When traversing, take out the type attribute (Class type) of the @ Signature annotation, and then get the method with the method attribute and args attribute according to the type. Because the @ Signature attribute of @ Interceptors annotation is an attribute, a Map with type as key and value as Set will be returned finally.

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

For example, the @ Interceptors annotation will return a key as Executor and a value as collection (this collection has only one element, that is, method instance, which is the update method of Executor interface, and this method has parameters of MappedStatement and Object type). This method instance is based on 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 explanation: according to the target instance target (the target is the class that MyBatis interceptor can intercept, Executor,ParameterHandler,ResultSetHandler,StatementHandler) and its parent classes, return the interface array with target implementation in the signatureMap.

So the function of the Plugin class is to get the @ Signature array of the annotation attribute 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, it is decided whether to return a proxy object to replace the original target object according to the interface implemented by the called target object.

For example, when Configuration calls the newExecutor method, the interceptor intercepts the update(MappedStatement ms, Object parameter) method of the Executor interface. So 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.

The 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 proceed method is to call the original method (without proxy).

summary

Among the three methods provided by MyBatis interceptor interface, plugin method is used for the construction process of some processors (handlers). The interceptor method is used to handle the execution of the proxy class. The setProperties method is used to set interceptor properties.

In fact, MyBatis official website provides the methods of using @ Interceptors and @ Signature annotation and plugin class to deal with Interceptors. We don't have to use them directly. We can also discard these three classes and directly perform corresponding operations within the plugin method according to the type of target instance.

Generally speaking, MyBatis interceptor is very simple. Interceptor itself does not need too many knowledge points, but learning interceptor needs to be familiar with each interface in MyBatis, because interceptor involves knowledge points of each interface.

Posted by phpnoobguy on Mon, 18 Nov 2019 01:05:42 -0800