准备系列-Mybatis(十一) TKmybatis 自定义Interceptor通用拦截器分页

517 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 11 天,点击查看活动详情

前面几篇文章,我们介绍了 逻辑分页及物理分页的几种实现方式,包括 内存分页,limit分页,RowRounds分页, 这几种都属于定制SQL, 操作起来不方便

今天我们实现一种通用的分页方式,通过自定义Interceptor来实现Mybatis分页

1. 创建Interceptor

Interceptor 底层实现逻辑还是Limit 来控制底层SQL 逻辑来实现分页效果

import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;

import java.sql.Connection;
import java.util.Properties;

/**
 *  利用MyBatis拦截器进行分页
 *
 *  @Intercepts 说明是一个拦截器
 *  @Signature 拦截器的签名
 *  type 拦截的类型 四大对象之一( Executor,ResultSetHandler,ParameterHandler,StatementHandler)
 *
 */
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
@Slf4j
public class DefinedPageInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //获取StatementHandler,默认的是RoutingStatementHandler
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        //获取StatementHandler的包装类
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        //分隔代理对象
        while (metaObject.hasGetter("h")) {
            Object obj = metaObject.getValue("h");
            metaObject = SystemMetaObject.forObject(obj);
        }
        while (metaObject.hasGetter("target")) {
            Object obj = metaObject.getValue("target");
            metaObject = SystemMetaObject.forObject(obj);
        }
        //获取查看接口映射的相关信息
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        String mapId = mappedStatement.getId();
        //拦截以ByInterceptor结尾的请求,统一实现分页
        if (mapId.matches(".+ByInterceptor$")) {
            log("已触发分页拦截器");
            //获取进行数据库操作时管理参数的Handler
            ParameterHandler parameterHandler = (ParameterHandler) metaObject.getValue("delegate.parameterHandler");
            //获取请求时的参数
            PageInfo info = (PageInfo) parameterHandler.getParameterObject();
            //获取原始SQL语句
            String originalSql = (String) metaObject.getValue("delegate.boundSql.sql");
            //构建分页功能的SQL语句
            String sql = originalSql.trim() + " limit " + info.getPageNo() + ", " + info.getPageSize();
            metaObject.setValue("delegate.boundSql.sql", sql);
        }
        //调用原对象方法,进入责任链下一级
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        //生成Object对象的动态代理对象
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        //如果分页每页数量是统一的,可以在这里进行统一配置,也就无需再传入PageInfo信息了
    }
}

他是如何实现 拦截的 ?

注意看这里 这里会对方法进行遍历 只要你的方法结尾是以 ByInterceptor 结尾的 方法, 都会被拦截到实现 分页逻辑,从而实现了对所有方法的统一处理

image.png

然后 里面的Page 就是 hutool工具包中的Page分页信息
!!! 注意这里 就是拼装limit信息
!!! 注意这里 就是拼装limit信息
!!! 注意这里 就是拼装limit信息

重要事情说三遍
我们要取的是 page的起始位置 getStartPosition信息

" limit " + info.getStartPosition() + ", " + info.getPageSize();

2.配置Interceptor

我们需要构造自己的 SqlSessionFactory 来配置interceptor

!!!!! 注意 SqlSession的Plugins的 interceptor 要用 mybatis 配置包中的信息

package com.jzj.tdmybatis.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.apache.ibatis.plugin.Interceptor;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;


import javax.sql.DataSource;
import java.io.IOException;

@Configuration
public class DataSourceConfiguration {
 
    @ConfigurationProperties(prefix = "spring.datasource.druid")
    @Bean
    public DataSource dataSource(){
        return new DruidDataSource();
    }


    @Bean
    public SqlSessionFactoryBean sqlSession() {
        SqlSessionFactoryBean sqlSession = new SqlSessionFactoryBean();
        sqlSession.setDataSource(dataSource());
        try {
            Resource[] resources = new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml");
            sqlSession.setMapperLocations(resources);
            //配置自定义的Interceptro作为MyBatis的Interceptor,完成分页操作
            DefinedPageInterceptor definedPageInterceptor = new DefinedPageInterceptor();

            //这里的 interceptor 要用 mybatis包中的
            sqlSession.setPlugins(new Interceptor[]{definedPageInterceptor});
            return sqlSession;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

}

3. XML 文件中方法实现

我们只需要在 XML文件中定义方法时候 以 "ByInterceptor"结尾即可

查询UserInfo信息


/**
 * Page 就是 hutool 工具包中的 Page分页
 */
List<UserInfoPO> selectMyUserByInterceptor(Page page);

=======================================================

<select id="selectMyUserByInterceptor" resultType="com.jzj.tdmybatis.domain.po.UserInfoPO">
    select * from user_info
</select>

4.测试Service及TestController信息

Service信息


/**
 * 测试自定义拦截器分页插件
 */
List<UserInfoPO> interceptorPage(Page page);

============================================

@Override
public List<UserInfoPO> interceptorPage(Page page) {
    return  mapper.selectMyUserByInterceptor(page);
}

TestController信息


/**
 * 查询user接口
 */
@RequestMapping("/temp/query9")
@ResponseBody
public void query9(Integer pageNumber) {
    //pageNumber 默认都是 从第1页开始,每页取3个
    Page page = new Page(pageNumber - 1, 3);


    List<UserInfoPO> interceptorPage = pageService.interceptorPage(page);
    log.info(" interceptorPage 第{}页 数据 \r\n:{} ", page.getPageNumber(), printUser(interceptorPage));
}


private String printUser(List<UserInfoPO> users) {
    StringBuffer sb = new StringBuffer();
    for (UserInfoPO user : users) {
        sb.append(user.toString()).append("\r\n");
    }
    return sb.toString();
}

5.查询结果

请求 curl 127.0.0.1:8800/temp/query9?pageNumber=3

image.png

可以看到查询结果
的确是查询出来3条数据, 分页信息 也是正确的 根据日志 可以看出来 的确是触发了分页拦截器
c.j.t.config.DefinedPageInterceptor : 已触发分页拦截器

日志信息

Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@75b875b5] was not registered for synchronization because synchronization is not active
2023-02-11 22:57:04.602  INFO 12840 --- [nio-8800-exec-1] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} inited
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@7e89eb56] will not be managed by Spring
2023-02-11 22:57:04.603  INFO 12840 --- [nio-8800-exec-1] c.j.t.config.DefinedPageInterceptor      : 已触发分页拦截器
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@75b875b5]
2023-02-11 22:57:04.625  INFO 12840 --- [nio-8800-exec-1] c.j.tdmybatis.controller.TestController  :  interceptorPage 第2页 数据 
:UserInfoPO{id=7, userId='null', userName='null', age=41, address='郑州'}
UserInfoPO{id=8, userId='null', userName='null', age=4, address='武汉'}
UserInfoPO{id=9, userId='null', userName='null', age=14, address='武汉'}

查询结果 image.png

现在我们已经可以通过自定义的Interceptor来实现 Mybatis的自定义分页逻辑了,但是这种方式归根到底就是 通过方法的 Interceptor规则来实现切面

通过对切面的每一个方法进行拦截,实现拼接Limit方法 来实现的, 这种如果 limit的起始页比较大的时候,游标位置会查询量很大,效率低下

我们还有一种以来插件分页的方法, 下篇文章我们着重介绍 PageHelper方式