Chapter 7 MybatisPlus plug-in

Chapter 7 MybatisPlus plug-in

1. Overview of plug-in mechanism

MybatisPlusInterceptor core plug-in

MybatisPlus can intercept the execution of relevant methods of four objects (Executor, StatementHandle, ParameterHandler and ResultSetHandler) through the plug-in (Interceptor), and complete the dynamic change of relevant data according to the demand.

MybatisPlusInterceptor plug-in is the core plug-in. At present, it proxies the Executor#query, Executor#update and StatementHandler#prepare methods

Private list < innerinterceptor > interceptors = new ArrayList < > () in MybatisPlusInterceptor; Field, which is used to store the built-in interceptor plug-in registered by the user (plug-in Collection)

MybatisPlusInterceptor#addInnerInterceptor() method is used to add interceptor plug-in; The MybatisPlusInterceptor#plugin() method is used to create the proxy object of the target object. Wrap means wrap, which means wrap the target object as a proxy object and return it to the caller; MybatisPlusInterceptor#intercept() is the method of intercepting the target and executing the built-in interceptor

The MybatisPlusInterceptor#intercept() method mainly proxies three types of operations:

  1. If it is a query operation, traverse all built-in interceptors and execute it first! query.willDoQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql) determines whether the proxy method needs to be executed
    1. If query If the willdoquery () method returns false, it means that there is no need to execute the interceptor agent and directly return collections emptyList()
    2. Otherwise, it means that the interceptor agent needs to be executed. For query operation, execute query beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql);
  2. If it is an update operation, traverse all built-in interceptors and execute it first! update.willDoUpdate(executor, ms, parameter) determines whether the proxy method needs to be executed
    1. If query If the willdoquery () method returns false, it means that there is no need to execute the interceptor agent, and - 1 is returned directly
    2. Otherwise, it means that the interceptor agent needs to be executed. For the update operation, execute update beforeUpdate(executor, ms, parameter);
  3. Otherwise, it is the processing of StatementHandler, traversing all built-in interceptors and executing innerinterceptor beforePrepare(sh, connections, transactionTimeout); method
  4. Finally, execute return invocation proceed(); Get the return value of the interception method and return it to the caller
/**
 * @author miemie
 * @since 3.4.0
 */
@SuppressWarnings({"rawtypes"})
@Intercepts(
    {
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @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 MybatisPlusInterceptor implements Interceptor {

    @Setter
    private List<InnerInterceptor> interceptors = new ArrayList<>();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object target = invocation.getTarget();
        Object[] args = invocation.getArgs();
        if (target instanceof Executor) {
            final Executor executor = (Executor) target;
            Object parameter = args[1];
            boolean isUpdate = args.length == 2;
            MappedStatement ms = (MappedStatement) args[0];
            if (!isUpdate && ms.getSqlCommandType() == SqlCommandType.SELECT) {
                RowBounds rowBounds = (RowBounds) args[2];
                ResultHandler resultHandler = (ResultHandler) args[3];
                BoundSql boundSql;
                if (args.length == 4) {
                    boundSql = ms.getBoundSql(parameter);
                } else {
                    // It's almost impossible to get into this unless you call query[args[6]] using the Executor's proxy object
                    boundSql = (BoundSql) args[5];
                }
                for (InnerInterceptor query : interceptors) {
                    if (!query.willDoQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql)) {
                        return Collections.emptyList();
                    }
                    query.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                }
                CacheKey cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
                return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
            } else if (isUpdate) {
                for (InnerInterceptor update : interceptors) {
                    if (!update.willDoUpdate(executor, ms, parameter)) {
                        return -1;
                    }
                    update.beforeUpdate(executor, ms, parameter);
                }
            }
        } else {
            // StatementHandler
            final StatementHandler sh = (StatementHandler) target;
            Connection connections = (Connection) args[0];
            Integer transactionTimeout = (Integer) args[1];
            for (InnerInterceptor innerInterceptor : interceptors) {
                innerInterceptor.beforePrepare(sh, connections, transactionTimeout);
            }
        }
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof Executor || target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        }
        return target;
    }

    public void addInnerInterceptor(InnerInterceptor innerInterceptor) {
        this.interceptors.add(innerInterceptor);
    }

    public List<InnerInterceptor> getInterceptors() {
        return Collections.unmodifiableList(interceptors);
    }

    /**
     * Using internal rules, take the paging plug-in as an example:
     * <p>
     * - key: "@page" ,value: "com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor"
     * - key: "page:limit" ,value: "100"
     * <p>
     * Interpretation 1: the key starts with "@" and defines that it is an 'InnerInterceptor' to be assembled. The end of "page" indicates the alias
     * value Is the full class name of 'InnerInterceptor'
     * Interpretation 2: key starts with "alias + ':'" defined above, which means that this' value 'is the value to be set for the defined' InnerInterceptor 'attribute
     * <p>
     * If the 'InnerInterceptor' does not need to be configured, it should also be aliased
     */
    @Override
    public void setProperties(Properties properties) {
        PropertyMapper pm = PropertyMapper.newInstance(properties);
        Map<String, Properties> group = pm.group(StringPool.AT);
        group.forEach((k, v) -> {
            InnerInterceptor innerInterceptor = ClassUtils.newInstance(k);
            innerInterceptor.setProperties(v);
            addInnerInterceptor(innerInterceptor);
        });
    }
}

The Plugin#wrap() method passes in two parameters: Object target is the target object; Interceptor interceptor is an interceptor plug-in object. The wrap() method crumples the two objects together, creates a proxy object and returns it to us

The three parameters of the proxy method: plugin #; Method method the target method to be intercepted; Object[] args is a method parameter. The invoke() method is our proxy target method and returns the execution result to us

/**
 * @author Clinton Begin
 */
public class Plugin implements InvocationHandler {

  private final Object target;
  private final Interceptor interceptor;
  private final Map<Class<?>, Set<Method>> signatureMap;

  private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
    this.target = target;
    this.interceptor = interceptor;
    this.signatureMap = signatureMap;
  }

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

  @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) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }

Interceptorchain. Will be called when creating the four objects Pluginall() method, which will traverse all Interceptor plug-ins internally and execute its Interceptor Plugin (target) method, which aims to create a proxy for the current four objects.

After processing, the proxy object can intercept the execution of the methods related to the four objects, because the methods of the four objects need to pass through the proxy object first

/**
 * @author Clinton Begin
 */
public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<>();

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

}

InnerInterceptor

All plug-ins provided by MybatisPlus will be based on InnerInterceptor interface to realize functions. At present, the existing functions are as follows:

  • Automatic paging: PaginationInnerInterceptor
  • Multi tenant: TenantLineInnerInterceptor
  • Dynamic table name: DynamicTableNameInnerInterceptor
  • Optimistic lock: OptimisticLockerInnerInterceptor
  • sql performance specification: IllegalSQLInnerInterceptor
  • Prevent full table update and deletion: BlockAttackInnerInterceptor
**
 * @author miemie
 * @since 3.4.0
 */
@SuppressWarnings({"rawtypes"})
public interface InnerInterceptor {

    /**
     * Judge whether to execute {@ link Executor#query(MappedStatement, Object, RowBounds, ResultHandler, CacheKey, BoundSql)}
     * <p>
     * If the query operation is not performed, {@ link Collections#emptyList()}
     *
     * @param executor      Executor((possibly a proxy object)
     * @param ms            MappedStatement
     * @param parameter     parameter
     * @param rowBounds     rowBounds
     * @param resultHandler resultHandler
     * @param boundSql      boundSql
     * @return New boundSql
     */
    default boolean willDoQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        return true;
    }

    /**
     * {@link Executor#query(MappedStatement, Object, RowBounds, ResultHandler, CacheKey, BoundSql)} Operation preprocessing
     * <p>
     * Change sql or something
     *
     * @param executor      Executor((possibly a proxy object)
     * @param ms            MappedStatement
     * @param parameter     parameter
     * @param rowBounds     rowBounds
     * @param resultHandler resultHandler
     * @param boundSql      boundSql
     */
    default void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        // do nothing
    }

    /**
     * Judge whether to execute {@ link Executor#update(MappedStatement, Object)}
     * <p>
     * If the update operation is not performed, the value of the number of affected rows is - 1
     *
     * @param executor  Executor((possibly a proxy object)
     * @param ms        MappedStatement
     * @param parameter parameter
     */
    default boolean willDoUpdate(Executor executor, MappedStatement ms, Object parameter) throws SQLException {
        return true;
    }

    /**
     * {@link Executor#update(MappedStatement, Object)} Operation preprocessing
     * <p>
     * Change sql or something
     *
     * @param executor  Executor((possibly a proxy object)
     * @param ms        MappedStatement
     * @param parameter parameter
     */
    default void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) throws SQLException {
        // do nothing
    }

    /**
     * {@link StatementHandler#prepare(Connection, Integer)} Operation preprocessing
     * <p>
     * Change sql or something
     *
     * @param sh                 StatementHandler((possibly a proxy object)
     * @param connection         Connection
     * @param transactionTimeout transactionTimeout
     */
    default void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
        // do nothing
    }

    default void setProperties(Properties properties) {
        // do nothing
    }
}

2. Automatic paging plug-in

Automatic paging plug-in: PaginationInnerInterceptor

Without adding a paging plug-in, we use Page to perform paging and sorting operations

/**
 * @Author Oneby
 * @Date 2021/4/18 17:54
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class MybatisPlusPluginTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    public void testPaginationInnerInterceptor() {
        Page<User> page = new Page<>(1,3);
        page.addOrder(OrderItem.asc("age"));
        Page<User> userPage = userMapper.selectPage(page, null);
        for (User user : userPage.getRecords()) {
            System.out.println(user);
        }
    }

}

SQL log: paging and sorting are not effective

Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1a2bcd56] was not registered for synchronization because synchronization is not active
2021-04-24 18:56:21.535  INFO 19080 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2021-04-24 18:56:21.681  INFO 19080 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
JDBC Connection [HikariProxyConnection@662000775 wrapping com.mysql.cj.jdbc.ConnectionImpl@4f0cab0a] will not be managed by Spring
==>  Preparing: SELECT id,username AS name,age,email FROM t_user
==> Parameters: 
<==    Columns: id, name, age, email
<==        Row: 1, Jone, 18, test1@baomidou.com
<==        Row: 2, Jack, 20, test2@baomidou.com
<==        Row: 3, Tom, 28, test3@baomidou.com
<==        Row: 4, Sandy, 21, test4@baomidou.com
<==        Row: 5, Billie, 24, test5@baomidou.com
<==        Row: 6, Oneby, 21, Oneby@baomidou.com
<==      Total: 6
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1a2bcd56]
User(id=1, name=Jone, age=18, email=test1@baomidou.com)
User(id=2, name=Jack, age=20, email=test2@baomidou.com)
User(id=3, name=Tom, age=28, email=test3@baomidou.com)
User(id=4, name=Sandy, age=21, email=test4@baomidou.com)
User(id=5, name=Billie, age=24, email=test5@baomidou.com)
User(id=6, name=Oneby, age=21, email=Oneby@baomidou.com)

We added an automatic paging plug-in for MybatisPlus and indicated that the database type was MySQL

/**
 * @Author Oneby
 * @Date 2021/4/24 18:42
 */
@Configuration
@MapperScan("com.oneby.mapper")
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }

}

The log indicates that the MybatisPlusInterceptor plug-in has been successfully registered.

Have you ever wondered why select count (*) from t is executed_ User statement? This is because page < user > userPage = usermapper selectPage(page, null); Method, the return value is the page < user > userPage object. You need to set the value of the total field of the userPage object (total number of entries)

