This article source code from mybatis-spring-boot-starter 2.1.2 version

PageHelper is a very good domestic open source Mybatis paging plug-in, it basically supports mainstream and commonly used databases. In this article, we understand the powerful plugin mechanism of Mybatis by exploring PageHelper. This article mainly introduces Mybatis plugin mechanism, PageHelper details are not discussed.

Mybais plugin mechanism

In order to explore how PageHelper works, please take a look at the extension mechanism of Mybatis plug-in. Executor, StatementHandler, ParameterHandler, and ResultSetHandler have a small tail called pluginAll. Let’s go back to where we first saw him.

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

2.1 InterceptorChain

Let’s return to the pluginAll method above:

public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
    // The InterceptorChain is iterated over and the Interception petor object's Plugin is called
      target = interceptor.plugin(target);
    }
    return target;
  }
Copy the code

theninterceptorsWhen was it initialized?

In the call interceptorChain. PluginAll before in the Configuration has the following methods:

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

AddInterceptor is added to the interceptorChain in the order in which the interceptors are configured. Inside the addInterceptor is the List

interceptors. Let’s look at the call relationship. It’s not hard to see that when initializing the SqlSessionFactory, you get this by parsing the plugin tag. If it is under Springboot to pagehelper – spring – the boot – starter for example, he is in the initialization PageHelperAutoConfiguration calls.

Let’s move on to the interceptor. Plugin and see what it does with the proxy object.

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

2.2 the Plugin

public static Object wrap(Object target, Interceptor interceptor) {
    // Get the method signature mapping table for the Interceptor to be wrappedMap<Class<? >, Set<Method>> signatureMap = getSignatureMap(interceptor); Class<? > type = target.getClass();// Get all interfaces declared on the Class of the object to be proppedClass<? >[] interfaces = getAllInterfaces(type, signatureMap);if (interfaces.length > 0) {
        // Use the JDK built-in Proxy to create Proxy objects
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }
Copy the code

Wrap is the core method of the Plugin and consists of three steps, one at a time.

2.2.1 getSignatureMap

private staticMap<Class<? >, Set<Method>> getSignatureMap(Interceptor interceptor) {// Get the Intercepts annotation
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
    // issue #251
    // If the Interceptor class does not have an Intercepts annotation, it will throw an exception, indicating that we must have an Intercepts annotation when we customize the plug-in
    if (interceptsAnnotation == null) {
      throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
    }
    // Parse the Interceptor's values attribute (Signature[]) array and store it in a HashMap, Set< Method>> container.Signature[] sigs = interceptsAnnotation.value(); Map<Class<? >, Set<Method>> signatureMap =new HashMap<>();
    for (Signature sig : sigs) {
      Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
      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

2.2.2 getAllInterfaces

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

All interfaces included in the interceptor method are returned.

2.2.3 invoke

When dynamic Proxy is implemented in proxy.newProxyInstance above, Plugin acts as the event handler and the invoke method must be called.

 @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      // This is a method intercepted by an interceptor
      if(methods ! =null && methods.contains(method)) {
        return interceptor.intercept(new Invocation(target, method, args));
      }
      // Execute the target method's invoke method
      return method.invoke(target, args);
    } catch (Exception e) {
      throwExceptionUtil.unwrapThrowable(e); }}Copy the code

Interceptor. Intercept:

Object intercept(Invocation invocation) throws Throwable;
Copy the code

An intercept is the logic that intercepts itself.

conclusion

Back in the pluginAll method, the pluginAll method iterates through the user-defined plug-in implementation class (Interceptor) and calls the Interceptor plugin method to intercept and extend the current object. That is, when we implement the intercept method of the self-defined Interceptor, we need to wrap the target object (proxy) according to our own logic in Plugin, and use Plugin’s wrap to create proxy class.

In layman’s terms, the Mybatis plugin mechanism is actually an extension to the Executor, StatementHandler, ParameterHandler, and ResultSetHandler target objects. You only need to be Interceptor’s intercept method as required.

