头图

插件即plugin,本质是一系列拦截器,便于用户自定义扩展。
预留了四处扩展点:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

接下来以Pagehelper为例,看看它怎么实现的Executor扩展。

一、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建立联系的?

1.配置

mybatis的xml中增加配置

<plugins>
    <!-- com.github.pagehelper为PageHelper类所在包名 -->
    <plugin interceptor="com.github.pagehelper.PageInterceptor">
        <!-- 使用下面的方式配置参数,后面会有所有的参数介绍 -->
        <property name="param1" value="value1"/>
    </plugin>
</plugins>

增加插件配置后,会在xml解析时放入configuration中

org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration{
    // 解析插件
    pluginElement(root.evalNode("plugins"));
}
org.apache.ibatis.builder.xml.XMLConfigBuilder#pluginElement{
    for (XNode child : parent.getChildren()) {
        // 根据配置创建Interceptor对象
        String interceptor = child.getStringAttribute("interceptor");
        Properties properties = child.getChildrenAsProperties();
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
        // 执行setProperties方法设置属性
        interceptorInstance.setProperties(properties);
        // == 将拦截器存入configuration中
        configuration.addInterceptor(interceptorInstance);
    }
}

看看Interceptor最终存放到了哪里?

org.apache.ibatis.session.Configuration#addInterceptor
org.apache.ibatis.plugin.InterceptorChain#addInterceptor

class InterceptorChain {
    public void addInterceptor(Interceptor interceptor) {
        interceptors.add(interceptor);
    }
    // == 最终存放位置
    private final List<Interceptor> interceptors = new ArrayList<>();

结构

观察PageInterceptor的结构

// == 被@Intercepts注解修饰
@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}),
    }
)
// == 实现Interceptor接口
public class PageInterceptor implements Interceptor

Interceptor接口有三个方法:

// == 出发拦截后,会执行此方法逻辑
Object intercept(Invocation invocation) throws Throwable;

// == 默认的包装
default Object plugin(Object target) {
    return Plugin.wrap(target, this);
}

// 初始化时设置属性
default void setProperties(Properties properties) {}

触发

PageInterceptor的入口在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);
}
org.apache.ibatis.plugin.InterceptorChain#pluginAll{
    for (Interceptor interceptor : interceptors) {
      // == 对目标对象进行包装
      target = interceptor.plugin(target);
    }
    return target;
}

查看具体的包装方法

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的初始化逻辑

通过数据库链接url中截取的数据库别名,来选择具体的方言类,并用反射进行初始化

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);
}

附录

P6-P7知识合辑


青鱼
268 声望25 粉丝

山就在那里,每走一步就近一些