Registered plugin: 'com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor@70e889e9'

Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@44536de4] was not registered for synchronization because synchronization is not active
2021-04-24 18:57:50.254  INFO 8344 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2021-04-24 18:57:50.379  INFO 8344 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
JDBC Connection [HikariProxyConnection@418731780 wrapping com.mysql.cj.jdbc.ConnectionImpl@67cefd84] will not be managed by Spring
==>  Preparing: SELECT COUNT(*) FROM t_user
==> Parameters: 
<==    Columns: COUNT(*)
<==        Row: 6
<==      Total: 1
==>  Preparing: SELECT id, username AS name, age, email FROM t_user ORDER BY age ASC LIMIT ?
==> Parameters: 3(Long)
<==    Columns: id, name, age, email
<==        Row: 1, Jone, 18, test1@baomidou.com
<==        Row: 2, Jack, 20, test2@baomidou.com
<==        Row: 6, Oneby, 21, Oneby@baomidou.com
<==      Total: 3
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@44536de4]
User(id=1, name=Jone, age=18, email=test1@baomidou.com)
User(id=2, name=Jack, age=20, email=test2@baomidou.com)
User(id=6, name=Oneby, age=21, email=Oneby@baomidou.com)

Properties of PaginationInnerInterceptor plug-in

Attribute nametypeDefault valuedescribe
overflowbooleanfalseWhether to process after overflowing the total number of pages (not by default, see plug-in #continuePage method)
maxLimitLongLimit on the number of pages per page (unlimited by default, see plug-in #handlerLimit method)
dbTypeDbTypeDatabase type (get the paging dialect that should be used according to the type, see plug-in #findIDialect method)
dialectIDialectDialect implementation class (see plug-in #findIDialect method)

Principle of PaginationInnerInterceptor

PaginationInnerInterceptor implements the InnerInterceptor interface, mainly focusing on the willDoQuery() method and beforeQuery() method

  1. willDoQuery() method:
    1. Execute MappedStatement countMs = buildCountMappedStatement(ms, page.countId()); Method: pass in the native MappedStatement ms object and page Countid() builds a new MappedStatement countMs object
    2. If countMs is not empty, execute countsql = countMs getBoundSql(parameter); Build BoundSql countSql object; If countMs is empty, the built-in builder of MP is used
    3. Execute object result = executor query(countMs, parameter, rowBounds, resultHandler, cacheKey, countSql). get(0); Query the total number of entries and execute page setTotal(result == null ? 0L : Long.parseLong(result.toString())); Set the total number of entries to page In the total field
    4. Execute continuePage(page); Method indicates whether to continue the paging query
  2. beforeQuery() method
    1. Execute string buildsql = boundsql getSql(); Method to get the native SQL statement
    2. If page If orders() is not empty, it means sorting is required, and buildsql = this concatOrderBy(buildSql, orders); Method splice sort clause
    3. Execute handlerLimit(page); Method. If the processing exceeds the limit of the number of paging entries, it is set to the limit by default
    4. Execute IDialect dialect = findIDialect(executor); Method to obtain the paging syntax of the database dialect
    5. Execute DialectModel model = dialect buildPaginationSql(buildSql, page.offset(), page. getSize()); Build DialectModel object with paging
    6. Then execute various methods to insert the paged SQL statements and parameters into the mpBoundSql object, but it's strange. This mpBoundSql object is neither a static shared object nor a return. What's the role of inserting SQL statements and parameters into it?
/**
 * Paging interceptor
 * <p>
 * left join is optimized by default. Although count can be optimized, if paging is added, the number of results is incorrect if 1 to many
 *
 * @author hubin
 * @since 3.4.0
 */
@Data
@NoArgsConstructor
@SuppressWarnings({"rawtypes"})
public class PaginationInnerInterceptor implements InnerInterceptor {

    protected static final List<SelectItem> COUNT_SELECT_ITEM = Collections.singletonList(defaultCountSelectItem());
    protected static final Map<String, MappedStatement> countMsCache = new ConcurrentHashMap<>();
    protected final Log logger = LogFactory.getLog(this.getClass());

    /**
     * Get the SelectItem of count in jsqlparser
     */
    private static SelectItem defaultCountSelectItem() {
        Function function = new Function();
        function.setName("COUNT");
        function.setAllColumns(true);
        return new SelectExpressionItem(function);
    }

    /**
     * Process after overflowing the total number of pages
     */
    protected boolean overflow;
    /**
     * Limit on the number of pages per page
     */
    protected Long maxLimit;
    /**
     * Database type
     * <p>
     * View {@ link #findIDialect(Executor)} logic
     */
    private DbType dbType;
    /**
     * Dialect implementation class
     * <p>
     * View {@ link #findIDialect(Executor)} logic
     */
    private IDialect dialect;

    public PaginationInnerInterceptor(DbType dbType) {
        this.dbType = dbType;
    }

    public PaginationInnerInterceptor(IDialect dialect) {
        this.dialect = dialect;
    }

    /**
     * Count here. If count is 0, false will be returned (that is, sql will not be executed again)
     */
    @Override
    public boolean willDoQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        IPage<?> page = ParameterUtils.findPage(parameter).orElse(null);
        if (page == null || page.getSize() < 0 || !page.isSearchCount()) {
            return true;
        }

        BoundSql countSql;
        MappedStatement countMs = buildCountMappedStatement(ms, page.countId());
        if (countMs != null) {
            countSql = countMs.getBoundSql(parameter);
        } else {
            countMs = buildAutoCountMappedStatement(ms);
            String countSqlStr = autoCountSql(page.optimizeCountSql(), boundSql.getSql());
            PluginUtils.MPBoundSql mpBoundSql = PluginUtils.mpBoundSql(boundSql);
            countSql = new BoundSql(countMs.getConfiguration(), countSqlStr, mpBoundSql.parameterMappings(), parameter);
            PluginUtils.setAdditionalParameter(countSql, mpBoundSql.additionalParameters());
        }

        CacheKey cacheKey = executor.createCacheKey(countMs, parameter, rowBounds, countSql);
        Object result = executor.query(countMs, parameter, rowBounds, resultHandler, cacheKey, countSql).get(0);
        page.setTotal(result == null ? 0L : Long.parseLong(result.toString()));
        return continuePage(page);
    }

    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        IPage<?> page = ParameterUtils.findPage(parameter).orElse(null);
        if (null == page) {
            return;
        }

        // Process orderBy splicing
        boolean addOrdered = false;
        String buildSql = boundSql.getSql();
        List<OrderItem> orders = page.orders();
        if (!CollectionUtils.isEmpty(orders)) {
            addOrdered = true;
            buildSql = this.concatOrderBy(buildSql, orders);
        }

        // If the size is less than 0, no paging sql will be constructed
        if (page.getSize() < 0) {
            if (addOrdered) {
                PluginUtils.mpBoundSql(boundSql).sql(buildSql);
            }
            return;
        }

        handlerLimit(page);
        IDialect dialect = findIDialect(executor);

        final Configuration configuration = ms.getConfiguration();
        DialectModel model = dialect.buildPaginationSql(buildSql, page.offset(), page.getSize());
        PluginUtils.MPBoundSql mpBoundSql = PluginUtils.mpBoundSql(boundSql);

        List<ParameterMapping> mappings = mpBoundSql.parameterMappings();
        Map<String, Object> additionalParameter = mpBoundSql.additionalParameters();
        model.consumers(mappings, configuration, additionalParameter);
        mpBoundSql.sql(model.getDialectSql());
        mpBoundSql.parameterMappings(mappings);
    }
    
    /**
     * count Continue paging after query
     *
     * @param page Paging object
     * @return whether
     */
    protected boolean continuePage(IPage<?> page) {
        if (page.getTotal() <= 0) {
            return false;
        }
        if (page.getCurrent() > page.getPages()) {
            if (overflow) {
                //Total overflow pages processing
                handlerOverflow(page);
            } else {
                // Exceeding the maximum range, overflow logic interrupt list execution is not set
                return false;
            }
        }
        return true;
    }
    
    /**
     * Processing exceeds the limit of the number of paging entries, which is set to the limit by default
     *
     * @param page IPage
     */
    protected void handlerLimit(IPage<?> page) {
        final long size = page.getSize();
        Long pageMaxLimit = page.maxLimit();
        Long limit = pageMaxLimit != null ? pageMaxLimit : maxLimit;
        if (limit != null && limit > 0 && size > limit) {
            page.setSize(limit);
        }
    }

3. Prevent full table update and deletion plug-ins

Prevent full table update and deletion: BlockAttackInnerInterceptor

BlockAttackInnerInterceptor uses the Stringutils class of commons-lang3. We introduce the dependency of commons-lang3

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
</dependency>

Register BlockAttackInnerInterceptor in MybatisPlusConfig configuration class to prevent full table update and deletion of plug-ins

/**
 * @Author Oneby
 * @Date 2021/4/24 18:42
 */
@Configuration
@MapperScan("com.oneby.mapper")
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
        return interceptor;
    }

}

Test code: delete all tables

@Test
public void BlockAttackInnerInterceptor() {
    int count = userMapper.delete(null);
    System.out.println("Deleted:" + count + "that 's ok");
}

The program throws an exception: error updating database Cause: com. baomidou. mybatisplus. core. exceptions. Mybatisplusexception: prevention of full table deletion. Full table deletion is not allowed

Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2aff9dff] was not registered for synchronization because synchronization is not active
2021-04-24 22:39:54.361  INFO 20340 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2021-04-24 22:39:54.518  INFO 20340 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
JDBC Connection [HikariProxyConnection@111702054 wrapping com.mysql.cj.jdbc.ConnectionImpl@ef60710] will not be managed by Spring
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2aff9dff]

