手撸Mybatis 分页插件的实现
1、分页插件
插件
- Mybatis 通过提供插件机制,让我们可以根据自己的需要去增强MyBtais的功能。
- Mybatis 的插件可以在不修复原来的代码情况下,通过拦截的方式,改变四大核心对象的行为,比如 处理参数、处理SQL、处理结果
MyBatis 插件典型适用场景
分页功能
- Mybatis的分页默认是基于内存分页的(查出所有,再截取),数据量大的情况下效率较低
- 使用mybatis插件可以改变该行为,只需要拦截StatementHandler类的prepare方法,改变要执行的SQL语句为分页语句即可;
公共字段统一赋值
一般业务系统都会有创建者,创建时间,修改者,修改时间四个字段,对于这四个字段的赋值,实际上可以在DAO层统一拦截处理,可以用mybatis插件拦截Executor类的update方法,对相关参数进行统一赋值即可;
性能监控
对于SQL语句执行的性能监控,可以通过拦截Executor类的update, query等方法,用日志记录每个方法执行的时间;
其它
其实mybatis扩展性还是很强的,
- 基于插件机制,基本上可以控制SQL执行的各个阶段,如初始化阶段、执行阶段,参数处理阶段,语法构建阶段,结果集处理阶段,具体可以根据项目业务来实现对应业务逻辑。
插件实现的思考
- 不修改对象的代码,怎么对对象的行为进行修改,比如说在原来的方法前面做一点事情,在原来的方法后面做一点事情?
- 答案:大家很容易能想到用代理模式,这个也确实是MyBatis 插件的原理。动态代理
- 我们可以定义很多的插件,那么这种所有的插件会形成一个链路,比如我们提交一个休假申请,先是项目经理审批,然后是部门经理审批,再是HR 审批,再到总经理审批,怎么实现层层的拦截?
-
答案:插件是层层拦截的,我们又需要用到另一种设计模式——责任链模式。
-
在之前的源码中我们也发现了,mybatis内部对于插件的处理确实使用的代理模式,既然是代理模式,
-
我们应该了解MyBatis 允许哪些对象的哪些方法允许被拦截,并不是每一个运行的节点都是可以被修改的。只有清楚了这些对象的方法的作用,当我们自己编写插件的时候才知道从哪里去拦截。
-
在MyBatis 官网有答案,我们来看一下:mybatis.org/mybatis-3/z…
-
上图可以看出
-
我们的分页插件实现:就是拦截了Executor对象 的 query方法
-
Executor 会拦截到CachingExcecutor 或者BaseExecutor。因为创建Executor 时是先创建CachingExcecutor,再包装拦截。从代码顺序上能看到。我们可以通过mybatis的分页插件来看看整个插件从包装拦截器链到执行拦截器链的过程;如下图
-
我们需要来看看官网对于自定义插件是怎么来做的,官网上有介绍:
通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。
2、自定义Mybatis 分页插件
自定义分页插件
/**
* @version V1.0.0
* @description 自定义分页插件:需要实现 Interceptor接口,重写 intercept方法
* @date 2022/4/23 11:32
* @copyright 2021
*
*/
// 通过注解:指定要拦截什么对象(Executor) 哪些方法(query)
@Intercepts({
@Signature(type = Executor.class,method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
@Signature(type = Executor.class,method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
})
public class MyInterceptor implements Interceptor {
/**
* 实现代理对象
*
* @param invocation
* @return
* @throws Throwable
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("简易版的分页插件:逻辑分页改为物理分页");
//修改sql 拼接Limit 0,10
//获取上面注解拦截的query方法中的参数
Object[] args = invocation.getArgs();
// MappedStatement 对mapper映射文件里面元素的封装
MappedStatement ms = (MappedStatement) args[0];
// boundSql 对sql和参数的封装
Object parameterObj = args[1];
BoundSql boundSql = ms.getBoundSql(parameterObj);
// rowBounds 封装了逻辑分页的参数:当前页 offset、每页数量 limit
RowBounds rowBounds = (RowBounds)args[2];
// 拿到原来的sql语句
String sql = boundSql.getSql();
String limitSql = sql+ " limit " + rowBounds.getOffset() + "," + rowBounds.getLimit();
// 将分页sql重新封装一个 BoundSql 进行后续执行
BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), limitSql, boundSql.getParameterMappings(), parameterObj);
// 被代理的对象
Executor executor = (Executor)invocation.getTarget();
CacheKey cacheKey = executor.createCacheKey(ms, parameterObj, rowBounds, boundSql);
//调用修改过后的sql 继续执行查询
return executor.query(ms,parameterObj,rowBounds,(ResultHandler) args[3],cacheKey,pageBoundSql);
}
}
分页插件使用
-
自定义分页插件:需要实现 Interceptor接口,实现 intercept方法(上步已完成)
-
插件注册,在mybatis-config.xml 中注册插件:
<configuration> -- 注意:插件标签的顺序有要求,否则会报错:properties?,settings?,typeAliases?,typeHandlers?,objectFactory?,objectWrapperFactory?,reflectorFactory?,plugins?,environments?,databaseIdProvider?,mappers? <plugins> <!--自定义分页插件--> <plugin interceptor="com.kaidph.interceptor.MyInterceptor"></plugin> </plugins> </configuration>
-
调用
/** * 自定义分页插件 测试 */ @Test public void test2(){ // 从 xml 中构建SqlSessionFactory String resource = "mybatis-config.xml"; InputStream inputStream = null; try { inputStream = Resources.getResourceAsStream(resource); } catch (IOException e) { e.printStackTrace(); } sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); // 获取sqlSession 获得mapper动态代理,执行方法 try (SqlSession session = sqlSessionFactory.openSession()) { UserMapper mapper = session.getMapper(UserMapper.class); List<User> userList = mapper.selectAllUser(new RowBounds(0,10)); for (User user : userList) { System.out.println(user); } } }
代理和拦截是怎么实现的?
-
上面提到的可以被代理的四大对象都是什么时候被代理的呢?
Executor 是openSession() 的时候创建的;
StatementHandler 是SimpleExecutor.doQuery()创建的;里面包含了处理参数的ParameterHandler 和处理结果集的ResultSetHandler 的创建,创建之后即调用InterceptorChain.pluginAll(),返回层层代理后的对象。
代理是由Plugin 类创建。
在我们重写的 plugin() 方法里面可以直接调用returnPlugin.wrap(target, this);返回代理对象。
-
当多个插件的情况下,代理能不能被代理?
可以被代理
-
代理顺序和调用顺序的关系?
- 因为代理类是Plugin插件,所以最后调用的是Plugin 的invoke()方法。它先调用了定义的拦截器的intercept()方法。可以通过invocation.proceed()调用到被代理对象被拦截的方法。
调用流程时序图: