Mybatis: source code analysis - interceptor plug-in

Implementation of Mybatis Interceptor:

Examples of official documents: https://mybatis.org/mybatis-3/zh/configuration.html#plugins

The interceptor uses the responsibility chain mode + dynamic agent + reflection mechanism. The interceptor can monitor some data permissions, SQL execution time and performance without invading business code.

  • Custom Interceptor class: the custom Interceptor needs to implement the Interceptor interface, and add @ Intercepts annotation on the custom Interceptor class;
  • Register interceptor class: mybatis interceptor configuration information, and register the plug-in in the global configuration file.

Use example:

// ExamplePlugin.java

// @Intercepts identity requires interceptor class
// @Signature specifies the method signature to be intercepted
@Intercepts(
    {@Signature(
        // Target object to intercept
        type= Executor.class,
        // Specifies the method to intercept the target object
        method = "update",
        // Method parameters
        args = {MappedStatement.class,Object.class})
    }
)
public class ExamplePlugin implements Interceptor {
  // The properties of the interceptor are set through setProperties during plug-in registration
  private Properties properties = new Properties();
  // Intercept the target object executor Execute the intercept method when the update method is called
  public Object intercept(Invocation invocation) throws Throwable {
    // implement pre processing if need
    // Execution target method
    Object returnObject = invocation.proceed();
    // implement post processing if need
    return returnObject;
  }
  // When the plug-in is registered, the property property property is set
  public void setProperties(Properties properties) {
    // Store the property configuration in properties
    this.properties = properties;
  }
}


<!-- mybatis-config.xml -->
// mybatis interceptor configuration information, and register the plug-in in the global configuration file
<plugins>
  // Configure the registration plug-in through the classpath
  <plugin interceptor="org.mybatis.example.ExamplePlugin">
    // Property configuration  
    <property name="someProperty" value="100"/>
  </plugin>
</plugins>

Four objects that Mybatis can intercept:

  • Executor: Mybatis executor, mainly responsible for generating and executing SQL statements;
  • ParameterHandler: convert the parameters passed by the user into the parameters required by JDBC Statement;
  • ResultHandler: converts the ResultSet result set object returned by JDBC into a List type set;
  • StatementHandler: encapsulates the JDBC statement operation and is responsible for the operation of JDBC statement, such as setting parameters and converting the Statement result set into a List set.

Interceptable class

Interceptable method

Executor

update, query, flushStatements, commit, rollback,getTransaction, close, isClosed

ParameterHandler

getParameterObject, setParameters

ResultSetHandler

handleResultSets, handleOutputParameters

StatementHandler

prepare, parameterize, batch, update, query

1. After the four objects are created, they do not return directly. The responsibility chain mode is used. They all pass through the interceptorchain of the interceptor chain Call the pluginall () method to determine whether it is necessary to intercept to return the wrapped target object (new dynamic proxy object);

2. Using interceptors will create a proxy object for the target object. If there are N interceptors, N proxy objects will be generated. Generating dynamic agents layer by layer is more performance consuming; Although there are specific methods to execute interception, reflection dynamic judgment is used when executing methods. So don't write unnecessary interceptors.

3. AOP aspect oriented method can create proxy objects for four objects and intercept each execution of each object.

Configuration source code four object generation and call interceptor chain source code:

// Configuration source code four object generation and call interceptor chain source code
package org.apache.ibatis.session;

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);
  }
  executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}

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;
}

XMLConfigBuilder resolves the plugins tag of MyBatis global configuration file:

// XMLConfigBuilder.
pluginElement(root.evalNode("plugins"));

// XMLConfigBuilder.pluginElement method
private void pluginElement(XNode parent) throws Exception {
    // Parsing plugins Tags
  if (parent != null) {
    // Traverse the lower sub label
    for (XNode child : parent.getChildren()) {
      // Get the interceptor attribute on the sub tag, that is, the class path of the interceptor implementation class
      String interceptor = child.getStringAttribute("interceptor");
      // Get the following property property configuration
      Properties properties = child.getChildrenAsProperties();
      // Generate interceptor class through reflection
      Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
      // Set properties for the interceptor's class
      interceptorInstance.setProperties(properties);
      // Add interceptor to interceptor chain
      configuration.addInterceptor(interceptorInstance);
    }
  }
}

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