org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException: 
### Error updating database.  Cause: com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: Prohibition of full table deletion
### The error may exist in com/oneby/mapper/UserMapper.java (best guess)
### The error may involve com.oneby.mapper.UserMapper.delete
### The error occurred while executing an update
### Cause: com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: Prohibition of full table deletion

Suggestions on the use of BlockAttackInnerInterceptor

  1. SQL execution analysis interceptor, which only supports MySQL 5 Version above 6.3
  2. The function of the plug-in is to analyze DELETE and UPDATE statements to prevent Xiaobai or malicious DELETE and UPDATE full table operations
  3. It is only recommended to use in the development environment, not in the production environment

Principle of BlockAttackInnerInterceptor

When deleting the whole table, an exception is thrown at the Plugin#invoke() method. The specific location is to execute interceptor intercept(new Invocation(target, method, args)); Threw an exception when: cause: com baomidou. mybatisplus. core. exceptions. MybatisPlusException: Prohibition of full table deletion

/**
 * @author Clinton Begin
 */
public class Plugin implements InvocationHandler {

  private final Object target;
  private final Interceptor interceptor;
  private final Map<Class<?>, Set<Method>> signatureMap;

  private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
    this.target = target;
    this.interceptor = interceptor;
    this.signatureMap = signatureMap;
  }

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

  @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) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }

This goes back to the MybatisPlusInterceptor#intercept() method: the innerinterceptor of the internal interceptor is called inside this method beforePrepare(sh, connections, transactionTimeout);

public class MybatisPlusInterceptor implements Interceptor {

    @Setter
    private List<InnerInterceptor> interceptors = new ArrayList<>();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object target = invocation.getTarget();
        Object[] args = invocation.getArgs();
        if (target instanceof Executor) {
            // ...
        } else {
            // StatementHandler
            final StatementHandler sh = (StatementHandler) target;
            Connection connections = (Connection) args[0];
            Integer transactionTimeout = (Integer) args[1];
            for (InnerInterceptor innerInterceptor : interceptors) {
                innerInterceptor.beforePrepare(sh, connections, transactionTimeout);
            }
        }
        return invocation.proceed();
    }

The BlockAttackInnerInterceptor interceptor interceptor overrides the beforePrepare() method of the innerinceptor interface. Execute parserMulti(boundSql.getSql(), null); Method threw an exception

/**
 * Attack SQL to block the parser and prevent the update and deletion of the whole table
 *
 * @author hubin
 * @since 3.4.0
 */
public class BlockAttackInnerInterceptor extends JsqlParserSupport implements InnerInterceptor {

    @Override
    public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
        PluginUtils.MPStatementHandler handler = PluginUtils.mpStatementHandler(sh);
        MappedStatement ms = handler.mappedStatement();
        SqlCommandType sct = ms.getSqlCommandType();
        if (sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
            if (InterceptorIgnoreHelper.willIgnoreBlockAttack(ms.getId())) return;
            BoundSql boundSql = handler.boundSql();
            parserMulti(boundSql.getSql(), null);
        }
    }

