一、我们先来看一下mybatis的执行流程。从图中可以看出在Excutor之前都是在加载配置,创建SqlSeesion等工作.
所以mybatis允许拦截的地方只有四处:
1、Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
2、ParameterHandler (getParameterObject, setParameters)
3、ResultSetHandler (handleResultSets, handleOutputParameters)
4、StatementHandler (prepare, parameterize, batch, update, query)
总体概括为:
1、拦截执行器的方法
2、拦截参数的处理
3、拦截结果集的处理
4、拦截Sql语法构建的处理
二、在我们插件实现中会用到MetaObject类,我们先简单的讲一下这个类,这是mybatis提供的一个的工具类,通过它包装一个对象后可以获取或设置该对象的原本不可访问的属性(比如那些私有属性,通过反射的方法)。它有个三个重要方法经常用到:
* MetaObject forObject(Object object,ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory) 用于包装对象;
* Object getValue(String name) 用于获取属性的值(支持OGNL的方法);
* void setValue(String name, Object value) 用于设置属性的值(支持OGNL的方法);
举个简单的例子
//java bean
Example javaBean = new Example();
javaBean.setContent("hello");
MetaObject javaBeanMeta = MetaObject.forObject(javaBean,
DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, DEFAULT_REFLECTOR_FACTORY);
System.out.println(javaBeanMeta.getValue("content"));
javaBeanMeta.setValue("content", "world");
System.out.println(javaBeanMeta.getValue("content"));
我们通过MetaObject就可以获得或者给对象的私有属性赋值。MetaObject还有一些其他的功能我们先不深究。
三、实现mybatis分页插件
1、设置拦截器类型
因为我们是对sql进行重新拼装,所以这了我们对StatementHandler进行拦截(上面说到的:拦截Sql语法构建的处理)
2、重写intercept、plugin、setProperties方法
3、把处理后的sql赋值给Boundsql.
4、在配置中添加拦截器配置
我们在这里做一个设定,如果查询方法以ByPage结尾的,我们才对他进行拦截分页处理。
四、具体代码PageIntercept
package com.lw.study.mybatisIntercepter;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.parameter.ParameterHandler;
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 java.sql.Connection;
import java.util.Map;
import java.util.Properties;
/**
* @author
* @date 14:21 2019/7/1
* 写一个mybatis拦截器需要实现Interceptor接口,同时类上面加上Interceptors注解,
* Signature标签中申明你要拦截的handler,我们这里是StatementHandler,以及你要拦截的方法,args是方法的参数类型
*/
@Intercepts({
@Signature(type = StatementHandler.class,method = "prepare",args = {Connection.class,Integer.class})
})
public class PageIntercept implements Interceptor{
private String dbType;
@Override
public Object intercept(Invocation invocation) throws Throwable {
//获取StatementHandler
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
//我们这里使用的SystemMetaObject是mybatis帮我们封装好了的,点进去看就知道,是一个默认的MetaObject构造方法。可以省去我们的一些操作
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
// 分离代理对象链(由于目标类可能被多个拦截器拦截,从而形成多次代理,通过下面的两次循环
// 可以分离出最原始的的目标类)
while (metaObject.hasGetter("h")) {
Object object = metaObject.getValue("h");
System.out.println(object.toString());
metaObject = SystemMetaObject.forObject(object);
}
// 分离最后一个代理对象的目标类
while (metaObject.hasGetter("target")) {
Object object = metaObject.getValue("target");
System.out.println(object.toString());
metaObject = SystemMetaObject.forObject(object);
}
//获取查询接口映射
// 只重写需要分页的sql语句。通过MappedStatement的ID匹配,默认重写以Page结尾的
// MappedStatement的sql
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
String mapId = mappedStatement.getId();
//正则匹配.ByPage结尾的请求。分页统一实现
if(mapId.matches(".+ByPage$")){
//管理参数的handler
ParameterHandler parameterHandler = (ParameterHandler) metaObject.getValue("delegate.parameterHandler");
//获取请求时的参数,这里我们查询sql使用的是map来封装查询参数,当然,优雅一点我们可以创建一个对象来封装。
Map<String,Object> map = (Map<String, Object>) parameterHandler.getParameterObject();
//当前页数
int currPage = (int) map.get("currPage");
//每页条数
int pageSize = (int) map.get("pageSize");
//原始sql
String originalSql = statementHandler.getBoundSql().getSql().trim();
System.out.println("获取原始sql:"+ originalSql);
if (originalSql.endsWith(";")) {
//去掉原始sql末尾的分号,以便我们对其进行拼接。
originalSql = originalSql.substring(0, originalSql.length() - 1);
}
//注意拼接时limit左右两边的空格,不然就是连在一起了,会有语法错误。
String limitSql = originalSql + " limit " + (currPage - 1) * pageSize + "," + pageSize;
//将拼装后的sql复制给BoundSQL
metaObject.setValue("delegate.boundSql.sql",limitSql);
}
//最后将处理权限返回给mybatis,让它继续执行。
return invocation.proceed();
}
@Override
public Object plugin(Object o) {
return Plugin.wrap(o, this);
}
@Override
public void setProperties(Properties properties) {
}
}
最后别忘了加上我们的拦截器配置
如果你是使用的xml配置。就在sqlSessionFactoryBean配置里加上
<plugins>
<plugin interceptor="com.xxx.xxx.utils.MyBatisPagePlugin">
</plugin>
</plugins>
我这里使用的是springboot搭建的项目,直接在代码里配置了,主要是加了写注释的那一行。
@Configuration
@EnableTransactionManagement
public class MyBatisConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource(){
return new org.apache.tomcat.jdbc.pool.DataSource();
}
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactoryBean() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource());
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath:/sqlmap/*.xml"));
//在这里加上我们的插件配置。
sqlSessionFactoryBean.setPlugins(new Interceptor[]{new PageIntercept()});
return sqlSessionFactoryBean.getObject();
}
}
来个测试类测试一下,查出来前五条数据。
@Test
public void testUser() {
Map<String, Object> map = new HashMap<>();
map.put("idStart", 1000);
map.put("idEnd", 2000);
map.put("currPage", 1);
map.put("pageSize", 5);
//根据id范围查询user信息
List<User> list = userExtMapper.getAllUserByPage(map);
System.out.println(list.toString());
}