What is a plug-in?

Plug-in is a program written in accordance with a certain standard application program interface, which can only run in the prescribed system platform, but can not run independently from the specified platform.

Plug-ins must rely on the application to function, and plug-ins alone will not work. Conversely, the application does not need to rely on plug-ins to run, so that plug-ins can be loaded into the application and dynamically updated without any changes to the application.

Plugin in Mybatis

Mybatis provides a plug-in system for developers to use, so that it can intervene in the operation of Mybatis. For example, the famous Mybatis paging plug-in Mybatis-PageHelper is implemented by using the plug-in mechanism provided by Mybatis.

MyBatis provides a powerful mechanism, using the plug-in is very simple, just implement the Interceptor interface, and specify the method signature you want to intercept.

@Intercepts({@Signature( type= Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class ExamplePlugin implements Interceptor {
  
  public Object intercept(Invocation invocation) throws Throwable {
    // implement pre processing if need
    Object returnObject = invocation.proceed();
    // implement post processing if need
    returnreturnObject; }}Copy the code

Once the plug-in is written, you can configure it in the configuration file.


      
<! DOCTYPEconfiguration
    PUBLIC "- / / mybatis.org//DTD Config / 3.0 / EN"
    "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <! -- Configure plugins -->
    <plugins>
        <plugin interceptor="ExamplePlugin"/>
    </plugins>
    <mappers>
        <mapper resource="mapper/Test.xml"/>
    </mappers>
</configuration>

Copy the code

How to obtain Mybatis plug-in

The way Mybatis gets the plug-in is very simple, because the fully qualified class name of the plug-in has been specified in the XML configuration file. You only need to get the plug-in class according to the fully qualified class name.

// The 
      
        element has been retrieved
      
private void pluginElement(XNode parent) throws Exception {
    if(parent ! =null) {
        // loop through the 
      
        element
      
        for (XNode child : parent.getChildren()) {
            // Get the interceptor property, which is the fully qualified class name/alias
            String interceptor = child.getStringAttribute("interceptor");
            Properties properties = child.getChildrenAsProperties();
            Interceptor interceptorInstance = (Interceptor)resolveClass(interceptor).getDeclaredConstructor().newInstance();
            interceptorInstance.setProperties(properties);
            // Add the plug-in to the global configuration class.configuration.addInterceptor(interceptorInstance); }}}Copy the code

ResolveClass (Interceptor) The main function of this method is to query the real class according to its alias, so we don’t need to directly write the plug-in’s fully qualified class name, but can also write its alias.

When you get the real class, you create an object for that class by reflection and place it in the global configuration.

public void addInterceptor(Interceptor interceptor) {
    interceptorChain.addInterceptor(interceptor);
}
Copy the code
public class InterceptorChain {

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

    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(a) {
        returnCollections.unmodifiableList(interceptors); }}Copy the code

As we can see from the source code, it puts the plug-in object into the InterceptorChain, which stores it through a collection.

How to configure Mybatis plug-in

Mybatis needs to be configured after the plugin is acquired. Set it to the execution place and wait for it to be used.

Mybatis allows plugins to intercept the following methods:

interface Executor ParameterHandler ResultSetHandler StatementHandler
update getParameterObject handleResultSets prepare
query setParameters handleOutputParameters parameterize
flushStatements batch
commit update
rollback query
getTransaction
close
isClosed

We usually see intercepting methods executed, so we can assume that dynamic proxies are used, so we need to find out where to proxy these four types of classes. By reading the code, we can find the following code to proxy executors:

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);
    }
    This statement performs a plugin-related operation on the newly created executor.
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}
Copy the code

As you can see in line 16, it executes the pluginAll method on the InterceptorChain object, taking the newly created Executor as an argument, so we can assume that there are some operations on the Executor object that can intercept the specified method.

public class InterceptorChain {

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

  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(a) {
    returnCollections.unmodifiableList(interceptors); }}Copy the code

The pluginAll method loops through the plugins obtained in the previous step and executes their plugin method, which takes Executor as an argument. This method is a default on the Interceptor interface.

default Object plugin(Object target) {
    return Plugin.wrap(target, this);
}
Copy the code
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;
}
Copy the code
private staticMap<Class<? >, Set<Method>> getSignatureMap(Interceptor interceptor) { 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()); } Signature[] sigs = interceptsAnnotation.value(); Map<Class<? >, Set<Method>> signatureMap =new HashMap<>();
    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;
}
Copy the code
private staticClass<? >[] getAllInterfaces(Class<? > type, Map<Class<? >, Set<Method>> signatureMap) { Set<Class<? >> interfaces =new HashSet<>();
    while(type ! =null) {
        for(Class<? > c : type.getInterfaces()) {if (signatureMap.containsKey(c)) {
                interfaces.add(c);
            }
        }
        type = type.getSuperclass();
    }
    return interfaces.toArray(newClass<? >[interfaces.size()]); }Copy the code
  1. To perform firstgetSignatureMapMethod resolutionInterceptorOn the interfaceInterceptsAnnotations, which take the classes and methods indicated on the annotations that need to be intercepted, and wrap them in a Map.
  2. performgetAllInterfacesTo check whether an interface is available. First getExecutorIf included in signatureMap, the interface is returned.
  3. Create a proxy object with Plugin as the proxy class that implements the interface obtained in the previous step.

So, the last thing you get is a proxy object that implements the interface you want to intercept. Assume that SimpleExecutor is used here. This class implements the Executor interface and, according to the example at the beginning of this article, indicates that the method to be intercepted is the Query method in the Executor interface. Finally, the actual method of creating a proxy object is as follows:

Proxy.newProxyInstance(
    type.getClassLoader(),
    Class[]{Executor.class},
    new Plugin(simpleExecutor, interceptor, signatureMap));
Copy the code

How to use Mybatis plug-in

From the above analysis, we know that when we create the Executor, a Plugin proxy object is returned, if any, rather than the original Executor object.

So when the relevant method is executed, it is intercepted. In the example, we specify that the plug-in needs to intercept Executor’s Query (MapperStatement MS, Object Args, RowBounds RB, ResultHandler RH) for us.

So, when this method is executed, the invoke method of the proxy object is entered.

// DefaultSqlSession.class
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
        MappedStatement ms = configuration.getMappedStatement(statement);
        return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
    } finally{ ErrorContext.instance().reset(); }}Copy the code

The Query method of the Executor object is executed in the selectList. At this point it enters the invoke method of the proxy object Plugin.

// Plugin.class
@Override
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) {
        throwExceptionUtil.unwrapThrowable(e); }}Copy the code

First, get all the methods in the execution object, that is, all the intercepted methods in the Executor, that is, methods annotated by @Signature, and then check to see if any of these methods are identical to the methods being executed. If so, that means that the method being executed now needs to be intercepted. At this point you need to execute the Intercept method in your plug-in.

Nested proxy objects

If we have multiple plugins, what will Mybatis do?

We can see by looking at the following code that it is nested for each proxy object.

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

Suppose we have three plug-ins: ExamplePlugin1, ExamplePlugin2, and ExamplePlugin3

The execution order is ExamplePlugin3 -> ExamplePlugin2 -> ExamplePlugin1 -> Executor.

This order is nested based on the order Mybatis got the plugins, so if there is an order between plugins then be careful because it may not be processed in the order you expect.