With that knowledge in mind, let’s take a look at how PageHelper works.

The use of PageHelper

Springboot integration with PageHelper is as simple as introducing a starter. PageHelperAutoConfiguration PageHelperProperties will help me to automatic injection PageHelper related configuration.

<! -- mybatis pageHelper -->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.2.13</version>
        </dependency>
Copy the code

Let’s write a unit test to use:

    @Test
    public void query(a) {
       List<Role> roles = roleMapper.selectALl(new RowBounds(0.10));
    }
    @Test
    public void query2(a){
        // Enable paging plug-in. The first query below is automatically paginated
        PageHelper.startPage(1.10);
        List<Role> roles = roleMapper.selectALl();
        PageInfo<Role> pageInfo = new PageInfo(roles);
    }
Copy the code

There are two ways to enable the paging plug-in:

  • First, paging queries are done directly with the RowBounds parameter.
  • Pagehelper.startpage () static method.

The pageHelper.startPage () method is set separately for each query using ThreadLocal to pass and save Page objects.

Let’s take a look at the results:

How PageHelper works

1. Initialization

PageHelperAutoConfiguration when structure will PageInterceptor load to InterceptorChain. The interceptors.

 @PostConstruct
    public void addPageInterceptor(a) {
        PageInterceptor interceptor = new PageInterceptor();
        Properties properties = new Properties();
        // Put the attributes in the normal configuration first
        properties.putAll(pageHelperProperties());
        // To put the special configuration in, the attribute name is close-conn instead of closeConn, so an extra step is required
        properties.putAll(this.properties.getProperties());
        interceptor.setProperties(properties);
        for(SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) { sqlSessionFactory.getConfiguration().addInterceptor(interceptor); }}Copy the code

Pagehelper.startpage () puts the page parameter page object into ThreadLocal

protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
Copy the code

2. Intercept the Query method

When executing the mapper method, create Executor, execute pluginAll, and then enter the PageInterceptor plugin, the implementation class of the Interceptor. Let’s take a look at PageInterceptor:

@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 {
Copy the code

Here, @signature marks two query methods, one with four parameters and the other with six parameters. You can check out the QueryInterceptor specification for an advanced Executor Interceptor tutorial to see why. Let’s return to the plugin.wrap method:

public static Object wrap(Object target, Interceptor interceptor) { Map<Class<? >, Set<Method>> signatureMap = getSignatureMap(interceptor);/ / type is cachingExecutorClass<? > type = target.getClass();// interface is an Executor interfaceClass<? >[] interfaces = getAllInterfaces(type, signatureMap);if (interfaces.length > 0) {
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }
Copy the code

All signatureMap needs to intercept are two Query methods. Then executes the Plugin invoke method, enter PageInterceptor, intercept, the invocation here = new invocation (target, method, args), The Target is the cachingExecutor, so let’s look at what the Intercept does when it intercepts the cachingExecutor Query method.

@Override
    public Object intercept(Invocation invocation) throws Throwable {
        try {
            Object[] args = invocation.getArgs();
            MappedStatement ms = (MappedStatement) args[0];
            Object parameter = args[1];
            RowBounds rowBounds = (RowBounds) args[2];
            ResultHandler resultHandler = (ResultHandler) args[3];
            Executor executor = (Executor) invocation.getTarget();
            CacheKey cacheKey;
            BoundSql boundSql;
            // Only one entry will be entered due to logic
            if (args.length == 4) {
                //4 arguments
                boundSql = ms.getBoundSql(parameter);
                cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
            } else {
                //6 parameters
                cacheKey = (CacheKey) args[4];
                boundSql = (BoundSql) args[5];
            }
            checkDialectExists();

            List resultList;
            // Call the method to determine whether paging is required, and return the result if not
            if(! dialect.skip(ms, parameter, rowBounds)) {// Determine whether a count query is required
                if (dialect.beforeCount(ms, parameter, rowBounds)) {
                    // Query the total number
                    Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                    // Process the total number of queries, return true to continue paging query, false directly return
                    if(! dialect.afterCount(count, parameter, rowBounds)) {// If the total number of queries is 0, an empty result is returned
                        return dialect.afterPage(newArrayList(), parameter, rowBounds); } } resultList = ExecutorUtil.pageQuery(dialect, executor, ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);  }else {
                //rowBounds takes the parameter value and supports the default memory paging when not handled by the paging plug-in
                resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
            }
            return dialect.afterPage(resultList, parameter, rowBounds);
        } finally {
            if(dialect ! =null){ dialect.afterAll(); }}}Copy the code

We can see the two query methods in the code:

@Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }
Copy the code

