Mybaits interceptor, purpose of interceptor, mybaits L1 cache

Posted by thepriest on Fri, 17 Dec 2021 12:51:48 +0100

1.mybaits interceptor

Original design intention

In order for users to implement their own logic at some time without moving the inherent logic of Mybatis. Through the Mybatis interceptor, we can intercept the calls of some methods. We can choose to add some logic before and after the execution of these intercepted methods, or we can execute our own logic when executing these intercepted methods instead of the intercepted methods.

Core object

Mybatis core objectexplain
SqlSessionAs the main top-level API of MyBatis, it represents the session interacting with the database and completes the necessary database addition, deletion, modification and query functions
ExecutorMyBatis executor, the core of MyBatis scheduling, is responsible for generating SQL statements and maintaining query cache
StatementHandlerIt encapsulates the JDBC statement operation and is responsible for the operation of JDBC statement, such as setting parameters and converting the Statement result set into a List set
ParameterHandlerIt is responsible for converting the parameters passed by the user into the parameters required by JDBC Statement
ResultSetHandlerIt is responsible for converting the ResultSet result set object returned by JDBC into a List type collection
TypeHandlerResponsible for the mapping and conversion between java data types and jdbc data types
MappedStatementMappedStatement maintains a mapper Encapsulation of select, update, delete and insert nodes in XML file
SqlSourceIt is responsible for dynamically generating SQL statements according to the parameterObject passed by the user, encapsulating the information into the BoundSql object and returning
BoundSqlRepresents dynamically generated SQL statements and corresponding parameter information
ConfigurationAll Configuration information of MyBatis is maintained in the Configuration object

explain

Not all methods in every object can be intercepted by the Mybatis interceptor. The Mybatis interceptor can only intercept the methods in the Executor, ParameterHandler, StatementHandler and ResultSetHandler objects

Executor

  • All Mapper statements in Mybatis are executed through the executor. Executor is the core interface of Mybatis. From the defined interface method, we can see that the corresponding addition, deletion and modification statements are performed through the update method of the executor interface, and the query is performed through the query method. The common interception methods in the executor are as follows:

    public interface Executor {
        /**
         * Execute update/insert/delete
         */
        int update(MappedStatement ms, Object parameter) throws SQLException;
    
        /**
         * To execute a query, first look it up in the cache
         */
        <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
    
        /**
         * Execute query
         */
        <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
    
        /**
         * Execute the query and put the query results in Cursor
         */
        <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
    

ParameterHandler

  • ParameterHandler is used to set parameter rules. When StatementHandler uses the prepare() method, it is used to set parameters. Therefore, if there is custom logic processing for parameters, it can be implemented by intercepting ParameterHandler. The methods that can be intercepted in ParameterHandler are explained as follows

    public interface ParameterHandler {
        /**
         * Get parameter object
         */
        Object getParameterObject();
    	/**
      	* Call -- PreparedStatement when setting parameter rules
      	*/
        void setParameters(PreparedStatement var1) throws SQLException;
    }
    

StatementHandler

  • The StatementHandler handles the interaction between Mybatis and JDBC

    public interface StatementHandler {
        /**
         * Get a Statement from the connection
         */
        Statement prepare(Connection connection, Integer transactionTimeout)
                throws SQLException;
    
        /**
         * Set the parameters required for statement execution
         */
        void parameterize(Statement statement)
                throws SQLException;
    
        /**
         * batch
         */
        void batch(Statement statement)
                throws SQLException;
    
        /**
         * Update: update/insert/delete statements
         */
        int update(Statement statement)
                throws SQLException;
    
        /**
         * Execute query
         */
        <E> List<E> query(Statement statement, ResultHandler resultHandler)
                throws SQLException;
    
        <E> Cursor<E> queryCursor(Statement statement)
                throws SQLException;
    
        ...
    
    }
    
    // Generally, only the prepare method in StatementHandler is intercepted
    @Intercepts({
            @Signature(
                    type = StatementHandler.class,
                    method = "prepare",
                    args = {Connection.class, Integer.class}
            )
    })
    public class TableShardInterceptor implements Interceptor {
    
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            if (invocation.getTarget() instanceof RoutingStatementHandler) {
                // TODO: do your own logic
            }
            return invocation.proceed();
        }
    
        @Override
        public Object plugin(Object target) {
            // When the target class is StatementHandler type, the target class is wrapped. Otherwise, the target class is directly returned to the target itself to reduce the number of times the target is proxied
            return (target instanceof RoutingStatementHandler) ? Plugin.wrap(target, this) : target;
        }
    
        @Override
        public void setProperties(Properties properties) {
    
        }
    }
    

