Mybatis 分页插件 进阶使用

2022-07-14  本文已影响0人  kacen

我们的创建顺序及简述
1.创建分页实体对
2.创建分页工具类对象

  1. 创建分页插件 PagePlugin.java 这个类主要是分页逻辑的具体实现
  2. 创建PageInterceptor分页拦截器类 PageInterceptor.java 这个类主要是在客户端传递分页信息时,进行拦截的作用。
  3. 创建 基于springmvc响应处理器 - 处理返回Result对象统一设置分页信息 PageResponseBodyAdvice
  4. 创建Mybatis相关的工具类 MybatisUtils.java

首先保证有以下依赖

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>

1. 创建分页实体对象

@Data
public class Page {
    /**
     * 当前页
     */
    private Integer pageNum;

    /**
     * 每页显示的条数
     */
    private Integer pageSize;

    /**
     * 总条数
     */
    private Integer pageCount;

    /**
     * 总页码
     */
    private Integer pageTotal;
}

2..创建分页工具类对象

public class ExamplePage {

    public static ThreadLocal<Page> pageThreadLocal = new ThreadLocal<>();

    /**
     * 设置分页信息
     */
    public static void setPage(Integer pageNum, Integer pageSize) {
        Page page = new Page();
        page.setPageNum(pageNum);
        page.setPageSize(pageSize);
        pageThreadLocal.set(page);
    }

    /**
     * 获取分页信息
     */
    public static Page getPage() {
        return  pageThreadLocal.get();
    }

    /**
     * 清除TreadLocal
     */
    public static void clear() {
        pageThreadLocal.remove();
    }
}

3. 创建分页插件 PagePlugin.java

这里我的话是实现了Interceptor来自定义mybatis分页插件。
因为我们需要通过Interceptor来重写相关方法。
@Intercepts的话是指定需要拦截的地方, 类StatementHandler, 方法 prepare, 参数args = {Connection.class, Integer.class}。

当我们在设置页码的时候,很多人会这样写。
我们会先用 pageTotal=PageCount/pageSize 这个来算总页码
pageNum < 1,我们会直接设置为1,
当pageNum > pageTotal总页码的时候,我们会直接设置会总页码。
但是这样写的话会有一个问题,就是PageCount和pageSize都为0的时候那就很尴尬了。
这样子的话会有一个问题
逻辑会先走pageTotal=PageCount/pageSize,并设置PageNum为1,
然后再走pageNum > pageTotal,那这里的pageNum是=1的,而pageTotal为0,最后pageNum又会设置成0.
然后往下走的话会走到我们都会用的分页算法
这里如果再往下走会是0-1=-1,然后报错。
sql += " limit " + (page.getPageNum() - 1 ) * page.getPageSize() + "," + page.getPageSize();

详细代码
这里是具体的步骤中的问题代码

if (page.getPageNum() > page.getPageTotal()) {
            page.setPageNum(page.getPageTotal());
        }

        // 开始进行分页,这里用的是拼接
        // 策略模式 - 根据不同的数据库,选择使用不同的sql分页模式
        // limit 后面需要 (加页码-1)*页面大小, 页面大小(这是算法)
        sql += " limit " + (page.getPageNum() - 1 ) * page.getPageSize() + "," + page.getPageSize();
        log.debug("[page - plugin] 分页sql - {}", sql)

这个是PagePlugin整个类哈,还有问题代码相关的修改

@Intercepts(
        @Signature(
                type = StatementHandler.class,
                method = "prepare",
                args = {Connection.class, Integer.class}
        )
)
@Slf4j
public class PagePlugin implements Interceptor {

