透过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建立联系的?

正是通过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的关键方法进行拦截,官网有详细介绍。

你可能感兴趣的:(mybatisjava源码分析)