In depth study of MyBatis application analysis and best practices of MyBatis series 12_SSM framework plug in use and principle analysis of integrating MyBatis

In depth study of MyBatis application analysis and best practices of MyBatis series 12_SSM framework plug in use and principle analysis of integrating MyBatis

Older procedural apes share knowledge while learning and recording

I hope this article is meaningful to you, like, collect and comment

Your support is the greatest encouragement to my persistence. I can make friends and learn from each other

Coordinates: Hangzhou, Zhejiang

Q Q: 873373549

1, Objective of this paper

  • I've mastered it Construction of basic SSM framework
  • Explain how the SSM framework applies and integrates the MyBatis plug-in
  • Analyze how the plug-in works
  • Master the writing method of custom plug-ins

2, Spring MVC mybatis integration plug-in steps

Take the paging plug-in as an example

2.1 add plug-in dependency

In POM Add plug-in dependencies to XML as follows:

<!--   Dependency of paging plug-in     -->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.0.0</version>
</dependency>

2.2 plug in configuration

In mybatis config Add the following configuration to XML:

<!--  Plug in collection configuration  -->
    <plugins>
        <!--    Paging plug-in configuration    -->
        <plugin interceptor="com.github.pagehelper.PageInterceptor">
            <!-- 4.0.0 This parameter may not be set in later versions ,Can automatically identify
            <property name="dialect" value="mysql"/>  -->
            <!-- This parameter defaults to false -->
            <!-- Set to true When, the RowBounds First parameter offset regard as pageNum Page usage -->
            <!-- and startPage Medium pageNum Same effect-->
            <property name="offsetAsPageNum" value="true"/>
            <!-- This parameter defaults to false -->
            <!-- Set to true When using RowBounds Paging occurs count query -->
            <property name="rowBoundsWithCount" value="true"/>
            <!-- Set to true When, if pageSize=0 perhaps RowBounds.limit = 0 All the results will be queried -->
            <!-- (It is equivalent to not executing the paging query, but the returned result is still Page (type)-->
            <property name="pageSizeZero" value="true"/>
            <!-- 3.3.0 Version available - Paging parameter rationalization, default false Disable -->
            <!-- When rationalization is enabled, if pageNum<1 The first page will be queried if pageNum>pages The last page will be queried -->
            <!-- When rationalization is disabled, if pageNum<1 or pageNum>pages Null data will be returned -->
            <property name="reasonable" value="true"/>
            <!-- 3.5.0 Version available - To support startPage(Object params)method -->
            <!-- Added one`params`Parameter to configure parameter mapping from Map or ServletRequest Medium value -->
            <!-- Can configure pageNum,pageSize,count,pageSizeZero,reasonable,orderBy,Default values for not configuring mappings -->
            <!-- Do not copy the configuration without understanding the meaning -->
            <property name="params" value="pageNum=start;pageSize=limit;"/>
            <!-- Support through Mapper Interface parameters to pass paging parameters -->
            <property name="supportMethodsArguments" value="true"/>
            <!-- always Always return PageInfo type,check Check whether the return type is PageInfo,none return Page -->
            <property name="returnPageInfo" value="check"/>
        </plugin>
    </plugins>

2.3 using plug-ins

Write a paging control layer method in BlogController and introduce plug-in query, as follows:

/**
     * blog paging
     *
     * @return
     */
    @GetMapping("/blogPageList")
    public String blogPageList(@Param(value = "pageNum") Integer pageNum, @Param(value = "pageSize") Integer pageSize, Model model) {
        PageHelper.startPage(pageNum, pageSize);
        List<Blog> blogList = blogService.findAll();
        model.addAttribute("blogList", blogList);
        log.info("blogList:{}", blogList);
        //The number of consecutive pages is 10
        //The result of the package check only needs to give the pageInfo to the page to encapsulate the detailed paging information
        //Including the queried data
        PageInfo<Blog> pageInfo = new PageInfo<>(blogList, pageSize);
        model.addAttribute("pageInfo", pageInfo);
        return "blogPageList";
    }

2.4 writing paged pages

In order to use PageContext, a new jar needs to be introduced

        <!-- https://mvnrepository.com/artifact/javax.servlet.jsp/javax.servlet.jsp-api -->
        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>javax.servlet.jsp-api</artifactId>
            <version>2.3.3</version>
            <scope>provided</scope>
        </dependency>

Then write the JSP page as follows:

<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="utf-8" %>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%
    pageContext.setAttribute("APP_PATH", request.getContextPath());
%>
<html>
<head>
    <title>Blog List page</title>
    <script type="text/javascript" src="jquery/jquery-2.0.3.min.js"></script>
    <link href="css/bootstrap.min.css" rel="stylesheet">
    <script src="js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-12">
            <h1>This is an BlogList Html</h1>
        </div>
    </div>
    <div class="row">
        <div class="col-md-12">
            <table class="table table-hover">
                <tr>
                    <th>#</th>
                    <th>name</th>
                    <th>authorId</th>
                    <th>operation</th>
                </tr>
                <c:forEach items="${pageInfo.list}" var="blog">
                    <tr>
                        <th>${blog.bid}</th>
                        <th>${blog.name}</th>
                        <th>${blog.authorId}</th>
                        <th>
                            <button type="button" class="btn btn-primary btn-sm"><span
                                    class="glyphicon glyphicon-pencil"
                                    aria-hidden="true"></span>edit
                            </button>
                            <button type="button" class="btn btn-danger btn-sm"><span class="glyphicon glyphicon-trash"
                                                                                      aria-hidden="true">delete</span>
                            </button>
                        </th>
                    </tr>
                </c:forEach>
            </table>
        </div>

        <div class="row">
            <%-- Paging information--%>
            <div class="col-md-6">
                current ${pageInfo.pageNum}Page, per page ${pageInfo.pageSize}Records, total ${pageInfo.pages}Page, total ${pageInfo.total}Records
            </div>
            <div class="col-md-6">
                <nav aria-label="Page navigation">
                    <ul class="pagination">
                        <li>
                            <a href="${APP_PATH}/blogPageList?pageNum=1&pageSize=${pageInfo.pageSize}">
                                home page
                            </a>
                        </li>
                        <c:if test="${pageInfo.hasPreviousPage}">
                            <li>
                                <a href="${APP_PATH}/blogPageList?pageNum=${pageInfo.pageNum-1}&pageSize=${pageInfo.pageSize}"
                                   aria-label="Previous">
                                    <span aria-hidden="true">&laquo;</span>
                                </a>
                            </li>
                        </c:if>
                        <c:forEach items="${pageInfo.navigatepageNums}" var="page_Number">
                            <c:if test="${page_Number==pageInfo.pageNum}">
                                <li class="active"><a href="#">${page_Number}</a></li>
                            </c:if>
                            <c:if test="${page_Number!=pageInfo.pageNum}">
                                <li>
                                    <a href="${APP_PATH}/blogPageList?pageNum=${page_Number}&pageSize=${pageInfo.pageSize}">${page_Number}</a>
                                </li>
                            </c:if>
                        </c:forEach>

                        <c:if test="${pageInfo.hasNextPage}">
                            <li>
                                <a href="${APP_PATH}/blogPageList?pageNum=${pageInfo.pageNum+1}&pageSize=${pageInfo.pageSize}"
                                   aria-label="Next">
                                    <span aria-hidden="true">&raquo;</span>
                                </a>
                            </li>
                        </c:if>
                        <li>
                            <a href="${APP_PATH}/blogPageList?pageNum=${pageInfo.pages}&pageSize=${pageInfo.pageSize}">
                                Last 
                            </a>
                        </li>
                    </ul>
                </nav>

            </div>

        </div>
    </div>
</div>
</body>
</html>

2.5 service startup and test

Access address: http://localhost:8080/blogPageList?pageNum=6&pageSize=2

Access address: http://localhost:8080/blogPageList?pageNum=1&pageSize=5

2.6 summary

  • 1. Introducing plug-in dependencies
  • 2,mybatis-config. Add plug-in configuration in XML
  • 3. Use the plug-in PageHelper in the business layer code Start (pagenum, PageSize) to page, and then PageInfo to page the queried list
  • 4. Run test

3, MyBatis plug-in configuration source code analysis

Q: in the use of plug-ins, we will use MyBatis config XML, how is the plug-in configuration parsed by MyBatis?

A: in fact, when MyBatis is started, our global Configuration file will parse the < plugins > < / plugins > tag, and then register it in the InterceptorChain in the Configuration object, and the parameters in Properties will call setProperties() to set. The source code is as follows:

  private void parseConfiguration(XNode root) {
    try {
      ...
      pluginElement(root.evalNode("plugins"));
      ...
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }
  ||
  ||
  \/
  private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        //The corresponding < plugin interceptor = "com. GitHub. PageHelper. Pageinterceptor" > tag takes the plug-in interceptor in her attribute
        String interceptor = child.getStringAttribute("interceptor");
        Properties properties = child.getChildrenAsProperties();
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
        interceptorInstance.setProperties(properties);
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }
  ||
  ||
  \/
   public void addInterceptor(Interceptor interceptor) {
    interceptorChain.addInterceptor(interceptor);
  }

Finally, we found that all plug-in interceptors are stored in InterceptorChain in the Configuration object Let's see what InterceptorChain is again?

protected final InterceptorChain interceptorChain = new InterceptorChain();

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

}

