Mybatis插件机制以及PageHelper插件的原理

首先现在已经有很多Mybatis源码分析的文章,之所以重复造*,只是为了督促自己更好的理解源码。

 

1.先看一段PageHelper拦截器的配置,在mybatis的配置文件<configuration>标签下配置。

<plugins>
      <plugin interceptor="com.github.pagehelper.PageInterceptor">
            <!--当设置为true的时候,如果pagesize设置为0(或RowBounds的limit=0),就不执行分页,返回全部结果--> 
            <property name="pageSizeZero" value="false"/>
      </plugin>
</plugins>

1.1 其它的PageHelper的属性配置以及默认值可以参考com.github.pagehelper.page.PageParams类

Mybatis插件机制以及PageHelper插件的原理
public class PageParams {
    //RowBounds参数offset作为PageNum使用 - 默认不使用
    protected boolean offsetAsPageNum = false;
    //RowBounds是否进行count查询 - 默认不查询
    protected boolean rowBoundsWithCount = false;
    //当设置为true的时候,如果pagesize设置为0(或RowBounds的limit=0),就不执行分页,返回全部结果
    protected boolean pageSizeZero = false;
    //分页合理化
    protected boolean reasonable = false;
    //是否支持接口参数来传递分页参数,默认false
    protected boolean supportMethodsArguments = false;
    //默认count(0)
    protected String countColumn = "0";

     public void setProperties(Properties properties) {
        //offset作为PageNum使用
        String offsetAsPageNum = properties.getProperty("offsetAsPageNum");
        this.offsetAsPageNum = Boolean.parseBoolean(offsetAsPageNum);
        //RowBounds方式是否做count查询
        String rowBoundsWithCount = properties.getProperty("rowBoundsWithCount");
        this.rowBoundsWithCount = Boolean.parseBoolean(rowBoundsWithCount);
        //当设置为true的时候,如果pagesize设置为0(或RowBounds的limit=0),就不执行分页
        String pageSizeZero = properties.getProperty("pageSizeZero");
        this.pageSizeZero = Boolean.parseBoolean(pageSizeZero);
        //分页合理化,true开启,如果分页参数不合理会自动修正。默认false不启用
        String reasonable = properties.getProperty("reasonable");
        this.reasonable = Boolean.parseBoolean(reasonable);
        //是否支持接口参数来传递分页参数,默认false
        String supportMethodsArguments = properties.getProperty("supportMethodsArguments");
        this.supportMethodsArguments = Boolean.parseBoolean(supportMethodsArguments);
        //默认count列
        String countColumn = properties.getProperty("countColumn");
        if(StringUtil.isNotEmpty(countColumn)){
            this.countColumn = countColumn;
        }
        //当offsetAsPageNum=false的时候,不能
        //参数映射
        PageObjectUtil.setParams(properties.getProperty("params"));
    }

}
View Code

 

1.2 添加配置后自定义的拦截器需要增加注解@Intercepts否则会报错

@Intercepts(
        {
          //拦截excutor类型里面的query方法 @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 { private volatile Dialect dialect; private String countSuffix = "_COUNT"; protected Cache<String, MappedStatement> msCountMap = null; private String default_dialect_class = "com.github.pagehelper.PageHelper"; }
 private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
    // issue #251
    //查看拦截器是否有Intercepts注解 没有抛出异常
    if (interceptsAnnotation == null) {
      throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
    }
    Signature[] sigs = interceptsAnnotation.value();
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
    for (Signature sig : sigs) {
      Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
      try {
        Method method = sig.type().getMethod(sig.method(), sig.args());
        methods.add(method);
      } catch (NoSuchMethodException e) {
        throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
      }
    }
    return signatureMap;
  }

 

1.3使用PageHelper

 1 public static void main(String[] args) throws IOException {
 2 
 3         InputStream is = Resources.getResourceAsStream("config/mybatis-config.xml");
 4         SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);//DefaultSqlSessionFactory
 5 
 6         SqlSession session = sqlSessionFactory.openSession();//打开session会执行 InterceptorChina.plugAll() --->Plugin.wrap()生成代理对象,被代理的就是Excutor
 7         
 8         IStudentDao sudentDaoProxy = session.getMapper(IStudentDao.class);
 9      //加上这句就会拦截查询方法并进行分页
10         Page page = PageHelper.startPage(1,1,"name");
11         List<Student> student = sudentDaoProxy.findStudentById(null);
12     }

 

 

2.第一部分是如何使用拦截器,第二部分则是拦截器如何执行的。Mybatis插件的核心接口是 org.apache.ibatis.plugin.Interceptor。

public interface Interceptor {
  //拦截器执行业务逻辑方法  
  Object intercept(Invocation invocation) throws Throwable;
  //设置拦截的真实对象
  Object plugin(Object target);
  //设置拦截器初始化属性
  void setProperties(Properties properties);
}

2.1.XMLConfigBuilder类解析xml所有配置包括<Plugins>标签,然后放Mybatis全局配置类Configuration中

