从零开始手写 spring boot mybatis 分页插件


 先上一下最终的效果


 

从零开始手写 spring boot mybatis 分页插件_第1张图片


从零开始手写 spring boot mybatis 分页插件_第2张图片


从零开始手写 spring boot mybatis 分页插件_第3张图片


从零开始手写 spring boot mybatis 分页插件_第4张图片


 通过传入页数page 和 分页大小size后 只需要传递特定的分页对象(Pageable)给Mapper后 无需任何操作即可实现分页

效果,类似于spring data jpa 以及 mybatis-plus的分页功能。


 

一.文件结构以及配置


 build.gradle:

buildscript {
	ext {
		springBootVersion = '1.5.9.RELEASE'
	}
	repositories {
		mavenCentral()
	}
	dependencies {
		classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
	}
}

apply plugin: 'java'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'

group = 'org.zhu'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
	mavenCentral()
}


dependencies {
	compile('org.mybatis.spring.boot:mybatis-spring-boot-starter:1.3.1')
	compile('org.springframework.boot:spring-boot-starter-web')
	runtime('mysql:mysql-connector-java')
	testCompile('org.springframework.boot:spring-boot-starter-test')
}

 文件结构:

从零开始手写 spring boot mybatis 分页插件_第5张图片
mybatis配置:

application.properties:

# ===============================
# = DATA SOURCE
# ===============================

spring.datasource.url = jdbc:mysql://localhost:3306/test?createDatabaseIfNotExist=true&characterEncoding\=utf-8
spring.datasource.userName = root
spring.datasource.password = 
spring.datasource.testWhileIdle = true
spring.datasource.validationQuery = SELECT 1

# ===============================
# = mybatis
# ===============================
mybatis.mapper-locations = classpath*:mapper/*.xml
mybatis.check-config-location=true
mybatis.config-location=classpath:mybatis/mybatis-config.xml
mybatis.type-aliases-package = org.zhu.mybatis_test.bean

mybatis-config.xml:




    
        
    

    
    
        
            
        
    





二.实现


1.总体思路


 spring Data Jpa 的分页很便捷

每次查询的时候只需要传递Pageable对象,即可返回分页的信息:
     "content":{},
     "last": false,
     "totalPages": 2,
     "totalElements": 21,
     "size": 20,
     "number": 0,
     "sort": null,
     "first": true,
    "numberOfElements": 20 


 本插件的分页 如下所示:


从零开始手写 spring boot mybatis 分页插件_第6张图片

从零开始手写 spring boot mybatis 分页插件_第7张图片

 通过传入自定义的Pageable对象,动态为对应的sql语句加上Limit xx,xx 来实现物理分页,并且自动查询分页总数,动态加入

自定义的Pageable对象中,然后返回查询的List列表,手动的把返回内容加入到自定义的Pageable对象中返回。

2.实现mybatis插件拦截器


 如果要实现如上的某些行为,让分页全程自动化运行,那么就需要在mapper对应的方法执行过程中,拦截并且修改默认行为

这个时候就需要自己手动实现mybatis的插件拦截器。

 引自官方对插件的介绍:MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。
链接:http://www.mybatis.org/mybatis-3/zh/configuration.html#plugins
 
mybatis可以拦截sqlSession执行下的4大行为的某些方法(按顺序进行排列):Executor ->StatementHandler->ParameterHandler -> ResultSetHandler

 4大行为可以参考blog:http://blog.csdn.net/ykzhen2015/article/details/50315027
插件原理参考blog:http://blog.csdn.net/abcd898989/article/details/51244351

首先根据官网,我们来写一个简单的拦截器:

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import java.sql.Connection;
import java.util.Properties;

/**
 * @author yingzhi zhu
 * date 2018/1/19.
 */
@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class, Integer.class}) })
public class PageInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("已拦截");
        return invocation.proceed();
    }
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    @Override
    public void setProperties(Properties properties) {
    }
}
 运行程序,执行一次mapper操作,已生效。


