Read the source code of mybatis -- Interceptor

Posted by steelmanronald06 on Mon, 27 Jan 2020 06:01:59 +0100

Reprinted from Read the source code of mybatis (8) -- Interceptor

1 Intercetor

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:

Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
Processing of intercepting parameters by ParameterHandler (getParameterObject, setParameters)
Processing of intercepting result set by ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query) intercepts the processing of Sql syntax construction

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) {
  return newExecutor(transaction, defaultExecutorType);
}

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

All interceptors are saved in interceptor chain, which is created when mybatis is initializes. The meaning of InterceptorChain.plugin all (executor) above is to call each interceptor in the interceptor chain to plug in the executor in turn. The code is as follows

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

}

Interceptor structure

public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  Object plugin(Object target);

  void setProperties(Properties properties);

}

2. Custom interceptor

Official source code example

@Intercepts({@Signature(type = Executor.class, method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class ExamplePlugin implements Interceptor {
  private Properties properties;
  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    return invocation.proceed();
  }

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

  @Override
  public void setProperties(Properties properties) {
    this.properties = properties;
  }

  public Properties getProperties() {
    return properties;
  }

}

mybatis-config.xml configuration

<plugins>
       <plugin interceptor="org.lpf.interceptor.ExamplePlugin"></plugin>
</plugins>

Each interceptor must implement the above three methods, including:

Object intercept(Invocation invocation) is the place to realize the interception logic. Internally, we need to explicitly advance the responsibility chain through invocation. Processed(), that is, to call the next interceptor to intercept the target method.

Object plugin(Object target) is to use the current interceptor to generate a proxy for the target target. In fact, it is completed by Plugin.wrap(target,this). It passes the target and interceptor this to the wrapper function.

setProperties(Properties properties) is used to set additional parameters, which are configured in the Properties node of the interceptor.

The annotation describes the signature [type,method,args] of the specified interception method (that is, which method of which object is intercepted), which is used for decision before interception.

The most important thing to define our own Interceptor is to implement the plugin method and intercept method. In the plugin method, we can decide whether to intercept and then decide what kind of target object to return. The intercept method is the method to be executed when intercepting.

For the plugin method, mybatis has provided us with an implementation. Mybatis has a class called plugin, which has a static method wrap(Object target,Interceptor interceptor), through which you can decide whether the object to be returned is the target object or the corresponding agent. Let's take a look at the source code of plugin:

/**
 * @author Clinton Begin
 * This class is the core of Mybatis interceptor. You can see that this class inherits the InvocationHandler
 * JDK dynamic agent mechanism
 */
public class Plugin implements InvocationHandler {
  //Target object
  private final Object target;
  // Interceptor
  private final Interceptor interceptor;
  //Record classes and methods that need to be intercepted to improve performance
  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;
  }

  //A static method that wraps a target object to generate a proxy class.
  public static Object wrap(Object target, Interceptor interceptor) {
    //First, obtain the information to be intercepted according to the annotation defined above in the interceptor
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    //Class of the target object
    Class<?> type = target.getClass();
    //Return interface information to be intercepted
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    //If the length is > 0, the proxy class will be returned; otherwise, it will not be processed
    if (interfaces.length > 0) {
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

  //Method called by proxy object every time
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      //Query the set of methods to be intercepted in signatureMap through the class defined by the method parameter
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      //Judge whether to intercept
      if (methods != null && methods.contains(method)) {
        //Intercepting method of executing interceptor
        return interceptor.intercept(new Invocation(target, method, args));
      }
      //Calling methods directly through the target object without blocking
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }


  //Obtain relevant information according to the annotation on the implementation class of Interceptor
  private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {

    //Get annotation information @ concepts
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
    // issue #251
    if (interceptsAnnotation == null) {//If it is empty, an exception will be thrown
      throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());      
    }
    //Getting Signature annotation information is an array
    Signature[] sigs = interceptsAnnotation.value();
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
    //Circular annotation information
    for (Signature sig : sigs) {
      //Query the set of interception methods in signatureMap according to the type information defined in the Signature annotation
      Set<Method> methods = signatureMap.get(sig.type());
      if (methods == null) { //The first time it must be null, create one and put it into the signatureMap
        methods = new HashSet<Method>();
        signatureMap.put(sig.type(), methods);
      }
      try {
        //Find the method defined in sig.type and add it to the set
        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;
  }

  //Get interface information according to object type and signatureMap
  private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
    Set<Class<?>> interfaces = new HashSet<Class<?>>();
    //The interface information of loop type is added to set if the type exists in signatureMap
    while (type != null) {
      for (Class<?> c : type.getInterfaces()) {
        if (signatureMap.containsKey(c)) {
          interfaces.add(c);
        }
      }
      type = type.getSuperclass();
    }
    //Convert to array return
    return interfaces.toArray(new Class<?>[interfaces.size()]);
  }

}

3. Interception on the agent chain

Combining with the statement (Executor)interceptorChain.pluginAll(executor), we can see that the statement has executed multiple plugins for the executor. After the first plugin, the first proxy class is generated through the Plugin.wrap method, let alone executorProxy1. The target attribute of this proxy class is the executor object. After the second plugin, the second proxy class is generated by the Plugin.wrap method, let alone executorProxy2. The target attribute of this proxy class is executorProxy1... Thus, a proxy chain is formed by the target attribute of each proxy class (looking forward from the last executorProxyN, the most original executor class can be found by the target attribute)

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:

//Method called by proxy object every time
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    //Query the set of methods to be intercepted in signatureMap through the class defined by the method parameter
    Set<Method> methods = signatureMap.get(method.getDeclaringClass());
    //Judge whether to intercept
    if (methods != null && methods.contains(method)) {
      //Intercepting method of executing interceptor
      return interceptor.intercept(new Invocation(target, method, args));
    }
    //Calling methods directly through the target object without blocking
    return method.invoke(target, args);
  } catch (Exception e) {
    throw ExceptionUtil.unwrapThrowable(e);
  }
}

