MyBatis 分页与插件深度解密:从插件机制到三大分页方案原理全解

作为 Java 后端开发者,分页是我们每天都在使用的功能。MyBatis 本身没有提供物理分页支持,但它强大的插件机制让我们可以轻松实现分页功能。从最原始的RowBounds到流行的PageHelper,再到现在的MyBatis-Plus分页插件,不同的分页方案背后有着不同的实现原理。

面试时,MyBatis 分页和插件机制更是 100% 的必考题,面试官会层层深挖:

  • MyBatis 插件的运行原理是什么?它基于什么设计模式?
  • MyBatis 可以拦截哪些组件的哪些方法?
  • RowBounds是如何实现分页的?为什么说它是内存分页?
  • PageHelper 的核心原理是什么?为什么分页参数要紧跟查询方法?
  • MyBatis-Plus 分页插件的实现机制是什么?和 PageHelper 有什么区别?
  • 物理分页和内存分页有什么区别?

这篇文章,我们就从插件运行原理→原生 RowBounds 分页→PageHelper 分页→MyBatis-Plus 分页四个维度,彻底搞懂 MyBatis 的分页实现。不仅会讲清楚理论,更会结合源码分析,让你看完既能轻松应对面试,又能解决实际项目中的分页问题。

一、先搞懂:MyBatis 插件运行原理

所有的 MyBatis 分页插件都是基于 MyBatis 的插件机制实现的。要理解分页原理,首先必须理解 MyBatis 的插件机制。

1. 什么是 MyBatis 插件?

MyBatis 插件是一种拦截器机制,它允许我们在 SQL 执行的各个阶段进行拦截,插入自定义逻辑。MyBatis 的插件本质上是一个动态代理,它为目标对象生成一个代理对象,在方法执行前后插入自定义逻辑。

MyBatis 允许拦截以下四个核心组件的方法:

组件 可拦截的方法 作用
Executor update、query、commit、rollback、getTransaction、close、isClosed 负责执行 SQL 语句,管理事务和缓存
StatementHandler prepare、parameterize、batch、update、query 负责与 JDBC 的 Statement 交互,设置参数、执行 SQL
ParameterHandler getParameterObject、setParameters 负责将 Java 对象转换为 JDBC 的 SQL 参数
ResultSetHandler handleResultSets、handleOutputParameters 负责处理结果集,将 ResultSet 映射到 Java 对象

这四个组件是 MyBatis 执行 SQL 的核心,所有的 SQL 执行都会经过这四个组件。通过拦截这些组件的方法,我们可以在 SQL 执行的任何阶段插入自定义逻辑。

2. 插件的实现原理

MyBatis 插件基于责任链模式JDK 动态代理实现。

核心接口:Interceptor

所有的 MyBatis 插件都必须实现Interceptor接口,该接口定义了三个方法:

java 复制代码
public interface Interceptor {
    // 拦截目标方法,在这里编写自定义逻辑
    Object intercept(Invocation invocation) throws Throwable;
    
    // 生成代理对象
    default Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    
    // 设置插件属性
    default void setProperties(Properties properties) {
    }
}
核心类:Plugin

Plugin类是 MyBatis 提供的工具类,用于生成代理对象。它实现了InvocationHandler接口,是动态代理的调用处理器:

java 复制代码
public class Plugin implements InvocationHandler {
    private final Object target;
    private final Interceptor interceptor;
    private final Map<Class<?>, Set<Method>> signatureMap;

    private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
        this.target = target;
        this.interceptor = interceptor;
        this.signatureMap = signatureMap;
    }

    // 生成代理对象
    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;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            Set<Method> methods = signatureMap.get(method.getDeclaringClass());
            // 如果当前方法需要被拦截,调用interceptor的intercept方法
            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);
        }
    }
}
核心注解:@Intercepts 和 @Signature

使用@Intercepts@Signature注解来指定插件需要拦截的方法:

java 复制代码
// 拦截Executor的query方法
@Intercepts({
    @Signature(
        type = Executor.class,                // 要拦截的组件类型
        method = "query",                     // 要拦截的方法名
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} // 方法参数类型
    )
})
public class MyInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 自定义逻辑
        System.out.println("方法执行前");
        // 执行原方法
        Object result = invocation.proceed();
        System.out.println("方法执行后");
        return result;
    }
}

3. 插件的执行流程

MyBatis 插件的执行流程可以分为三个阶段:

阶段 1:插件注册

当 MyBatis 初始化时,会读取配置文件中的插件配置,创建插件实例并添加到拦截器链中:

XML 复制代码
<!-- mybatis-config.xml中配置插件 -->
<plugins>
    <plugin interceptor="com.example.MyInterceptor">
        <property name="property1" value="value1"/>
    </plugin>
</plugins>
阶段 2:生成代理对象

当 MyBatis 创建四大核心组件时,会遍历所有注册的插件,为每个组件生成代理对象:

java 复制代码
// Configuration类中创建Executor的方法
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;
}
阶段 3:方法拦截

当调用代理对象的方法时,会触发Plugin.invoke()方法。如果当前方法需要被拦截,就会调用插件的intercept()方法,执行自定义逻辑;否则直接执行原方法。

4. 多插件的执行顺序

MyBatis 支持多个插件同时存在,它们的执行顺序遵循责任链模式

  • 插件的执行顺序与配置顺序相反,先配置的插件后执行
  • 可以通过实现Ordered接口或使用@Order注解指定执行顺序,值越小优先级越高

例如,如果配置了两个插件 A 和 B,配置顺序是 A→B,那么执行顺序是:

复制代码
B的前置逻辑 → A的前置逻辑 → 原方法 → A的后置逻辑 → B的后置逻辑

二、MyBatis 原生分页:RowBounds 原理

MyBatis 本身提供了一个简单的分页方式:RowBounds。但它并不是真正的物理分页,而是内存分页

1. RowBounds 的使用方式

java 复制代码
// 创建RowBounds对象,指定偏移量和每页条数
RowBounds rowBounds = new RowBounds(0, 10);
// 执行查询
List<User> users = sqlSession.selectList("com.example.mapper.UserMapper.listUsers", null, rowBounds);

2. RowBounds 的实现原理

RowBounds的实现非常简单,它只是一个包含offsetlimit两个字段的对象:

java 复制代码
public class RowBounds {
    public static final int NO_ROW_OFFSET = 0;
    public static final int NO_ROW_LIMIT = Integer.MAX_VALUE;
    public static final RowBounds DEFAULT = new RowBounds();

    private final int offset;
    private final int limit;

    public RowBounds() {
        this.offset = NO_ROW_OFFSET;
        this.limit = NO_ROW_LIMIT;
    }

    public RowBounds(int offset, int limit) {
        this.offset = offset;
        this.limit = limit;
    }

    // getter方法
}

真正的分页逻辑在DefaultResultSetHandlerhandleResultSets()方法中:

java 复制代码
public List<Object> handleResultSets(Statement stmt) throws SQLException {
    final List<Object> multipleResults = new ArrayList<>();
    int resultSetCount = 0;
    ResultSetWrapper rsw = getFirstResultSet(stmt);

    while (rsw != null && resultSetCount < mappedStatement.getResultMaps().size()) {
        ResultMap resultMap = mappedStatement.getResultMaps().get(resultSetCount++);
        handleResultSet(rsw, resultMap, multipleResults, null);
        rsw = getNextResultSet(stmt);
    }

    return collapseSingleResultList(multipleResults);
}

private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
    try {
        if (parentMapping != null) {
            handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
        } else {
            if (resultHandler == null) {
                DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
                handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
                multipleResults.add(defaultResultHandler.getResultList());
            } else {
                handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
            }
        }
    } finally {
        // 关闭ResultSet
        closeResultSet(rsw.getResultSet());
    }
}

public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    if (resultMap.hasNestedResultMaps()) {
        handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    } else {
        handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    }
}

private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    ResultSet rs = rsw.getResultSet();
    // 跳过offset条记录
    skipRows(rs, rowBounds);
    // 读取limit条记录
    int rowsProcessed = 0;
    while (shouldProcessMoreRows(rs, rowBounds, rowsProcessed)) {
        Object row = getRowValue(rsw, resultMap);
        resultHandler.handleResult(row);
        rowsProcessed++;
    }
}

private void skipRows(ResultSet rs, RowBounds rowBounds) throws SQLException {
    if (rs.getType() != ResultSet.TYPE_FORWARD_ONLY) {
        if (rowBounds.getOffset() != RowBounds.NO_ROW_OFFSET) {
            rs.absolute(rowBounds.getOffset());
        }
    } else {
        // 对于只能向前滚动的ResultSet,只能逐条跳过
        for (int i = 0; i < rowBounds.getOffset(); i++) {
            rs.next();
        }
    }
}

private boolean shouldProcessMoreRows(ResultSet rs, RowBounds rowBounds, int rowsProcessed) throws SQLException {
    return rs.next() && (rowBounds.getLimit() == RowBounds.NO_ROW_LIMIT || rowsProcessed < rowBounds.getLimit());
}

从源码可以看出,RowBounds的分页逻辑是:

  1. 执行原始的 SQL 查询,获取所有符合条件的记录
  2. 跳过前offset条记录
  3. 只读取前limit条记录

3. RowBounds 的缺点

RowBounds虽然使用简单,但存在致命的缺点:

  • 内存分页:先查询所有数据,再在内存中进行分页。当数据量很大时,会导致内存溢出
  • 性能差:需要查询所有数据,然后丢弃大部分数据,浪费数据库和网络资源
  • 不支持总条数查询:只能获取当前页的数据,无法获取总记录数和总页数

因此,RowBounds只适用于数据量很小的场景,生产环境不推荐使用。

三、PageHelper 分页原理

PageHelper 是目前最流行的 MyBatis 分页插件,它基于 MyBatis 的插件机制实现,支持多种数据库,使用简单,功能强大。

1. PageHelper 的使用方式

java 复制代码
// 设置分页参数,紧跟查询方法
PageHelper.startPage(1, 10);
// 执行查询
List<User> users = userMapper.listUsers();
// 获取分页信息
PageInfo<User> pageInfo = new PageInfo<>(users);
System.out.println("总条数:" + pageInfo.getTotal());
System.out.println("总页数:" + pageInfo.getPages());

2. PageHelper 的核心原理

PageHelper 的核心原理可以概括为三句话:

  1. 基于 ThreadLocal 保存分页参数:使用 ThreadLocal 保存分页参数,避免线程安全问题
  2. 拦截 Executor 的 query 方法:在 SQL 执行前,动态修改 SQL,添加分页语句
  3. 自动查询总条数:在执行分页查询前,先执行 count 查询获取总记录数

3. PageHelper 的执行流程

PageHelper 的核心是PageInterceptor拦截器,它拦截了Executorquery方法。整个执行流程如下:

步骤 1:设置分页参数

调用PageHelper.startPage(pageNum, pageSize)方法,将分页参数保存到 ThreadLocal 中:

java 复制代码
public static <T> Page<T> startPage(int pageNum, int pageSize) {
    return startPage(pageNum, pageSize, true);
}

public static <T> Page<T> startPage(int pageNum, int pageSize, boolean count) {
    return startPage(pageNum, pageSize, count, null, null);
}

public static <T> Page<T> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
    Page<T> page = new Page<>(pageNum, pageSize, count);
    page.setReasonable(reasonable);
    page.setPageSizeZero(pageSizeZero);
    // 将分页参数保存到ThreadLocal中
    LOCAL_PAGE.set(page);
    return page;
}

// ThreadLocal保存分页参数
protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<>();
步骤 2:拦截 Executor 的 query 方法

当执行查询方法时,PageInterceptorintercept方法会被调用:

java 复制代码
@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;

        // 从ThreadLocal中获取分页参数
        Page<?> page = getLocalPage();
        if (page == null) {
            // 没有分页参数,直接执行原方法
            return invocation.proceed();
        }

        // 1. 执行count查询,获取总记录数
        if (page.isCount()) {
            count(ms, parameter, rowBounds, resultHandler, boundSql, page);
        }

        // 2. 生成分页SQL
        String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, page);
        // 3. 执行分页查询
        List<?> result = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
        // 4. 封装分页结果
        page.addAll(result);
        // 5. 清除ThreadLocal中的分页参数
        clearLocalPage();
        return page;
    } finally {
        // 确保清除ThreadLocal中的分页参数
        clearLocalPage();
    }
}
步骤 3:动态生成分页 SQL

PageHelper 会根据不同的数据库方言,生成对应的分页 SQL。例如:

  • MySQL:添加LIMIT offset, limit
  • Oracle:使用ROWNUM
  • SQL Server:使用TOPROW_NUMBER()

以 MySQL 为例,原始 SQL 是:

sql 复制代码
SELECT * FROM user WHERE age > 18

PageHelper 会将其修改为:

sql 复制代码
-- count查询
SELECT COUNT(*) FROM user WHERE age > 18

-- 分页查询
SELECT * FROM user WHERE age > 18 LIMIT 0, 10

4. PageHelper 的常见坑点

坑 1:分页参数没有紧跟查询方法

问题 :如果在PageHelper.startPage()和查询方法之间有其他逻辑,可能会导致分页参数被其他线程覆盖,或者分页参数失效。

错误示例

java 复制代码
PageHelper.startPage(1, 10);
// 其他业务逻辑,可能会导致分页参数被覆盖
Thread.sleep(1000);
List<User> users = userMapper.listUsers();

解决方案PageHelper.startPage()必须紧跟查询方法,中间不要有任何其他逻辑。

坑 2:ThreadLocal 内存泄漏

问题:如果查询方法抛出异常,可能会导致 ThreadLocal 中的分页参数没有被清除,造成内存泄漏。

解决方案 :PageHelper 在finally块中会清除 ThreadLocal 中的分页参数,正常情况下不会有问题。但如果在拦截器之外手动设置了分页参数,一定要记得手动清除。

坑 3:不支持嵌套查询

问题:PageHelper 只能拦截最外层的查询,对于嵌套查询(如一对多查询)无法正确分页。

解决方案:避免在分页查询中使用嵌套查询,或者使用 MyBatis-Plus 的分页插件。

四、MyBatis-Plus 分页原理

MyBatis-Plus 是一个 MyBatis 的增强工具,它提供了更强大的分页功能,使用更加简单,性能也更好。

1. MyBatis-Plus 分页的使用方式

java 复制代码
// 创建分页对象
Page<User> page = new Page<>(1, 10);
// 执行分页查询
IPage<User> userPage = userMapper.selectPage(page, null);
// 获取分页信息
System.out.println("总条数:" + userPage.getTotal());
System.out.println("总页数:" + userPage.getPages());

2. MyBatis-Plus 分页的核心原理

MyBatis-Plus 的分页插件是PaginationInnerInterceptor,它同样基于 MyBatis 的插件机制实现,但与 PageHelper 有很大的不同:

  1. 不使用 ThreadLocal:分页参数直接通过方法参数传递,避免了 ThreadLocal 的线程安全问题
  2. 基于动态 SQL 生成:使用 MyBatis 的动态 SQL 机制生成分页语句
  3. 更强大的方言支持:支持更多的数据库,并且可以自定义方言

3. MyBatis-Plus 分页的执行流程

步骤 1:配置分页插件

首先需要在 Spring 配置中注册PaginationInnerInterceptor

java 复制代码
@Configuration
public class MyBatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 添加分页插件,指定数据库方言
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}
步骤 2:拦截 Executor 的 query 方法

PaginationInnerInterceptor拦截了Executorquery方法:

java 复制代码
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    // 检查参数是否是IPage类型
    IPage<?> page = findPage(parameter);
    if (page == null) {
        // 不是分页查询,直接执行
        return;
    }

    // 1. 执行count查询
    if (page.searchCount()) {
        long total = count(executor, ms, parameter, boundSql);
        page.setTotal(total);
        // 如果总条数为0,直接返回空结果
        if (total == 0) {
            return;
        }
    }

    // 2. 生成分页SQL
    String pageSql = buildPageSql(ms, boundSql, page);
    // 3. 修改BoundSql中的SQL为分页SQL
    ReflectionUtils.setFieldValue(boundSql, "sql", pageSql);
}
步骤 3:生成分页 SQL

MyBatis-Plus 会根据数据库方言生成对应的分页 SQL。与 PageHelper 不同的是,MyBatis-Plus 使用了更优雅的方式来修改 SQL,而不是直接替换整个 SQL 字符串。

以 MySQL 为例,原始 SQL 是:

sql 复制代码
SELECT * FROM user WHERE age > 18

MyBatis-Plus 会将其修改为:

sql 复制代码
-- count查询
SELECT COUNT(*) FROM user WHERE age > 18

-- 分页查询
SELECT * FROM user WHERE age > 18 LIMIT ?, ?

4. MyBatis-Plus 分页的优势

与 PageHelper 相比,MyBatis-Plus 分页有以下优势:

  • 无 ThreadLocal 问题:分页参数直接通过方法参数传递,避免了 ThreadLocal 的线程安全问题和内存泄漏问题
  • 更优雅的实现:基于 MyBatis 的动态 SQL 机制,而不是直接修改 SQL 字符串
  • 更好的兼容性:支持更多的数据库,并且可以自定义方言
  • 更强大的功能:支持自定义分页 SQL、多表联查分页、嵌套查询分页等
  • 更好的性能:优化了 count 查询,支持自定义 count 方法

五、三大分页方案对比与最佳实践

1. 三大分页方案对比

特性 RowBounds PageHelper MyBatis-Plus 分页
分页类型 内存分页 物理分页 物理分页
总条数查询 不支持 自动支持 自动支持
线程安全 安全 不安全(依赖 ThreadLocal) 安全
数据库支持 所有 主流数据库 主流数据库 + 自定义方言
性能 差(大数据量内存溢出)
使用复杂度 简单 简单 简单
功能丰富度
适用场景 极小数据量 中小型项目 所有项目

2. 最佳实践

  1. 生产环境禁止使用 RowBounds:内存分页在数据量稍大时就会导致性能问题和内存溢出
  2. 新项目优先使用 MyBatis-Plus 分页:无 ThreadLocal 问题,功能更强大,性能更好
  3. PageHelper 使用注意事项
    • PageHelper.startPage()必须紧跟查询方法
    • 不要在异步方法中使用 PageHelper
    • 避免在同一个线程中多次调用PageHelper.startPage()
  4. 优化 count 查询
    • 对于复杂查询,自定义 count 方法,避免使用自动生成的 count 查询
    • 对于不需要总条数的场景,关闭 count 查询:PageHelper.startPage(1, 10, false)
  5. 避免深分页:深分页(如 offset=100000)会导致数据库性能下降,可以使用游标分页或 ID 分页优化

六、高频面试题解答

  1. 问:MyBatis 插件的运行原理是什么? 答:MyBatis 插件基于责任链模式和 JDK 动态代理实现。它允许拦截 Executor、StatementHandler、ParameterHandler 和 ResultSetHandler 四个核心组件的方法。当创建这些组件时,MyBatis 会为它们生成代理对象,调用方法时会触发拦截器链,执行自定义逻辑。

  2. 问:MyBatis 可以拦截哪些组件的哪些方法? 答:MyBatis 可以拦截四个核心组件的方法:

    • Executor:update、query、commit、rollback 等
    • StatementHandler:prepare、parameterize、update、query 等
    • ParameterHandler:getParameterObject、setParameters 等
    • ResultSetHandler:handleResultSets、handleOutputParameters 等
  3. 问:RowBounds 是如何实现分页的?为什么说它是内存分页? 答:RowBounds 的实现原理是先执行原始 SQL 查询所有数据,然后在内存中跳过前 offset 条记录,只返回前 limit 条记录。因为它是在内存中进行分页,而不是在数据库层面进行分页,所以称为内存分页。

  4. 问:PageHelper 的核心原理是什么? 答:PageHelper 的核心原理是:使用 ThreadLocal 保存分页参数,拦截 Executor 的 query 方法,在 SQL 执行前动态修改 SQL 添加分页语句,并自动执行 count 查询获取总记录数。

  5. 问:为什么 PageHelper 的分页参数要紧跟查询方法? 答:因为 PageHelper 使用 ThreadLocal 保存分页参数,如果在设置分页参数和查询方法之间有其他逻辑,可能会导致分页参数被其他线程覆盖,或者分页参数失效。

  6. 问:MyBatis-Plus 分页和 PageHelper 有什么区别? 答:主要区别有:

    • MyBatis-Plus 不使用 ThreadLocal,分页参数直接通过方法参数传递,更安全
    • MyBatis-Plus 基于动态 SQL 生成,实现更优雅
    • MyBatis-Plus 支持更多的数据库和更强大的功能
    • MyBatis-Plus 性能更好,优化了 count 查询
  7. 问:物理分页和内存分页有什么区别? 答:物理分页是在数据库层面进行分页,只查询当前页的数据,性能好,适合大数据量;内存分页是先查询所有数据,再在内存中进行分页,性能差,只适合极小数据量。

七、总结

MyBatis 的分页功能是日常开发中最常用的功能之一,理解它的实现原理不仅能帮助我们写出更高效的代码,还能轻松应对面试中的相关问题。

回顾一下全文的核心内容:

  • MyBatis 插件基于责任链模式和 JDK 动态代理实现,可以拦截四大核心组件的方法
  • RowBounds 是内存分页,先查询所有数据再在内存中分页,生产环境不推荐使用
  • PageHelper 基于 ThreadLocal 和动态修改 SQL 实现,使用简单但有 ThreadLocal 安全问题
  • MyBatis-Plus 分页基于方法参数传递和动态 SQL 生成,更安全、更强大、性能更好
  • 生产环境优先使用 MyBatis-Plus 分页,避免使用 RowBounds

理解了这些原理,你就能在实际项目中选择合适的分页方案,避免常见的坑点,写出高效、稳定的分页代码。

相关推荐
小白学大数据11 小时前
电商关键词挖掘:Java 爬虫抓取 1688 推荐搜索词
java·开发语言·爬虫·python
狼与自由11 小时前
jdk版本升级
java·开发语言
云姜.11 小时前
Langchain快速上手编程-Runnable 与 LCEL
java·开发语言·langchain
折哥的程序人生 · 物流技术专研11 小时前
《Java 100 天进阶之路》第40篇:浮点数转成十进制问题
java·开发语言·后端·面试·求职招聘
woai336411 小时前
线上日志排查
java
身如柳絮随风扬11 小时前
List 与 Set 的区别及体系全览
java·list
ZengLiangYi11 小时前
任务队列设计:p-queue 限速 + 重试策略
前端·javascript·后端
xxl大卡11 小时前
Redis 主从复制与哨兵模式
java·开发语言
XovH11 小时前
Docker Compose 文件详解:服务、网络与卷
后端