Source code of Mybatis Interceptor:

The @ Intercepts and @ Signature annotations on the Interceptor:

  • The interceptor annotation is used to identify the class of an interceptor and configure the method signature to be intercepted by the interceptor;
  • @Intercepts: identifies a class as an interceptor, and the content can contain annotations for multiple @ signatures
  • @Signature: declare the class (type), method and method parameter (args) to be intercepted;
  • Configuration example:
@Intercepts(
    {
        @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}),
    }
)

// Intercepts source code
package org.apache.ibatis.plugin;

// The annotation of interceptor is used to identify the class of an interceptor. The content can contain multiple @ Signature annotations
// @The Signature annotation specifies the class, method and input parameter of the target object to be intercepted
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
  /**
   * Returns method signatures to intercept.
   * @return method signatures
   */
  Signature[] value();
}
// Signature source code
package org.apache.ibatis.plugin;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
// Intercept method Signature annotation, @ Signature annotation specifies the class, method and input parameter of the target object to be intercepted
public @interface Signature {
  /**
   * Returns the java type.
   * @return the java type
   */
  // The class of the target object that needs to be intercepted
  Class<?> type();

  /**
   * Returns the method name.
   * @return the method name
   */
   // Method of target object to be intercepted
  String method();

  /**
   * Returns java types for method argument.
   * @return java types for method argument
   */
   // The input parameter of the target object method to be intercepted
  Class<?>[] args();
}

Interceptor interceptor interface:

  • Provide the implementation interface class of the development interceptor and implement the interceptor object;
  • Plugin method: the interface has been implemented by default. It is implemented through the dynamic proxy method of JDK, which calls plugin Wrap (target, this). If there is a specified interception class and method, wrap the target object target and return a new dynamic proxy object class. When calling the method of the new dynamic proxy object class target, it will persistently implement the invoke method in the plugin class of InvocationHandler, and the intercept method interceptor of the interceptor will be called in the invoke method Intercept() implements interception; If the method is not specified, it will directly return to the target object and call the original method without interception;
  • Intercept: the implementation of the interception method, which is called when it is necessary to intercept the method of the hair class.
  • setProperties: set some configurable properties of the interceptor.
  • The current interceptor proxy object is multi-layer proxy. The method to obtain the real interceptor object is as follows:

@SuppressWarnings("unchecked")
public static <T> T realTarget(Object target) {
    if (Proxy.isProxyClass(target.getClass())) {
        MetaObject metaObject = SystemMetaObject.forObject(target);
        return realTarget(metaObject.getValue("h.target"));
    }
    return (T) target;
}
// Interceptor source code
package org.apache.ibatis.plugin;

// Interceptor interface
public interface Interceptor {
  // Intercept: intercepts the execution of the target method of the target object
  Object intercept(Invocation invocation) throws Throwable;
  
  // Wrap the target object and create a proxy object for the target object
  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }
  
  // When the plug-in is registered, set the properties property of the plug-in
  default void setProperties(Properties properties) {
    // NOP
  }

}
// InterceptorChain source code
package org.apache.ibatis.plugin;

// Interceptor chain 
public class InterceptorChain {
  // Interceptor set
  private final List<Interceptor> interceptors = new ArrayList<>();
  // Wrap the target object with the interceptors in all interceptor sets and return the wrapped target object
  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      // Wrap the target object
      target = interceptor.plugin(target);
    }
    return target;
  }
  // Add interceptors to interceptor collection
  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }
  // Gets the interceptor collection and returns interceptors that cannot be modified
  public List<Interceptor> getInterceptors() {
    return Collections.unmodifiableList(interceptors);
  }

}

JDK dynamic proxy, Java lang.reflect. Proxy:

  • Using the java reflection class Proxy+InvocationHandler callback interface implementation, create a new class (also known as "dynamic proxy class") and its instance (object) to implement some interfaces. The proxy is interfaces, not classes or abstract classes. In fact, it can only proxy the methods defined in the interface implemented by this class;
  • Implementation method: proxy Newproxyinstance (classloader, loader, class [] interfaces, invocationhandler h) returns the proxy object of an object.
  • ClassLoader loader: class loader. You can use this type of loader to load the generated proxy class into the JVM, i.e. Java virtual machine, when the program is running, so as to meet the needs of runtime;
  • Class[] interfaces: the interfaces to be implemented by the dynamic proxy class,
  • InvocationHandler h: call / trigger the processor to call the invoke callback method that implements the InvocationHandler class. When the dynamic proxy method is executed, it will call the invoke method in h to execute.

