“This is the 28th day of my participation in the Gwen Challenge in November. Check out the details: The Last Gwen Challenge in 2021.”

MyBatis is a persistent layer framework that Java developers must master. It can make it easier for us to operate databases through Java code, and it has high scalability. We can customize plug-ins to make the function of MyBatis more powerful. Let’s talk about how to develop MyBatis plug-in.

preface

If you are not familiar with MyBatis source code, you can read my article, specifically on MyBatis source code read juejin.cn/post/701763…

If you want to know how to integrate MyBatis into the actual project, please refer to my open source project gitee.com/zhuhuijie/b…

The plugin section is located under Base-platform /base-common/common-db-mysql

Click a star if you are interested in it.

MyBatis four built-in objects

  • Executor The object that the Executor actually uses to execute SQL

  • The StatementHandler database session processor that compiles/processes SQL statements

    • PreparedStatementHanler Creates the most commonly used placeholder for a PreparedStatement
    • CallableStatementHandler creates a CallableStatement to execute the stored procedure
    • SimpleStatementHanler creates Statement string concatenation with SQL injection risk
  • ParameterHandler specifies the ParameterHandler

    public interface ParameterHandler {
        Object getParameterObject();
        void setParameter(PreparedStatement ps)
    }
    Copy the code
  • ResultSetHandler processes the result set

    public interface ResultSetHandler {
        <E> list<E> handlerResultSets(Statement stmt) throws SQLException;
        <E> Cursor<E> handlerCursorResultSets(Statement stmt) throws SQLException;
        void handlerOutputParameters(CallableStatement cs) throws SQLException;
    }
    Copy the code

MyBatis executes SQL procedure

  • Obtain the SQLSession object based on the configuration

  • Obtain the proxy object of Mapper through dynamic proxy

    StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
    Copy the code
  • Invoke concrete SQL through proxy objects

    Student student = mapper.getStudentById(id);
    Copy the code
    • This method is called by reflection

    • mapperMethod.execute(sqlSession, args);

      • INSERT sqlSession.insert()

      • UPDATE sqlSession.update()

      • DELETE sqlSession.delete()

      • SELECT sqlSession.select()

        • selectList

          The executor.query() call to CachingExecutor [Decorator mode] actually uses the SimpleExecutor– > baseexcutor.query () –> doQuery() abstraction –> simpleExecutor.doquery ()

          • The Handler object is initialized

            • Create a delegate that creates different objects based on different StatementTypes. New PreparedStatementHanler()

              • JDBC Statement STMT = preparedStatementHanler. InstantiateStatement () – > connection. The preparedStatement ()

              • Handler.parameterize (STMT) parameter processing

                • ParameterHandler
          • ResultSetHandler. HandlerResultSets (preparedStatement) encapsulates the results

        • .

    • results

How to develop MyBatis plug-in

The MyBatis plug-in is essentially an enhancement of the four built-in objects of MyBatis.

It is an interceptor based on MyBatis and is used in an AOP way.

Example 1: Print the SQL plug-in:

  • Creating interceptors

    Note that interceptors are implemented under the IBATIS package. The annotations above determine where our interceptors are cut from in MyBatis and then extended via AOP.

    package com.zhj.common.db.mysql.plugins; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.plugin.Intercepts; import org.apache.ibatis.plugin.Invocation; import org.apache.ibatis.plugin.Signature; import org.apache.ibatis.session.ResultHandler; import java.math.BigDecimal; import java.math.RoundingMode; import java.sql.Statement; /** * print SQL statements * 1. Record the time of execution * type The type of the enhanced built-in object (it must be one of the four built-in objects statementhandler.class (most enhanced)) * method The enhanced method name * args{} is a list of parameters to prevent method overloading, * @author ZHJ */ @slf4j @intercepts ({@signature (type = StatementHandler. Class, method = "query", args = {Statement.class, ResultHandler.class} ), @Signature( type = StatementHandler.class, method = "update", Args = {Statement. Class})}) public class PrintSQLPlugins implements Interceptor {/** * intercepts * @Param Invocation * @return * @throws Throwable */ @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler= (StatementHandler) invocation.getTarget(); BoundSql boundSql = statementHandler.getBoundSql(); String sql = boundSql.getSql(); The info (" -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- [SQL] -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - "); log.info(sql.replace("\n","")); long beginTime = System.currentTimeMillis(); Object proceed = invocation.proceed(); Long endTime = System.currentTimemillis (); Log.info ("---------------------------- {} s】", Bigdecimal.valueof (endTime - beginTime).divide(bigdecimal.valueof (1000)).setScale(6, RoundingMode.DOWN).doubleValue()); return proceed; }}Copy the code
  • Make the plug-in work

    package com.zhj.common.db.mysql.config; import com.zhj.common.db.mysql.plugins.PrintSQLPlugins; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.transaction.annotation.EnableTransactionManagement; /** * @author zhj */ @Configuration @MapperScan("com.zhj.data.mapper") @EnableTransactionManagement public class DBAutoConfiguration { @Bean @ConditionalOnProperty(value = "zhj.plugins.printSql.enable", havingValue = "true", matchIfMissing = false) public PrintSQLPlugins getPrintSQLPlugins(){ return new PrintSQLPlugins(); }}Copy the code
  • The configuration determines whether to enable the plug-in

    @ConditionalOnProperty(value = “zhj.plugins.printSql.enable”, havingValue = “true”, matchIfMissing = false)

  • Import dependencies and create beans that the plug-in can prompt automatically at configuration time

    package com.zhj.common.db.mysql.entity;
    ​
    import lombok.Data;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.stereotype.Component;
    ​
    /**
     * @author zhj
     */
    @Component
    @ConfigurationProperties(prefix = "zhj.plugins.printSql")
    @Data
    public class ZhjConfigInfo {
        private Boolean enable;
    }
    Copy the code

    Rely on:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>
    Copy the code
  • Enable plug-in in configuration file:

    zhj:
      plugins:
        printSql:
          enable: true
    Copy the code

