Teach you how to develop mybatis plug-in

Time:2021-9-22

Happy Lantern Festival, my friends. Remember to eat Lantern Festival

In daily development, my friends have used mybatis plug-in more or less. Brother song guesses that the paging plug-in of mybatis is the most used! I wonder if my friends have ever thought of developing a mybatis plug-in one day?

In fact, it’s not difficult to roll out a mybatis plug-in by yourself. Today, brother song will take you to roll out a mybatis plug-in!

1. Mybatis plug-in interface

Even if you haven’t developed mybatis plug-in, you can guess that mybatis plug-in works through interceptors. When mybatis framework is designed, relevant interfaces have been reserved for plug-in development, as follows:

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
  }

}

There are only three methods in this interface. The first method must be implemented, and the latter two methods are optional. The functions of the three methods are as follows:

  1. Intercept: This is the specific interception method. When we customize the mybatis plug-in, we generally need to rewrite this method, and the work completed by our plug-in is also completed in this method.
  2. Plugin: the parameter target of this method is the object to be intercepted by the interceptor. Generally speaking, we don’t need to rewrite this method. The plugin.wrap method will automatically determine whether the signature of the interceptor matches the interface of the intercepted object. If so, the target object will be intercepted through the dynamic proxy.
  3. Setproperties: this method is used to pass the parameters of the plug-in. You can change the behavior of the plug-in through the parameters. After defining the plug-in, we need to configure the plug-in. During configuration, we can set relevant properties for the plug-in, and the set properties can be obtained through this method. The plug-in property settings are as follows:
<plugins>
    <plugin interceptor="org.javaboy.mybatis03.plugin.CamelInterceptor">
        <property name="xxx" value="xxx"/>
    </plugin>
</plugins>

2. Mybatis interceptor signature

After the interceptor is defined, who will intercept?

This requires interceptor signature to complete!

Interceptor signature is an annotation named @ intercepts, in which multiple signatures can be configured through @ [email protected] The signature annotation contains three attributes:

  • Type: the interface to be intercepted by the interceptor. There are four options: executor, parameterhandler, resultsethandler and statementhandler.
  • Method: the method name in the interface intercepted by the interceptor, that is, the method name in the first four interfaces. The interface and method should correspond to each other.
  • Args: the parameter type of the method intercepted by the interceptor. The only method can be locked through the method name and parameter type.

A simple signature might look like this:

@Intercepts(@Signature(
        type = ResultSetHandler.class,
        method = "handleResultSets",
        args = {Statement.class}
))
public class CamelInterceptor implements Interceptor {
    //...
}

3. Intercepted object

According to the previous introduction, the intercepted objects are mainly as follows:

Executor

public interface Executor {

  ResultHandler NO_RESULT_HANDLER = null;

  int update(MappedStatement ms, Object parameter) throws SQLException;

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

  <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;

  List<BatchResult> flushStatements() throws SQLException;

  void commit(boolean required) throws SQLException;

  void rollback(boolean required) throws SQLException;

  CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);

  boolean isCached(MappedStatement ms, CacheKey key);

  void clearLocalCache();

  void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);

  Transaction getTransaction();

  void close(boolean forceRollback);

  boolean isClosed();

  void setExecutorWrapper(Executor executor);

}

The meanings of each method are as follows:

  • Update: this method will be called when all insert, update and delete operations are executed. If you want to intercept these operations, you can use this method.
  • Query: this method will be called when the select query method is executed. The method parameters carry a lot of useful information. If you need to obtain it, you can implement it through this method.
  • Querycursor: this method will be called when the return type of select is cursor.
  • Flushstatements: when the sqlsession method calls the flushstatements method or executes an interface method with @ flush annotation, the method will be triggered.
  • Commit: when the sqlsession method calls the commit method, the method will be triggered.
  • Rollback: this method will be triggered when the sqlsession method calls the rollback method.
  • Gettransaction: the sqlsession method is triggered when it gets a database connection.
  • Close: this method will be triggered after lazy loading obtains a new executor.
  • Isclosed: this method will be triggered before the lazy load executes the query.

ParameterHandler

public interface ParameterHandler {

  Object getParameterObject();

  void setParameters(PreparedStatement ps) throws SQLException;

}