ResultSetHandler

  • ResultSetHandler is used to process the query results. Therefore, if you need to do special processing on the returned results, you can intercept the processing of ResultSetHandler. The common interception methods in ResultSetHandler are as follows

    public interface ResultSetHandler {
    
        /**
         * Map the result set generated after Statement execution (there may be multiple result sets) to the result list
         */
        <E> List<E> handleResultSets(Statement stmt) throws SQLException;
        <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;
    
        /**
         * Process the output parameters after the execution of the stored procedure
         */
        void handleOutputParameters(CallableStatement cs) throws SQLException;
    
    }
    

Use of interceptors

  • The use of Mybatis interceptor is divided into two steps: Customizing interceptor class and registering interceptor class

Custom interceptor class

  • The custom Interceptor needs to implement the Interceptor interface, and the @ Intercepts annotation needs to be added on the custom Interceptor class

    public interface Interceptor {
    
        /**
         * The method called by the proxy object every time is the method to be executed when intercepting. Do our custom logic processing in this method
         */
        Object intercept(Invocation invocation) throws Throwable;
    
        /**
         * plugin Method is used by the interceptor to encapsulate the target object. Through this method, we can return the target object itself or a proxy
         *
         * When the proxy is returned, we can intercept the method to call the intercept method -- plugin wrap(target, this)
         * When the current object is returned, the intercept method will not be called, which is equivalent to that the current interceptor is invalid
         */
        Object plugin(Object target);
    
        /**
         * It is used to specify some attributes in the Mybatis configuration file. Some attributes can be set when registering the current interceptor
         */
        void setProperties(Properties properties);
    
    }
    

@Intercepts annotation

  • The Intercepts annotation requires an array of Signature parameters. Use the Signature to specify which method in which object to intercept

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface Intercepts {
        /**
         * Define intercept point
         * Only when the conditions of the interception point are met will it enter the interceptor
         */
        Signature[] value();
    }
    
  • Signature to specify which method we need to intercept that kind of object

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target({})
    public @interface Signature {
        /**
         * Define one of the intercepted classes Executor, ParameterHandler, StatementHandler and ResultSetHandler
         */
        Class<?> type();
    
        /**
         * On the basis of defining the interception class, define the interception method
         */
        String method();
    
        /**
         * On the basis of defining the interception method, define the parameters corresponding to the interception method,
         * JAVA The method may be overloaded. No parameters are specified. I don't know which method it is
         */
        Class<?>[] args();
    }
    

Simple example

  • For example, we customize a MybatisInterceptor class to intercept two queries in the Executor class. Custom interception class MybatisInterceptor
@Intercepts({
        @Signature(
                type = Executor.class,
                method = "query",
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
        ),
        @Signature(
                type = Executor.class,
                method = "update",
                args = {MappedStatement.class, Object.class}
        )
})
public class MybatisInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        // TODO: Custom interception logic

    }

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

    @Override
    public void setProperties(Properties properties) {

    }
}

Register interceptor

  • To register an interceptor is to tell Mybatis to use our interceptor. Registering the interceptor class is very simple. In the @ Configuration annotated class, @ Bean is our custom interceptor class. For example, we need to register a custom MybatisInterceptor interceptor
/**
 * mybatis to configure
 */
@Configuration
public class MybatisConfiguration {

    /**
     * Register interceptor
     */
    @Bean
    public MybatisInterceptor mybatisInterceptor() {
        MybatisInterceptor interceptor = new MybatisInterceptor();
        Properties properties = new Properties();
        // You can call properties Setproperty method to set some custom parameters for the interceptor
        interceptor.setProperties(properties);
        return interceptor;
    }
    // Then set the interceptor into SqlSessionFactoryBean to intercept
}

Application scenario

  • sql log printing
  • Implement paging
  • Encrypt the queried data

2. Implement the sub database through the data source druid of alibaba

Configure different data sources

  • Specify mapper interface package (corresponding data source scans corresponding mapper interface)
  • Configure the data source to load the specified database
  • Configure sqlsessionfactory to load the specified xml file
@Configuration
@MapperScan(basePackages = "com.yu.mapper.db1", sqlSessionTemplateRef = "db1SqlSessionTemplate")
public class DataSourcedb1Config {
    private static final Logger LOGGER = LoggerFactory.getLogger(DataSourcedb1Config.class);

    @Bean(name = "db1DataSource", initMethod = "init", destroyMethod = "close")
    @ConfigurationProperties(prefix = "spring.datasource.db1")
    public DruidDataSource dataSource() {
        return DruidDataSourceBuilder.create().build();
    }
    @Bean("db1SqlSessionFactory")
    public SqlSessionFactory dbSqlSessionFactory() {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        // set up data sources
        sqlSessionFactoryBean.setDataSource(dataSource());
        // Auto rename
        sqlSessionFactoryBean.setTypeAliasesPackage("com.yu.entity");
        // Set mapper file
        try {
            ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
            sqlSessionFactoryBean.setMapperLocations(resolver
                    .getResources("com/yu/mapper/db1/*.xml"));
            return sqlSessionFactoryBean.getObject();
        } catch (Exception e) {
            LOGGER.error("initialization DBSqlSessionFactory fail", e);
            throw new RuntimeException(e);
        }
    }
    @Bean("db1SqlSessionTemplate")
    public SqlSessionTemplate dbSqlSessionTemplate(@Qualifier("db1SqlSessionFactory") SqlSessionFactory sqlSessionTemplateHosDataSourc) {
        return new SqlSessionTemplate(sqlSessionTemplateHosDataSourc);
    }
    @Bean("db1TxManager")
    public PlatformTransactionManager ucTxManager(@Qualifier("db1DataSource") DruidDataSource prodDataSource) {
        return new DataSourceTransactionManager(prodDataSource);
    }
}

3. Realize the read-write separation of mysql through mybaits

Idea: I realized the dynamic switching of mybatis data source when I was working on a project. Based on the original scheme, aop is used to intercept dao layer methods. According to the method name, the type of sql to be executed can be determined, and the master-slave data sources can be switched dynamically

4.mybaits cache

preface

MyBatis is a common Java database access layer framework. In daily work, developers mostly use the default cache configuration of MyBatis, but MyBatis cache mechanism has some shortcomings, which is easy to cause dirty data and form some potential hidden dangers in use. I have also handled some development problems caused by MyBatis cache in business development. With personal interest, I hope to sort out MyBatis cache mechanism for readers from the perspective of application and source code

L1 cache

  • introduce

    During the application running, we may execute SQL with identical query conditions many times in a database session. MyBatis provides a scheme to optimize the first level cache. If the SQL statements are the same, we will give priority to hit the first level cache to avoid querying the database directly and improve performance.

  • Execution process

    [the external chain picture transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-jlCYWjTe-1639736288791)(H: \ note \ img \ mybatis process. PNG)]

Each SqlSession holds an Executor, and each Executor has a LocalCache. When the user initiates a query, MyBatis generates a MappedStatement according to the currently executed statement and queries in the Local Cache. If the cache hits, the result is directly returned to the user. If the cache does not hit, the database is queried, the result is written to the Local Cache, and finally the result is returned to the user

  • to configure

Developers only need to add the following statement in the MyBatis configuration file to use the L1 cache. There are two options: SESSION or state. The default is SESSION level, that is, all statements executed in a MyBatis SESSION will share this cache. One is the state level, which can be understood as that the cache is only valid for the currently executed statement

<setting name="localCacheScope" value="SESSION"/>
  • Workflow

    [the external chain picture transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the picture and upload it directly (IMG okoqmxpx-1639736288792) (H: \ notes \ img\mybaits-doing.png)]

  • Source code analysis

The sqlsession interface provides users with basic operations on the database and hides the underlying details. The default implementation class is DefaultSqlSession.

public interface SqlSession extends Closeable {
    <T> T selectOne(String var1);

    <T> T selectOne(String var1, Object var2);

    <E> List<E> selectList(String var1);

    <E> List<E> selectList(String var1, Object var2);

    <E> List<E> selectList(String var1, Object var2, RowBounds var3);