In fact, it is the collection List of plug-ins, that is, in the startup phase of MyBatis, N plug-ins have been saved in the List object of Configuration and will be used later.

4, MyBatis plug-in usage conjecture

We all know that the paging plug-in of MyBatis can change the behavior of the core object without modifying the code in the original jar package, such as processing parameters in the front, processing SQL in the middle, and processing the result set in the end. So how is it implemented? We can have the following conjecture:

4.1 how to enhance the function without modifying the code?

After reading this question, it is easy to think of a design pattern - proxy pattern, which can enhance the functions before and after without modifying the original code. Therefore, it is obvious that MyBatis plug-in uses the proxy pattern

4.2 how to intercept multiple plug-ins?

In fact, the answer to this question has been clearly given during the configuration analysis of plug-ins. The definition of InterceptorChain is interceptor chain, so we have defined many plug-ins, so these plug-ins form a plug-in link, and continue to execute the logic of the next plug-in after executing the logic of one plug-in, and so on.

So the guess is that multiple plug-ins are intercepted layer by layer. The design pattern - responsibility chain pattern is used here

4.3 in MyBatis, what objects can be intercepted?

In fact, this problem was also mentioned in the previous MyBatis configuration explanation. You can have a look Official website As follows:

MyBatis allows you to intercept calls at some point during the execution of mapping statements. By default, MyBatis allows plug-ins to intercept method calls, including:

Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
ParameterHandler (getParameterObject, setParameters)
ResultSetHandler (handleResultSets, handleOutputParameters)
StatementHandler (prepare, parameterize, batch, update, query)

