Inventory of MyBatis: the use of custom plug-ins and PageHelper

Inventory of MyBatis: the use of custom plug-ins and PageHelper

Total documentation: article directory
Github: github.com/black-ant

I. Introduction

Article purpose:

  • Understand the usage of MyBatis plug-in
  • Understand the main source code logic
  • Understand the relevant source code of PageHelper

This is a general document, which is used to quickly use related functions in the follow-up, and the overall difficulty is relatively low.

2. Process

2.1 Basic usage

Basic interceptor class

@Intercepts( {@org.apache.ibatis.plugin.Signature( type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})}) public class DefaultInterceptor implements Interceptor { private Logger logger = LoggerFactory.getLogger( this .getClass()); @Override public Object intercept (Invocation invocation) throws Throwable { logger.info( "------> this is in intercept <-------" ); return invocation.proceed(); } @Override public Object plugin (Object target) { logger.info( "------> this is in plugin <-------" ); return Plugin.wrap(target, this ); } @Override public void setProperties (Properties properties) { logger.info( "------> this is in setProperties <-------" ); } } Copy code

Basic configuration class

@Configuration public class PluginsConfig { @Autowired private List<SqlSessionFactory> sqlSessionFactoryList; @PostConstruct public void addPageInterceptor () { DefaultInterceptor interceptor = new DefaultInterceptor(); //Add for (SqlSessionFactory sqlSessionFactory: sqlSessionFactoryList) { sqlSessionFactory.getConfiguration().addInterceptor(interceptor); } } } Copy code

As you can see here, the intercepted parameters are as follows:

2.2 Detailed function

The entire interception process has several main components:

Interceptor interface

M- Interceptor#intercept(Invocation invocation): Intercept method M- plugin: call Plugin#wrap(Object target, Interceptor interceptor) method to execute the creation of proxy object M- setProperties: Get some required property values from properties //As you can see here, the only mandatory method is intercept public interface Interceptor { Object intercept (Invocation invocation) throws Throwable ; default Object plugin (Object target) { return Plugin.wrap(target, this ); } default void setProperties (Properties properties) { } } Copy code

InterceptorChain interceptor chain

InterceptorChain is an interceptor chain, used to perform related Interceptor operations, in which there is a collection of interceptors

//The main outline looks like this F- List<Interceptor> ArrayList M- addInterceptor: add interceptor -Called in the #pluginElement(XNode parent) method of Configuration -Create an Interceptor object and call the Interceptor#setProperties(properties) method -Call the Configuration#addInterceptor(interceptorInstance) method -Add to Configuration.interceptorChain M- pluginAll: apply all interceptors to the specified target object Copy code

Annotation @Intercepts and @Signature

These are the two main notes involved in the process:

  • @Intercepts: Define interceptors, Intercepts can contain a Signature array
  • @Signature: Define the type
    • type: the class handled by the interceptor
    • method: method of interception
    • args: method parameters (reason for overloading)

2.3 Source code tracking

Step 1: Loading of resources

The loading of resources is mainly the proxy logic of the corresponding method.As we can see from the previous article, a plugin operation mainly consists of 2 steps :

//Step 1: Declare the interceptor object @Intercepts( {@org.apache.ibatis.plugin.Signature( type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})}) //Step 2: Add the interceptor object to sqlSessionFactory sqlSessionFactory.getConfiguration().addInterceptor(interceptor); Copy code

Let's follow up the related operations:

C01- Configuration F01_01- InterceptorChain interceptorChain M01_01- addInterceptor -interceptorChain.addInterceptor(interceptor) ?- As you can see, an interceptor is added to InterceptorChain here //M01_01 source code public void addInterceptor (Interceptor interceptor) { interceptorChain.addInterceptor(interceptor); } public class InterceptorChain { private final List<Interceptor> interceptors = new ArrayList<Interceptor>(); public Object pluginAll (Object target) { for (Interceptor interceptor: interceptors) { target = interceptor.plugin(target); } return target; } //As you can see, the interceptor is added to the interceptor chain here//PS: But this is not completely completed, just add, the specific operation will be completed in pluginAll above public void addInterceptor (Interceptor interceptor) { interceptors.add(interceptor); } public List<Interceptor> getInterceptors () { return Collections.unmodifiableList(interceptors); } } Copy code

Step 2: plugin construction

The relevant interceptors have been added in Step 1. In this link , the corresponding Plugin needs to be constructed through the Interceptor

Let's take a look at the call chain first:

  • C- SqlSessionTemplate # selectList: initiate a Select request
  • C- SqlSessionInterceptor # invoke: Build a Session proxy
  • C- SqlSessionUtils # getSqlSession: Get Session object
  • C- DefaultSqlSessionFactory # openSessionFromDataSource
  • C- Configuration # newExecutor

On the whole, start from getSqlSession to pay attention

public static SqlSession getSqlSession (SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) { notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED); notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED); SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory); SqlSession session = sessionHolder(executorType, holder); if (session != null ) { return session; } //Core node, open Session session = sessionFactory.openSession(executorType); registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session); return session; } Copy code

DefaultSqlSessionFactory builds a Session and calls Plugin to generate logic at the same time

While building Session here, finally call Plugin warp to build Plugin

C- DefaultSqlSessionFactory //Starting point of opening: When opening the session, private SqlSession openSessionFromDataSource (ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null ; try { final Environment environment = configuration.getEnvironment(); final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); //Build Executor here and put in Session final Executor executor = configuration.newExecutor(tx, execType); return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); //may have fetched a connection so lets call close() throw ExceptionFactory.wrapException( "Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } } //Call the interception chain C- Configuration public Executor newExecutor (Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType: executorType; executorType = executorType == null ? ExecutorType.SIMPLE: executorType; Executor executor; if (ExecutorType.BATCH == executorType) { executor = new BatchExecutor( this , transaction); } else if (ExecutorType.REUSE == executorType) { executor = new ReuseExecutor( this , transaction); } else { executor = new SimpleExecutor( this , transaction); } if (cacheEnabled) { executor = new CachingExecutor(executor); } //Call the interception chain here to construct an Executor object executor = (Executor) interceptorChain.pluginAll(executor); return executor; } //Interceptor chain processing C- InterceptorChain public Object pluginAll (Object target) { //Here is building the interceptor chain, and what is returned is the final interceptor processing class for (Interceptor interceptor: interceptors) { target = interceptor.plugin(target); } return target; } C- Plugin //Call Plugin wrap to generate a new plugin, which contains the interceptor method of the corresponding interceptor 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 ) { //The core, the method is a proxy, and a Plugin is passed in for the proxy class return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } return target; } Copy code

Step 3: invoke execution

The implementation of the interceptor is based on the Proxy proxy implementation. I saw the generation of the proxy above. Here is a look at the proxy call:

Take Query as an example:

When the Query method in Executor is called, the proxy class will be called by default . The construction of Execute is mentioned above, so what role does it play in the entire logic?

Executor function: Executor is the top-level interface in Mybatis, which defines the main database operation methods

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

The call of execute:

When the Session was built earlier, Plugin was already defined for it, and the following is the main process of Plugin

//1. Choose the appropriate plugin C- DefaultSqlSession public <E> List<E> selectList (String statement, Object parameter, RowBounds rowBounds) { try { MappedStatement ms = configuration.getMappedStatement(statement); return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception e) { throw ExceptionFactory.wrapException( "Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } } //2. Through proxy in the middle //3. Call the interceptor in plugin C05- Plugin F05_01- private final Interceptor interceptor; M05_01- invoke(Object proxy, Method method, Object[] args) -Get methods that can be intercepted -Determine whether the current method is interceptable //M05_01: invoke@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)) { //The interceptor is called here return interceptor.intercept( new Invocation(target, method, args)); } return method.invoke(target, args); } catch (Exception e) { throw ExceptionUtil.unwrapThrowable(e); } } //4. Invoke the interceptor public Object intercept (Invocation invocation) throws Throwable { //The actual logic of calling the proxy method here return invocation.proceed(); } Copy code

3. Extend PageHelper

The following is how to use related interceptor methods in PageHelper:

There are mainly 2 interceptors in PageHelper:

  • QueryInterceptor: query operation plugin
  • PageInterceptor: Paging operation plugin

Mainly look at the PageInterceptor interceptor:

Interceptor code

