Implementation principle of Mybatis interceptor

Posted by crimsonmoon on Fri, 17 Dec 2021 15:35:55 +0100

Implementation principle of Mybatis interceptor

Analyze the implementation principle from the perspective of source code, and do not involve specific use examples.

1. What does the interceptor look like

/**
 * @author Clinton Begin
 */
public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

  default void setProperties(Properties properties) {
    // NOP
  }

}

Most implementations implement the intercept method, and pay little attention to the following two methods. Then let's see what he looks like

default Object plugin(Object target)
  1. Plugin implements InvocationHandler, so there must be a dynamic agent of jdk working here. It must depend on his invoke method. I'll talk about it later

  2. Look, there are three parameters in it

      private final Object target; //Original object
      private final Interceptor interceptor; //Interceptor
      private final Map<Class<?>, Set<Method>> signatureMap; //
    
  3. Before looking at the wrap method, first look at the getSignatureMap in the warp method

      private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
        //Get the Intercepts annotation on the 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 annotation
        Signature[] sigs = interceptsAnnotation.value();
        
        //This map stores the classes to be intercepted and the method s to be intercepted in this class.
        Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
        for (Signature sig : sigs) {
          Set<Method> methods = MapUtil.computeIfAbsent(signatureMap, sig.type(), k -> new HashSet<>());
          try {
            //Get the method specified in the Signature through type, args and method.
            //Here, the specific methods above the specified class are obtained through reflection and cached.
            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;
      }
    
  4. Continue to look at the wrap method

      public static Object wrap(Object target, Interceptor interceptor) {
        Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
        Class<?> type = target.getClass();
    
        // Get whether all the interfaces of the search type are included in the signatureMap, and return the included. Note that this interface is to find all interfaces, including the parent interface
        // Isn't the signatureMap already cached and a collection of classes and specific methods that need to be intercepted. Then you need to traverse the target class and its parent interface to see which methods in the interface of the target class need to use proxies.
        Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
        if (interfaces.length > 0) {
          
          //It's very familiar here. Direct new agent.
          return Proxy.newProxyInstance(
              type.getClassLoader(),
              interfaces,
              new Plugin(target, interceptor, signatureMap));
        }
        return target;
      }
    
  5. As an agent, it is necessary to look at the invoke method

      @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
          //After the proxy object is generated. The invoke method is invoked when the method is invoked, and the signatureMap is used to determine whether the current call method is needed.
          // Use interceptors.
          Set<Method> methods = signatureMap.get(method.getDeclaringClass());
          if (methods != null && methods.contains(method)) {
            //Directly call the method of the proxy object. Note that the past instance passed here is an unpackaged object, not a proxy. This proxy is actually a proxy object, if
            // Calling proxy directly will cause stack overflow.
            return interceptor.intercept(new Invocation(target, method, args));
          }
          //The original object is also used when calling the method
          return method.invoke(target, args);
        } catch (Exception e) {
          throw ExceptionUtil.unwrapThrowable(e);
        }
      }
    
default void setProperties(Properties properties)

This method is only used to set properties. I'll talk about it next

2. When was the interceptor created.

First of all, you should know that after the code is written, the interceptor needs to be configured in the xml file in mybatis. If it is in spring or Springboot, it can be managed by spring or mybatis.

Here is just a simple look at how it is done in Mybatis.

  <plugins>
    <plugin interceptor="org.apache.ibatis.session.mybatis.MyInterceptor">
      <property name="name" value="myInterceptor"/>
    </plugin>
  </plugins>

Just start from the classic new sqlsessionfactorybuilder() build(reader); When you start looking, you can see the following code

 private void parseConfiguration(XNode root) {
    try {
      // issue #117 read properties first
      propertiesElement(root.evalNode("properties")); //Parse the properties tag and put it in the Variables of parser and config
      Properties settings = settingsAsProperties(root.evalNode("settings"));//Load setting tag
      loadCustomVfs(settings); //lcnote what is vfs here? How to use it?
      loadCustomLogImpl(settings);
      typeAliasesElement(root.evalNode("typeAliases"));

      //From here on, we all parse the specific tag, new out the object, and set the properties under the tag,
      // From the analysis here, we can basically see several important points in mybatis. The first is objectfactory, objectfactory. objectFactory,plugins
      pluginElement(root.evalNode("plugins"));
      
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectFactory"));
      reflectorFactoryElement(root.evalNode("objectFactory"));

      //Here you will set the setting tag
      settingsElement(settings);

      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      
      //lcnote here is the focus, parsing mapper files,
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

The above is the important code for mybatis to parse the configuration file. Here, just look at the pluginElement(root.evalNode("plugins")

pluginElement(root.evalNode("plugins"))
  // Here, you will directly new out the class that implements the Interceptor interface, call setProperties to set the properties under the Interceptor tag, and add this class to the Interceptor
  private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        String interceptor = child.getStringAttribute("interceptor");
        Properties properties = child.getChildrenAsProperties();//This is the element in the property tag, which will become a Properties
        
        // The key point is to resolve to one and create it directly through the construction method. Note that this is a parameterless construction.
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
        //To set properties, the setProperties method in the interceptor is called here. Set properties
        interceptorInstance.setProperties(properties);
        
        //Add to the interceptor collection, and notice a very important object configuration in Mybatis. This object basically contains all the in Mybatis.
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }

From here, you can see that it is created when parsing the configuration file and the plugins tag. By parameterless construction.

3. Where can the interceptor be used?

There is a simple way. You can directly see that the configuration object contains all the things of Mybatis runtime, and you can also see from the above code that all proxy objects are added to configuration addInterceptor(interceptorInstance); Inside. So just look at where to call it. It's very simple. Look directly.

Four places

  1. ParameterHandler (set parameters)
  2. ResultSetHandler (processing results)
  3. StatementHandler (this is the Statement obtained through Connection in jdbc)
  4. Executor (from a logical point of view, the whole process, including setting parameters, parsing sql and processing the returned results, needs to be completed in one executor. In addition, Mybatis supports three types of SIMPLE, REUSE, BATCH and caching executor, which are only agents. The real implementation is entrusted to the first three types)

When creating these four objects, call interceptorchain Pluginall (parameterhandler) through the above analysis, we can know that a proxy object will be created here. If the interceptor is configured. Through the previous JDK, we can know that if it is a dynamic proxy object, InvocationHandler will be called during each method call, and the same will be true here. It means that interceptors may be used when calling all methods.

4. Why should Intercepts be annotated?

Think about the dynamic proxy of Jdk. As long as it is a dynamic proxy object, InvocationHandler will be called when calling methods. All the ways, isn't that good? What you want is to intercept the specified methods, not all of them. This is the function of Intercepts and Signature annotations. From these two annotations, we can analyze which methods on which interfaces need to apply interceptors. So as to achieve the purpose.

At this point, I thought of cglib

Cglib and jdk proxy are different. Except that the implementation of jdk is an interface, cglib can enhance the class, but both implementations are essentially bytecode. Both generate a new class, and then inherit the class to be proxy or implement the interface to be proxy. I think the biggest difference between the two is that cglib can support multiple methodinterceptors and specify which MethodInterceptor each method should execute through CallbackFilter.

That's all for the Mybatis interceptor. If there are any errors, please point them out. thank you.

Topics: Java mybaties