Analysis of mybatis plug-in

Time:2020-2-18

Preface

Mybatis provides a powerful extension function, that is, the plugins function of mybatis. Mybatis allows you to intercept and call at a point during the execution of mapped statements. After interception, you can add some customized functions to existing methods, such as common page splitting functions. When you try to modify or rewrite the behavior of existing methods, you are likely to destroy mybatis These are lower level classes and methods, so be careful when using plug-ins.

How to expand

1. interception point

The intercepted point consists of four objects and several methods, as follows:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

With the intercept point, we need to tell mybatis which method to extend under which class; mybatis provides a simple configuration to implement;

2. How to expand

Using plug-ins is very simple. You only need to implement the interceptor interface and specify the signature of the method you want to intercept. For example, the following custom plug-in myplugin:

@Intercepts({
        @Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }),
        @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }) })
public class MyPlugin implements Interceptor {
    private Properties prop;
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.err.println("====before===="+invocation.getTarget());
        Object obj = invocation.proceed();
        System.err.println("====after===="+invocation.getTarget());
        return obj;
    }
    
    @Override
    public Object plugin(Object target) {
        System. Err. Println ("===== call to generate proxy object === target:" + target);
        return Plugin.wrap(target, this);
    }
    
    @Override
    public void setProperties(Properties properties) {
        this.prop = properties;
    }
}

The signature of the method is specified by annotation: type, method name, parameter list; if multiple methods are to be intercepted, multiple methods can be configured@SignatureSeparated by commas. The above configuration indicates that intercepting when executing the update and query methods of executor, and also executing the intercept methods defined in the interceptor interface. We can simply make some changes, or simply rewrite the corresponding methods in the interface. Of course, the former is familiar with the source code. Of course, we also need mybatis to know our custom myplugin Configuration configuration is required:

<plugins>
        <plugin interceptor="com.mybatis.plugin.MyPlugin">
            <property name="dbType" value="mysql" />
        </plugin>
    </plugins>

After the above configuration, you can simply do a test and perform a query function of mybatis. The log output is as follows:

====Call build agent object = = = target: org.apache.ibatis.executor.cachexecutor @ 31d7b7bf
[email protected]f
====Call build agent object = = = target: org.apache.ibatis.scripting.defaults.defaultparameterhandler @ f5ac9e4
====Call build agent object = = = target: org.apache.ibatis.executor.resultset.defaultresultsethandler @ 7334aada
====Call build agent object = = = target: org.apache.ibatis.executor.statement.routingstatementhandler @ 4d9e68d0
[email protected]

The generated four proxy objects are actually the four interception points we described above. Although proxy objects are generated, when the proxy object is executed, it will also check whether the specified interception method is configured. Therefore, when the mybatis query function is executed, check whether the query method is configured.

Plug in analysis

1. Interceptor registration

The plug-ins that we configured through tag plugins in configuration will be registered to a blockchain finallyInterceptorChain, where the list of interceptors is maintained:

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

When mybatis parses the plugins tag, it will calladdInterceptor()Method to add plug-ins to interceptors. Multiple plug-ins willpluginAll()Methods are called in turn when they are executed;

2. Trigger interceptor

Provided in the filter chainpluginAll()Method, this method will be called when the four classes described above are instantiated. You can simply use the shortcut key in the development tool to query where this method is called:
Analysis of mybatis plug-in
You can see one of the most common executor instantiations:

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

First, create different executors according to different executortype types. The default is cacheexecutor. Finally, the pluginall method of interceptorchain will be called. The incoming and return parameters are executors. It can be simply understood that the proxy class of executor is returned through pluginall;

3. Generate agent class

There are two ways to generate proxy classes: the proxy and cglib provided by JDK. Mybatis provides the plugin class with the function of generating related proxy classes, so it is directly used in the plugin method of myplugin in the above examplePlugin.wrap(target, this), two parameters are respectively: target corresponds to the above four types, this represents the current custom plug-in:

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

This method first obtains the signature configured in the custom plug-in, and then checks whether the class configured in the signature is the current target type. If it matches, it creates a proxy class through the proxy provided by the JDK. Otherwise, it directly returns the target without any proxy processing. In the example above, the target is cacheexecutor, and its corresponding interface is executor, while our signatu in myplugin The executor class is configured in re, so it can be matched successfully;

4. Trigger execution

When a specific method is actually executed, it is actually the invoke method of the invocationhandler specified when creating a proxy class. It can be found that the invocationhandler specified in the previous section is a plugin object, and plugin itself inherits from the invocationhandler, providing the invoke method:

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

First, obtain the method list corresponding to the intercepting class from signaturemap, and then check whether the currently executed method is in the method list to be intercepted. If so, call the custom plug-in interceptor, otherwise, perform the default invoke operation. When the interceptor calls the intercept method, it is the incoming invocation object, which contains three parameters: target corresponding is The above four types are method currently executing method and args currently executing method parameters. The method name and parameters here can be viewed in the source code, such as executor’s query method:

 <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

5. Interception

Call the intercept method of the custom interceptor and pass in an invocation object. If you only record the following logs, such as the myplugin plug-in above, you only need to execute the proceed method:

public class Invocation {
  private final Object target;
  private final Method method;
  private final Object[] args;

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

In fact, this method is the default processing, which is the same as the result of not using the proxy class, except that the proxy class is called in the way of reflection; of course, the plugins of mybatis are not only simple to record logs, such as the commonly used plugins PageHelper to do physical paging, etc;

summary

Mybatis provides four classes, which can be used to intercept and extend functions. This paper introduces how to use plug-ins to extend functions, and then analyzes how to extend functions through proxy classes from the source level.

Sample code

Github