The interceptor methods that will execute the interceptor interface are as follows

public Object intercept(Invocation invocation) throws Throwable {
  return invocation.proceed();
}

An Invocation object is passed to the interceptor as follows

public class Invocation {

  private final Object target;
  private final Method method;
  private final 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);
  }

As you can see, the Invocation class holds the target class of the proxy object, the target class method executed and the parameters passed to it.

In the intercept method of each interceptor, the last statement must be return invocation. Processed() (otherwise, the interceptor chain will be broken, and your mybatis will not work normally basically). Invocation. Processed() simply calls the corresponding method of the lower target. If target is still a proxy, it will return to the Plugin.invoke method above. This forms the call chain push of the interceptor.

4. summary

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.

Let's assume that a plug-in is configured in MyBatis. What happens at run time?

  1. All processing classes that may be intercepted will generate a proxy
  2. When the processing agent executes the corresponding method, it determines whether to execute the interception method in the plug-in
  3. After the interception method in the plug-in is executed, the execution of the target is promoted

If there are N plug-ins, there are N agents, and each agent has to perform the above logic. In this case, the level-by-level agents need to generate dynamic agents many times, which affects the performance. Although the location of plug-in interception can be specified, this is a dynamic judgment during method execution. During initialization, the plug-in is simply packaged to all places that can be intercepted.

Therefore, when writing plug-ins, you should pay attention to the following principles:

  1. Do not write unnecessary plug-ins;
  2. When implementing the plugin method, judge the target type. The Plugin.wrap method is executed only for the object to be intercepted by the plug-in. Otherwise, it will directly return to the target Province, which can reduce the number of times the target is proxied.
354 original articles published, 522 praised, 1.28 million visitors+
His message board follow

Topics: Mybatis Attribute SQL xml