MyBatis插件的用法与源码逻辑及PageHelper相关源码(万字长文干货)

Posted 守夜人爱吃兔子

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MyBatis插件的用法与源码逻辑及PageHelper相关源码(万字长文干货)相关的知识,希望对你有一定的参考价值。

一 . 前言

今天主要带大家了解 MyBatis 插件的用法与主要的源码逻辑,和PageHelper 的相关源码,本文是一篇概括性文档 , 用于后续快速使用相关功能 , 整体难度较低。文章很长,干货很多,一时半会看不完的建议先一键三连

 

二 . 流程

2.1 基础用法

基础拦截器类:

@Intercepts(
        {@org.apache.ibatis.plugin.Signature(
                type = Executor.class, 
                method = "query", 
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class DefaultInterceptor implements Interceptor {

    private Logger logger = LoggerFactory.getLogger(this.getClass());


    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        logger.info("------> this is in intercept <-------");
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        logger.info("------> this is in plugin <-------");
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        logger.info("------> this is in setProperties <-------");
    }
}

 

基础配置类:

@Configuration
public class PluginsConfig {

    @Autowired
    private List<SqlSessionFactory> sqlSessionFactoryList;

    @PostConstruct
    public void addPageInterceptor() {
        DefaultInterceptor interceptor = new DefaultInterceptor();
        // 此处往 SqlSessionFactory 中添加
        for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
            sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
        }
    }
}

 

这里可以看到 , 拦截到的参数如下 :

 

2.2 功能详解

整个拦截过程有几个主要的组成部分 :

Interceptor 拦截器接口:


M- Interceptor#intercept(Invocation invocation) : 拦截方法
M- plugin : 调用 Plugin#wrap(Object target, Interceptor interceptor) 方法,执行代理对象的创建
M- setProperties : 从 properties 获取一些需要的属性值

// 这里能看到 , 其中强制实现的方法只有 intercept


public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

  default void setProperties(Properties properties) {
  }

}

 

InterceptorChain 拦截器链:

InterceptorChain 是一个拦截器链 , 用于执行相关的Interceptor 操作 , 其中有一个拦截器集合

// 主要的大纲如此所示
F- List<Interceptor> ArrayList
M- addInterceptor : 添加拦截器
    - 在 Configuration 的 #pluginElement(XNode parent) 方法中被调用
        - 创建 Interceptor 对象,并调用 Interceptor#setProperties(properties) 方法
        - 调用 Configuration#addInterceptor(interceptorInstance) 方法
        - 添加到 Configuration.interceptorChain 中
M- pluginAll : 应用所有拦截器到指定目标对象

 

注解@Signature 和 @Intercepts:

这是过程中主要涉及的2个注解 :

  • @Signature : 定义类型
  • @Intercepts : 定义拦截器 , Intercepts 中可以包含一个 Signature 数组
    • type : 拦截器处理的类
    • args : 方法参数 (重载的原因)
    • method : 拦截的方法

 

2.3 源码跟踪

Step 1 : 资源的加载:

资源的加载主要是对对应方法的代理逻辑 , 一个 plugin 操作 , 主要包含2个步骤 :

// 步骤一 : 声明拦截器对象
@Intercepts(
        {@org.apache.ibatis.plugin.Signature(
                type = Executor.class, 
                method = "query", 
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
                
                
//  步骤二 : sqlSessionFactory 中添加拦截器对象
sqlSessionFactory.getConfiguration().addInterceptor(interceptor);

来跟进一下相关的操作 :

C01- Configuration
    F01_01- InterceptorChain interceptorChain
    M01_01- addInterceptor
        - interceptorChain.addInterceptor(interceptor)
                ?- 可以看到 , 这里往 InterceptorChain 添加了 interceptor
                
// M01_01 源代码            
public void addInterceptor(Interceptor interceptor) {
    interceptorChain.addInterceptor(interceptor);
}



public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<Interceptor>();

  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }

  // 这里将拦截器加到了拦截器链中
  // PS : 但是此处未完全完成 , 仅仅只是添加 , 具体的操作会在上面 pluginAll 中完成
  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }
  
  public List<Interceptor> getInterceptors() {
    return Collections.unmodifiableList(interceptors);
  }

}

 

Step 2 : plugin 的构建:

Step 1 中已经完成了相关 interceptors 的添加 , 这个环节需要通过 Interceptor 构建对应的 Plugin

先来看一下调用链 :

  • C- SqlSessionTemplate # selectList : 发起 Select 请求
  • C- SqlSessionInterceptor # invoke : 构建一个 Session 代理
  • C- SqlSessionUtils # getSqlSession : 获取 Session 对象
  • C- DefaultSqlSessionFactory # openSessionFromDataSource
  • C- Configuration # newExecutor