@Intercepts( { @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}), } ) public class PageInterceptor implements Interceptor { private volatile Dialect dialect; private String countSuffix = "_COUNT" ; protected Cache<String, MappedStatement> msCountMap = null ; private String default_dialect_class = "com.github.pagehelper.PageHelper" ; @Override public Object intercept (Invocation invocation) throws Throwable { try { Object[] args = invocation.getArgs(); //Get the four requested parameters MappedStatement ms = (MappedStatement) args[ 0 ]; Object parameter = args[ 1 ]; RowBounds rowBounds = (RowBounds) args[ 2 ]; ResultHandler resultHandler = (ResultHandler) args[ 3 ]; Executor executor = (Executor) invocation.getTarget(); CacheKey cacheKey; BoundSql boundSql; //Due to the logical relationship, it will only be entered once if (args.length == 4 ) { //4 parameters //Get binding SQL: select id, type_code, type_class, type_policy, type_name, supplier_id, supplier_name from sync_type boundSql = ms.getBoundSql(parameter); cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql); } else { //6 parameters cacheKey = (CacheKey) args[ 4 ]; boundSql = (BoundSql) args[ 5 ]; } checkDialectExists(); List resultList; //Call the method to determine whether paging is needed, if not, return the result directly if (!dialect.skip(ms, parameter, rowBounds)) { //Determine whether a count query is needed if (dialect.beforeCount(ms, parameter, rowBounds)) { //Total number of queries Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql); //Process the total number of queries. If it returns true, continue the pagination query, if it is false, return directly if (!dialect.afterCount(count, parameter, rowBounds)) { //When the total number of queries is 0, directly return an empty result return dialect.afterPage ( new ArrayList(), parameter, rowBounds); } } resultList = ExecutorUtil.pageQuery(dialect, executor, ms, parameter, rowBounds, resultHandler, boundSql, cacheKey); } else { //rowBounds uses parameter values. When the paging plug-in is not used for processing, the default memory paging is still supported resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql); } return dialect.afterPage(resultList, parameter, rowBounds); } finally { dialect.afterAll(); } } /** * When configuring in Spring bean mode, if there is no configuration property, the following setProperties method will not be executed and will not be initialized * <p> * So there will be a null case fixed #26 */ private void checkDialectExists () { if (dialect == null ) { synchronized (default_dialect_class) { if (dialect == null ) { setProperties( new Properties()); } } } } private Long count (Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { String countMsId = ms.getId() + countSuffix; Long count; //First determine whether there is a handwritten count query MappedStatement countMs = ExecutorUtil.getExistedMappedStatement(ms.getConfiguration(), countMsId); if (countMs != null ) { count = ExecutorUtil.executeManualCount(executor, countMs, parameter, boundSql, resultHandler); } else { countMs = msCountMap.get(countMsId); //Automatically create if (countMs == null ) { //Create a ms with a return value of Long type according to the current ms countMs = MSUtils.newCountMappedStatement(ms, countMsId); msCountMap.put(countMsId, countMs); } count = ExecutorUtil.executeAutoCount(dialect, executor, countMs, parameter, boundSql, rowBounds, resultHandler); } return count; } @Override public Object plugin (Object target) { return Plugin.wrap(target, this ); } @Override public void setProperties (Properties properties) { //Cache count ms msCountMap = CacheFactory.createCache(properties.getProperty( "msCountCache" ), "ms" , properties); String dialectClass = properties.getProperty( "dialect" ); if (StringUtil.isEmpty(dialectClass)) { dialectClass = default_dialect_class; } try { Class<?> aClass = Class.forName(dialectClass); dialect = (Dialect) aClass.newInstance(); } catch (Exception e) { throw new PageException(e); } dialect.setProperties(properties); String countSuffix = properties.getProperty( "countSuffix" ); if (StringUtil.isNotEmpty(countSuffix)) { this .countSuffix = countSuffix; } } } Copy code

Supplement: ExecutorUtil.executeAutoCount related logic

/** * Execute automatically generated count query **/ public static Long executeAutoCount (Dialect dialect, Executor executor, MappedStatement countMs, Object parameter, BoundSql boundSql, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { Map<String, Object> additionalParameters = getAdditionalParameter(boundSql); // count key CacheKey countKey = executor.createCacheKey(countMs, parameter, RowBounds.DEFAULT, boundSql); // count sql //SELECT count(0) FROM sync_type String countSql = dialect.getCountSql(countMs, boundSql, parameter, rowBounds, countKey); //countKey.update(countSql); BoundSql countBoundSql = new BoundSql(countMs.getConfiguration(), countSql, boundSql.getParameterMappings(), parameter); // SQL BoundSql for (String key : additionalParameters.keySet()) { countBoundSql.setAdditionalParameter(key, additionalParameters.get(key)); } // count Object countResultList = executor.query(countMs, parameter, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql); Long count = (Long) ((List) countResultList).get(0); return count; }

: pageQuery

public static <E> List<E> pageQuery(Dialect dialect, Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql, CacheKey cacheKey) throws SQLException { // if (dialect.beforePage(ms, parameter, rowBounds)) { // key CacheKey pageKey = cacheKey; // , parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey); // sql String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey); BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter); Map<String, Object> additionalParameters = getAdditionalParameter(boundSql); // for (String key : additionalParameters.keySet()) { pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key)); } // return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql); } else { //Do not perform memory paging without performing paging return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql); } } Copy code

Because the source code is highly commented, there is basically no need to make additional annotations. The overall process is:

  • Step 1: The interceptor intercept method defines the overall logic
  • Step 2: The count method determines whether it is paged
  • Step 3: pageQuery calls dialects to splice things SQL

There are several points worthy of attention in the overall:

  • Only generate the paged row and pageKey, etc., and finally combine them through dialects to adapt to multiple database structures
  • The core is to call the executor.query native method

Briefly describe the binding process of PageHepler

The core processing class is AbstractHelperDialect, start with the creation:

PageHelper.startPage(page, size); List<SyncType> allOrderPresentList = syncTypeDAO.findAll(); //Step 1: startPage core code public static <E> Page<E> startPage ( int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) { Page<E> page = new Page<E>(pageNum, pageSize, count); page.setReasonable(reasonable); page.setPageSizeZero(pageSizeZero); //When orderBy has been executed Page<E> oldPage = getLocalPage(); if (oldPage != null && oldPage.isOrderByOnly()) { page.setOrderBy(oldPage.getOrderBy()); } setLocalPage(page); return page; } The core here is setLocalPage, which will use ThreadLocal to maintain thread parameters protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>(); //Step 2: Parameter acquisition in the interceptor //You can see that the first sentence is to get C- AbstractHelperDialect # processParameterObject from getLocalPage-ThreadLocal public Object processParameterObject (MappedStatement ms, Object parameterObject, BoundSql boundSql, CacheKey pageKey) { //Processing parameters Page page = getLocalPage(); //If it's just order by, you don't have to deal with the parameter if (page.isOrderByOnly()) { return parameterObject; } Map<String, Object> paramMap = null ; if (parameterObject == null ) { paramMap = new HashMap<String, Object>(); } else if (parameterObject instanceof Map) { //Solve the situation of immutable Map paramMap = new HashMap<String, Object>(); paramMap.putAll((Map) parameterObject); } else { paramMap = new HashMap<String, Object>(); //The judgment condition of dynamic sql will not appear in ParameterMapping, but it must be, so here we need to collect all the getter attributes //TypeHandlerRegistry that can be directly processed will be treated as a direct The object used for processing boolean hasTypeHandler = ms.getConfiguration().getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass()); MetaObject metaObject = MetaObjectUtil.forObject(parameterObject); //Need to save the original value for MyProviderSqlSource in the form of annotations if (!hasTypeHandler) { for (String name: metaObject.getGetterNames()) { paramMap.put(name, metaObject.getValue(name)); } } //The following method mainly solves the problem of a common type of parameter if (boundSql.getParameterMappings() != null && boundSql.getParameterMappings().size()> 0 ) { for (ParameterMapping parameterMapping: boundSql.getParameterMappings( )) { String name = parameterMapping.getProperty(); if (!name.equals(PAGEPARAMETER_FIRST) && !name.equals(PAGEPARAMETER_SECOND) && paramMap.get(name) == null ) { if (hasTypeHandler || parameterMapping.getJavaType().equals(parameterObject.getClass())) { paramMap.put(name, parameterObject); break ; } } } } } return processPageParameter(ms, paramMap, page, boundSql, pageKey); } Copy code

The rest of the logic is relatively clear, and it doesn t involve too much logic here. If you need to see more

summary

This should be the simplest one to write a source code analysis. In addition to its uncomplicated structure, the comments of the relevant source code are also very clear, and there is basically no need for analysis.

Use of interceptors:

  • Prepare the interceptor class
  • sqlSessionFactory.getConfiguration().addInterceptor(interceptor) Add interceptor

PageHelper core:

  • parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey): Get the parameters of the page
  • dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey): the parameter is parsed as SQL
  • ThreadLocal save parameters

In the follow-up, we will pay attention to the other points of Mybatis and the overall advantages of PageHelper.