    <K, V> Map<K, V> selectMap(String var1, String var2);

    <K, V> Map<K, V> selectMap(String var1, Object var2, String var3);

    <K, V> Map<K, V> selectMap(String var1, Object var2, String var3, RowBounds var4);

    <T> Cursor<T> selectCursor(String var1);

    <T> Cursor<T> selectCursor(String var1, Object var2);

    <T> Cursor<T> selectCursor(String var1, Object var2, RowBounds var3);

    void select(String var1, Object var2, ResultHandler var3);

    void select(String var1, ResultHandler var2);

    void select(String var1, Object var2, RowBounds var3, ResultHandler var4);

    int insert(String var1);

    int insert(String var1, Object var2);

    int update(String var1);

    int update(String var1, Object var2);

    int delete(String var1);

    int delete(String var1, Object var2);

    void commit();

    void commit(boolean var1);

    void rollback();

    void rollback(boolean var1);

    List<BatchResult> flushStatements();

    void close();

    void clearCache();

    Configuration getConfiguration();

    <T> T getMapper(Class<T> var1);

    Connection getConnection();
}

sqlsession delegates specific responsibilities to the Executor.

public interface Executor {
    ResultHandler NO_RESULT_HANDLER = null;

    int update(MappedStatement var1, Object var2) throws SQLException;

    <E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4, CacheKey var5, BoundSql var6) throws SQLException;

    <E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4) throws SQLException;

    <E> Cursor<E> queryCursor(MappedStatement var1, Object var2, RowBounds var3) throws SQLException;

    List<BatchResult> flushStatements() throws SQLException;

    void commit(boolean var1) throws SQLException;

    void rollback(boolean var1) throws SQLException;

    CacheKey createCacheKey(MappedStatement var1, Object var2, RowBounds var3, BoundSql var4);

    boolean isCached(MappedStatement var1, CacheKey var2);

    void clearLocalCache();

    void deferLoad(MappedStatement var1, MetaObject var2, String var3, CacheKey var4, Class<?> var5);

    Transaction getTransaction();

    void close(boolean var1);

    boolean isClosed();

    void setExecutorWrapper(Executor var1);
}

The Executor has several implementation classes. The first level Cache mainly uses BaseExecutor, including a constant perpetualcache. Localcache is the most basic implementation of the Cache interface. Its implementation is very simple. It holds HashMap internally, and the operation on the first level Cache is actually the operation on HashMap

public abstract class BaseExecutor implements Executor {
    private static final Log log = LogFactory.getLog(BaseExecutor.class);
    protected Transaction transaction;
    protected Executor wrapper;
    protected ConcurrentLinkedQueue<BaseExecutor.DeferredLoad> deferredLoads;
    protected PerpetualCache localCache;
    protected PerpetualCache localOutputParameterCache;
    protected Configuration configuration;
    protected int queryStack;
    private boolean closed;
}

PerpetualCache implements the Cache interface, and redesigns some methods in the interface and equals()

public class PerpetualCache implements Cache {
    private final String id;
    private Map<Object, Object> cache = new HashMap();

    public PerpetualCache(String id) {
        this.id = id;
    }

    public String getId() {
        return this.id;
    }

    public int getSize() {
        return this.cache.size();
    }

    public void putObject(Object key, Object value) {
        this.cache.put(key, value);
    }

    public Object getObject(Object key) {
        return this.cache.get(key);
    }

    public Object removeObject(Object key) {
        return this.cache.remove(key);
    }

    public void clear() {
        this.cache.clear();
    }

    public ReadWriteLock getReadWriteLock() {
        return null;
    }

    public boolean equals(Object o) {
        if (this.getId() == null) {
            throw new CacheException("Cache instances require an ID.");
        } else if (this == o) {
            return true;
        } else if (!(o instanceof Cache)) {
            return false;
        } else {
            Cache otherCache = (Cache)o;
            return this.getId().equals(otherCache.getId());
        }
    }

    public int hashCode() {
        if (this.getId() == null) {
            throw new CacheException("Cache instances require an ID.");
        } else {
            return this.getId().hashCode();
        }
    }
}

