The last article Mybatis interceptor data encryption and decryption introduced the simple use of Mybatis interceptor, this article will thoroughly analyze Mybatis is how to find interceptor and call the intercept method of interceptor

The little friend first understands the whole interceptor execution process according to the article content in detail but not in detail, Outlines each point on the paper, then reads the source code in detail, connects these points with the line, so standing in the perspective of God, understands more deeply

Discovery interceptor

According to the website instructions, by implementing our org. Apache. Ibatis. Plugin. The Interceptor interface custom interceptors, there are two ways to add custom interceptors to the Mybatis configuration

Configuration file Mode

Add plugin to mybatis-config.xml

<! -- mybatis-config.xml -->
<plugins>
  <plugin interceptor="org.mybatis.example.ExamplePlugin">
    <property name="someProperty" value="100"/>
  </plugin>
</plugins>
Copy the code

XMLConfigBuilder parses the Mybatis global configuration file with the pluginElement method:

  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); }}}Copy the code

Can be seen from this method, the traverse XML XNode, if the node is interceptor attributes, we configured interceptor, through the configuration. The addInterceptor (interceptorInstance); Add the interceptor instance to Mybatis Configuration

Annotation way

In the article Mybatis interceptor data encryption and decryption, I see that I have added @Component annotation to the custom interceptor class. In the current microservice framework, it mostly exists in the form of Spring Boot adding Mybatis Starter dependency. To see MybatisAutoConfiguration. Java constructor:

public MybatisAutoConfiguration(MybatisProperties properties, ObjectProvider
       
         interceptorsProvider, ResourceLoader resourceLoader, ObjectProvider
        
          databaseIdProvider, ObjectProvider
         
          > configurationCustomizersProvider)
         
        
       []> {
    this.properties = properties;
    this.interceptors = interceptorsProvider.getIfAvailable();
    this.resourceLoader = resourceLoader;
    this.databaseIdProvider = databaseIdProvider.getIfAvailable();
    this.configurationCustomizers = configurationCustomizersProvider.getIfAvailable();
}
Copy the code

In the constructor interceptorsProvider. GetIfAvailable (); Get all the injected interceptors and add our Interceptor when building the SqlSessionFactory:

if(! ObjectUtils.isEmpty(this.interceptors)) {
  factory.setPlugins(this.interceptors);
}
Copy the code

Call procedure resolution

The Configuration class contains all the Configuration information for Mybatis and contains four very important methods that the interceptor intercepts

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) {
    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;
  }
Copy the code

StatementHandler -> ParameterHandler -> ResultSetHandler -> StatementHandler -> StatementHandler We know that in MyBatis, SqlSessionFactory is used to create SqlSession. Once you have a session, you can use it to execute mapping statements, commit or roll back connections, and finally, close the session when it is no longer needed. And see, in DefaultSqlSessionFactory openSessionFromDataSource method in the Java classes of newExecutor method invokes the Configuration class

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      // Call the newExecutor method of the Configuration class to create an executor
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
    } finally{ ErrorContext.instance().reset(); }}Copy the code

Sqlsessiontemplate. Java implements the SqlSession interface, has a private inner class SqlSessionInterceptor and implements InvocationHandler. This is how Java dynamic proxies are implemented, focusing on the overridden Invoke method, where the method is actually called, and tracing the call stack finds that the newStatementHandler method of the Configuration class ends up being called

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      SqlSession sqlSession = getSqlSession(
          SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType,
          SqlSessionTemplate.this.exceptionTranslator);
      try {
        // The newStatementHandler method of the Configuration class will be called if you follow the actual method executionObject result = method.invoke(sqlSession, args); . }catch (Throwable t) {
        ...
      } finally {
        if(sqlSession ! =null) {
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory); }}}}Copy the code

In the newStatementHandler method of the Configuration class, pass the new RoutingStatementHandler(…) Method to build a StatementHandler, in which the statementType is used to determine which StatementHandler is generated

    public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

    switch (ms.getStatementType()) {
      case STATEMENT:
        delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case PREPARED:
        delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case CALLABLE:
        delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      default:
        throw new ExecutorException("Unknown statement type: "+ ms.getStatementType()); }}Copy the code

All three types of StatementHandler inherited BaseStatementHandler. Java, look at the following class diagram

When instantiating a specific StatementHandler, the parent BaseStatementHandler constructor is called first. The newParameterHandler and newResultSetHandler methods in the Configuration class are called sequentially in the constructor of the parent class:

this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
Copy the code

So the entire call process looks like this: newExecutor -> StatementHandler -> ParameterHandler -> ResultSetHandler -> StatementHandler

The four methods in the Configuratin class all have the same type of code:

interceptorChain.pluginAll(...)
Copy the code

This is the key to the interceptor. The interceptorChain is a member variable of the Configuration class.

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

In pluginAll, we iterate over the plugin methods of all interceptors. In our custom interceptor, we override the plugin method. Here we explain how to call the interceptor using the generic page interceptor.

@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}), })public class PageInterceptor implements Interceptor {

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

Plugin.java implements the InvocationHandler interface, which is also a Java dynamic proxy, calling its static method wrap:

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

If interfaces.length > 0 also generates the proxy object for target, That is for the Configuration class four method call executor/parameterHandler/resultSetHandler/statementHandler generates a proxy object, The two important methods getSignatureMap(interceptor) and getAllInterfaces(type, SignatureMap(interceptor) getSignatureMap(interceptor)

private staticMap<Class<? >, Set<Method>> getSignatureMap(Interceptor interceptor) { Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);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 =newHashMap<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;
  }
Copy the code

This method reads the annotation information on the interceptor class through Java reflection and returns a key of Type, Method set to the Value of a HashMap to paging blocker for example above, the key is org. Apache. Ibatis. Executor. Executor, Value is two overloaded query methods getAllInterfaces(type, signatureMap)

private staticClass<? >[] getAllInterfaces(Class<? > type, Map<Class<? >, Set<Method>> signatureMap) { Set<Class<? >> interfaces =newHashSet<Class<? > > ();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

This method returns an array of interfaces based on the target instance and its parents. Looking back at the plugin.wrap method, if the interface array length is greater than zero, it generates the proxy object for target. As in the selectList method, the executor is the newly generated proxy object

return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
Copy the code

The executor calls a method that executes the invoke method overridden by the Plugin:

  @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

Finally, the Intercept method of a custom interceptor is executed, which is how interceptors are executed. We found that in Mybatis framework, Java dynamic proxy is widely used. For example, we only need to define methods in Mapper interface, and there is no specific implementation class. All these are applied to Java dynamic proxy, so understanding dynamic proxy can better understand the whole execution process.

Interceptor annotations in detail

In this article, some of the key code of the page interceptor is captured and the interceptor is annotated as follows:

@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}), })Copy the code

In the data encryption and decryption of Mybatis interceptor, the contents of request parameter interceptor and return result set interceptor are respectively:

@Intercepts({
		@Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class),
})
Copy the code
@Intercepts({
		@Signature(type = ResultSetHandler.class, method = "handleResultSets", args={Statement.class})
})
Copy the code

Each interceptor intercepts a different method Signature. Actually very simple, all of these are interfaces in the Executor/ParameterHandler/ResultSetHandler method, according to the corresponding interface method configuration here, at the time of analytical interceptors through reflection will determine whether to find the corresponding method signature, If you can’t find NoSuchMethodException, look at the Executor interface, which has two overloaded query methods.

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

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

Combined with the article said the foreshadowing in the beginning, four methods of interceptors to intercept the Configuration class, that’s right, is the Executor/ParameterHandler/ResultSetHandler/StatementHandler, Continue to back to see Mybatis interceptor data encryption to decrypt the opening of the interceptor is introduced content, fully understand the role of the Executor/ParameterHandler/ResultSetHandler/StatementHandler, We can use the interceptor to play our own game,

Problem of eggs

We see that interceptors are called through the interceptorChain, which literally translates to interceptorChain. In fact, this is the chain of responsibility pattern of design pattern.

  1. Do you understand the chain of responsibility model?
  2. Can you think of any frameworks or scenarios in which the chain of responsibility model is applied?
  3. Where can the chain of responsibility model be applied in the real business to make our code more flexible and robust?
  4. If we define multiple interceptors of the same type, such as multiple Executor interceptors, how do we control the order of interceptors?

Efficiency tools

Follow public accounts, reply Tools for more tools that help us do our jobs more efficiently

Free Mybatis Plugin

We need to define methods in Mapper interface when using Mybatis and need handwriting SQL, and at the same time define SQL with the same name statementId in XML. The Intellij IDEA plug-in helps us quickly locate methods and XML, and switch back and forth: From Java to SQL

From SQL to Java

Recommended reading: Why do programmers read source code


Welcome to pay attention to the public account, interesting talk about coding those things, improve the hard power