// Plugin source code
package org.apache.ibatis.plugin;

// The Plugin class implements the invoke method of the InvocationHandler class
// A new dynamically proxied target object is returned through wrap wrapping
// When calling the method of the target object target, the invoke method in Plugin will be called first
public class Plugin implements InvocationHandler {
  // Target object to intercept
  private final Object target;
  // Interceptor
  private final Interceptor interceptor;
  // A HashMap that stores the mapping of the interception target object class and method set
  private final Map<Class<?>, Set<Method>> signatureMap;
  
  private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
    this.target = target;
    this.interceptor = interceptor;
    this.signatureMap = signatureMap;
  }
  
  // Wrap the target object and return a new proxy object class
  public static Object wrap(Object target, Interceptor interceptor) {
    // Get the HashMap of the method set of the intercepting target object class configured by the interceptor @ Intercepts annotation
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    // Gets the class of the intercepted target object
    Class<?> type = target.getClass();
    // Get the target object class to be intercepted and all interface classes contained
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
      // Using the dynamic proxy of JDK, the interfaces interface class is represented through the class loader of type
      // Returns the new proxy object class target
      // When calling the method of the new proxy object target, it will insist on implementing the invoke method in the Plugin class of InvocationHandler
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    // If there is no interface requiring proxy, the original object will be returned directly
    return target;
  }

  @Override
  // Proxy method
  // Proxy has been the target object of dynamic proxy
  // Method the method of the target object
  // args parameter object array
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      // Gets the method set of the method declaration class
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      if (methods != null && methods.contains(method)) {
        // If the method is included in the method set to be intercepted, execute the interceptor's interception method intercept
        return interceptor.intercept(new Invocation(target, method, args));
      }
      // If the method is not in the method set to be intercepted, the target method is executed directly
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }
  
  // Get the configuration information corresponding to the @ Intercepts annotation
  // Returns the HashMap of the method set that needs to intercept the target object class - > intercept the target object class
  private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    // Gets the @ Intercepts annotation on the Interceptor object of the Interceptor
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
    // issue #251
    // If the @ Intercepts annotation is missing on the interceptor, an error is thrown
    if (interceptsAnnotation == null) {
      throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
    }
    // Get the array @ Signature annotation in the @ Intercepts annotation
    Signature[] sigs = interceptsAnnotation.value();
    // The returned result stores the class of the intercepted target object and the Map of the method set of the target object to be intercepted
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
    // Traverse @ Signature annotation array
    for (Signature sig : sigs) {
      // Get the method set of the interception target object class corresponding to the Map. If there is no corresponding target object class, create an empty HashSet method set
      Set<Method> methods = MapUtil.computeIfAbsent(signatureMap, sig.type(), k -> new HashSet<>());
      try {
        // Obtain the corresponding Method object according to the annotation configured Method and input parameters
        Method method = sig.type().getMethod(sig.method(), sig.args());
        // Add method to intercepted method set
        methods.add(method);
      } catch (NoSuchMethodException e) {
        throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
      }
    }
    return signatureMap;
  }
  
  // Get the target object class to be intercepted and all interface classes contained
  private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
    Set<Class<?>> interfaces = new HashSet<>();
    while (type != null) {
      // Gets all the implementation interfaces of the type class object
      for (Class<?> c : type.getInterfaces()) {
        // If the Map that stores the class of the intercepted target object and the method set of the target object to be intercepted contains this interface class
        if (signatureMap.containsKey(c)) {
          // Add to interface collection
          interfaces.add(c);
        }
      }
      // Get the parent class of type, and end while until the parent class is null
      type = type.getSuperclass();
    }
    // Converts an interface set to an array and returns
    return interfaces.toArray(new Class<?>[0]);
  }
}

 

Keywords: Mybatis

Added by tensitY on Fri, 14 Jan 2022 08:33:15 +0200