    /**
     * 拦截Mybatis PrepareStatement
     * @param invocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取StatementHandler
        // 这里获取到的将是RoutingStatement
        StatementHandler statementHandler = (StatementHandler) MybatisUtils.getProxyObject(invocation.getTarget());
        System.out.println("当前stament对象:"+ statementHandler.getClass().getSimpleName());
        //获取boundSql,里面含有sql语句
        //因为在进行sql语句执行前会通过boundSql进行绑定赋值,所以在这里先获取,以便在拦截并处理后做相应的替换动作
        BoundSql boundSql = statementHandler.getBoundSql();

        // 获取拦截的sql语句
        String sql = statementHandler.getBoundSql().getSql().toLowerCase().replaceAll("\n", "");

        // 判断当前是否为查询的sql,是才可能分页,不是的话就不分页
        if (!sql.startsWith("select")){
            //非查询语句
            return invocation.proceed();
        }

        // 根据当前请求的上下文,获取Page分页对象,如果能获取到,说明需要分页,如果不需要获取,说明无须分页
        // TreadLocal 主要做线程隔离
        Page page = ExamplePage.getPage();
        if (page == null) {
            // 未获取到分页对象
            return invocation.proceed();
        }
        // 开始分页
        log.debug("[page - plugin] 开始分页 - {}", sql);
        // 计算出总条数
        Integer count = getCount(sql, invocation, statementHandler);
        // 进行相关的参数设置
        // 默认显示10条
        if (page.getPageSize() == null) {
            page.setPageSize(10);
        }
        // 设置总条数
        page.setPageCount(count);
        // 总条数%设置的页数大小,如果可以整除证明只显示一页,如果!=0则需要添加页数
        page.setPageTotal(page.getPageCount() % page.getPageSize() == 0 ?
                page.getPageCount() / page.getPageSize() :
                page.getPageCount() / page.getPageSize() + 1
                );
        // 判断页码的临界点,如果<1的话就直接设置为1,大于页码数的话就直接设置为页码数
        if (page.getPageNum() < 1) {
            page.setPageNum(1);
        }
        if (page.getPageNum() > page.getPageTotal() && page.getPageTotal() > 0) {
            page.setPageNum(page.getPageTotal());
        }

        // 开始进行分页,这里用的是拼接
        // 策略模式 - 根据不同的数据库,选择使用不同的sql分页模式
        // limit 后面需要 (加页码-1)*页面大小, 页面大小(这是算法)
        sql += " limit " + (page.getPageNum() - 1 ) * page.getPageSize() + "," + page.getPageSize();
        log.debug("[page - plugin] 分页sql - {}", sql);

        // 开始执行分页sql
        // 将修改后的sql替换原来的sql,然后放行
        MetaObject metaObject = SystemMetaObject.forObject(boundSql);
        metaObject.setValue("sql", sql);
        log.debug("[page - plugin] 分页完成!");
        //放行,因为已经修改好了,Mybatis会执行修改后的sql
        return invocation.proceed();
    }

    /**
     * 计算查询到总条数
     * @return 返回总条数
     */
    private  Integer getCount(String sql, Invocation invocation, StatementHandler statementHandler) throws SQLException {

        int count = 0;
        // 获取相关参数
        Connection connection = (Connection) invocation.getArgs()[0];
        // *后面必须有 空格
        String countSql = "select count(*) as count " + sql.substring(sql.indexOf("from"));
        log.debug("[page - plugin] 生成记录总条数的sql语句 - {}", countSql);
        // 执行当前的sql语句
        ResultSet resultSet = null;
        PreparedStatement preparedStatement = null;
        try{
            preparedStatement = connection.prepareStatement(countSql);
            // 设置sql相关参数, Mybatis自带的StatamentHandler中页有parametersize这个方法来将参数进行设置,
            // 所以只要直接调用其方法既可跟平时写的sql一样将参数传入
            statementHandler.parameterize(preparedStatement);
            // 执行sql
            resultSet = preparedStatement.executeQuery();

            if (resultSet.next()) {
                // 直接获取count结果
                count = resultSet.getInt("count");
                log.debug("[page - plugin] 获取查询的记录总条数 {}", count);
                return count;
            }
        }catch (SQLException e) {
            throw new RuntimeException(e);
        } finally{
            if (resultSet != null) {
                resultSet.close();
            }
            if (preparedStatement != null) {
                preparedStatement.close();
            }
        }
        return count;
    }
}