整体来说 ,从 getSqlSession 开始关注即可


public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
    notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);

    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

    SqlSession session = sessionHolder(executorType, holder);
    if (session != null) {
      return session;
    }
    
    // 核心节点 , 开启 Session
    session = sessionFactory.openSession(executorType);

    registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

    return session;
  }

 

DefaultSqlSessionFactory 构建一个 Session , 同时调用 Plugin 生成逻辑:

此处构建 Session 的同时 , 最终调用 Plugin warp 构建 Plugin

C- DefaultSqlSessionFactory
// 打开的起点 : 打开 session 的时候 , 
  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      
      // 此处构建 Executor 并且放入 Session
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }


// 调用拦截链
C- Configuration
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    // 此处调用拦截链 , 构建 Executor 对象
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

// 拦截链处理
C- InterceptorChain
  public Object pluginAll(Object target) {
    // 此处是在构建拦截器链 , 返回的是最后的拦截器处理类  
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }


C- Plugin
// 调用 Plugin wrap , 生成了一个新的 plugin , 该 plugin 包含对应的 interceptor 的 拦截方法
  public static Object wrap(Object target, Interceptor interceptor) {
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
      // 核心 , 对方法做了代理 , 同时为代理类传入了 Plugin  
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

 

Step 3 : invoke 执行:

拦截器是基于 Proxy 代理实现的 , 在这里看一下代理的调用 :

以 Query 为例 :

当调用 Executor 中 Query 方法时,会默认调用代理类 , 那么他在整个逻辑中是作为什么角色的?

Executor 作用 : Executor 是 Mybatis 中的顶层接口 , 定义了主要的数据库操作方法

public interface Executor {

  ResultHandler NO_RESULT_HANDLER = null;

  int update(MappedStatement ms, Object parameter) throws SQLException;

  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;

  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

  <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;

  List<BatchResult> flushStatements() throws SQLException;

  void commit(boolean required) throws SQLException;

  void rollback(boolean required) throws SQLException;

  CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);

  boolean isCached(MappedStatement ms, CacheKey key);

  void clearLocalCache();

  void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);

  Transaction getTransaction();

  void close(boolean forceRollback);

  boolean isClosed();

  void setExecutorWrapper(Executor executor);

}

execute 的调用 :

之前构建 Session 的时候 , 已经为其定义了Plugin , 以下为 Plugin 的主要流程

// 1 . 选择合适的 plugin
C- DefaultSqlSession
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

// 2 . 中间通过 proxy 代理
    

// 3 . plugin 中调用拦截器
C05- Plugin
    F05_01- private final Interceptor interceptor;
    M05_01- invoke(Object proxy, Method method, Object[] args)
		- 获取可以拦截的 method
		- 判断当前的 method 是否在可拦截的
		
        
// M05_01 : invoke@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        Set<Method> methods = signatureMap.get(method.getDeclaringClass());
        if (methods != null && methods.contains(method)) {
            // 此处调用了拦截器
            return interceptor.intercept(new Invocation(target, method, args));
        }
        return method.invoke(target, args);
    } catch (Exception e) {
        throw ExceptionUtil.unwrapThrowable(e);
    }
}

// 4 . 调用 interceptor
public Object intercept(Invocation invocation) throws Throwable {
    // 此处调用代理方法的实际逻辑
    return invocation.proceed();
}

 

三 . 扩展 PageHelper

以下为 PageHelper 中如何使用相关的拦截器方法的 :

PageHelper 中主要有 2个拦截器 :

  • PageInterceptor : 分页操作插件
  • QueryInterceptor : 查询操作插件

我们这次主要看一下 PageInterceptor 拦截器 :

拦截器代码:

@Intercepts(
        {
                @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
                @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
        }
)
public class PageInterceptor implements Interceptor {
    private volatile Dialect dialect;
    private String countSuffix = "_COUNT";
    protected Cache<String, MappedStatement> msCountMap = null;
    private String default_dialect_class = "com.github.pagehelper.PageHelper";

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        try {
            Object[] args = invocation.getArgs();
            
            // 获取请求的四个参数
            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;
            //由于逻辑关系,只会进入一次
            if (args.length == 4) {
                //4 个参数时
                // 获取绑定 SQL : select id, type_code, type_class, type_policy, type_name, supplier_id, supplier_name from sync_type
                boundSql = ms.getBoundSql(parameter);
                cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
            } else {
                //6 个参数时
                cacheKey = (CacheKey) args[4];
                boundSql = (BoundSql) args[5];
            }
            checkDialectExists();

            List resultList;
            //调用方法判断是否需要进行分页,如果不需要,直接返回结果
            if (!dialect.skip(ms, parameter, rowBounds)) {
                //判断是否需要进行 count 查询
                if (dialect.beforeCount(ms, parameter, rowBounds)) {
                    //查询总数
                    Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                    //处理查询总数,返回 true 时继续分页查询,false 时直接返回
                    if (!dialect.afterCount(count, parameter, rowBounds)) {
                        //当查询总数为 0 时,直接返回空的结果
                        return dialect.afterPage(new ArrayList(), parameter, rowBounds);
                    }
                }
                resultList = ExecutorUtil.pageQuery(dialect, executor,
                        ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
            } else {
                //rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
                resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
            }
            return dialect.afterPage(resultList, parameter, rowBounds);
        } finally {
            dialect.afterAll();
        }
    }

    /**
     * Spring bean 方式配置时,如果没有配置属性就不会执行下面的 setProperties 方法,就不会初始化
     * <p>
     * 因此这里会出现 null 的情况 fixed #26
     */
    private void checkDialectExists() {
        if (dialect == null) {
            synchronized (default_dialect_class) {
                if (dialect == null) {
                    setProperties(new Properties());
                }
            }
        }
    }

    private Long count(Executor executor, MappedStatement ms, Object parameter,
                       RowBounds rowBounds, ResultHandler resultHandler,
                       BoundSql boundSql) throws SQLException {
        String countMsId = ms.getId() + countSuffix;
        Long count;
        //先判断是否存在手写的 count 查询
        MappedStatement countMs = ExecutorUtil.getExistedMappedStatement(ms.getConfiguration(), countMsId);
        if (countMs != null) {
            count = ExecutorUtil.executeManualCount(executor, countMs, parameter, boundSql, resultHandler);
        } else {
            countMs = msCountMap.get(countMsId);
            //自动创建
            if (countMs == null) {
                //根据当前的 ms 创建一个返回值为 Long 类型的 ms
                countMs = MSUtils.newCountMappedStatement(ms, countMsId);
                msCountMap.put(countMsId, countMs);
            }
            count = ExecutorUtil.executeAutoCount(dialect, executor, countMs, parameter, boundSql, rowBounds, resultHandler);
        }
        return count;
    }

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

    @Override
    public void setProperties(Properties properties) {
        //缓存 count ms
        msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties);
        String dialectClass = properties.getProperty("dialect");
        if (StringUtil.isEmpty(dialectClass)) {
            dialectClass = default_dialect_class;
        }
        try {
            Class<?> aClass = Class.forName(dialectClass);
            dialect = (Dialect) aClass.newInstance();
        } catch (Exception e) {
            throw new PageException(e);
        }
        dialect.setProperties(properties);

        String countSuffix = properties.getProperty("countSuffix");
        if (StringUtil.isNotEmpty(countSuffix)) {
            this.countSuffix = countSuffix;
        }
    }

}

补充 :ExecutorUtil.executeAutoCount 相关逻辑

/**
* 执行自动生成的 count 查询
**/
public static Long executeAutoCount(Dialect dialect, Executor executor, MappedStatement countMs,
                                        Object parameter, BoundSql boundSql,
                                        RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        Map<String, Object> additionalParameters = getAdditionalParameter(boundSql);
        //创建 count 查询的缓存 key
        CacheKey countKey = executor.createCacheKey(countMs, parameter, RowBounds.DEFAULT, boundSql);
        // 调用方言获取 count sql
        // SELECT count(0) FROM sync_type
        String countSql = dialect.getCountSql(countMs, boundSql, parameter, rowBounds, countKey);
        //countKey.update(countSql);
        BoundSql countBoundSql = new BoundSql(countMs.getConfiguration(), countSql, boundSql.getParameterMappings(), parameter);
        //当使用动态 SQL 时,可能会产生临时的参数,这些参数需要手动设置到新的 BoundSql 中
        for (String key : additionalParameters.keySet()) {
            countBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
        }
        //执行 count 查询
        Object countResultList = executor.query(countMs, parameter, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql);
        Long count = (Long) ((List) countResultList).get(0);
        return count;
}

补充 : pageQuery 查询

public static  <E> List<E> pageQuery(Dialect dialect, Executor executor, MappedStatement ms, Object parameter,
                                 RowBounds rowBounds, ResultHandler resultHandler,
                                 BoundSql boundSql, CacheKey cacheKey) throws SQLException {
        //判断是否需要进行分页查询
        if (dialect.beforePage(ms, parameter, rowBounds)) {
            //生成分页的缓存 key
            CacheKey pageKey = cacheKey;
            //处理参数对象 , 此处生成分页的参数
            parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
            //调用方言获取分页 sql
            String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
            BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);

            Map<String, Object> additionalParameters = getAdditionalParameter(boundSql);
            //设置动态参数
            for (String key : additionalParameters.keySet()) {
                pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
            }
            //执行分页查询
            return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
        } else {
            //不执行分页的情况下,也不执行内存分页
            return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
        }
}