Therefore, the above description is summarized as the following table:

objectdescribeInterceptable methodeffect
ExecutorThe upper layer object, the whole process of SQL execution, including assembling SQL parameters, executing SQL and assembling result set return
update()Perform insert, update and delete operations
query()Execute query operation
flushStatements()It is called automatically at the time of commit. Simpleexecution, reuseexecution and batchexecutor handle different tasks
commit()Commit transaction
rollBack()Transaction rollback
getTransaction()Get transaction
close()End close transaction
isClosed()Judge whether the transaction is closed
StatementHandlerThe process of executing SQL, the most commonly used interception object
prepare()SQL precompile phase
parametrize()Set parameters
batch()Batch operation
update()Addition, deletion and modification
query()Query operation
ParameterHandlerSQL parameter assembly process
getParameterObject()Get parameters
setParameters()Set parameters
ResultSetHandlerAssembly of result sets
handleResultSets()Processing result set
handleOutputParameters()Processing stored procedure output parameters

4.4 when the proxy object of the plug-in is decorated by the cache, is it decorated first and then proxy or proxy and then decoration?

The main proxy object involved in this problem is the Executor, because it may be decorated by the L2 cache. We can find the defaultsqlsession during SQL execution Opensessionfromdatasource() look at the source code as follows:

//This step starts to create the Executor, so we enter this method
final Executor executor = configuration.newExecutor(tx, execType);
||
||
\/
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);
    }
    //It can be seen from here that it decorates a simple basic actuator first, then judges whether there is a cache decorator, and if so, decorates the cache decorator first
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    //After the L2 cache is decorated, it enters the plug-in processing for proxy.
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

