乐趣区

关于mybatis:透过PageHelper看Mybatis插件机制

一、startPage 做了什么

PageHelper.startPage(1,20);

这是 PageHelper 举荐的应用形式,后续的第一个查询方法会分页。

察看这个办法的外部逻辑

public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count) {
    // 封装成 page 对象
    Page<E> page = new Page<E>(pageNum, pageSize, count);
    // == 将 page 对象存在 `PageMethod#LOCAL_PAGE`——这是一个 ThreadLocal
    setLocalPage(page);
    return page;
}

二、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<Interceptor> 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 的要害办法进行拦挡,官网有具体介绍。

退出移动版