//XMLConfigBuilder类 
private void pluginElement(XNode parent) throws Exception {
    //如果有Plugins标签
    if (parent != null) {
     //拿到所有的子标签也就是<Plugin>,所有的拦截器配置
      for (XNode child : parent.getChildren()) {
        //拦截器的类
        String interceptor = child.getStringAttribute("interceptor");
        //<Property>配置
        Properties properties = child.getChildrenAsProperties();
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
        //把property属性设置到拦截器里面
        interceptorInstance.setProperties(properties);
       //加入到拦截器链中List<Interceptor> interceptors
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }    

2.2.解析完Configuration后生成DefaultSqlSessionFactory,打开session执行pluginAll

Mybatis插件机制以及PageHelper插件的原理
//DefaultSqlSessionFactory
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
     //选择excutor没有配置默认使用SimpleExecutor
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
View Code

newExecutor() 

Mybatis插件机制以及PageHelper插件的原理
//Configuration 
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    
    //给Executor生成代理对象
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }
View Code
//PageInterceptor
public Object plugin(Object target) {
        return Plugin.wrap(target, this);
   }


public class Plugin implements InvocationHandler {
  //生成Plugin代理对象 public static Object wrap(Object target, Interceptor interceptor) { //拿到PageInterceptor类型的需要拦截的方法即query方法 Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); Class<?> type = target.getClass(); Class<?>[] interfaces = getAllInterfaces(type, signatureMap); if (interfaces.length > 0) { //生成Plugin代理对象,所以当后面执行executor的query时其实执行下面的invoke方法 return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } return target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { Set<Method> methods = signatureMap.get(method.getDeclaringClass()); if (methods != null && methods.contains(method)) { //执行PageInterceptor的intercept方法 return interceptor.intercept(new Invocation(target, method, args)); } return method.invoke(target, args); } catch (Exception e) { throw ExceptionUtil.unwrapThrowable(e); } } }

2.3.已上就是为什么配置了拦截器后会执行拦截器的调用流程,本质上就是给Executor类型生成Plugin代理对象,以后executor执行的query方法通过plugin的invoke方法执行,invoke方法会调用自定义拦截器的intercept()方法。

2.4.所以PagerHelper分页现在需要做2件事情:1.拿到executor的查询sql变成 Select count(0) from table 形式查询count值;2.通过前面的coun值计算分页边界生成select * from table limit ?,?查询结果;

 

3. PagerHelper中的拦截方法比较清晰

Mybatis插件机制以及PageHelper插件的原理
public class PageInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        try {
            Object[] args = invocation.getArgs();
            MappedStatement ms = (MappedStatement) args[0];
            Object parameter = args[1];
            RowBounds rowBounds = (RowBounds) args[2];
            ResultHandler resultHandler = (ResultHandler) args[3];
            Executor executor = (Executor) invocation.getTarget();
            CacheKey cacheKey;
            BoundSql boundSql;
            //由于逻辑关系,只会进入一次
            if (args.length == 4) {
                //4 个参数时
                boundSql = ms.getBoundSql(parameter);
                cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
            } else {
                //6 个参数时
                cacheKey = (CacheKey) args[4];
                boundSql = (BoundSql) args[5];
            }
            checkDialectExists();

            List resultList;
            //调用方法判断是否需要进行分页,如果不需要,直接返回结果
            if (!dialect.skip(ms, parameter, rowBounds)) {
                //判断是否需要进行 count 查询
                if (dialect.beforeCount(ms, parameter, rowBounds)) {
                    //查询总数
                    Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                    //处理查询总数,返回 true 时继续分页查询,false 时直接返回
                    if (!dialect.afterCount(count, parameter, rowBounds)) {
                        //当查询总数为 0 时,直接返回空的结果
                        return dialect.afterPage(new ArrayList(), parameter, rowBounds);
                    }
                }
                resultList = ExecutorUtil.pageQuery(dialect, executor,
                        ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
            } else {
                //rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
                resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
            }
            return dialect.afterPage(resultList, parameter, rowBounds);
        } finally {
            dialect.afterAll();
        }
    }

}
View Code

通过ExecutorUtil.executeAutoCount()生成对应的countSql ----->经过了很多步到达CountSqlParser.sqlToCount()

public void sqlToCount(Select select, String name) {
        SelectBody selectBody = select.getSelectBody();
        // 是否能简化count查询
        List<SelectItem> COUNT_ITEM = new ArrayList<SelectItem>();
        COUNT_ITEM.add(new SelectExpressionItem(new Column("count(" + name +")")));
        if (selectBody instanceof PlainSelect && isSimpleCount((PlainSelect) selectBody)) {
            //selectItems 就是sql中的select xxxx from table;把xxxx替换成count(0)
            ((PlainSelect) selectBody).setSelectItems(COUNT_ITEM);
        } else {
            PlainSelect plainSelect = new PlainSelect();
            SubSelect subSelect = new SubSelect();
            subSelect.setSelectBody(selectBody);
            subSelect.setAlias(TABLE_ALIAS);
            plainSelect.setFromItem(subSelect);
            plainSelect.setSelectItems(COUNT_ITEM);
            select.setSelectBody(plainSelect);
        }
    }    

 

3.1 ExecutorUtil.pageQuery()分页查询

public static  <E> List<E> pageQuery(Dialect dialect, Executor executor, MappedStatement ms, Object parameter,
                                 RowBounds rowBounds, ResultHandler resultHandler,
                                 BoundSql boundSql, CacheKey cacheKey) throws SQLException {
        //判断是否需要进行分页查询
        if (dialect.beforePage(ms, parameter, rowBounds)) {
            //生成分页的缓存 key
            CacheKey pageKey = cacheKey;
            //处理参数对象
            parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
            //调用方言获取分页 sql
            String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
            BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);

            Map<String, Object> additionalParameters = getAdditionalParameter(boundSql);
            //设置动态参数
            for (String key : additionalParameters.keySet()) {
                pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
            }
            //执行分页查询
            return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
        } else {
            //不执行分页的情况下,也不执行内存分页
            return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
        }
    }

 

上一篇:Pagehelper的基本使用


下一篇:MyBatis PageHelper分页插件