After reading the source code and my comments above, I believe students can see that it is to create the basic executor first, then the secondary cache executor, and finally the plug-in agent, so we intercept the caching executor So it is: decorate first and then act as an agent

5, Principle analysis of MyBatis plug-in

5.1 introduction

After reading the above analysis, conjecture and use of MyBatis plug-ins, we already know one thing: the core idea of MyBatis plug-ins is agent mode and responsibility chain mode

Since the agent mode is used, the following problems need to be solved:

  • 1. How to create a proxy class? How to create an agent? Is JDK dynamic proxy or CGLIB dynamic proxy used?
  • 2. When was the proxy class created? Is it during configuration parsing, session acquisition, or call?
  • 3. How is the core object called after being proxied? How about the execution process? How to execute the logic of multiple plug-ins at one time? How to execute the original logic after executing the logic of the plug-in?

After we understand the above three big problems and the small problems in each big problem, we have a very clear understanding of the principle of the plug-in. Let's analyze its principle one by one as follows:

5.2 when is the proxy class created?

In fact, this problem has been found when guessing. The source code in the openSqlSession() method of each request is defaultsqlsession Opensessionfromdatasource() will process the configuration as follows:·

final Executor executor = configuration.newExecutor(tx, execType);
==>
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);
    }
    //This section is the processing of our plug-in
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

You can find executor = (executor) interceptorchain pluginAll(executor); This section of the source code is the source code address of the proxy processing of all our plug-ins

Therefore, we come to the conclusion that the agent class of the Executor creates the agent when our SqlSessionFactory creates a session.

5.3 how are proxy classes created?

Let's continue to look at the source code:

  public Object pluginAll(Object target) {
  //Generate proxy classes for plug-ins in batch
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }
  
  ==> interceptor.plugin(target)The source code is as follows:
    default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }
  ==> 
  //Here is the process of proxy. Look at the source code. JDK dynamic proxy is used
    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;
  }

As you can see here, you call the pluginAll(Object target) method first and then call Plugin.. wrap(target, this);, This method is the specific process method of the agent. We also see the source code. In fact, it is
The trigger management plug-in class plugin is used, then the InvocationHandler interface is implemented, then the packaging is processed, and finally the proxy class is generated: proxy newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap))

Summary: use JDK dynamic proxy to generate proxy classes. Use Plugin class to implement InvocationHandler interface to create proxy classes.

5.4 how to execute the call after the proxy class is generated?

From the above analysis process, we know that the Plugin class implements InvocationHandler, so it will enter the invoke() of this class when calling, so let's look at the source code as follows:

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

You can see that a judgment is made here: if (methods! = null & & methods. Contains (method)) only executes return interceptor when the intercepted method is not empty intercept(new Invocation(target, method, args));, This is the logic implementation of the plug-in.

We have noticed here that the plug-in logic still passes in the intercepted information such as the intercepted method and the parameters of the intercepted method. In fact, this is to complete the execution of multiple plug-ins. Then execute method invoke(target, args); This is the logical execution code.

Summary: after the proxy class is generated, the invoke() of Plugin is continuously called to intercept and execute multiple plug-ins. After the plug-ins are executed, the original logical methods are executed.

5.5 summary of plug-in principle

The execution flow chart of the plug-in is as follows:

[the external link image transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-xcNEK1L2-1628576720489)(image/plugin-executor.png)]

5.6 execution sequence of plug-ins

First, we configure plug-ins in a certain order, which is arranged according to labels, but the execution of plug-ins is executed in the reverse order of your configuration. This needs attention.