We enter the abstract parent class JsqlParserSupport of BlockAttackInnerInterceptor and execute processparser (statement, I, SQL, obj); -- > this. processDelete((Delete) statement, index, sql, obj); Method, an exception was thrown when parsing the SQL statement

/**
 * https://github.com/JSQLParser/JSqlParser
 *
 * @author miemie
 * @since 2020-06-22
 */
public abstract class JsqlParserSupport {

    /**
     * journal
     */
    protected final Log logger = LogFactory.getLog(this.getClass());

    public String parserSingle(String sql, Object obj) {
        if (logger.isDebugEnabled()) {
            logger.debug("original SQL: " + sql);
        }
        try {
            Statement statement = CCJSqlParserUtil.parse(sql);
            return processParser(statement, 0, sql, obj);
        } catch (JSQLParserException e) {
            throw ExceptionUtils.mpe("Failed to process, Error SQL: %s", e, sql);
        }
    }

    public String parserMulti(String sql, Object obj) {
        if (logger.isDebugEnabled()) {
            logger.debug("original SQL: " + sql);
        }
        try {
            // fixed github pull/295
            StringBuilder sb = new StringBuilder();
            Statements statements = CCJSqlParserUtil.parseStatements(sql);
            int i = 0;
            for (Statement statement : statements.getStatements()) {
                if (i > 0) {
                    sb.append(StringPool.SEMICOLON);
                }
                sb.append(processParser(statement, i, sql, obj));
                i++;
            }
            return sb.toString();
        } catch (JSQLParserException e) {
            throw ExceptionUtils.mpe("Failed to process, Error SQL: %s", e, sql);
        }
    }
    
    /**
     * Perform SQL parsing
     *
     * @param statement JsqlParser Statement
     * @return sql
     */
    protected String processParser(Statement statement, int index, String sql, Object obj) {
        if (logger.isDebugEnabled()) {
            logger.debug("SQL to parse, SQL: " + sql);
        }
        if (statement instanceof Insert) {
            this.processInsert((Insert) statement, index, sql, obj);
        } else if (statement instanceof Select) {
            this.processSelect((Select) statement, index, sql, obj);
        } else if (statement instanceof Update) {
            this.processUpdate((Update) statement, index, sql, obj);
        } else if (statement instanceof Delete) {
            this.processDelete((Delete) statement, index, sql, obj);
        }
        sql = statement.toString();
        if (logger.isDebugEnabled()) {
            logger.debug("parse the finished SQL: " + sql);
        }
        return sql;
    }

this is the BlockAttackInnerInterceptor interceptor object. The method call chain that parses the DELETE statement is processdelete() -- > checkwhere() -- > fullmatch()

In the fullMatch() method, various judgment conditions of the WHERE clause in the DELETE statement will be parsed to check whether the deletion operation is safe and avoid the deletion operation of the whole table

/**
 * Attack SQL to block the parser and prevent the update and deletion of the whole table
 *
 * @author hubin
 * @since 3.4.0
 */
public class BlockAttackInnerInterceptor extends JsqlParserSupport implements InnerInterceptor {

    @Override
    public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
        PluginUtils.MPStatementHandler handler = PluginUtils.mpStatementHandler(sh);
        MappedStatement ms = handler.mappedStatement();
        SqlCommandType sct = ms.getSqlCommandType();
        if (sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
            if (InterceptorIgnoreHelper.willIgnoreBlockAttack(ms.getId())) return;
            BoundSql boundSql = handler.boundSql();
            parserMulti(boundSql.getSql(), null);
        }
    }

    @Override
    protected void processDelete(Delete delete, int index, String sql, Object obj) {
        this.checkWhere(delete.getTable().getName(), delete.getWhere(), "Prohibition of full table deletion");
    }

    @Override
    protected void processUpdate(Update update, int index, String sql, Object obj) {
        this.checkWhere(update.getTable().getName(), update.getWhere(), "Prohibition of table update operation");
    }

    protected void checkWhere(String tableName, Expression where, String ex) {
        Assert.isFalse(this.fullMatch(where, this.getTableLogicField(tableName)), ex);
    }

    private boolean fullMatch(Expression where, String logicField) {
        if (where == null) {
            return true;
        }
        if (StringUtils.isNotBlank(logicField) && (where instanceof BinaryExpression)) {

            BinaryExpression binaryExpression = (BinaryExpression) where;
            if (StringUtils.equals(binaryExpression.getLeftExpression().toString(), logicField) || StringUtils.equals(binaryExpression.getRightExpression().toString(), logicField)) {
                return true;
            }
        }

        if (where instanceof EqualsTo) {
            // example: 1=1
            EqualsTo equalsTo = (EqualsTo) where;
            return StringUtils.equals(equalsTo.getLeftExpression().toString(), equalsTo.getRightExpression().toString());
        } else if (where instanceof NotEqualsTo) {
            // example: 1 != 2
            NotEqualsTo notEqualsTo = (NotEqualsTo) where;
            return !StringUtils.equals(notEqualsTo.getLeftExpression().toString(), notEqualsTo.getRightExpression().toString());
        } else if (where instanceof OrExpression) {

            OrExpression orExpression = (OrExpression) where;
            return fullMatch(orExpression.getLeftExpression(), logicField) || fullMatch(orExpression.getRightExpression(), logicField);
        } else if (where instanceof AndExpression) {

            AndExpression andExpression = (AndExpression) where;
            return fullMatch(andExpression.getLeftExpression(), logicField) && fullMatch(andExpression.getRightExpression(), logicField);
        } else if (where instanceof Parenthesis) {
            // example: (1 = 1)
            Parenthesis parenthesis = (Parenthesis) where;
            return fullMatch(parenthesis.getExpression(), logicField);
        }

        return false;
    }

4. SQL performance specification plug-in

SQL performance specification: IllegalSQLInnerInterceptor

Register the IllegalSQLInnerInterceptor SQL performance specification plug-in in the MybatisPlusConfig configuration class

Note: it is only recommended to use in the development environment, which will help us find out the non-standard SQL statements

/**
 * @Author Oneby
 * @Date 2021/4/24 18:42
 */
@Configuration
@MapperScan("com.oneby.mapper")
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
        interceptor.addInnerInterceptor(new IllegalSQLInnerInterceptor());
        return interceptor;
    }

}

