Senior Programmer's Master: Detailed explanation of Mybatis interceptor (from use to source code)

Detailed explanation of Mybatis interceptor (from use to source code)

MyBatis provides a plug-in function. Although it is called a plug-in, it is actually an interceptor function.

This article analyzes from configuration to source code.

1. Introduction of Interceptor

MyBatis allows you to intercept calls at certain points during the execution of mapped statements. By default, MyBatis allows plug-ins to intercept method calls including:

  1. Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  2. ParameterHandler (getParameterObject, setParameters)
  3. ResultSetHandler (handleResultSets, handleOutputParameters)
  4. StatementHandler (prepare, parameterize, batch, update, query)

To sum up, they are the interceptor executor, parameter controller, result controller, and SQL statement construction controller to change the execution process. Corresponding to four objects:

2. Interceptor icon

3. Use of interceptors

1. Test class

    @Test
    public  void testQueryByNo() throws IOException {
        Reader reader =
                Resources.getResourceAsReader("mybatis-config.xml");
        SqlSessionFactory sessionFactory
                = new SqlSessionFactoryBuilder().build(reader);              
        SqlSession session = sessionFactory.openSession();
        //传入StudentMapper接口,返回该接口的mapper代理对象studentMapper
        StudentMapper studentMapper = session.getMapper(StudentMapper.class);//接口
        //通过mapper代理对象studentMapper,来调用IStudentMapper接口中的方法
        Student student = studentMapper.queryStudentByNo(1);
        System.out.println(student);
        session.close();
    }

2. Implement the interface

Custom classes implement the Interceptor interface

public class MyInterceptor implements Interceptor {
    public Object intercept(Invocation invocation) throws Throwable {
        //放行方法
        Object proceed = invocation.proceed();
        return proceed;
    }
    public Object plugin(Object target) {
        Object wrap = Plugin.wrap(target, this);
        return wrap;
    }
    public void setProperties(Properties properties) {
    }
}

Method explanation in class:

intercept: Intercept, define some functions here

plugin: Integrate the method that needs to be enhanced and the enhanced part of the interceptor and return it. This method will be executed four times, and all four processors will be increased.

Strong, the first execution is after sessionFactory.openSession(). From the source code, we can know that the four default interceptors are:

		>		>	>CachingExecutor
		>		>	>
		>		>	>DefaultParameterHandler
		>		>	>
		>		>	>DefaultResultSetHandler
		>		>	>
		>		>	>RoutingStatementHandler

setProperties : Set variable properties, this method is executed after SqlSessionFactoryBuilder().build(reader)

3. Add annotations

Add annotations to the above implementation class

@Intercepts({
        @Signature(
                type = StatementHandler.class,
                method = "query",
                args = {Statement.class, ResultHandler.class}
        )

})

Signature parameter explanation:

​ type : This is mainly the type of the interceptor object, which is one of the four types in the introduction of the interceptor, or more (the return value of type in the source code is an array) method : The method can only
be one value,
​ args: The value is the previous The content in the parentheses of the four interceptors introduced can have more than one.

4. Configuration file

Modify the mybatis-config.xml file

Position: after typeAliases, before environments (if you don't remember, you can make a mistake on purpose, and then idea will prompt)

    <plugins>
       <plugin interceptor="com.courage.mybatis.my.interceptors.MyInterceptor">
           <property name="name" value="zs"/>
           <property name="age" value="23"/>
       </plugin>
    </plugins>

4. Execution order of multiple interceptors

If there are multiple interceptors, the order of interception will follow the order of interceptors in the plugins tag in mybatis-config.xml, but the order of execution is reversed

5. Modify after the interceptor intercepts

    public Object intercept(Invocation invocation) throws Throwable {
        Object target = invocation.getTarget();
        MetaObject metaObject = SystemMetaObject.forObject(target);
        Object value = metaObject.getValue("parameterHandler.parameterObject");
        metaObject.setValue("parameterHandler.parameterObject",2);
        Object proceed = invocation.proceed();
        return proceed;
    }

main points:

The metaObject.getValue() method is a parameter value. You can print the target in the previous step to get the source code of the object (class) and view the value contained in the class.

In the RoutingStatementHandler analyzed in this article, there are two values ​​​​that can be obtained (get means that there is this attribute), and the parameterHandler in metaObject.setValue("parameterHandler.parameterObject", 2); is the default DefaultParameterHandler, which can be printed in the interceptor The wrap in the plugin method knows:

org.apache.ibatis.executor.CachingExecutor@bcef303
org.apache.ibatis.scripting.defaults.DefaultParameterHandler@2f1de2d6
org.apache.ibatis.executor.resultset.DefaultResultSetHandler@289710d9
org.apache.ibatis.executor.statement.RoutingStatementHandler@5143c662

Re-assign the obtained parameter object to 2 (originally 1) query, the result:

学号:2	姓名:lixiaosi	年龄:30	年级:middle

6. Source code analysis

First start the analysis from the source -> configuration file:

1. pluginElement method

XMLConfigBuilder parses the pluginElement private method of the MyBatis global configuration file:

private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        String interceptor = child.getStringAttribute("interceptor");
        Properties properties = child.getChildrenAsProperties();
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
        interceptorInstance.setProperties(properties);
        configuration.addInterceptor(interceptorInstance);
      }
    }
}