Because the plug-in is configured, when it parses the plug-in, the interceptorChain is added in the order of the plug-in from top to bottom, and the proxy class is created in the order of this list. Therefore, the proxy class must be called first by the last entered proxy class.

5.7 summary of core objects used by plug-ins

Core objecteffect
InterceptorThe plug-in library to be implemented by the user-defined plug-in implements four methods
InterceptorChainThe configured plug-in Configuration will be saved in interceptorChain of Configuration after it is resolved
PluginTrigger management classes can also be used to create proxy classes
InvocationWrapping the proxy class, you can call the processed () method to call the intercepted method

6, Principle analysis of paging plug-in

6.1 introduction

We already know that we first configured the PageInterceptor plug-in, and then used PageHelper in the method Start (pagenum, PageSize) automatically turns pages. MyBatis realizes paging without processing anything.

6.2 how to intercept page turning?

Then PageHelper How does start (pagenum, PageSize) intercept page turning?

A: first, let's look at the source code of the interceptor PageInterceptor as follows:

@Override
    public Object intercept(Invocation invocation) throws Throwable {
        try {
            Object[] args = invocation.getArgs();
            MappedStatement ms = (MappedStatement) args[0];
            Object parameter = args[1];
            RowBounds rowBounds = (RowBounds) args[2];
            ResultHandler resultHandler = (ResultHandler) args[3];
            Executor executor = (Executor) invocation.getTarget();
            CacheKey cacheKey;
            BoundSql boundSql;
            //Due to the logical relationship, it will only enter once
            if (args.length == 4) {
                //4 parameters
                boundSql = ms.getBoundSql(parameter);
                cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
            } else {
                //6 parameters
                cacheKey = (CacheKey) args[4];
                boundSql = (BoundSql) args[5];
            }
            checkDialectExists();
            //Interception of boundSql
            if (dialect instanceof BoundSqlInterceptor.Chain) {
                boundSql = ((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.ORIGINAL, boundSql, cacheKey);
            }
            List resultList;
            //Call the method to determine whether paging is required. If not, directly return the result
            if (!dialect.skip(ms, parameter, rowBounds)) {
                //Judge whether count query is required
                if (dialect.beforeCount(ms, parameter, rowBounds)) {
                    //Total queries
                    Long count = count(executor, ms, parameter, rowBounds, null, boundSql);
                    //The total number of queries processed. When true is returned, paging queries will continue, and when false, paging queries will be returned directly
                    if (!dialect.afterCount(count, parameter, rowBounds)) {
                        //When the total number of queries is 0, an empty result is returned directly
                        return dialect.afterPage(new ArrayList(), parameter, rowBounds);
                    }
                }
                resultList = ExecutorUtil.pageQuery(dialect, executor,
                        ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
            } else {
                //rowBounds uses the parameter value. When it is not processed by the paging plug-in, it still supports the default memory paging
                resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
            }
            return dialect.afterPage(resultList, parameter, rowBounds);
        } finally {
            if(dialect != null){
                dialect.afterAll();
            }
        }
    }

Look at the Interceptor() method. Here is the logic of the intercepted plug-in. Let's take a closer look at the logic. It first processes the logical paging, and then judges whether physical paging is required. If necessary, it will first query the number of data. Long count = count(executor, ms, parameter, rowBounds, null, boundSql);

Then change the query statement and add the limit statement for paging processing. The method source code is as follows:

resultList = ExecutorUtil.pageQuery(dialect, executor,
                        ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
==>
//Determine whether paging query is required
        if (dialect.beforePage(ms, parameter, rowBounds)) {
            //Generate paging cache key
            CacheKey pageKey = cacheKey;
            //Processing parameter objects
            parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
            //Call the dialect to get the core method of paging sql 
            String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
            BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);

            Map<String, Object> additionalParameters = getAdditionalParameter(boundSql);
            //Set dynamic parameters
            for (String key : additionalParameters.keySet()) {
                pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
            }
            //Interception of boundSql
            if (dialect instanceof BoundSqlInterceptor.Chain) {
                pageBoundSql = ((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.PAGE_SQL, pageBoundSql, pageKey);
            }
            //Execute paging query
            return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
        } else {
            //If paging is not performed, memory paging is not performed
            return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
        }
==> 
/**
     * Generate paging query sql
     *
     * @param ms              MappedStatement
     * @param boundSql        Bind SQL object
     * @param parameterObject Method parameters
     * @param rowBounds       Paging parameters
     * @param pageKey         Paging cache key
     * @return
     */
    String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey);
==> There will be many implementations here, mainly based on different plug-ins. Let's take a look PageHelper of
    @Override
    public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
        String sql = boundSql.getSql();
        Page page = getLocalPage();
        //Support order by
        String orderBy = page.getOrderBy();
        if (StringUtil.isNotEmpty(orderBy)) {
            pageKey.update(orderBy);
            sql = OrderByParser.converToOrderBySql(sql, orderBy);
        }
        if (page.isOrderByOnly()) {
            return sql;
        }
        return getPageSql(sql, page, pageKey);
    }
 ==> Continue to enter getPageSql(sql, page, pageKey) method 
     public abstract String getPageSql(String sql, Page page, CacheKey pageKey);
//This is an abstract method hook. There are many implementation classes here. We choose Mysql  
    @Override
    public String getPageSql(String sql, Page page, CacheKey pageKey) {
        StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
        sqlBuilder.append(sql);
        if (page.getStartRow() == 0) {
            sqlBuilder.append("\n LIMIT ? ");
        } else {
            sqlBuilder.append("\n LIMIT ?, ? ");
        }
        return sqlBuilder.toString();
    }
    //It is obvious here that the limit statement is added at the end of the statement 

Therefore, through our layer by layer analysis and source code tracking, we know that the essence of physical paging is to change SQL statements and add limit paging

6.3 how to save paging information?

This question goes back to PageHelper Start() method. We found that there is no such method in PageHelper, so we found its parent class. We found that there is this method in the parent class, so we tracked the source code:

PageMethod.java

    /**
     * Start paging
     *
     * @param pageNum  Page number
     * @param pageSize Display quantity per page
     */
    public static <E> Page<E> startPage(int pageNum, int pageSize) {
        return startPage(pageNum, pageSize, DEFAULT_COUNT);
    }
    ==>
    /**
     * Start paging
     *
     * @param pageNum  Page number
     * @param pageSize Display quantity per page
     * @param count    count query
     */
    public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count) {
        return startPage(pageNum, pageSize, count, null, null);
    }
    ==>
    /**
     * Start paging
     *
     * @param pageNum      Page number
     * @param pageSize     Display quantity per page
     * @param count        count query
     * @param reasonable   Paging rationalization, default configuration when null
     * @param pageSizeZero true When pageSize=0, all results are returned. When false, paging is performed. When null, the default configuration is used
     */
    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;
    }
    ==>
        protected static void setLocalPage(Page page) {
        LOCAL_PAGE.set(page);
    }
   