Test code: unconditional paging query

@Test
public void IllegalSQLInnerInterceptor() {
    Page<User> page = new Page<>(1, 3);
    page.addOrder(OrderItem.asc("age"));
    Page<User> userPage = userMapper.selectPage(page, null);
    for (User user : userPage.getRecords()) {
        System.out.println(user);
    }
}

Query throw exception: cause: com baomidou. mybatisplus. core. exceptions. Mybatisplusexception: illegal SQL. There must be a where condition. Sure enough, it's standard enough. I won't check if select doesn't add where

Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@77b919a3] was not registered for synchronization because synchronization is not active
2021-04-24 23:24:05.996  INFO 19332 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2021-04-24 23:24:06.105  INFO 19332 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
JDBC Connection [HikariProxyConnection@1487059223 wrapping com.mysql.cj.jdbc.ConnectionImpl@48904d5a] will not be managed by Spring
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@77b919a3]

org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException: 
### Error querying database.  Cause: com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: illegal SQL. There must be a where condition
### The error may exist in com/oneby/mapper/UserMapper.java (best guess)
### The error may involve com.oneby.mapper.UserMapper.selectPage_mpCount
### The error occurred while executing a query
### Cause: com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: illegal SQL. There must be a where condition

Principle of IllegalSQLInnerInterceptor

The illegalsqlinnerinceptor plug-in intercepts the relevant methods of the StatementHandler object. This class overrides the beforePrepare() method of the innerinceptor interface

In the beforePrepare() method, execute parserSingle(originalSql, connection); Method to analyze whether the SQL statement is standardized

/**
 * Due to the uneven level of developers, many people do not abide by even if they set development specifications
 * <p>SQL Is the most important factor affecting the system performance, so intercept the garbage SQL statements</p>
 * <br>
 * <p>Scenario of intercepting SQL type</p>
 * <p>1.The index must be used, including the left join connection field, which conforms to the leftmost principle of the index</p>
 * <p>You must use index benefits</p>
 * <p>1.1 If the where condition of update is not brought because of dynamic SQL bug, the whole table will update tens of thousands of data</p>
 * <p>1.2 If it is checked that indexes are used, the SQL performance will not be too poor</p>
 * <br>
 * <p>2.SQL Try to execute in a single table. If there is a statement to query left join, the SQL must be allowed to run in the comment, otherwise it will be intercepted. If there is a statement with left join, if it cannot be split into SQL executed in a single table, please consult the leader</p>
 * <p>https://gaoxianglong.github.io/shark</p>
 * <p>SQL Try to the benefits of single table execution</p>
 * <p>2.1 Query conditions are simple, easy to understand and maintain</ p>
 * <p>2.2 Strong expansibility; (it can be prepared for sub warehouse and sub table)</p>
 * <p>2.3 High cache utilization</ p>
 * <p>2.Using functions on fields</p>
 * <br>
 * <p>3.where Condition is empty</p>
 * <p>4.where Conditions of use=</ p>
 * <p>5.where The condition uses the not keyword</p>
 * <p>6.where The condition uses the or keyword</p>
 * <p>7.where The condition uses a subquery</p>
 *
 * @author willenfoo
 * @since 3.4.0
 */
public class IllegalSQLInnerInterceptor extends JsqlParserSupport implements InnerInterceptor {

    /**
     * Cache validation results to improve performance
     */
    private static final Set<String> cacheValidResult = new HashSet<>();
    /**
     * Index information of cache table
     */
    private static final Map<String, List<IndexInfo>> indexInfoMap = new ConcurrentHashMap<>();

    @Override
    public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
        PluginUtils.MPStatementHandler mpStatementHandler = PluginUtils.mpStatementHandler(sh);
        MappedStatement ms = mpStatementHandler.mappedStatement();
        SqlCommandType sct = ms.getSqlCommandType();
        if (sct == SqlCommandType.INSERT || InterceptorIgnoreHelper.willIgnoreIllegalSql(ms.getId())
            || SqlParserHelper.getSqlParserInfo(ms)) return;
        BoundSql boundSql = mpStatementHandler.boundSql();
        String originalSql = boundSql.getSql();
        logger.debug("inspect SQL Compliance, SQL:" + originalSql);
        String md5Base64 = EncryptUtils.md5Base64(originalSql);
        if (cacheValidResult.contains(md5Base64)) {
            logger.debug("Should SQL Verified, no need to verify again,, SQL:" + originalSql);
            return;
        }
        parserSingle(originalSql, connection);
        //Cache validation results
        cacheValidResult.add(md5Base64);
    }

We enter the abstract parent class JsqlParserSupport of IllegalSQLInnerInterceptor and execute parsersingle (originalsql, connection); -- > this. processSelect((Select) statement, index, sql, obj); Method, an exception was thrown when parsing the SQL statement

/**
 * https://github.com/JSQLParser/JSqlParser
 *
 * @author miemie
 * @since 2020-06-22
 */
public abstract class JsqlParserSupport {

    /**
     * journal
     */
    protected final Log logger = LogFactory.getLog(this.getClass());

    public String parserSingle(String sql, Object obj) {
        if (logger.isDebugEnabled()) {
            logger.debug("original SQL: " + sql);
        }
        try {
            Statement statement = CCJSqlParserUtil.parse(sql);
            return processParser(statement, 0, sql, obj);
        } catch (JSQLParserException e) {
            throw ExceptionUtils.mpe("Failed to process, Error SQL: %s", e, sql);
        }
    }

