先上一下最终的效果:
通过传入页数page 和 分页大小size后 只需要传递特定的分页对象(Pageable)给Mapper后 无需任何操作即可实现分页
效果,类似于spring data jpa 以及 mybatis-plus的分页功能。
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')
}
# ===============================
# = 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
"content":{},"last": false,"totalPages": 2,"totalElements": 21,"size": 20,"number": 0,"sort": null,"first": true,"numberOfElements": 20
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 是方法中的参数。
@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中
/**
* 设置哪些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;
}
/**
* 设置分页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参数,便可以直接影响到
/**
* 通过反射强行的把值赋予给目标对象的某个属性
* @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;
}
/**
* 分页的总数设置
* @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对象,即是修改
写了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
但是 还是再次设置一个拦截器是会损耗性能的,没有想到好的办法,并且意义不是很大,所以目前就只能做到手动设置返回的数据。
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;
}
}