Finally, we found that we stored pageNum and pageSize into the page object, and then there was LOCAL_PAGE, then local_ What is page?

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

After looking at the source code, it is found that it is a local thread variable, and the PageHelper is directly used when obtaining it Getlocalpage () gets it. This is the storage of thread variables. Therefore, each query has a private thread variable, which stores paging information.

6.4 summary

In this way, we realize the effect of physical paging through PageInterceptor and PageHelper tool classes.

7, Customize a MyBatis plug-in

Now, in order to simulate a scenario, we need to print SQL.

7.1 step 1: create a new Interceptor to implement the Interceptor interface

package com.gitee.qianpz.interceptor;

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.TypeHandlerRegistry;

import java.text.DateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Properties;


/**
 * Plug in for printing SQL
 *
 * @author pengzhan.qian
 * @since 1.0.0
 */
//Define the intercepted object. Because it is print SQL, you can intercept the addition, deletion, modification and query operations
@Intercepts({
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
        @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})}
)
@Slf4j
public class MyBatisPrintSqlInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        Object parameter = null;
        if (invocation.getArgs().length > 1) {
            parameter = invocation.getArgs()[1];
        }
        String sqlId = mappedStatement.getId();
        BoundSql boundSql = mappedStatement.getBoundSql(parameter);
        Configuration configuration = mappedStatement.getConfiguration();

        long startTime = System.currentTimeMillis();
        Object result = null;
        try {
            result = invocation.proceed();
        } finally {
            try {
                long sqlCostTime = System.currentTimeMillis() - startTime;
                String sql = getSql(configuration, boundSql, sqlId, sqlCostTime, result);
                System.out.println("custom SQL: " + sql);
            } catch (Exception ignored) {

            }
        }
        return result;
    }

    public static String getSql(Configuration configuration, BoundSql boundSql, String sqlId, long time, Object returnValue) {
        String sql = showSql(configuration, boundSql);
        StringBuilder str = new StringBuilder(100);
        str.append(sqlId);
        str.append("-->[");
        str.append(sql);
        str.append("]-->");
        str.append(time);
        str.append("ms-->");
        str.append("returnValue=").append(returnValue);
        return str.toString();
    }

    private static String getParameterValue(Object obj) {
        String value = null;
        if (obj instanceof String) {
            value = "'" + obj.toString() + "'";
        } else if (obj instanceof Date) {
            DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
            value = "'" + formatter.format(new Date()) + "'";
        } else {
            if (obj != null) {
                value = obj.toString();
            } else {
                value = "";
            }

        }
        return value;
    }

    public static String showSql(Configuration configuration, BoundSql boundSql) {
        Object parameterObject = boundSql.getParameterObject();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
        if (parameterMappings.size() > 0 && parameterObject != null) {
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                sql = sql.replaceFirst("\\?", getParameterValue(parameterObject));

            } else {
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                for (ParameterMapping parameterMapping : parameterMappings) {
                    String propertyName = parameterMapping.getProperty();
                    if (metaObject.hasGetter(propertyName)) {
                        Object obj = metaObject.getValue(propertyName);
                        sql = sql.replaceFirst("\\?", getParameterValue(obj));
                    } else if (boundSql.hasAdditionalParameter(propertyName)) {
                        Object obj = boundSql.getAdditionalParameter(propertyName);
                        sql = sql.replaceFirst("\\?", getParameterValue(obj));
                    }
                }
            }
        }
        return sql;
    }

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

    @Override
    public void setProperties(Properties properties) {
    }
}

