透过PageHelper看Mybatis插件机制
一、startPage做了什么
PageHelper.startPage(1,20);
这是PageHelper推荐的使用方式,后续的第一个查询方法会分页。
观察这个方法的内部逻辑
public static Page startPage(int pageNum, int pageSize, boolean count) {
// 封装成page对象
Page page = new Page(pageNum, pageSize, count);
// == 将page对象存在`PageMethod#LOCAL_PAGE`——这是一个ThreadLocal
setLocalPage(page);
return page;
}
二、PageHelper如何与Mybatis建立联系的? 【透过PageHelper看Mybatis插件机制】正是通过mybatis的插件机制plugin,入口在executor创建部分。
org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSession()
org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSessionFromDataSource
org.apache.ibatis.session.Configuration#newExecutor(org.apache.ibatis.transaction.Transaction, org.apache.ibatis.session.ExecutorType){
// == 过滤器链对executor做封装
executor = (Executor) interceptorChain.pluginAll(executor);
}
观察
InterceptorChain interceptorChain
结构:class InterceptorChain {
// ## 1.拦截器
private final List interceptors = new ArrayList<>();
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
// ## 2.包装
target = interceptor.plugin(target);
}
return target;
}
1.拦截器 被@Intercepts注解修饰的类就是拦截器,也就是所谓的插件。
以PageInterceptor为例:
@Intercepts(
{
// 通过@Signature指定被拦截的方法
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
}
)
public class PageInterceptor implements Interceptor
2.包装 查看具体的包装方法
default Object plugin(Object target) {
return Plugin.wrap(target, this);
??????
// == 核心逻辑是动态代理
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
观察invokcationHandler(Plugin)的invoke方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 执行拦截器的拦截方法
return interceptor.intercept(new Invocation(target, method, args));
}
观察PageInterceptor#intercept实现:
com.github.pagehelper.PageInterceptor#intercept{
// == 判断是否需要进行分页,如果不需要,直接返回结果
if (!dialect.skip(ms, parameter, rowBounds)) {
// -- 判断是否需要进行分页查询,page.getPageSize>0
if (dialect.beforePage(ms, parameter, rowBounds)) {
// -- 调用方言获取分页sql,添加limit语句
String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
//执行分页查询
resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
}
}
}
- 怎么判断是否需要分页?
com.github.pagehelper.PageHelper#skip
public boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
// == PageMethod#LOCAL_PAGE中是否有值
Page page = pageParams.getPage(parameterObject, rowBounds);
// -- PageMethod#LOCAL_PAGE有值情况,初始化方言
autoDialect.initDelegateDialect(ms);
return false;
}
这里就和第一节连上了。
PageHelper.startPage向ThreadLocal中存放了page对象
此处skip()方法判断:如果有值返回false,需分页同时初始化方言dialect;无值返回true,不必分页。
- 方言dialect的初始化逻辑
com.github.pagehelper.page.PageAutoDialect#initDelegateDialect{
this.delegate = getDialect(ms);
}
com.github.pagehelper.page.PageAutoDialect#getDialect{
// 通过数据库链接截取别名
String dialectStr = fromJdbcUrl(url);
// -- 通过别名初始化
AbstractHelperDialect dialect = initDialect(dialectStr, properties);
}com.github.pagehelper.page.PageAutoDialect#initDialect{
// 解析方言的class
Class sqlDialectClass = resloveDialectClass(dialectClass);
?????
// == 通过dialectAliasMap获取
dialectAliasMap.get(className.toLowerCase());
// 通过反射创建
dialect = (AbstractHelperDialect) sqlDialectClass.newInstance();
}
我们看看dialectAliasMap中存了什么
com.github.pagehelper.page.PageAutoDialect#dialectAliasMap
static {
//初始化时注册别名
dialectAliasMap.put("hsqldb", HsqldbDialect.class);
dialectAliasMap.put("h2", HsqldbDialect.class);
dialectAliasMap.put("postgresql", HsqldbDialect.class);
dialectAliasMap.put("mysql", MySqlDialect.class);
}
mybatis预留扩展
// 参数处理
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
// 参数结果
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
ResultHandler resultHandler, BoundSql boundSql) {
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
}
// Statement处理
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
上述方法都有interceptorChain.pluginAll的影子,我们可以自己写拦截器对ParameterHandler、ResultSetHandler、StatementHandler的关键方法进行拦截,官网有详细介绍。
推荐阅读
- vue|Vue | 原生audio样式不好看,自己写一个简易的音乐播放控件,实现播放暂停可拖动
- 译文 | 一文看懂技术债
- 从|从 TikTok“重 QA 轻测试”来看中美软件开发之间的差异
- 如何利用Matlab绘制出好看的火山图
- 源码系列|查看动态代理生成的类文件
- 软件测试|老大说要自动化测试,我是怎么做的可以看看
- java|2022年支付宝集五福|看这里100%扫敬业福
- 遇见SQL|下课看着文档走回实验室,我重新拾起了遗忘的SQL运算符
- 看到字节跳动28岁员工猝死,我沉默了......
- IDEA高效查看源码的快捷键及小技巧