The meanings of each method are as follows:

  • Getparameterobject: this method will be triggered when the stored procedure is executed to process the parameters.
  • Setparameters: this method will be triggered when setting SQL parameters.

ResultSetHandler

public interface ResultSetHandler {

  <E> List<E> handleResultSets(Statement stmt) throws SQLException;

  <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;

  void handleOutputParameters(CallableStatement cs) throws SQLException;

}

The meanings of each method are as follows:

  • Handleresultsets: this method will be triggered in all query methods (except the query methods with the return value type of cursor < E >). Generally speaking, if we want to re process the query results, we can intercept this method.
  • Handlecursorresultsets: this method will be triggered when the return value type of the query method is cursor < E >.
  • Handleoutputparameters: this method will be called when the stored procedure is used to process the parameters.

StatementHandler

public interface StatementHandler {

  Statement prepare(Connection connection, Integer transactionTimeout)
      throws SQLException;

  void parameterize(Statement statement)
      throws SQLException;

  void batch(Statement statement)
      throws SQLException;

  int update(Statement statement)
      throws SQLException;

  <E> List<E> query(Statement statement, ResultHandler resultHandler)
      throws SQLException;

  <E> Cursor<E> queryCursor(Statement statement)
      throws SQLException;

  BoundSql getBoundSql();

  ParameterHandler getParameterHandler();

}

The meanings of each method are as follows:

  • Prepare: this method is triggered before the database is executed.
  • Parameterize: this method is executed after the prepare method to process parameter information.
  • Batch: if mybatis is configured in the whole play configurationdefaultExecutorType=”BATCH”, this method is called when performing data operations.
  • Update: this method will be triggered during the update operation.
  • Query: this method will be triggered when the select method is executed.
  • Querycursor: this method will be triggered when the select method is executed and the return value is cursor.

When developing a specific plug-in, we should decide which method to intercept according to our own needs.

4. Develop paging plug-ins

4.1 memory paging

Mybatis provides a memory paging function that is not easy to use. It is to query all data at one time and then perform paging processing in memory. This paging method is inefficient and basically useless. However, if we want to customize the paging plug-in, we need to have a simple understanding of this paging method.

The usage of memory paging is as follows. First, add the rowboundaries parameter in mapper, as follows:

public interface UserMapper {
    List<User> getAllUsersByPage(RowBounds rowBounds);
}

Then define the relevant SQL in the XML file:

<select id="getAllUsersByPage" resultType="org.javaboy.mybatis03.model.User">
    select * from user
</select>

You can see that when defining SQL, you don’t have to worry about paging at all. Mybatis will query all the data and then perform paging processing in memory.

Methods in mapper are called as follows:

@Test
public void test3() {
    UserMapper userMapper = sqlSessionFactory.openSession().getMapper(UserMapper.class);
    RowBounds rowBounds = new RowBounds(1,2);
    List<User> list = userMapper.getAllUsersByPage(rowBounds);
    for (User user : list) {
        System.out.println("user = " + user);
    }
}

When building rowbounds, two parameters are passed in, namely offset and limit, corresponding to the two parameters in paging SQL. You can also build a rowboundaries instance through rowboundaries.default. For the rowboundaries instance built in this way, offset is 0 and limit is integer.max_ Value, which is equivalent to no paging.

This is a very impractical memory paging function provided in mybatis.

After understanding the memory paging provided by mybatis, we can take a look at how to customize the paging plug-in.

4.2 custom paging plug-in

First of all, I want to make a statement. SongGe takes you to customize the mybatis paging plug-in, mainly to let you know some rules of the custom mybatis plug-in and understand the whole process of the custom plug-in. The paging plug-in is not our purpose. The self-defined paging plug-in is just to make your learning process more interesting.

Next, let’s start the journey of custom paging plug-in.

First of all, we need to customize a rowboundaries. Because mybatis’s native rowboundaries are memory pages, and there is no way to get the total number of records (we also need to get the total number of records in general page query), we customize pagerowboundaries and enhance the native rowboundaries function, as follows:

public class PageRowBounds extends RowBounds {
    private Long total;

    public PageRowBounds(int offset, int limit) {
        super(offset, limit);
    }

    public PageRowBounds() {
    }

    public Long getTotal() {
        return total;
    }

