MyBatis - 拦截器分页(原理机制 + 功能进阶)

简介: MyBatis - 拦截器分页(原理机制 + 功能进阶)

前言:在上一篇博客中,我们讲到了mybatis的四种简单分页方式。分别是基于数组、Sql语句、分页插件和RowBounds的简单分页实现。不清楚的可以移步 MyBatis - 分页之四大方式(数组、SQL、拦截器,RowBounds),这里详细讲解了几种分页方式的原理和优缺点,适合于初学者,很容易理解,不清楚的同学可以回去瞟上几眼。



任务分析

当然,这并不是我们这篇博客讲解的重点。记得在上一篇中,我们只是实现了最简单的插件分页实现,还非常简陋,功能也还不够完善,日常使用起来也还不够简便。所以在这里,我们对插件分页的实现原理进行一下详细的介绍,并且实现一个功能完善的分页插件。

原理剖析


// 注解拦截器并且签名
@Intercepts(@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}))

和StatementHandler服务类中prepare方法相对应。

public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException

自定义的插件类,都需要使用@Intercepts注解,@Signature是对插件需要拦截的对象进行签名,type表示要拦截的类型,method表示拦截类中的方法,args是需要的参数,这里的参数在后面也可以获取到。


StatementHandler:数据库会话器,专门用于处理数据库会话,statement的执行操作,是一个接口。


MetaObject:mybatis工具类,可以有效的读取或修改一些重要对象的属性,基本思想是通过反射去获取和设置对象的属性值,只是MetaObject类不需要我们自己去实现具体反射的方法,已经封装好了。


通过MetaObject.getValue()和MetaObject.setValue(name,value)方法去获取对象属性值和设置对象属性值。

通过MetaObject属性的获取流程:

MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement")

上面代码是怎么获取到MappedStatement对象的?这里的metaStatementHandler是一个MetaObject对象。

首先通过metaStatementHandler.getValue(“delegate”)拿到真正实现StatementHandler接口的服务对象。

public class RoutingStatementHandler implements StatementHandler {
    // delegate属性来自这里,是一个实现了StatementHandler接口的类
    private final StatementHandler delegate;
    // 通过这里给delegate属性赋值
    public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
        switch(RoutingStatementHandler.SyntheticClass_1.$SwitchMap$org$apache$ibatis$mapping$StatementType[ms.getStatementType().ordinal()]) {
        case 1:
            this.delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
            break;
        case 2:
            this.delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
            break;
        case 3:
            this.delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
            break;
        default:
            throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
        }
    }
}

拿到具体的服务对象(处理逻辑的StatementHandler实现类)后,再获取mappedStatement属性,我们再来看mappedStatement属性的定义:

public abstract class BaseStatementHandler implements StatementHandler {
    protected final Configuration configuration;
    protected final ObjectFactory objectFactory;
    protected final TypeHandlerRegistry typeHandlerRegistry;
    protected final ResultSetHandler resultSetHandler;
    protected final ParameterHandler parameterHandler;
    protected final Executor executor;
    //定义在这里
    protected final MappedStatement mappedStatement;
    protected final RowBounds rowBounds;
    protected BoundSql boundSql;
}

可以看出是定义在BaseStatementHandler中的属性,三个具体的服务对象都会继承BaseStatementHandler。这里有很多和执行数据库操作相关的属性,如果我们需要的话,都可以通过上述方式获取,如果相获取下层对象的属性,按照这个写法一次获取也可以拿到。


RoutingStatementHandler:不是真正的服务对象,它通过适配器模式找到正确的StatementHandler去执行操作。通过invocation.getTarget()获取到的是一个RoutingStatementHandler代理对象,再通过MappedStatement中不同的类型,找到具体的处理类。


真正实现StatementHandler接口的服务对象有:SimpleStatementHandler,PreparedStatementHandler,CallableStatementHandler都继承BaseStatementHandler,它们分别对应是三种不同的执行器:SIMPLE:默认的简单执行器;REUSE:重用预处理语句的执行期;BATCH:重用语句和批量更新处理器。


BoundSql: 用于组装SQL和参数,使用插件时需要通过它拿到当前运行的SQL和参数及参数规则。它有如下几个重要属性:

public class BoundSql {
    private String sql;
    private List<ParameterMapping> parameterMappings;
    private Object parameterObject;
    private Map<String, Object> additionalParameters;
    private MetaObject metaParameters;
}

parameterObject:是参数本身,调用方法时传递进来的参数。可以是pojo,map或@param注解的参数等。


parameterMappings:它是一个List,存储了许多ParameterMapping对象。这个对象会描述我们的参数,参数包括属性、名称、表达式、javaType、jdbcType等。


sql:我们书写在mapper.xml文件中的一条sql语句

MappedStatement:存储mapper.xml文件中一条sql语句配置的所有信息。

Connection:连接对象,在插件中会依赖它去进行一些数据库操作。

Configuration:包含mybatis所有的配置信息。

ParameterHandler:接口,对预编译语句进行参数设置。即将参数设置到sql语句中。它有两个重要方法:getParameterObject()用于获取参数对象和

setParameters(PreparedStatement var1)用于设置参数对象。

在对自定义分页插件中会使用到的各个参数有了理解后,我们就来具体实现这个分页插件。

在插件中我们使用了一个辅助类,来封装分页时会用到的一些参数,定义如下:

package com.cbg.interceptor;
/**
 * description:实现分页的辅助类,用于封装用于分页的一些参数
 */
public class PageParam {
    private Integer defaultPage;
    // 默认每页显示条数
    private Integer defaultPageSize;
    // 是否启用分页功能
    private Boolean defaultUseFlag;
    // 是否检测当前页码的合法性(大于最大页码或小于最小页码都不合法)
    private Boolean defaultCheckFlag;
    // 当前sql查询的总记录数,回填
    private Integer totle;
    // 当前sql查询实现分页后的总页数,回填
    private Integer totlePage;
    public Integer getDefaultPage() {
        return defaultPage;
    }
    public void setDefaultPage(Integer defaultPage) {
        this.defaultPage = defaultPage;
    }
    public Integer getDefaultPageSize() {
        return defaultPageSize;
    }
    public void setDefaultPageSize(Integer defaultPageSize) {
        this.defaultPageSize = defaultPageSize;
    }
    public Boolean isDefaultUseFlag() {
        return defaultUseFlag;
    }
    public void setDefaultUseFlag(Boolean defaultUseFlag) {
        this.defaultUseFlag = defaultUseFlag;
    }
    public Boolean isDefaultCheckFlag() {
        return defaultCheckFlag;
    }
    public void setDefaultCheckFlag(Boolean defaultCheckFlag) {
        this.defaultCheckFlag = defaultCheckFlag;
    }
    public Integer getTotle() {
        return totle;
    }
    public void setTotle(Integer totle) {
        this.totle = totle;
    }
    public Integer getTotlePage() {
        return totlePage;
    }
    public void setTotlePage(Integer totlePage) {
        this.totlePage = totlePage;
    }
}

当需要使用到分页功能时,我们只需要将分页参数封装到PageParam对象中,并且作为参数传递到查询方法中,插件中就会自动获取到这些参数,并且动态组分页的Sql查询语句。下面就是我们自定义的分页插件类实现:

package com.cbg.interceptor;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.RoutingStatementHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
import javax.security.auth.login.Configuration;
import java.lang.reflect.InvocationTargetException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
import java.util.Properties;
/**
 * description:插件分页
 */
@Intercepts(@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}))
public class PageInterceptor implements Interceptor {
    // 默认页码
    private Integer defaultPage;
    // 默认每页显示条数
    private Integer defaultPageSize;
    // 是否启用分页功能
    private boolean defaultUseFlag;
    // 检测当前页码的合法性(大于最大页码或小于最小页码都不合法)
    private boolean defaultCheckFlag;
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = getActuralHandlerObject(invocation);
        MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
        String sql = statementHandler.getBoundSql().getSql();
        // 检测未通过,不是select语句
        if (!checkIsSelectFalg(sql)) {
            return invocation.proceed();
        }
        BoundSql boundSql = statementHandler.getBoundSql();
        Object paramObject = boundSql.getParameterObject();
        PageParam pageParam = getPageParam(paramObject);
        if (pageParam == null)
            return invocation.proceed();
        Integer pageNum = pageParam.getDefaultPage() == null ? defaultPage : pageParam.getDefaultPage();
        Integer pageSize = pageParam.getDefaultPageSize() == null ? defaultPageSize : pageParam.getDefaultPageSize();
        Boolean useFlag = pageParam.isDefaultUseFlag() == null ? defaultUseFlag : pageParam.isDefaultUseFlag();
        Boolean checkFlag = pageParam.isDefaultCheckFlag() == null ? defaultCheckFlag : pageParam.isDefaultCheckFlag();
        // 不使用分页功能
        if (!useFlag) {
            return invocation.proceed();
        }
        int totle = getTotle(invocation, metaStatementHandler, boundSql);
        // 将动态获取到的分页参数回填到pageParam中
        setTotltToParam(pageParam, totle, pageSize);
        // 检查当前页码的有效性
        checkPage(checkFlag, pageNum, pageParam.getTotlePage());
        // 修改sql
        return updateSql2Limit(invocation, metaStatementHandler, boundSql, pageNum, pageSize);
    }
    @Override
    public Object plugin(Object o) {
        return Plugin.wrap(o, this);
    }
    // 在配置插件的时候配置默认参数
    @Override
    public void setProperties(Properties properties) {
        String strDefaultPage = properties.getProperty("default.page");
        String strDefaultPageSize = properties.getProperty("default.pageSize");
        String strDefaultUseFlag = properties.getProperty("default.useFlag");
        String strDefaultCheckFlag = properties.getProperty("default.checkFlag");
        defaultPage = Integer.valueOf(strDefaultPage);
        defaultPageSize = Integer.valueOf(strDefaultPageSize);
        defaultUseFlag = Boolean.valueOf(strDefaultUseFlag);
        defaultCheckFlag = Boolean.valueOf(strDefaultCheckFlag);
    }
    // 从代理对象中分离出真实statementHandler对象,非代理对象
    private StatementHandler getActuralHandlerObject(Invocation invocation) {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
        Object object = null;
        // 分离代理对象链,目标可能被多个拦截器拦截,分离出最原始的目标类
        while (metaStatementHandler.hasGetter("h")) {
            object = metaStatementHandler.getValue("h");
            metaStatementHandler = SystemMetaObject.forObject(object);
        }
        if (object == null) {
            return statementHandler;
        }
        return (StatementHandler) object;
    }
    // 判断是否是select语句,只有select语句,才会用到分页
    private boolean checkIsSelectFalg(String sql) {
        String trimSql = sql.trim();
        int index = trimSql.toLowerCase().indexOf("select");
        return index == 0;
    }
    /*
     * 获取分页的参数
     * 参数可以通过map,@param注解进行参数传递。或者请求pojo继承自PageParam,将PageParam中的分页数据放进去
     */
    private PageParam getPageParam(Object paramerObject) {
        if (paramerObject == null) {
            return null;
        }
        PageParam pageParam = null;
        // 通过map和@param注解将PageParam参数传递进来,pojo继承自PageParam不推荐使用  这里从参数中提取出传递进来的pojo继承自PageParam
        // 首先处理传递进来的是map对象和通过注解方式传值的情况,从中提取出PageParam,循环获取map中的键值对,取出PageParam对象
        if (paramerObject instanceof Map) {
            Map<String, Object> params = (Map<String, Object>) paramerObject;
            for (Map.Entry<String, Object> entry : params.entrySet()) {
                if (entry.getValue() instanceof PageParam) {
                    return (PageParam) entry.getValue();
                }
            }
        } 
        else if (paramerObject instanceof PageParam) {
            // 继承方式 pojo继承自PageParam 只取出我们希望得到的分页参数
            pageParam = (PageParam) paramerObject;
        }
        return pageParam;
    }
    // 获取当前sql查询的记录总数
    private int getTotle(Invocation invocation, MetaObject metaStatementHandler, BoundSql boundSql) {
        // 获取mapper文件中当前查询语句的配置信息
        MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
        // 获取所有配置Configuration
        org.apache.ibatis.session.Configuration configuration = mappedStatement.getConfiguration();
        // 获取当前查询语句的sql
        String sql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");
        // 将sql改写成统计记录数的sql语句,这里是mysql的改写语句,将第一次查询结果作为第二次查询的表
        String countSql = "select count(*) as totle from (" + sql + ") $_paging";
        // 获取connection连接对象,用于执行countsql语句
        Connection conn = (Connection) invocation.getArgs()[0];
        PreparedStatement ps = null;
        int totle = 0;
        try {
            // 预编译统计总记录数的sql
            ps = conn.prepareStatement(countSql);
            // 构建统计总记录数的BoundSql
            BoundSql countBoundSql = new BoundSql(configuration, countSql, boundSql.getParameterMappings(), boundSql.getParameterObject());
            // 构建ParameterHandler,用于设置统计sql的参数
            ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, boundSql.getParameterObject(), countBoundSql);
            // 设置总数sql的参数
            parameterHandler.setParameters(ps);
            // 执行查询语句
            ResultSet rs = ps.executeQuery();
            while (rs.next()) {
                // 与countSql中设置的别名对应
                totle = rs.getInt("totle");
            }
        } 
        catch (SQLException e) {
            e.printStackTrace();
        } 
        finally {
            if (ps != null)
                try {
                    ps.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
        }
        return totle;
    }
    // 设置条数参数到pageparam对象
    private void setTotltToParam(PageParam param, int totle, int pageSize) {
        param.setTotle(totle);
        param.setTotlePage(totle % pageSize == 0 ? totle / pageSize : (totle / pageSize) + 1);
    }
    // 修改原始sql语句为分页sql语句
    private Object updateSql2Limit(Invocation invocation, MetaObject metaStatementHandler, BoundSql boundSql, int page, int pageSize) throws InvocationTargetException, IllegalAccessException, SQLException {
        String sql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");
        // 构建新的分页sql语句
        String limitSql = "select * from (" + sql + ") $_paging_table limit ?,?";
        // 修改当前要执行的sql语句
        metaStatementHandler.setValue("delegate.boundSql.sql", limitSql);
        // 相当于调用prepare方法,预编译sql并且加入参数,但是少了分页的两个参数,它返回一个PreparedStatement对象
        PreparedStatement ps = (PreparedStatement) invocation.proceed();
        // 获取sql总的参数总数
        int count = ps.getParameterMetaData().getParameterCount();
        // 设置与分页相关的两个参数
        ps.setInt(count - 1, (page - 1) * pageSize);
        ps.setInt(count, pageSize);
        return ps;
    }
    // 验证当前页码的有效性
    private void checkPage(boolean checkFlag, Integer pageNumber, Integer pageTotle) throws Exception {
        if (checkFlag) {
            if (pageNumber > pageTotle) {
                throw new Exception("查询失败,查询页码" + pageNumber + "大于总页数" + pageTotle);
            }
        }
    }
}

这次的分页插件的实现相比上一遍讲到的简单拦截器来说,进行了更强一步的封装,功能性也变得更强,使用起来也更加方便,完全适用于日常数据库常见的分页场景。


总结:现在几乎所有的互联网项目都会用到插件分页的技术,这里只是提出了一些简单的实现思路和逻辑,如果你有更多的想法或者更好的方式,不妨提出来打架一起探讨下。


最后:SSM+拦截器分页源码下载

目录
相关文章
|
2月前
|
SQL Java 数据库连接
MyBatis分页
MyBatis作为Java持久层框架,需结合数据库特性或插件实现分页。分页分为物理分页(如MySQL的LIMIT)和逻辑分页(内存截取),推荐使用PageHelper插件自动注入分页语句,提升开发效率与性能。需注意索引优化、深分页问题及多表关联时的兼容性,结合业务场景选择合适方案。
135 4
|
8月前
|
SQL Java 数据库连接
微服务——MyBatis分页
本文介绍了分页的多种实现方式,包括自带RowBounds分页、第三方插件PageHelper分页、SQL分页、数组分页及拦截器分页。其中,RowBounds是先查询全部结果再内存分页;PageHelper通过修改SQL动态添加分页关键字;SQL分页依赖数据库自身的分页功能如`LIMIT`;数组分页则是查询全量数据后用`subList`方法截取;拦截器分页则统一在SQL后添加分页语句。最后总结逻辑分页适合小数据量,但大数据量易内存溢出;物理分页虽小数据量效率较低,但更适合大数据场景,优先推荐使用。
118 0
|
8月前
|
SQL Oracle 关系型数据库
【YashanDB知识库】Mybatis-Plus调用YashanDB怎么设置分页
【YashanDB知识库】Mybatis-Plus调用YashanDB怎么设置分页
|
6月前
|
SQL Java 数据安全/隐私保护
发现问题:Mybatis-plus的分页总数为0,分页功能失效,以及多租户插件的使用。
总的来说,使用 Mybatis-plus 确实可以极大地方便我们的开发,但也需要我们理解其工作原理,掌握如何合适地使用各种插件。分页插件和多租户插件是其中典型,它们的运用可以让我们的代码更为简洁、高效,理解和掌握好它们的用法对我们的开发过程有着极其重要的意义。
671 15
|
8月前
|
SQL Java 关系型数据库
MyBatis篇-分页
本文介绍了多种分页方式,包括自带rowbound内存分页、第三方插件pagehelper(通过修改SQL实现分页)、SQL分页(依赖limit或rownum等关键字)、数组分页(先查询全部数据再用subList分页)、拦截器分页(自定义拦截器为SQL添加分页语句)。最后总结了逻辑分页(内存分页,适合小数据量)和物理分页(直接在数据库层面分页,适合大数据量)的优缺点,强调物理分页优先于逻辑分页。
|
8月前
|
SQL Java 数据库连接
MyBatis 实现分页的机制
MyBatis 的分页机制主要依赖于 `RowBounds` 对象和分页插件。`RowBounds` 实现内存分页,适合小数据量场景,通过设定偏移量和限制条数对结果集进行筛选。而针对大数据量,则推荐使用分页插件(如 PageHelper),实现物理分页。插件通过拦截 SQL 执行,动态修改语句添加分页逻辑,支持多种数据库方言。配置插件后,无需手动调整查询方法即可完成分页操作,提升性能与灵活性。
186 0
|
5月前
|
Java 数据库连接 数据库
Spring boot 使用mybatis generator 自动生成代码插件
本文介绍了在Spring Boot项目中使用MyBatis Generator插件自动生成代码的详细步骤。首先创建一个新的Spring Boot项目,接着引入MyBatis Generator插件并配置`pom.xml`文件。然后删除默认的`application.properties`文件,创建`application.yml`进行相关配置,如设置Mapper路径和实体类包名。重点在于配置`generatorConfig.xml`文件,包括数据库驱动、连接信息、生成模型、映射文件及DAO的包名和位置。最后通过IDE配置运行插件生成代码,并在主类添加`@MapperScan`注解完成整合
976 1
Spring boot 使用mybatis generator 自动生成代码插件
|
8月前
|
XML Java 数据库连接
微服务——SpringBoot使用归纳——Spring Boot集成MyBatis——基于注解的整合
本文介绍了Spring Boot集成MyBatis的两种方式:基于XML和注解的形式。重点讲解了注解方式,包括@Select、@Insert、@Update、@Delete等常用注解的使用方法,以及多参数时@Param注解的应用。同时,针对字段映射不一致的问题,提供了@Results和@ResultMap的解决方案。文章还提到实际项目中常结合XML与注解的优点,灵活使用两者以提高开发效率,并附带课程源码供下载学习。
672 0
|
10月前
|
前端开发 Java 数据库连接
Java后端开发-使用springboot进行Mybatis连接数据库步骤
本文介绍了使用Java和IDEA进行数据库操作的详细步骤,涵盖从数据库准备到测试类编写及运行的全过程。主要内容包括: 1. **数据库准备**:创建数据库和表。 2. **查询数据库**:验证数据库是否可用。 3. **IDEA代码配置**:构建实体类并配置数据库连接。 4. **测试类编写**:编写并运行测试类以确保一切正常。
450 2
|
Java 数据库连接 Maven
mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和MyBatis Generator,使用逆向工程来自动生成Java代码,包括实体类、Mapper文件和Example文件,以提高开发效率。
589 2
mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。