The specific parsing code is actually relatively simple, so I won’t post it here. It is mainly to instantiate the class represented by the interceptor attribute in the plugin node through reflection. Then call the addInterceptor method of the global configuration class Configuration.

public void addInterceptor(Interceptor interceptor) {
	   interceptorChain.addInterceptor(interceptor);
     }

2. InterceptorChain class

This interceptorChain is an internal attribute of Configuration, and its type is InterceptorChain, which is an interceptor chain. Let's look at its definition:

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

}

3. Why does the interceptor intercept several processors?

Now that we understand the analysis of the interceptor configuration and the ownership of the interceptor, let's look back and see why the interceptor intercepts these methods (some methods of Executor, ParameterHandler, ResultSetHandler, and StatementHandler):

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, ExecutorType executorType, boolean autoCommit) {
    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, autoCommit);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

The above four methods are all methods of Configuration. These methods will be executed in an operation of MyBatis (addition, deletion, modification, query), and the order of execution is Executor, ParameterHandler, ResultSetHandler, StatementHandler (the creation of ParameterHandler and ResultSetHandler is in the creation of StatementHandler [3 available When the implementation class CallableStatementHandler, PreparedStatementHandler, SimpleStatementHandler], its constructor calls [the constructors of these three implementation classes actually call the constructor of the parent class BaseStatementHandler]).

After these four methods instantiate the corresponding objects, they will call the pluginAll method of interceptorChain. The pluginAll of InterceptorChain has just been introduced, which is to traverse all interceptors and then call the plugin method of each interceptor.

Note: The return value of the plugin method of the interceptor will be directly assigned to the original object

Since StatementHandler can be intercepted, this interface mainly deals with the construction of sql syntax, so for example, the function of paging can be realized by interceptor. It only needs to process the sql in the StatementHandler interface implementation class in the plugin method of the interceptor, which can be realized by reflection .

4. Use of Plugin class

MyBatis also provides @Intercepts and @Signature annotations on interceptors. The example on the official website uses these two annotations, and also includes the use of the Plugin class:

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

Next, let's analyze the source code of these three "new combinations". First, let's look at the wrap method of the Plugin class:

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

The Plugin class implements the InvocationHandler interface. Obviously, we see that a dynamic proxy class provided by JDK itself is returned here. Let's dissect other methods called by this method:

5. getSignatureMap method

getSignatureMap method:

private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
    if (interceptsAnnotation == null) { // issue #251
      throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());      
    }
    Signature[] sigs = interceptsAnnotation.value();
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
    for (Signature sig : sigs) {
      Set<Method> methods = signatureMap.get(sig.type());
      if (methods == null) {
        methods = new HashSet<Method>();
        signatureMap.put(sig.type(), methods);
      }
      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;
}

GetSignatureMap method explanation: First, you will get the @Interceptors annotation of the interceptor class, and then get the attribute @Signature annotation collection of this annotation, and then traverse this collection, and take out the type attribute (Class type) of the @Signature annotation when traversing. Then get the Method with method attribute and args attribute according to this type. Since the @Signature attribute of the @Interceptors annotation is an attribute, it will eventually return a Map with the type as the key and the value as the Set.

@Intercepts({@Signature(
  type= Executor.class,
  method = "update",
  args = {MappedStatement.class,Object.class})})

For example, this @Interceptors annotation will return a key as Executor and value as a set (this set has only one element, that is, a Method instance, and this Method instance is the update method of the Executor interface, and this method has parameters of MappedStatement and Object types). This Method instance is obtained from the method and args attributes of @Signature. If the args parameter does not correspond to the method method of type type, an exception will be thrown.

6、getAllInterfaces

getAllInterfaces method:

private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
    Set<Class<?>> interfaces = new HashSet<Class<?>>();
    while (type != null) {
      for (Class<?> c : type.getInterfaces()) {
        if (signatureMap.containsKey(c)) {
          interfaces.add(c);
        }
      }
      type = type.getSuperclass();
    }
    return interfaces.toArray(new Class<?>[interfaces.size()]);
}

getAllInterfaces method explanation: According to the target instance target (this target is the class that can be intercepted by the MyBatis interceptor mentioned earlier, Executor, ParameterHandler, ResultSetHandler, StatementHandler) and its parent classes, return the interface array containing the target implementation in the signatureMap.

So the role of the Plugin class is to get the attribute @Signature array of this annotation according to the @Interceptors annotation, and then use reflection to find the corresponding Method according to the type, method, and args attributes of each @Signature annotation. Finally, it is determined whether to return a proxy object to replace the original target object according to the interface implemented by the called target object.

For example, in the example of MyBatis official website, when Configuration calls the newExecutor method, the update(MappedStatement ms, Object parameter) method of the Executor interface is intercepted by the interceptor. So what is finally returned is a proxy class Plugin, not Executor. When calling a method like this, if it is a proxy class, it will execute:

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

That's right, if the corresponding method is found and proxied, the interceptor method of the Interceptor interface will be executed.

The Invocation class is as follows:

public class Invocation {
  private Object target;
  private Method method;
  private 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);
  }

}

Its proceed method is to call the original method (without proxy).

Explanation: This article is limited in length, so only part of the java content is shown. The complete Java learning document editor has been compiled for you. If you need it, please like + pay attention to private message me (need) to receive free Java and Dachang interview learning materials. !

Guess you like

Origin blog.csdn.net/m0_67788957/article/details/123772936