    public void setTotal(Long total) {
        this.total = total;
    }
}

You can see that the total field is added to our customized pagerowboundaries to save the total number of records queried.

Next, we customize the interceptor pageinterceptor as follows:

@Intercepts(@Signature(
        type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
))
public class PageInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        Object parameterObject = args[1];
        RowBounds rowBounds = (RowBounds) args[2];
        if (rowBounds != RowBounds.DEFAULT) {
            Executor executor = (Executor) invocation.getTarget();
            BoundSql boundSql = ms.getBoundSql(parameterObject);
            Field additionalParametersField = BoundSql.class.getDeclaredField("additionalParameters");
            additionalParametersField.setAccessible(true);
            Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql);
            if (rowBounds instanceof PageRowBounds) {
                MappedStatement countMs = newMappedStatement(ms, Long.class);
                CacheKey countKey = executor.createCacheKey(countMs, parameterObject, RowBounds.DEFAULT, boundSql);
                String countSql = "select count(*) from (" + boundSql.getSql() + ") temp";
                BoundSql countBoundSql = new BoundSql(ms.getConfiguration(), countSql, boundSql.getParameterMappings(), parameterObject);
                Set<String> keySet = additionalParameters.keySet();
                for (String key : keySet) {
                    countBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
                }
                List<Object> countQueryResult = executor.query(countMs, parameterObject, RowBounds.DEFAULT, (ResultHandler) args[3], countKey, countBoundSql);
                Long count = (Long) countQueryResult.get(0);
                ((PageRowBounds) rowBounds).setTotal(count);
            }
            CacheKey pageKey = executor.createCacheKey(ms, parameterObject, rowBounds, boundSql);
            pageKey.update("RowBounds");
            String pageSql = boundSql.getSql() + " limit " + rowBounds.getOffset() + "," + rowBounds.getLimit();
            BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameterObject);
            Set<String> keySet = additionalParameters.keySet();
            for (String key : keySet) {
                pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
            }
            List list = executor.query(ms, parameterObject, RowBounds.DEFAULT, (ResultHandler) args[3], pageKey, pageBoundSql);
            return list;
        }
        //No paging is required and the result is returned directly
        return invocation.proceed();
    }

    private MappedStatement newMappedStatement(MappedStatement ms, Class<Long> longClass) {
        MappedStatement.Builder builder = new MappedStatement.Builder(
                ms.getConfiguration(), ms.getId() + "_count", ms.getSqlSource(), ms.getSqlCommandType()
        );
        ResultMap resultMap = new ResultMap.Builder(ms.getConfiguration(), ms.getId(), longClass, new ArrayList<>(0)).build();
        builder.resource(ms.getResource())
                .fetchSize(ms.getFetchSize())
                .statementType(ms.getStatementType())
                .timeout(ms.getTimeout())
                .parameterMap(ms.getParameterMap())
                .resultSetType(ms.getResultSetType())
                .cache(ms.getCache())
                .flushCacheRequired(ms.isFlushCacheRequired())
                .useCache(ms.isUseCache())
                .resultMaps(Arrays.asList(resultMap));
        if (ms.getKeyProperties() != null && ms.getKeyProperties().length > 0) {
            StringBuilder keyProperties = new StringBuilder();
            for (String keyProperty : ms.getKeyProperties()) {
                keyProperties.append(keyProperty).append(",");
            }
            keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
            builder.keyProperty(keyProperties.toString());
        }
        return builder.build();
    }
}