    /**
     * Perform SQL parsing
     *
     * @param statement JsqlParser Statement
     * @return sql
     */
    protected String processParser(Statement statement, int index, String sql, Object obj) {
        if (logger.isDebugEnabled()) {
            logger.debug("SQL to parse, SQL: " + sql);
        }
        if (statement instanceof Insert) {
            this.processInsert((Insert) statement, index, sql, obj);
        } else if (statement instanceof Select) {
            this.processSelect((Select) statement, index, sql, obj);
        } else if (statement instanceof Update) {
            this.processUpdate((Update) statement, index, sql, obj);
        } else if (statement instanceof Delete) {
            this.processDelete((Delete) statement, index, sql, obj);
        }
        sql = statement.toString();
        if (logger.isDebugEnabled()) {
            logger.debug("parse the finished SQL: " + sql);
        }
        return sql;
    }

this is the interceptor object of IllegalSQLInnerInterceptor, which overrides the processSelect(), processUpdate(), and processDelete() methods in the abstract parent class JsqlParserSupport

In these methods, it is mainly through validWhere(where, table, (Connection) obj); And validJoins(joins, table, (Connection) obj); Method to check the performance specification of SQL statements

public class IllegalSQLInnerInterceptor extends JsqlParserSupport implements InnerInterceptor {

    /**
     * Cache validation results to improve performance
     */
    private static final Set<String> cacheValidResult = new HashSet<>();
    /**
     * Index information of cache table
     */
    private static final Map<String, List<IndexInfo>> indexInfoMap = new ConcurrentHashMap<>();

    @Override
    public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
        PluginUtils.MPStatementHandler mpStatementHandler = PluginUtils.mpStatementHandler(sh);
        MappedStatement ms = mpStatementHandler.mappedStatement();
        SqlCommandType sct = ms.getSqlCommandType();
        if (sct == SqlCommandType.INSERT || InterceptorIgnoreHelper.willIgnoreIllegalSql(ms.getId())
            || SqlParserHelper.getSqlParserInfo(ms)) return;
        BoundSql boundSql = mpStatementHandler.boundSql();
        String originalSql = boundSql.getSql();
        logger.debug("inspect SQL Compliance, SQL:" + originalSql);
        String md5Base64 = EncryptUtils.md5Base64(originalSql);
        if (cacheValidResult.contains(md5Base64)) {
            logger.debug("Should SQL Verified, no need to verify again,, SQL:" + originalSql);
            return;
        }
        parserSingle(originalSql, connection);
        //Cache validation results
        cacheValidResult.add(md5Base64);
    }

    @Override
    protected void processSelect(Select select, int index, String sql, Object obj) {
        PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
        Expression where = plainSelect.getWhere();
        Assert.notNull(where, "illegal SQL,Must have where condition");
        Table table = (Table) plainSelect.getFromItem();
        List<Join> joins = plainSelect.getJoins();
        validWhere(where, table, (Connection) obj);
        validJoins(joins, table, (Connection) obj);
    }

    @Override
    protected void processUpdate(Update update, int index, String sql, Object obj) {
        Expression where = update.getWhere();
        Assert.notNull(where, "illegal SQL,Must have where condition");
        Table table = update.getTable();
        List<Join> joins = update.getJoins();
        validWhere(where, table, (Connection) obj);
        validJoins(joins, table, (Connection) obj);
    }

    @Override
    protected void processDelete(Delete delete, int index, String sql, Object obj) {
        Expression where = delete.getWhere();
        Assert.notNull(where, "illegal SQL,Must have where condition");
        Table table = delete.getTable();
        List<Join> joins = delete.getJoins();
        validWhere(where, table, (Connection) obj);
        validJoins(joins, table, (Connection) obj);
    }

5. Optimistic lock plug-in

Optimistic lock plug-in: OptimisticLockerInnerInterceptor

Register the OptimisticLockerInnerInterceptor optimistic lock plug-in in the MybatisPlusConfig configuration class

/**
 * @Author Oneby
 * @Date 2021/4/24 18:42
 */
@Configuration
@MapperScan("com.oneby.mapper")
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
        interceptor.addInnerInterceptor(new IllegalSQLInnerInterceptor());
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }

}

In t_ Add a new version field to the User table and User entity class

  1. The only supported data types are: int, Integer, long, long, Date, Timestamp, LocalDateTime
  2. newVersion = oldVersion + 1 under integer type
  3. newVersion will be written back to entity
  4. Only updateById(id) and update(entity, wrapper) methods are supported
  5. Under the update(entity, wrapper) method, the wrapper cannot be reused!!!