但是这个例子和官网的有些许差异
@Intercepts({@Signature(
  type= Executor.class,
  method = "update",
  args = {MappedStatement.class,Object.class})})
 @Signature里面的内容不一样,type是拦截的接口 method是接口中的方法 args 是方法中的参数。

 这里需要提出的一点,插件拦截器只能拦截 如下4个接口中的某个方法
 通过需求可以发现,需要实现的功能 

  1. 动态的修改sql语句

  2.动态的执行查找总页数的sql语句 

  3. 动态的获取传递过来的参数

 那么拦截的方法中应该做到

  1.可以在拦截的方法中获取数据库连接 并且单独的进行数据库操作

  2.可以获取原sql语句

  3.动态的修改原sql语句

  4.可以获取传递过来的方法中的参数

 满足条件并且最佳的是Statement接口中的prepare方法

从零开始手写 spring boot mybatis 分页插件_第8张图片
 在这个接口中方法参数自带数据库连接对象,并且可以获取BoundSql 和ParameterHander对象。mapper中的sql语句就是

储存在BoundSql中的 ,mapper中参数相关信息就是存储在ParameterHander中的。
 
从零开始手写 spring boot mybatis 分页插件_第9张图片


 
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //获取数据库连接
        Connection connection = (Connection) invocation.getArgs()[0];
        //获取拦截的StatementHandler接口示例对象
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        //获取mybatis中存储sql语句实例的对象
        BoundSql boundSql = statementHandler.getBoundSql();
        //获取mybatis中存储mapper中参数 实例的对象
        ParameterHandler parameterHandler = statementHandler.getParameterHandler();
        return invocation.proceed();
    }
 intercept方法是在拦截方法执行之前进行一段额外的操作,传递过来的实例Invocation 是通过反射机制获取的@Signature中

拦截的类,方法,参数的实例。最后需要通过invocation.procceed()方法执行原有方法,让mybatis从新回到他的生命周期中默认

实现的方法。

 通过以上的代码 即可获取到数据库连接 sql语句 和参数的实例对象。
 
 当然说到这里这节还有最大的一个工作没有做。不是所有的方法我们都能拦截,我们只拦截特定的方法:参数中带有自定义的

Pageable对象的方法。
 
  /**
     * 设置哪些Mybatis对象需要被该插件拦截
     * @param target Mybatis对象
     * @return
     */
    @Override
    public Object plugin(Object target) {
        //只拦截StatementHandler中并且参数中有Pageable对象的方法
        if (target instanceof StatementHandler){
            if (getPageable((StatementHandler) target) != null){
                return Plugin.wrap(target, this);
            }
        }
        //放行
        return target;
    }
    /**
     * 获取方法中传递过来的Pageable对象参数 没有返回NULL
     * @param statementHandler
     * @return
     */
    private Pageable getPageable(StatementHandler statementHandler){
        //取得传递过来的参数对象
        Object o = statementHandler.getParameterHandler().getParameterObject();
        //方式一 如果单只有一个参数
        if (o instanceof Pageable){
            return (Pageable) o;
        }
        //方式二 如果有多个参数 则会封装成Map
        if (o instanceof HashMap){
            for (Object value:((HashMap) o).values()){
                if (value instanceof Pageable){
                    return (Pageable) value;
                }
            }
        }
        return null;
    }