Specifically, through sessionfactory Opensession() creates an Executor

    private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
        Transaction tx = null;

        DefaultSqlSession var8;
        try {
            Environment environment = this.configuration.getEnvironment();
            TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
            tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
            // Create Executor keys
            Executor executor = this.configuration.newExecutor(tx, execType);
            var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
        } catch (Exception var12) {
            this.closeTransaction(tx);
            throw ExceptionFactory.wrapException("Error opening session.  Cause: " + var12, var12);
        } finally {
            ErrorContext.instance().reset();
        }

        return var8;
    }

The client executes the query method select and passes it to the query method in the executor

    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameter);
        CacheKey key = this.createCacheKey(ms, parameter, rowBounds, boundSql);
        return this.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }   
// To create the cached key, the first is the member variable and constructor. It has an initial hachcode and multiplier, and maintains an internal updatelist. In the update method of CacheKey, a hashcode and checksum calculation will be performed, and the passed in parameters will be added to the updatelist
//Except for the comparison of hashcode, checksum and count, as long as the one-to-one correspondence of the elements in the updatelist is equal, it can be considered that the CacheKey is equal. As long as the following five values of two SQL statements are the same, they can be considered as the same SQL statement
    public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
        if (this.closed) {
            throw new ExecutorException("Executor was closed.");
        } else {
            CacheKey cacheKey = new CacheKey();
            cacheKey.update(ms.getId());
            cacheKey.update(rowBounds.getOffset());
            cacheKey.update(rowBounds.getLimit());
            cacheKey.update(boundSql.getSql());
            List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
            TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
            Iterator var8 = parameterMappings.iterator();

            while(var8.hasNext()) {
                ParameterMapping parameterMapping = (ParameterMapping)var8.next();
                if (parameterMapping.getMode() != ParameterMode.OUT) {
                    String propertyName = parameterMapping.getProperty();
                    Object value;
                    if (boundSql.hasAdditionalParameter(propertyName)) {
                        value = boundSql.getAdditionalParameter(propertyName);
                    } else if (parameterObject == null) {
                        value = null;
                    } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                        value = parameterObject;
                    } else {
                        MetaObject metaObject = this.configuration.newMetaObject(parameterObject);
                        value = metaObject.getValue(propertyName);
                    }

                    cacheKey.update(value);
                }
            }

            if (this.configuration.getEnvironment() != null) {
                cacheKey.update(this.configuration.getEnvironment().getId());
            }

            return cacheKey;
        }
    }

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
        if (this.closed) {
            throw new ExecutorException("Executor was closed.");
        } else {
            if (this.queryStack == 0 && ms.isFlushCacheRequired()) {
                this.clearLocalCache();
            }
                this.deferredLoads.clear();
                if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
                    this.clearLocalCache();
                }
                        List list;
            try {
                ++this.queryStack;
                list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
                if (list != null) {
                    // This is mainly used to process stored procedures.
                    this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
                } else {
                    // This is mainly to query the database
                    list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
                }
            } finally {
                --this.queryStack;
            }
            if (this.queryStack == 0) {
                Iterator var8 = this.deferredLoads.iterator();

                while(var8.hasNext()) {
                    BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)var8.next();
                    deferredLoad.load();
                }
//At the end of the query method execution, it will judge whether the L1 cache level is the state level. If so, it will empty the cache, which is why the L1 cache at the state level cannot share the localCache
                this.deferredLoads.clear();
                if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
                    this.clearLocalCache();
                }
            }
            return list;
        }
    }
  • At the end of the source code analysis, we confirm the reason why the cache will be refreshed if it is the insert/delete/update method.

    The insert method and delete method of SqlSession will follow the update process uniformly, and the localCache will be cleared before each update

    public int insert(String statement, Object parameter) {
        return this.update(statement, parameter);
    }

    public int update(String statement) {
        return this.update(statement, (Object)null);
    }
    public int delete(String statement) {
        return this.update(statement, (Object)null);
    }

    public int delete(String statement, Object parameter) {
        return this.update(statement, parameter);
    }

summary

  1. The life cycle of MyBatis L1 cache is consistent with SqlSession.
  2. The internal design of MyBatis L1 cache is simple. It is just a HashMap with no capacity limit, which lacks the functionality of cache.
  3. The maximum range of the first level cache of MyBatis is within sqlsessions. In multiple sqlsessions or distributed environments, database write operations will cause dirty data. It is recommended to set the cache level to Statement

Topics: Java Mybatis Spring Boot Cache