This is the core code we defined today. Let’s give you an analysis of the knowledge points involved.

  1. First, configure the interceptor signature through the @ intercepts annotation. From the definition of @ signature, we can see that the interceptor #query method is an overloaded method. The overloaded method is locked by specifying the method parameters through args (in fact, we cannot intercept another overloaded method of this method, which is called internally by mybatis, which is not discussed here).
  2. After intercepting the query operation, our next operation is mainly completed in the pageinterceptor #intercept method. The parameters of this method contain a lot of information about the intercepted object.
  3. adoptinvocation.getArgs()Get the parameters of the interception method. What you get is an array. Normally, the length of this array is 4. The first item of the array is a mappedstatement. Various operation nodes and SQL defined in mapper.xml are encapsulated into mappedstatement objects; The second item of the array is the specific parameters of the intercepted method, that is, the method parameters defined in the mapper interface; The third item of the array is a rowbounds object. We do not necessarily use the rowbounds object when defining methods in the mapper interface. If we do not define the rowbounds object, the system will provide us with a default rowbounds.default; The fourth item of the array is a resulthandler that handles the return value.
  4. Next, judge whether the rowboundaries object extracted in the previous step is not rowboundaries.default. If it is rowboundaries.default, it indicates that the user does not want to page; If it is not rowboundaries.default, it indicates that the user wants to page. If the user does not want to page, the last page will be executed directlyreturn invocation.proceed();, just let the method go on.
  5. If paging is required, first take out the executor, boundsql and the additional parameters saved in boundsql through reflection from the invocation object (this parameter may exist if we use dynamic SQL). Boundsql encapsulates the SQL we execute and related parameters.
  6. Next, judge whether rowboundaries is an instance of pagerowboundaries. If yes, it indicates that you want to query the total number of records in addition to paging query. If not, it indicates that rowboundaries may be an instance of rowboundaries. At this time, you only need to page without querying the total number of records.
  7. If you need to query the total number of records, first call the newmappedstatement method to construct a new mappedstatement object. The return value of the new mappedstatement object is of type long. Then create the cachekey of the query and the countsql of the splicing query respectively, build the countboundsql according to the countsql, and add additional parameters to the countboundsql. Finally, complete the query operation through the executor. Query method, and assign the query result to the total attribute in pagerowbounds.
  8. Next, perform paging query. After the introduction of step 7, paging query is very simple. I won’t elaborate here. The only thing to emphasize is that after we start this paging plug-in, the original rowboundaries memory page of mybatis will become a physical page. Here’s why we modified the query SQL.
  9. Finally, the query result is returned.

In the previous code, we reorganized the SQL in two places: one is when querying the total records, and the other is when paging. We obtained the SQL in mapper.xml through boundsql. Getsql() and then modified it. Some children didn’t notice when writing SQL in mapper.xml, and may have added it at the end;, this will cause errors in the SQL run of the reassembly of the paging plug-in, which should be noted. The same is true for other mybatis paging plug-ins that SongGe saw on GitHub. The SQL end in mapper.xml cannot have;

After that, our paging plug-in is successfully defined.

5. Test

Next, let’s do a simple test of our paging plug-in.

First, we need to configure the paging plug-in in the global configuration. The configuration method is as follows:

<plugins>
    <plugin interceptor="org.javaboy.mybatis03.plugin.PageInterceptor"></plugin>
</plugins>

Next, we define the query interface in mapper:

public interface UserMapper {
    List<User> getAllUsersByPage(RowBounds rowBounds);
}

Next, define usermapper.xml as follows:

<select id="getAllUsersByPage" resultType="org.javaboy.mybatis03.model.User">
    select * from user
</select>

Finally, we test:

@Test
public void test3() {
    UserMapper userMapper = sqlSessionFactory.openSession().getMapper(UserMapper.class);
    List<User> list = userMapper.getAllUsersByPage(new RowBounds(1,2));
    for (User user : list) {
        System.out.println("user = " + user);
    }
}

Here, when we use the rowboundaries object in the query, we will only page instead of counting the total records. It should be noted that the paging at this time is not memory paging, but physical paging. We can also see this from the printed SQL, as follows:

Teach you how to develop mybatis plug-in

You can see that paging has been performed during query.

Of course, we can also use pagerowboundaries for testing, as follows:

@Test
public void test4() {
    UserMapper userMapper = sqlSessionFactory.openSession().getMapper(UserMapper.class);
    PageRowBounds pageRowBounds = new PageRowBounds(1, 2);
    List<User> list = userMapper.getAllUsersByPage(pageRowBounds);
    for (User user : list) {
        System.out.println("user = " + user);
    }
    System.out.println("pageRowBounds.getTotal() = " + pageRowBounds.getTotal());
}

At this point, we can get the total number of records through the pagerowboundaries. Gettotal () method.

6. Summary

Well, today I mainly shared with my friends how to develop a mybatis plug-in. The plug-in functions are actually secondary. The most important thing is that I hope my friends can understand the workflow of mybatis.