3.为原SQL语句加上Limit


 
    /**
     * 设置分页sql语句
     * @param boundSql
     * @param pageable
     */
    private void setSqlStatement(BoundSql boundSql,Pageable pageable){
        int start = (pageable.getPage()-1)*pageable.getSize();
        int size = pageable.getSize();
        String pagingSql = boundSql.getSql() + " Limit " + start + "," + size;
        //方法1:利用mybatis原生的方法 强行将sql语句写入boundSql中 private final String sql 的字段
        MetaObject metaObject =
                    MetaObject.forObject(boundSql, SystemMetaObject.DEFAULT_OBJECT_FACTORY,
                            SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,new DefaultReflectorFactory());
        metaObject.setValue("sql",pagingSql);
        //方法2:自己写通过java反射机制 强行将sql语句写入boundSql中
//      writeDeclaredField(boundSql, "sql", pagingSql);

    }
 因为BoundSql实例对象是mybatis的生命周期中唯一存放sql语句的地方,所以如果修改对象中的sql参数,便可以直接影响到

最终执行查询数据时的sql。

 但是BoundSql中的sql的属性为 private final String sql; 从零开始手写 spring boot mybatis 分页插件_第10张图片
 这个时候就可以借助java的反射机制,动态的读取,动态的修改类中申明为privite final的属性
 
 解决办法有两种:

  1.利用M ybatis原生的MeteObject 来实现

  2.自己编写来实现

/**
     * 通过反射强行的把值赋予给目标对象的某个属性
     * @param target 目标对象
     * @param fieldName 属性名
     * @param value 值
     * @throws IllegalAccessException
     */
    private void writeDeclaredField(Object target, String fieldName, Object value)
            throws IllegalAccessException {
        if (target == null) {
            throw new IllegalArgumentException("target object must not be null");
        }
        Class cls = target.getClass();
        Field field = getField(cls, fieldName);
        if (field == null) {
            throw new IllegalArgumentException("Cannot locate declared field " + cls.getName() + "." + fieldName);
        }
        field.set(target, value);
    }

    /**
     * 获取某个对象的某个域
     * @param cls 对象
     * @param fieldName 属性名
     * @return
     */
    private static Field getField(final Class cls, String fieldName) {
        for (Class acls = cls; acls != null; acls = acls.getSuperclass()) {
            try {
                Field field = acls.getDeclaredField(fieldName);
                if (!Modifier.isPublic(field.getModifiers())) {
                    field.setAccessible(true);
                    return field;
                }
            } catch (NoSuchFieldException ex) {
                // ignore
            }
        }
        return null;
    }