/**
 * @Author Oneby
 * @Date 2021/4/18 17:53
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("t_user")
public class User extends Model<User> {

    @TableId(type = IdType.AUTO)
    private Long id;

    @TableField("username")
    private String name;

    private Integer age;

    private String email;

    @Version
    private Integer version;

}

Test code: bring the version number when updating the data

@Test
public void OptimisticLockerInnerInterceptor() {
    User user = new User();
    user.setId(1L);
    user.setName("Oneby");
    user.setVersion(1);
    int count = userMapper.updateById(user);
    System.out.println("Update rows:" + count);
}

SQL statement: UPDATE t_user SET username=?, version=? WHERE id=? AND version=?, You can see that the version field will be judged for equality during update, and the value of the version field will be added by 1 and written back to the database record during update

Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@54d8c20d] was not registered for synchronization because synchronization is not active
2021-04-25 07:36:36.763  INFO 20980 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2021-04-25 07:36:36.919  INFO 20980 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
JDBC Connection [HikariProxyConnection@1296670053 wrapping com.mysql.cj.jdbc.ConnectionImpl@313f8301] will not be managed by Spring
==>  Preparing: UPDATE t_user SET username=?, version=? WHERE id=? AND version=?
==> Parameters: Heygo(String), 2(Integer), 1(Long), 1(Integer)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@54d8c20d]
Update rows: 1

The following SQL log shows the failure of optimistic lock update

Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7100dea] was not registered for synchronization because synchronization is not active
2021-04-25 07:38:10.198  INFO 7624 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2021-04-25 07:38:10.323  INFO 7624 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
JDBC Connection [HikariProxyConnection@912440831 wrapping com.mysql.cj.jdbc.ConnectionImpl@1bb15351] will not be managed by Spring
==>  Preparing: UPDATE t_user SET username=?, version=? WHERE id=? AND version=?
==> Parameters: Oneby(String), 2(Integer), 1(Long), 1(Integer)
<==    Updates: 0
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7100dea]
Update rows: 0

Principle of OptimisticLockerInnerInterceptor

OptimisticLockerInnerInterceptor optimistic lock implementation method: when you want to update a record, you hope that the record has not been updated by others

  1. When the record is fetched, the current version is obtained
  2. When updating, bring this version
  3. When updating, set version = newVersion where version = oldVersion
  4. If the version is incorrect, the update fails

In MybatisPlusInterceptor#intercept() method: when the execution type of SQL statement is update, the beforeUpdate() method of InnerInterceptor object will be executed

Optimizationlockerinnerinterceptor rewrites beforeUpdate() of the InnerInterceptor interface. This method mainly calls dooptimizationlocker (map, ms.getid()); Method to construct an SQL statement with a version field

Execution logic of doOptimisticLocker() method:

  1. Execute tablefieldinfo. Fieldinfo = tableinfo getVersionFieldInfo(); Method to obtain the version field in the table, and use the reflection field versionfield = fieldinfo getField();, First check whether the version field exists in the following table and whether it is null. If it is null, there is no need to deal with it and return to the interface directly
  2. If version= Null, execute object updatedversionval = this getUpdatedVersionVal(fieldInfo.getPropertyType(), originalVersionVal); Get the new version field value
  3. If it is an update operation, use updatewrapper <? > uw = new UpdateWrapper<>(); Object, conditions for constructing update: UW eq(versionColumn, originalVersionVal); And map put(Constants.WRAPPER, uw);
  4. For other operations, use abstractwrapper <?,?,? > aw = (AbstractWrapper<?, ?, ?>) map. getOrDefault(Constants.WRAPPER, null); Object, build filter criteria: aw apply(versionColumn + " = {0}", originalVersionVal);
  5. Execute versionfield set(et, updatedVersionVal); Method to set the new version value to tableinfo In the versionfieldinfo field
/**
 * Optimistic Lock Light version
 * <p>Intercept on {@link Executor}.update;</p>
 * <p>Support version types: int/Integer, long/Long, java.util.Date, java.sql.Timestamp</p>
 * <p>For extra types, please define a subclass and override {@code getUpdatedVersionVal}() method.</p>
 * <br>
 * <p>How to use?</p>
 * <p>(1) Define an Entity and add {@link Version} annotation on one entity field.</p>
 * <p>(2) Add {@link OptimisticLockerInnerInterceptor} into mybatis plugin.</p>
 * <br>
 * <p>How to work?</p>
 * <p>if update entity with version column=1:</p>
 * <p>(1) no {@link OptimisticLockerInnerInterceptor}:</p>
 * <p>SQL: update tbl_test set name='abc' where id=100001;</p>
 * <p>(2) add {@link OptimisticLockerInnerInterceptor}:</p>
 * <p>SQL: update tbl_test set name='abc',version=2 where id=100001 and version=1;</p>
 *
 * @author yuxiaobin
 * @since 3.4.0
 */
@SuppressWarnings({"unchecked"})
public class OptimisticLockerInnerInterceptor implements InnerInterceptor {

    private static final String PARAM_UPDATE_METHOD_NAME = "update";

    @Override
    public void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) throws SQLException {
        if (SqlCommandType.UPDATE != ms.getSqlCommandType()) {
            return;
        }
        if (parameter instanceof Map) {
            Map<String, Object> map = (Map<String, Object>) parameter;
            doOptimisticLocker(map, ms.getId());
        }
    }

    protected void doOptimisticLocker(Map<String, Object> map, String msId) {
        //updateById(et), update(et, wrapper);
        Object et = map.getOrDefault(Constants.ENTITY, null);
        if (et != null) {
            // entity
            String methodName = msId.substring(msId.lastIndexOf(StringPool.DOT) + 1);
            TableInfo tableInfo = TableInfoHelper.getTableInfo(et.getClass());
            if (tableInfo == null || !tableInfo.isWithVersion()) {
                return;
            }
            try {
                TableFieldInfo fieldInfo = tableInfo.getVersionFieldInfo();
                Field versionField = fieldInfo.getField();
                // Old version value
                Object originalVersionVal = versionField.get(et);
                if (originalVersionVal == null) {
                    return;
                }
                String versionColumn = fieldInfo.getColumn();
                // New version value
                Object updatedVersionVal = this.getUpdatedVersionVal(fieldInfo.getPropertyType(), originalVersionVal);
                if (PARAM_UPDATE_METHOD_NAME.equals(methodName)) {
                    AbstractWrapper<?, ?, ?> aw = (AbstractWrapper<?, ?, ?>) map.getOrDefault(Constants.WRAPPER, null);
                    if (aw == null) {
                        UpdateWrapper<?> uw = new UpdateWrapper<>();
                        uw.eq(versionColumn, originalVersionVal);
                        map.put(Constants.WRAPPER, uw);
                    } else {
                        aw.apply(versionColumn + " = {0}", originalVersionVal);
                    }
                } else {
                    map.put(Constants.MP_OPTLOCK_VERSION_ORIGINAL, originalVersionVal);
                }
                versionField.set(et, updatedVersionVal);
            } catch (IllegalAccessException e) {
                throw ExceptionUtils.mpe(e);
            }
        }
    }

    /**
     * This method provides the control for version value.<BR>
     * Returned value type must be the same as original one.
     *
     * @param originalVersionVal ignore
     * @return updated version val
     */
    protected Object getUpdatedVersionVal(Class<?> clazz, Object originalVersionVal) {
        if (long.class.equals(clazz) || Long.class.equals(clazz)) {
            return ((long) originalVersionVal) + 1;
        } else if (int.class.equals(clazz) || Integer.class.equals(clazz)) {
            return ((int) originalVersionVal) + 1;
        } else if (Date.class.equals(clazz)) {
            return new Date();
        } else if (Timestamp.class.equals(clazz)) {
            return new Timestamp(System.currentTimeMillis());
        } else if (LocalDateTime.class.equals(clazz)) {
            return LocalDateTime.now();
        }
        //not supported type, return original val.
        return originalVersionVal;
    }
}

Keywords: Mybatis

Added by chiprivers on Fri, 18 Feb 2022 13:59:25 +0200