Case 2 paging plug-in:

Basic paging plug-in implementation:

  • Creating a paging object

    package com.zhj.common.db.mysql.page; import lombok.Data; import lombok.experimental.Accessors; import java.io.Serializable; @author ZHJ */ @data @accessors (chain = true) public class Page implements Serializable {/** * current Page */ private Integer pageNo; /** ** / private Integer pageSize; /** ** private Integer pageTotal; /** * total number of entries */ private Integer pageCount; }Copy the code
  • Creating the paging tool

    Here we set the paging object using ThreadLocal

    package com.zhj.common.db.mysql.page; Public class PageUtils {private static ThreadLocal<Page> pageThreadLocal = new ThreadLocal<>(); @param pageNo @param pageSize public static void setPage(Integer pageNo, Integer pageSize){ pageThreadLocal.set(new Page().setPageNo(pageNo).setPageSize(pageSize)); } @return */ public static Page getPage(){return pagethreadLocal.get (); } /** * public static void clear(){pagethReadLocal.remove (); }}Copy the code
  • Create an interceptor that implements the paging plug-in

    package com.zhj.common.db.mysql.plugins; import com.zhj.common.db.mysql.page.Page; import com.zhj.common.db.mysql.page.PageUtils; import com.zhj.common.db.mysql.util.MybatisUtils; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.plugin.Intercepts; import org.apache.ibatis.plugin.Invocation; import org.apache.ibatis.plugin.Signature; import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.reflection.SystemMetaObject; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; /** ** ** * @author ZHJ */ @slf4j @intercepts ({@signature (type = StatementHandler. Class, method = "prepare", args = {Connection.class, Integ.class} // The corresponding version needs to be consistent)}) public class PagePlugins implements Interceptor {@override public Object Throws Throwable {// Get the non-proxy object StatementHandler target = MybatisUtils.getNoProxyTarget(invocation.getTarget()); BoundSql boundSql = target.getBoundSql(); String SQL = boundSQL.getsQL ().tolowerCase ().trim(); // Determine whether paging needs to be added if (! sql.startsWith("select")) { return invocation.proceed(); } // getPage parameters Page = pageutils.getpage (); if (page == null) { return invocation.proceed(); } // Handle paging log.info("[SQL that requires paging: {}", sql.replace("\n","")); // Build a SQL query on the total number of pages; Integer count = count(Target, Invocation, SQL); log.info(" "+ count); / / processing pageNo if (page. GetPageNo () = = null | | page. GetPageNo () < 1) page. SetPageNo (1); / / processing pageSize if (page. GetPageSize () = = null | | page. GetPageSize () < 1) page. SetPageSize (10); // Set page.setPagecount (count);  page.setPageTotal(page.getPageCount() % page.getPageSize() == 0 ? page.getPageCount()/ page.getPageSize() : page.getPageCount()/ page.getPageSize() + 1); If (page.getPageTotal() > page.getPageTotal()) page.setPageno (page.getPageTotal()); log.info(" " + page); sql += " limit " + (page.getPageNo() * page.getPageSize() - 1) + "," + page.getPageSize(); Log.info ("[paged SQL: {}", sql.replace("\n","")); / / by reflecting set BoundSql SQL / / MyBatis provides tools, the tool by reflection MetaObject MetaObject = SystemMetaObject. ForObject (BoundSql);  metaObject.setValue("sql", sql); return invocation.proceed(); @param SQL * @return */ private Integer count(StatementHandler StatementHandler, String SQL) throws SQLException {int orderByIndex = -1;  if (sql.lastIndexOf("order by") ! SQL int fromIndex = sql.indexof ("from"); String countSQL = "select count(*) "+ sql.substring(fromIndex); log.info(" "+ countSQL); // Run the SQL // Get the invocation Connection Connection = (Connection) Invocation. GetArgs ()[0]; PreparedStatement ps = null; the ResultSet ResultSet = null; try {/ / SQL processor ps = connection. PrepareStatement (countSQL); / / processing parameters statementHandler parameterize (ps); / / execute SQL resultSet = ps. The executeQuery (); If (resultSet.first()) {return resultSet.getint (1); }} catch (SQLException SQLException) {log.info(" ]"); throw sqlException; } finally { if (resultSet != null) { resultSet.close(); } if (ps ! = null) { ps.close(); } } return -1; } }Copy the code
  • Because the proxy mode is used to enhance the four built-in objects of MyBatis, it will interfere when creating multiple paging plug-ins. Sometimes the target object we get is not the real target object, but the proxy object formed by other plug-ins. We need to write a tool class to get the real target object.

    package com.zhj.common.db.mysql.util; import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.reflection.SystemMetaObject; /** * @author ZHJ */ public class MybatisUtils {@param target * @param <T> * @return */ public static <T> T getNoProxyTarget(Object target) { MetaObject invocationMetaObject = SystemMetaObject.forObject(target); While (invocationMetaObject. HasGetter (" h ")) {/ / that is a proxy object for target = invocationMetaObject. GetValue (" h.t arget "); invocationMetaObject = SystemMetaObject.forObject(target); } return (T) target; }}Copy the code
  • Inject the paging plug-in to make it work

    package com.zhj.common.db.mysql.config; import com.zhj.common.db.mysql.plugins.PagePlugins; import com.zhj.common.db.mysql.plugins.PrintSQLPlugins; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.transaction.annotation.EnableTransactionManagement; /** * @author zhj */ @Configuration @MapperScan("com.zhj.data.mapper") @EnableTransactionManagement public class DBAutoConfiguration { @Bean @ConditionalOnProperty(value = "zhj.plugins.printSql.enable", havingValue = "true", matchIfMissing = false) public PrintSQLPlugins getPrintSQLPlugins(){ return new PrintSQLPlugins(); } @Bean public PagePlugins getPagePlugins(){ return new PagePlugins(); }}Copy the code
  • Enable paging in Controller (Service)

    package com.zhj.business.controller; import com.zhj.business.protocol.input.StudentInput; import com.zhj.business.service.StudentService; import com.zhj.common.core.result.Result; import com.zhj.common.core.util.ResultUtils; import com.zhj.common.db.mysql.page.PageUtils; import com.zhj.data.entity.example.Student; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; import java.util.List; /** * @author zhj */ @Slf4j @RestController @RequestMapping("/student") public class StudentController { @Autowired private StudentService studentService; @getmapping ("/list") public Result< list <Student>> list() {pageutils.setpage (1,2); List<Student> list = studentService.list(); return ResultUtils.createSuccess(list); }}Copy the code

Make the paging plug-in more elegant:

  • Remove the intrusive part, turn on paging through AOP, and return paging information

    package com.zhj.common.db.mysql.aop; import com.zhj.common.db.mysql.page.BasePageResult; import com.zhj.common.db.mysql.page.Page; import com.zhj.common.db.mysql.page.PageUtils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.util.StringUtils; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; @author ZHJ */ @aspect public class WebPageAOP { @Around("@within(org.springframework.web.bind.annotation.RestController) || @within(org.springframework.stereotype.Controller)") public Object pageAOP(ProceedingJoinPoint joinPoint) throws RequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = requestAttributes.getRequest(); String pageNo = request.getParameter("pageNo"); String pageSize = request.getParameter("pageSize"); if (! StringUtils.isEmpty(pageNo) && ! StringUtils.isEmpty(pageSize)) { PageUtils.setPage(Integer.parseInt(pageNo), Integer.parseInt(pageSize)); } Object proceed = null; try { proceed = joinPoint.proceed(); Page page = PageUtils.getPage(); if (proceed instanceof BasePageResult && page ! = null) { BasePageResult basePageResult = (BasePageResult) proceed; basePageResult.setPage(page); } } catch (Throwable e) { throw e; } finally { PageUtils.clear(); } return proceed; }}Copy the code
    package com.zhj.common.db.mysql.config; import com.zhj.common.db.mysql.aop.PageAOP; import com.zhj.common.db.mysql.aop.WebPageAOP; import com.zhj.common.db.mysql.plugins.PagePlugins; import com.zhj.common.db.mysql.plugins.PrintSQLPlugins; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.transaction.annotation.EnableTransactionManagement; /** * @author zhj */ @Configuration @MapperScan("com.zhj.data.mapper") @EnableTransactionManagement public class DBAutoConfiguration { @Bean @ConditionalOnProperty(value = "zhj.plugins.printSql.enable", havingValue = "true", matchIfMissing = false) public PrintSQLPlugins getPrintSQLPlugins(){ return new PrintSQLPlugins(); } @Bean public PagePlugins getPagePlugins(){ return new PagePlugins(); } @Bean public WebPageAOP getWebPageAOP(){ return new WebPageAOP(); }}Copy the code
  • Annotations control the granularity of paging to finer granularity

    • Create annotation

      package com.zhj.common.db.mysql.annotation; import java.lang.annotation.*; /** * @author zhj */ @Documented @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Page {}Copy the code
    • Page object added switch

      package com.zhj.common.db.mysql.page; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; import lombok.experimental.Accessors; import java.io.Serializable; @author ZHJ */ @data @accessors (chain = true) public class Page implements Serializable {/** * current Page */ private Integer pageNo; /** ** / private Integer pageSize; /** ** private Integer pageTotal; /** * total number of entries */ private Integer pageCount; /** * Whether paging is enabled */ @jsonignore private Boolean enable; }Copy the code
    • Add judgment criteria to the original page interceptor

      Page = pageutils.getPage (); if (page == null || ! page.isEnable()) { return invocation.proceed(); }Copy the code
    • Set the switch via AOP

      package com.zhj.common.db.mysql.aop; import com.zhj.common.db.mysql.page.Page; import com.zhj.common.db.mysql.page.PageUtils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; /** * @author zhj */ @Aspect public class PageAOP { @Around("@annotation(com.zhj.common.db.mysql.annotation.Page)") public Object pageAOP(ProceedingJoinPoint joinPoint) throws Throwable { Page page = PageUtils.getPage(); if (page ! = null) { page.setEnable(true); } try { return joinPoint.proceed(); } finally { if (page ! = null) { page.setEnable(false); }}}}Copy the code
      package com.zhj.common.db.mysql.config; import com.zhj.common.db.mysql.aop.PageAOP; import com.zhj.common.db.mysql.aop.WebPageAOP; import com.zhj.common.db.mysql.plugins.PagePlugins; import com.zhj.common.db.mysql.plugins.PrintSQLPlugins; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.transaction.annotation.EnableTransactionManagement; /** * @author zhj */ @Configuration @MapperScan("com.zhj.data.mapper") @EnableTransactionManagement public class DBAutoConfiguration { @Bean @ConditionalOnProperty(value = "zhj.plugins.printSql.enable", havingValue = "true", matchIfMissing = false) public PrintSQLPlugins getPrintSQLPlugins(){ return new PrintSQLPlugins(); } @Bean public PagePlugins getPagePlugins(){ return new PagePlugins(); } @Bean public WebPageAOP getWebPageAOP(){ return new WebPageAOP(); } @Bean public PageAOP getPageAOP(){ return new PageAOP(); }}Copy the code
    • Turn on paging on the corresponding service or DAO

      package com.zhj.business.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.zhj.business.service.StudentService; import com.zhj.common.db.mysql.annotation.Page; import com.zhj.data.entity.example.Student; import com.zhj.data.mapper.example.dao.StudentDao; import org.springframework.stereotype.Service; import java.util.List; /** * @author zhj */ @Service public class StudentServiceImpl extends ServiceImpl<StudentDao, Student> implements StudentService { @Page @Override public List<Student> list() { return super.list(); }}Copy the code

MyBatis plug-in development summary

Want to carry on the extension to the framework, must first understand the framework source code, only to have a more in-depth understanding of the source code, we can better grasp from which point to cut into the expansion. In this paper, the two cases are the simplest implementation, to be honest, there are many loopholes, for example, the first plug-in to print SQL, we did not fill in the parameters, did not get the parameters, the second case paging, can only meet some relatively simple scenes, if the SQL is too complex, it is likely to appear bugs. These things require us to continue to learn source code, continue to learn open source projects, the more we accumulate, the more perfect we write tools. We can refer to GitHub MyBatis paging open source project, to write their own paging plug-in for continuous improvement, of course, we can also communicate in the comment area, common learning.