7.2 step 2: configure interceptors

        <!--  custom SQL Print plug-in -->
        <plugin interceptor="com.gitee.qianpz.interceptor.MyBatisPrintSqlInterceptor"/>

Because I don't need to configure other properties here, I don't need to configure properties

7.3 testing

http://localhost:8080/blogPageList?pageNum=1&pageSize=5

give the result as follows:
custom SQL: com.gitee.qianpz.mapper.BlogMapper.findAll-->[select * from blog]-->1ms-->returnValue=[Blog{bid=1, name='Blog 01-modify', authorId=1}, Blog{bid=2, name='batch_insert_2', authorId=1}, Blog{bid=3, name='batch_insert_3', authorId=1}, Blog{bid=4, name='batch_insert_4', authorId=1}, Blog{bid=5, name='batch_insert_5', authorId=1}]

8, Application scenario analysis of MyBatis plug-in

effectdescribeImplementation mode
Data desensitizationMobile phone ID number and ID number encryption method is stored in the database, but when you take it out, you need to decrypt and hide the middle number of the mobile phone number, the middle date of birth date of the ID card to show when you are taken out, and you can decrypt it directly when you look at it, if you read it, you can see it in four places.Intercepting the query method of the Executor
Process the ResultHandler and return.
Menu permission controlWhen different users log in, they can view the permission table to obtain different results, and display different menus on the front endIntercepting the query method of the Executor
Add comments on the method, and add permission filtering conditions on SQL according to permission configuration and user login information
Horizontal sub tableIf the data of a single table is too large, split the single table into N tables, and the table name is suffixed with 0 ~ N-1Intercept query and update
Modify the table name in the SQL statement

9, Source address

Plug in source code

Keywords: Java Mybatis Spring Spring MVC plugin

Added by TitanKing on Sat, 25 Dec 2021 10:18:40 +0200