4. 创建PageInterceptor分页拦截器类 PageInterceptor.java

@Component
@Slf4j
public class PageInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String pageNumStr = request.getParameter("pageNum");
        String pageSizeStr = request.getParameter("pageSize");
        log.debug("[page interceptor] 分页拦截器设置pageNum:{},设置pageSize{}",pageNumStr,pageSizeStr);
        if (pageNumStr != null && pageSizeStr != null) {
            try {
                int pageNum = Integer.parseInt(pageNumStr);
                int pageSize = Integer.parseInt(pageSizeStr);
                PbShopPage.setPage(pageNum,pageSize);
            } catch (Throwable e) {
                return true;
            }
        }
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //清空ThreadLocal
        log.debug("[page interceptor] 分页拦截器后置处理,清除ThreadLocal");
        PbShopPage.clear();
    }
}

5. 创建 基于springmvc响应处理器 - 处理返回Result对象统一设置分页信息 PageResponseBodyAdvice

@Slf4j
@RestControllerAdvice
public class PageResponseBodyAdvice implements ResponseBodyAdvice<Result> {

    /**
     * 判断什么条件出发beforeBodyWrite方法
     * @param returnType the return type
     * @param converterType the selected converter type
     * @return
     */
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        // 判断类型是否是Result类型的
        return returnType.getMethod().getReturnType() == Result.class;
    }

    @Override
    public Result beforeBodyWrite(Result body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        //获取Threadlocal中的page对象
        Page page = PbShopPage.getPage();
        if (page == null) {
            return body;
        }
        log.debug("[page responseAdvice] 分页返回值处理器生效: {}",page);
        //将分页信息放到Result实体类中一并返回给客户端
        body.setPage(page);
        return body;
    }
}

6. 创建Mybatis相关的工具类 MybatisUtils.java

/**
     * 直接获取到代理的内置对象
     * @param target
     * @return
     */
    public static Object getProxyObject(Object target) {
        MetaObject metaObject = SystemMetaObject.forObject(target);
        while (metaObject.hasGetter("h")) {
            // 说明当前是一个代理对象
            // 获取代理对象中持有的对象
            target = metaObject.getValue("h.target");
            metaObject = SystemMetaObject.forObject(target);
        }
        return target;
    }

    /**
     * 返回一个sql语句中 主标的from 的index的位置
     * @param beginIndex 起始from位置
     * @param sql 传入主sql语句
     * @return
     */
    public static Integer getMainFrom(int beginIndex, String sql) {
        if (sql == null) {
            return -1;
        }
        //从index开始处往后查找,获取from关键词的下表
        int fromIndex = sql.indexOf(" from ", beginIndex);
        if (fromIndex == -1) {
            return -1;
        }

        String sqlSub = sql.substring(0, fromIndex);
        System.out.println(sqlSub);
        int count = 0;
        //转换成char数组
        char[] chars = sqlSub.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            char c = chars[i];
            if (c == '(') {
                count++;
            }
            if (c == ')') {
                count--;
            }
        }

        //判断计数器
        if (count == 0) {
            return fromIndex;
        }else {
            return getMainFrom(fromIndex + 6, sql);
        }
    }

    public static void main(String[] args) {
        String sql = "select asdf,asdf,(select a,(select as from sjs),(select 1,3 from b),a from A) from Order where id = (select id from t)";
    System.out.println(getMainFrom(0, sql));
        String s = "select count(*) as count" + sql.substring(getMainFrom(0, sql));
        System.out.println(s);
    }
以上配置完后就可以直接使用啦。有具体的需求需要更改的可以再进心更改。
上一篇下一篇

猜你喜欢

热点阅读