The 6-argument method is executed in a 4-argument method. So the interception of the first entry is also a four-parameter query method. BoundSql and CacheKey are retrieved in the Intercept if (args.length == 4). Then you go back and determine if you need to do paging.

3. count

Count (Executor, MS, parameter, rowBounds, resultHandler, boundSql) determines if we have a handwritten count statement, If there is no call com. Making. Pagehelper. Util. ExecutorUtil# executeAutoCount automatically create a count of SQL statements and query results.

public static Long executeAutoCount(Dialect dialect, Executor executor, MappedStatement countMs, Object parameter, BoundSql boundSql, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        Map<String, Object> additionalParameters = getAdditionalParameter(boundSql);
        // Create a cache key for the count query
        CacheKey countKey = executor.createCacheKey(countMs, parameter, RowBounds.DEFAULT, boundSql);
        // Call dialect to get count SQL
        String countSql = dialect.getCountSql(countMs, boundSql, parameter, rowBounds, countKey);
        //countKey.update(countSql);
        BoundSql countBoundSql = new BoundSql(countMs.getConfiguration(), countSql, boundSql.getParameterMappings(), parameter);
        // When dynamic SQL is used, temporary parameters may be generated that need to be manually set into the new BoundSql
        for (String key : additionalParameters.keySet()) {
            countBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
        }
        // Execute the count query
        Object countResultList = executor.query(countMs, parameter, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql);
        Long count = (Long) ((List) countResultList).get(0);
        return count;
Copy the code

Notice that the executor. Query directly calls the query 6-argument method.

ExecuteAutoCount = pageHelper; pageHelper = pageHelper; pageHelper = pageHelper; pageHelper = pageHelper; And directly execute the query6 parameter method to obtain the query results.

4. pageQuery

Count queries, and proceed to the ExecutorUtil.pagequery method.

public static  <E> List<E> pageQuery(Dialect dialect, Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql, CacheKey cacheKey) throws SQLException {
        // Determine whether paging queries are required
        if (dialect.beforePage(ms, parameter, rowBounds)) {
            // Generate a cache key for paging
            CacheKey pageKey = cacheKey;
            // Process the parameter object
            parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
            // Call dialect to get paging SQL
            String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
            BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);

            Map<String, Object> additionalParameters = getAdditionalParameter(boundSql);
            // Set dynamic parameters
            for (String key : additionalParameters.keySet()) {
                pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
            }
            // Perform paging queries
            return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
        } else {
            // Memory paging is not performed when paging is not performed
            returnexecutor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql); }}Copy the code

The getPageSql method will concatenate the limit statement for us based on the database type. Such as I use mysql is called com. Making. Pagehelper. The dialect. Helper. MySqlDialect# getPageSql:

 @Override 
    public String getPageSql(String sql, Page page, CacheKey pageKey) {
        StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
        sqlBuilder.append(sql);
        if (page.getStartRow() == 0) {
            sqlBuilder.append(" LIMIT ? ");
        } else {
            sqlBuilder.append(" LIMIT ?, ? ");
        }
        return sqlBuilder.toString();
    }
Copy the code

We then call query’s 6-argument method to execute the SQL, retrieve the result, and return it. That is, after pageHelper is introduced, SQL operations performed by paging queries are handled in Intercept.