4.统计分页总数


 
  /**
     * 分页的总数设置
     * @param connection 数据库连接
     * @param boundSql 绑定的sql
     * @param pageable 分页对象
     * @param parameterHandler 参数
     */
    private void setPageTotal(Connection connection,BoundSql boundSql,Pageable pageable,ParameterHandler parameterHandler){
        String countSql = convertToCountSql(boundSql.getSql());
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        try {
            preparedStatement = connection.prepareStatement(countSql);
            //利用mybatis原生方法 对sql语句设置参数
            parameterHandler.setParameters(preparedStatement);
            resultSet = preparedStatement
                    .executeQuery();
            if (resultSet.next()){
                pageable.setTotal(resultSet.getInt(1));
                pageable.setTotalPages((int)Math.ceil((double)pageable.getTotal()/(double)pageable.getSize()));
            }

        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            //释放资源
            if(resultSet != null) {
                try {
                    resultSet.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if(preparedStatement != null) {
                try {
                    preparedStatement.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 将原有语句转化成 统计数量的语句 select XX From XXXXXXXX -> select count(*) From XXXXXXXX
     * @param originSql
     * @return
     */
    private String convertToCountSql(String originSql){
        originSql = originSql.toLowerCase();
        StringBuilder countSql = new StringBuilder("select count(*) ");
        String[] sql = originSql.split("from");
        sql[0] = "";
        boolean flag = true;
        for (String ss:sql){
            if (flag){
                flag = false;
                countSql.append(ss);
            }else {
                countSql.append("from").append(ss);
            }
        }
        return countSql.toString();
    }
 因为在实际操作中 这里的Pageable对象即是通过传递过来的同一个Pageable对象,所以在此处修改Pageable对象,即是修改

原有mapper中传递过来的pageable对象。

 需要注意一下 不能通过 select count(*) from (原sql语句)a 来 这样就相当于把数据查询出来后 在通过统计查询出来的数量进行

计数,性能非常的差,所以需要自己手动写一个语句转化方法,做个测试来对比一下:
从零开始手写 spring boot mybatis 分页插件_第11张图片

从零开始手写 spring boot mybatis 分页插件_第12张图片


5.测试


 写了5个测试用例

/**
 * @author yingzhi zhu
 * date 2018/1/19.
 */
@Mapper
public interface TestMapper {

    /**
     * 获取所有的用户 不分页
     * @return
     */
    List getAll();

    /**
     * 更新用户
     * @return
     */
    int updateUser();

    /**
     * 分页获取用户 
     * @param pageable 分页对象
     * @return
     */
    List selectPage(Pageable pageable);

    /**
     * 分页获取用户 
     * @param pageable 分页对象
     * @param flag 参数1
     * @param id 参数2
     * @return
     */
    List selectPage1(Pageable pageable,@Param("flag") String flag,@Param("id") int id);

    /**
     * 分页获取用户 
     * @param userPage 继承自Pageable的实体对象
     * @return
     */
    List selectPage2(UserPage userPage);
}
 结果是完美通过这5个测试用例,其实可以再次通过拦截ResultSetHandler 来对参数进行设置 自动返回Page对象而不是List 但是 还是再次设置一个拦截器是会损耗性能的,没有想到好的办法,并且意义不是很大,所以目前就只能做到手动设置返回的数据。


6.源码

gitosc: https://gitee.com/1098626303/practice/tree/master/mybatis_test

PageInterceptor.java:

package org.zhu.mybatis_test;

import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.zhu.mybatis_test.bean.Pageable;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Optional;
import java.util.Properties;

/**
 * @author yingzhi zhu
 * date 2018/1/19.
 */
@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class, Integer.class}) })
public class PageInterceptor implements Interceptor {

    /**
     * 拦截后的实际处理
     * @param invocation 通过反射机制获取的@Signature中拦截的类,方法,参数的实例
     * @return 拦截方法的返回值 这里的返回值则为StatementHandler.prepare方法执行后的返回值.
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //获取数据库连接
        Connection connection = (Connection) invocation.getArgs()[0];
        //获取拦截的StatementHandler接口示例对象
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        //获取mybatis中存储sql语句实例的对象
        BoundSql boundSql = statementHandler.getBoundSql();
        //获取mybatis中存储mapper中参数 实例的对象
        ParameterHandler parameterHandler = statementHandler.getParameterHandler();
        //Pageable对象检测
        Optional.ofNullable(getPageable(statementHandler)).ifPresent(
                pageable->{
                      //设置分页的总数
               setPageTotal(connection,boundSql,pageable,parameterHandler);
               //设置分页sql语句
               setSqlStatement(boundSql,pageable);
                }
        );
   //执行拦截的方法 并返回
        return invocation.proceed();
    }

    /**
     * 设置分页sql语句
     * @param boundSql
     * @param pageable
     */
    private void setSqlStatement(BoundSql boundSql,Pageable pageable){
        int start = (pageable.getPage()-1)*pageable.getSize();
        int size = pageable.getSize();
        String pagingSql = boundSql.getSql() + " Limit " + start + "," + size;
        //方法1:利用mybatis原生的方法 强行将sql语句写入boundSql中 private final String sql 的字段
        MetaObject metaObject =
                    MetaObject.forObject(boundSql, SystemMetaObject.DEFAULT_OBJECT_FACTORY,
                            SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,new DefaultReflectorFactory());
        metaObject.setValue("sql",pagingSql);
        //方法2:自己写通过java反射机制 强行将sql语句写入boundSql中
//      writeDeclaredField(boundSql, "sql", pagingSql);
    }

    /**
     * 分页的总数设置
     * @param connection 数据库连接
     * @param boundSql 绑定的sql
     * @param pageable 分页对象
     * @param parameterHandler 参数
     */
    private void setPageTotal(Connection connection,BoundSql boundSql,Pageable pageable,ParameterHandler parameterHandler){
        String countSql = convertToCountSql(boundSql.getSql());
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        try {
            preparedStatement = connection.prepareStatement(countSql);
            //利用mybatis原生方法 对sql语句设置参数
            parameterHandler.setParameters(preparedStatement);
            resultSet = preparedStatement
                    .executeQuery();
            if (resultSet.next()){
                pageable.setTotal(resultSet.getInt(1));
                pageable.setTotalPages((int)Math.ceil((double)pageable.getTotal()/(double)pageable.getSize()));
            }

        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            //释放资源
            if(resultSet != null) {
                try {
                    resultSet.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if(preparedStatement != null) {
                try {
                    preparedStatement.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 将原有语句转化成 统计数量的语句 select XX From XXXXXXXX -> select count(*) From XXXXXXXX
     * @param originSql
     * @return
     */
    private String convertToCountSql(String originSql){
        originSql = originSql.toLowerCase();
        StringBuilder countSql = new StringBuilder("select count(*) ");
        String[] sql = originSql.split("from");
        sql[0] = "";
        boolean flag = true;
        for (String ss:sql){
            if (flag){
                flag = false;
                countSql.append(ss);
            }else {
                countSql.append("from").append(ss);
            }
        }
        return countSql.toString();
    }

    /**
     * 设置哪些Mybatis对象需要被该插件拦截
     * @param target Mybatis对象
     * @return
     */
    @Override
    public Object plugin(Object target) {
        //只拦截StatementHandler中并且参数中有Pageable对象的方法
        if (target instanceof StatementHandler){
            if (getPageable((StatementHandler) target) != null){
                return Plugin.wrap(target, this);
            }
        }
        //放行
        return target;
    }
    /**
     * 获取方法中传递过来的Pageable对象参数 没有返回NULL
     * @param statementHandler
     * @return
     */
    private Pageable getPageable(StatementHandler statementHandler){
        //取得传递过来的参数对象
        Object o = statementHandler.getParameterHandler().getParameterObject();
        //方式一 如果单只有一个参数
        if (o instanceof Pageable){
            return (Pageable) o;
        }
        //方式二 如果有多个参数 则会封装成Map
        if (o instanceof HashMap){
            for (Object value:((HashMap) o).values()){
                if (value instanceof Pageable){
                    return (Pageable) value;
                }
            }
        }
        return null;
    }

    @Override
    public void setProperties(Properties properties) {
    }

    /**
     * 通过反射强行的把值赋予给目标对象的某个属性
     * @param target 目标对象
     * @param fieldName 属性名
     * @param value 值
     * @throws IllegalAccessException
     */
    private void writeDeclaredField(Object target, String fieldName, Object value)
            throws IllegalAccessException {
        if (target == null) {
            throw new IllegalArgumentException("target object must not be null");
        }
        Class cls = target.getClass();
        Field field = getField(cls, fieldName);
        if (field == null) {
            throw new IllegalArgumentException("Cannot locate declared field " + cls.getName() + "." + fieldName);
        }
        field.set(target, value);
    }

    /**
     * 获取某个对象的某个域
     * @param cls 对象
     * @param fieldName 属性名
     * @return
     */
    private static Field getField(final Class cls, String fieldName) {
        for (Class acls = cls; acls != null; acls = acls.getSuperclass()) {
            try {
                Field field = acls.getDeclaredField(fieldName);
                if (!Modifier.isPublic(field.getModifiers())) {
                    field.setAccessible(true);
                    return field;
                }
            } catch (NoSuchFieldException ex) {
                // ignore
            }
        }
        return null;
    }
}




你可能感兴趣的:(项目经验,源码分析)