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">«</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">»</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:
object | describe | Interceptable method | effect |
---|---|---|---|
Executor | The 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 | ||
StatementHandler | The 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 | ||
ParameterHandler | SQL parameter assembly process | ||
getParameterObject() | Get parameters | ||
setParameters() | Set parameters | ||
ResultSetHandler | Assembly 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 object | effect |
---|---|
Interceptor | The plug-in library to be implemented by the user-defined plug-in implements four methods |
InterceptorChain | The configured plug-in Configuration will be saved in interceptorChain of Configuration after it is resolved |
Plugin | Trigger management classes can also be used to create proxy classes |
Invocation | Wrapping 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
effect | describe | Implementation mode |
---|---|---|
Data desensitization | Mobile 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 control | When different users log in, they can view the permission table to obtain different results, and display different menus on the front end | Intercepting 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 table | If 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-1 | Intercept query and update Modify the table name in the SQL statement |