MyBatis source code analysis - plug-in mechanism

Posted by connex on Wed, 26 Jan 2022 11:24:32 +0100

This series of documents is summarized in the process of learning the source code of Mybatis. It may not be friendly to readers. Please combine my source code comments( Mybatis source code analysis GitHub address,Mybatis spring source code analysis GitHub address,Spring boot starter source code analysis GitHub address )Read

MyBatis version: 3.5.2

Mybatis spring version: 2.0.3

Mybatis spring boot starter version: 2.1.4

For other documents in this series, please see: "Jingjin MyBatis source code analysis - article guide"

Plug in mechanism

Open source frameworks generally provide plug-ins or other forms of extension points for developers to expand by themselves to increase the flexibility of the framework

Of course, MyBatis also provides a plug-in mechanism. Based on it, developers can extend and enhance the functions of MyBatis, such as paging, SQL analysis and monitoring. This paper will describe the principle of MyBatis plug-in mechanism and how to implement a custom plug-in

When writing a plug-in, we need to make the plug-in class implement org apache. ibatis. plugin. The interceptor interface also needs to annotate the interception point of the plug-in, that is, the methods that the plug-in needs to enhance. MyBatis only provides methods defined in the following classes that can be enhanced:

  • Executor: executor

  • ParameterHandler: parameter handler

  • ResultSetHandler: result set handler

  • StatementHandler: Statement handler

Embedded plug-in logic

stay SQL execution process of MyBatis In a series of documents, it is mentioned that when creating Executor, ParameterHandler, ResultSetHandler and StatementHandler objects, the pluginAll method of InterceptorChain will be called to traverse all plug-ins, and the plugin method of Interceptor plug-in will be called to implant corresponding plug-in logic. Therefore, in MyBatis, only the methods in the above four objects can be enhanced

The code is as follows:

// Configuration.java

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    // <1> Get actuator type
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    // <2> Create the Executor object corresponding to the implementation
    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);
    }
    // <3> If caching is enabled, a cachengexecution object is created and wrapped
    if (cacheEnabled) {
        executor = new CachingExecutor(executor);
    }
    // <4> Application plug-in
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject,
        BoundSql boundSql) {
    // Create ParameterHandler object
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    // Application plug-in
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
}

public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds,
        ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) {
	// Create DefaultResultSetHandler object
    ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, 
                                                                    parameterHandler, resultHandler, boundSql, rowBounds);
	// Application plug-in
    resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
    return resultSetHandler;
}

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement,
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, 
                                                                    parameterObject, rowBounds, resultHandler, boundSql);
    // Apply all plug-ins in Configuration global Configuration to StatementHandler
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
}

Paging plug-in example

Let's take a look at a simple plug-in example. The code is as follows:

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

  // Query method of Executor:
  // public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler)

  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    Object[] args = invocation.getArgs();
    RowBounds rowBounds = (RowBounds) args[2];
    if (rowBounds == RowBounds.DEFAULT) { // No paging required
      return invocation.proceed();
    }
    /*
     * Set the RowBounds input parameter of the query method to an empty object
     * That is, close the paging implemented inside MyBatis (logical paging, paging after getting the query results, rather than physical paging)
     */
    args[2] = RowBounds.DEFAULT;

    MappedStatement mappedStatement = (MappedStatement) args[0];
    BoundSql boundSql = mappedStatement.getBoundSql(args[1]);

    // Get SQL statement and splice limit statement
    String sql = boundSql.getSql();
    String limit = String.format("LIMIT %d,%d", rowBounds.getOffset(), rowBounds.getLimit());
    sql = sql + " " + limit;

    // Create a StaticSqlSource object
    SqlSource sqlSource = new StaticSqlSource(mappedStatement.getConfiguration(), sql, boundSql.getParameterMappings());

    // Get and set the sqlSource field of MappedStatement through reflection
    Field field = MappedStatement.class.getDeclaredField("sqlSource");
    field.setAccessible(true);
    field.set(mappedStatement, sqlSource);

    // Execute intercepted method
    return invocation.proceed();
  }

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

  @Override
  public void setProperties(Properties properties) {
    // default nop
  }
}

In the paging plug-in above, @ Intercepts and @ Signature annotations specify that the enhanced method is Executor Query (mappedstatement MS, object parameter, rowboundaries, rowboundaries, resulthandler, resulthandler), that is, the method used by the Executor to perform database query operations

