Инвентарь MyBatis: использование пользовательских плагинов и PageHelper

Java
Инвентарь MyBatis: использование пользовательских плагинов и PageHelper

Общая документация:Каталог статей
Github : github.com/black-ant

Введение

Цель статьи:

  • Узнайте, как использовать плагин MyBatis
  • Понимать основную логику исходного кода
  • Понимать соответствующий исходный код PageHelper

Это общий документ, который используется для последующего быстрого использования связанных функций, и общая сложность невелика.

2. Процесс

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


Здесь вы можете видеть, что перехваченные параметры выглядят следующим образом:

mybatis_plugin001.jpg

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 : 应用所有拦截器到指定目标对象

аннотация@Interceptsа также@Signature

Вот 2 аннотации, в основном задействованные в процессе:

  • @Intercepts: определить перехватчики, Intercepts может содержать массив Signature.
  • @Signature : определяет тип
    • type : класс, который обрабатывает перехватчик
    • метод: перехваченный метод
    • args : аргументы метода (причина перегрузки)

2.3 Отслеживание источника

Шаг 1: Загрузка ресурсов

Загрузка ресурсов в основном является прокси-логикой для соответствующего метода.Как мы видим из предыдущей статьи, операция плагина,В основном состоит из 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);
  }

}

Шаг 2: Создание плагина

Добавление соответствующих перехватчиков было завершено на шаге 1,Эта ссылка необходима для сборки соответствующего плагина через Interceptor.

Сначала рассмотрим цепочку вызовов:

  • C- SqlSessionTemplate # selectList : инициировать запрос Select
  • C-SqlSessionInterceptor # invoke: создание прокси-сервера сеанса
  • C-SqlSessionUtils # getSqlSession: получить объект сеанса
  • 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 создает сеанс и вызывает плагин для генерации логики.

При построении сеанса здесь, наконец, вызовите деформацию плагина, чтобы построить плагин.

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

Шаг 3: вызвать выполнение

Реализация перехватчика основана на прокси-сервере. Генерацию прокси мы видели выше. Вот посмотрите на вызов прокси:

Возьмите запрос в качестве примера:

При вызове метода Query в Executor по умолчанию будет вызываться прокси-класс,Конструкция Execute упоминалась выше, так какую же роль он играет во всей логике?

Функция исполнителя: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);

}

Вызов для выполнения:

Когда сессия была построена ранее, для нее был определен плагин.

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


		

3. Расширение PageHelper

Ниже показано, как соответствующие методы перехватчика используются в PageHelper:

В PageHelper в основном есть 2 перехватчика:

  • QueryInterceptor : плагин для работы с запросами
  • PageInterceptor : плагин операций пейджинга

В основном взгляните на перехватчик 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);
        }
}

Поскольку исходный код сильно прокомментирован, в принципе нет необходимости делать дополнительные аннотации.Общий процесс таков:

  • Шаг 1: Метод перехвата Interceptor определяет общую логику
  • Шаг 2: Метод подсчета определяет, выполняет ли он пейджинг.
  • Шаг 3: pageQuery вызывает диалект для объединения вещей SQL

В целом стоит отметить несколько моментов:

  • Только сгенерируйте строку подкачки и 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(перехватчик) Добавить перехватчик

Ядро PageHelper:

  • параметр = диалект.процессПараметрОбъект(мс, параметр,boundSql, pageKey) : Получить параметры пейджинга
  • dialect.getPageSql (ms,boundSql, параметр, rowBounds, pageKey): параметр анализируется как SQL
  • Параметры сохранения ThreadLocal

Позже мы остановимся на других моментах Mybatis и общих преимуществах PageHelper.