因为源码的注释程度很高 , 所以基本上不需要做额外的标注了 , 整体的流程就是 :

  • Step 1 : 拦截器 intercept方法定义整体逻辑
  • Step 2 : count 方法决定他是否分页
  • Step 3 :pageQuery 调用方言进行事情SQL的拼接

整体中有几点值得关注 :

  • 只生成分页的 row 和 pageKey 等 , 在最终通过方言组合 , 以适应多种数据库结构
  • 核心还是调用 executor.query 原生方法

 

简述一下 PageHepler 的绑定流程:

核心处理类为 AbstractHelperDialect , 先从创建开始 :

PageHelper.startPage(page, size);
List<SyncType> allOrderPresentList = syncTypeDAO.findAll();

// Step 1 : startPage 核心代码
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);
        //当已经执行过orderBy的时候
        Page<E> oldPage = getLocalPage();
        if (oldPage != null && oldPage.isOrderByOnly()) {
            page.setOrderBy(oldPage.getOrderBy());
        }
        setLocalPage(page);
        return page;
}

此处最核心的就是 setLocalPage , 会使用 ThreadLocal 保持线程参数
protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();

// Step 2 : 拦截器中的参数获取
// 可以看到第一句就是从 getLocalPage - ThreadLocal 中获取
C- AbstractHelperDialect # processParameterObject

public Object processParameterObject(MappedStatement ms, Object parameterObject, BoundSql boundSql, CacheKey pageKey) {
        //处理参数
        Page page = getLocalPage();
        //如果只是 order by 就不必处理参数
        if (page.isOrderByOnly()) {
            return parameterObject;
        }
        Map<String, Object> paramMap = null;
        if (parameterObject == null) {
            paramMap = new HashMap<String, Object>();
        } else if (parameterObject instanceof Map) {
            //解决不可变Map的情况
            paramMap = new HashMap<String, Object>();
            paramMap.putAll((Map) parameterObject);
        } else {
            paramMap = new HashMap<String, Object>();
            //动态sql时的判断条件不会出现在ParameterMapping中,但是必须有,所以这里需要收集所有的getter属性
            //TypeHandlerRegistry可以直接处理的会作为一个直接使用的对象进行处理
            boolean hasTypeHandler = ms.getConfiguration().getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass());
            MetaObject metaObject = MetaObjectUtil.forObject(parameterObject);
            //需要针对注解形式的MyProviderSqlSource保存原值
            if (!hasTypeHandler) {
                for (String name : metaObject.getGetterNames()) {
                    paramMap.put(name, metaObject.getValue(name));
                }
            }
            //下面这段方法,主要解决一个常见类型的参数时的问题
            if (boundSql.getParameterMappings() != null && boundSql.getParameterMappings().size() > 0) {
                for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
                    String name = parameterMapping.getProperty();
                    if (!name.equals(PAGEPARAMETER_FIRST)
                            && !name.equals(PAGEPARAMETER_SECOND)
                            && paramMap.get(name) == null) {
                        if (hasTypeHandler
                                || parameterMapping.getJavaType().equals(parameterObject.getClass())) {
                            paramMap.put(name, parameterObject);
                            break;
                        }
                    }
                }
            }
        }
        return processPageParameter(ms, paramMap, page, boundSql, pageKey);
}

 

总结

拦截器的使用 :

  • 准备拦截器类
  • sqlSessionFactory.getConfiguration().addInterceptor(interceptor) 添加拦截器

PageHelper 核心 :

  • parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey) : 获取分页的参数
  • dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey) : 参数解析为 SQL
  • ThreadLocal 保存参数

 

这应该是写的最简单的一篇源码分析了 , 除了本身结构不复杂以外 , 相关源码的注释也很清晰 , 基本上没有什么分析的需求。如果觉得文章对你有帮助,可以点下一键三连,后续我会持续更新更多java相关干货文章。祝大家万事胜意!

 

以上是关于MyBatis插件的用法与源码逻辑及PageHelper相关源码(万字长文干货)的主要内容,如果未能解决你的问题,请参考以下文章

Mybatis分页插件测试项目

Mybatis分页插件PageHelper简单使用

MyBatis 源码分析 - 插件机制

MyBatis 源码分析 - 插件机制

Mybatis拦截器介绍及分页插件

MyBatisPlus 分页插件的用法和基于行锁的分布式锁方案分析