In the implemented intercept method, the paging information is obtained through the rowboundaries parameter, and the corresponding SQL is generated (spliced limit), and the SQL is used as a parameter to re create a StaticSqlSource object. Finally, the sqlSource field in the MappedStatement object is replaced by reflection, so as to realize a simple paging plug-in

The above is just a simple example, which should be used with caution in actual scenarios

Interceptor

org.apache.ibatis.plugin.Interceptor: interceptor interface. The code is as follows:

public interface Interceptor {
  /**
   * interceptor method 
   *
   * @param invocation Call information
   * @return Call result
   * @throws Throwable In case of abnormality
   */
  Object intercept(Invocation invocation) throws Throwable;

  /**
   * Application plug-ins. If the application is successful, a proxy object for the target object will be created
   *
   * @param target Target object
   * @return The result object of the application can be a proxy object, a target object, or any object. Specifically, look at the code implementation
   */
  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

  /**
   * Set interceptor properties
   *
   * @param properties attribute
   */
  default void setProperties(Properties properties) {
    // NOP
  }
}
  • Intercept method: intercept method, plug-in enhanced logic
  • Plugin method: apply the plug-in and implant the corresponding plug-in logic into the target object. If the application is successful, a proxy object (JDK dynamic proxy) will be returned. Otherwise, the original object will be returned and the wrap method of plugin will be called by default
  • setProperties method: set interceptor properties

Invocation

org.apache.ibatis.plugin.Invocation: intercepted object information. The code is as follows:

public class Invocation {
	/**
	 * Target object
	 */
	private final Object target;
	/**
	 * method
	 */
	private final Method method;
	/**
	 * parameter
	 */
	private final Object[] args;

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

Plugin

org.apache.ibatis.plugin.Plugin: implements the InvocationHandler interface, which is used to intercept the intercepted object. On the one hand, it provides the method of creating dynamic proxy object, on the other hand, it implements the interception processing of the specified method of the specified class. It is the core class of MyBatis plug-in mechanism

Construction method

public class Plugin implements InvocationHandler {
	/**
	 * Target object
	 */
	private final Object target;
	/**
	 * Interceptor
	 */
	private final Interceptor interceptor;
	/**
	 * Intercepted method mapping
	 *
	 * KEY: class
	 * VALUE: 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 method

Wrap (object, target, interceptor) method to create the proxy object of the target class. The method is as follows:

public static Object wrap(Object target, Interceptor interceptor) {
    // <1> Get the method collection of the class to be intercepted in the interceptor
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    // <2> Gets the Class object of the target object
    Class<?> type = target.getClass();
    // <3> Obtain all Class objects (parent Class or interface) of the target object that need to be intercepted
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    // <4> If there is something that needs to be intercepted, create a dynamic proxy object (JDK dynamic proxy) for the target object, and the proxy class is Plugin object
    if (interfaces.length > 0) {
        // Because Plugin implements the InvocationHandler interface, it can be used as the calling processor of JDK dynamic agent
        return Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap));
    }
    // <5> If not, the original target object is returned
    return target;
}
  1. Call the getSignatureMap method to obtain the method collection of the classes to be intercepted in the interceptor, including the enhanced methods specified through the @ Intercepts and @ Signature annotations
  2. Get the Class object (parent Class or interface) of the target object
  3. Get all Class objects of the target object that need to be intercepted
  4. If it needs to be intercepted, create a dynamic proxy object (JDK dynamic proxy) for the target object, the proxy class is Plugin object, and return the dynamic proxy object
  5. Otherwise, the original target object is returned

getSignatureMap method

getSignatureMap(Interceptor interceptor) method to obtain the methods to be enhanced by the plug-in, as follows:

private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    // Get @ Intercepts annotation
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
    // issue #251
    if (interceptsAnnotation == null) {
        throw new PluginException( "No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
    }
    // Get @ Signature annotation in @ Intercepts annotation
    Signature[] sigs = interceptsAnnotation.value();
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
    for (Signature sig : sigs) {
      // Create a method array for the class name defined in the @ Signature annotation
        Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
        try {
          // Gets the method object defined in the @ Signature annotation
            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;
}
  • Through the @ Intercepts and @ Signature annotations on the plug-in, get the methods that need to be enhanced in all objects that need to be intercepted

getAllInterfaces method

Getallinterfaces (class <? > type, map < class <? >, set < method > > signaturemap) method to judge whether the target object needs to be applied by the plug-in. The method is as follows:

private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
    // Collection of interfaces
    Set<Class<?>> interfaces = new HashSet<>();
    // Circular recursive type class, machine parent class
    while (type != null) {
        // Traverse the interface collection. If it is in the signatureMap, it will be added to the interfaces
        for (Class<?> c : type.getInterfaces()) {
            if (signatureMap.containsKey(c)) {
                interfaces.add(c);
            }
        }
        // Get parent class
        type = type.getSuperclass();
    }
    // Create an array of interfaces
    return interfaces.toArray(new Class<?>[interfaces.size()]);
}
  • The input parameter signatureMap is the method returned by the getSignatureMap method, which needs to be enhanced by the plug-in
  • Returns the parent class or interface of all target objects existing in the signatureMap collection

invoke method

Invoke (object proxy, method, method, object [] args) method is used to intercept dynamic proxy objects. The methods are as follows:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        // Get the intercepted method of the class where the target method is located
        Set<Method> methods = signatureMap.get(method.getDeclaringClass());
        if (methods != null && methods.contains(method)) { // If the intercepted method contains the current method
            // Use the plug-in to intercept this method for processing
            return interceptor.intercept(new Invocation(target, method, args));
        }
        // If there is no method to be intercepted, the original method is called
        return method.invoke(target, args);
    } catch (Exception e) {
        throw ExceptionUtil.unwrapThrowable(e);
    }
}
  1. Get the intercepted method of the class where the target method is located
  2. If the intercepted method contains the current method, encapsulate the current method into an Invocation object, call the intercept method of the Interceptor plug-in, and execute the plug-in logic
  3. Otherwise, execute the original method

In this way, when you call the corresponding method of the target object, you will enter the intercept method of the plug-in to execute the plug-in logic and extend the functions

InterceptorChain

org.apache.ibatis.plugin.InterceptorChain: interceptor chain, which is used to implant the plug-in logic of all interceptors into the target object in order. The code is as follows:

public class InterceptorChain {

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

	public Object pluginAll(Object target) {
    	// Traversal interceptor collection
		for (Interceptor interceptor : interceptors) {
      		// Call the plugin method of the interceptor to implant the corresponding plug-in logic
			target = interceptor.plugin(target);
		}
		return target;
	}

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

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

The Configuration MyBatis plug-ins will be saved in the interceptors collection. You can recall the pluginElement method in the XMLConfigBuilder section of initialization (I) loading mybatis-config.xml. All parsed will be added to the InterceptorChain object of Configuration in turn. The code is as follows:

private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
        // Traverse < plugins / > tags
        for (XNode child : parent.getChildren()) {
            String interceptor = child.getStringAttribute("interceptor");
            Properties properties = child.getChildrenAsProperties();
            // <1> Create an Interceptor object and set properties
            Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
            interceptorInstance.setProperties(properties);
            // <2> Add to configuration
            configuration.addInterceptor(interceptorInstance);
        }
    }
}

summary

This paper analyzes the plug-in mechanism in MyBatis. Generally speaking, it is relatively simple. To implement a plug-in, you need to implement the Interceptor interface, and specify the interception point of the plug-in through the @ Intercepts and @ Signature annotations (it supports the enhancement of methods in the four objects of Executor, ParameterHandler, ResultSetHandler and StatementHandler), Perform logical processing in the implemented intercept method

When MyBatis is initialized, the plug-in will be scanned and added to InterceptorChain

Then, during SQL execution, when MyBatis creates the above four objects, it will submit the created objects to InterceptorChain for processing, traverse all plug-ins, create a dynamic proxy object for it through the plugin method of the plug-in and return it. The proxy class is the plugin object

In the invoke method in the Plugin object, the request is handled by the intercept method of the plug-in

Although the plug-in mechanism of MyBatis is relatively simple, it is complex to implement a perfect and efficient plug-in. You can refer to it PageHelper paging plug-in

Here, I believe you have a certain understanding of MyBatis plug-in mechanism. Thank you for reading!!!