Mybatis pagehelper源码解析

1,788 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第19天,点击查看活动详情

前言

笔者之前有对老项目的Mybatis进行升级成Plus。因为很多老代码都用了pagehelper同时pagehelper挺好用的,所以不打算使用Plus自带的PaginationInterceptor(只需要定义好bean,plus的starter的自动装配MybatisPlusAutoConfiguration会扫描interceptor并添加进MybatisConfiguration),打算一致性编程继续使用pagehelper。

因为之前的老项目都是使用自定义Configuration进行装配自定义的SqlSessionFactory(自定义TypeAliasesPackage包路径、mapper.xml路径、指定mybatis-config.xml配置路径等)、MapperScannerConfigurer(指定dao包路径)等等,笔者都想mybatis这块都依赖yml进行配置,所以在把自定义的Configuration都去掉了之后发现分页失效了,原因是之前使用的是自定义Configuration指定mybatis-config.xml进行配置插件的,所以使用springboot的starter进行自定装配即可。

因此笔者对pagehelper的自动装配做了什么、pagehelper实现原理进行了源码的阅读。后面在使用自定义mybatis插件时遇到的问题和通过阅读源码了解其原因及最终解决方案也放在此篇文章中。

注意:文章中的示例并非实际生产代码。

springboot引入

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.1.2</version>
</dependency>
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.2.10</version>
</dependency>

PageHelperAutoConfiguration-自动装配

/**
 * 自定注入分页插件
 *
 * @author liuzh
 */
@Configuration
@ConditionalOnBean(SqlSessionFactory.class)
@EnableConfigurationProperties(PageHelperProperties.class)
@AutoConfigureAfter(MybatisAutoConfiguration.class)
public class PageHelperAutoConfiguration {

    @Autowired
    private List<SqlSessionFactory> sqlSessionFactoryList;

    @Autowired
    private PageHelperProperties properties;

    /**
     * 接受分页插件额外的属性
     *
     * @return
     */
    @Bean
    @ConfigurationProperties(prefix = PageHelperProperties.PAGEHELPER_PREFIX)
    public Properties pageHelperProperties() {
        return new Properties();
    }

    @PostConstruct
    public void addPageInterceptor() {
        PageInterceptor interceptor = new PageInterceptor();
        Properties properties = new Properties();
        //先把一般方式配置的属性放进去
        properties.putAll(pageHelperProperties());
        //在把特殊配置放进去,由于close-conn 利用上面方式时,属性名就是 close-conn 而不是 closeConn,所以需要额外的一步
        properties.putAll(this.properties.getProperties());
        interceptor.setProperties(properties);
        for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
            sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
        }
    }

}

image.png 可以看到最后一行,configuration为Plus的MybatisConfiguration,向其中添加了Interceptor为PageInteceptor。所以大概知道了pagehelper是利用了mybatis的interceptor来扩展的。

例子

Page page = PageHelper.startPage(1, 5);
Wrapper<UserDo> userDoWrapper = new EntityWrapper<>();
this.selectList(userDoWrapper);
PageInfo<UserDo> pageHelper = page.toPageInfo();

可以看到只需要PageHelper.start就能开启&指定分页,那我们看下是怎么实现的。

PageHelper#start

public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
    Page<E> page = new Page(pageNum, pageSize, count);
    page.setReasonable(reasonable);
    page.setPageSizeZero(pageSizeZero);
    Page<E> oldPage = getLocalPage();
    if (oldPage != null && oldPage.isOrderByOnly()) {
        page.setOrderBy(oldPage.getOrderBy());
    }

    setLocalPage(page);
    return page;
}
public abstract class PageMethod {
    protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal();

    public PageMethod() {
    }

    protected static void setLocalPage(Page page) {
        LOCAL_PAGE.set(page);
    }
}    

可以看到使用了ThreadLocal,这样就方便PageInterceptor进行读取了。

DefaultSqlSession

@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
  try {
    MappedStatement ms = configuration.getMappedStatement(statement);
    return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

image.png

可以看到executor为代理对象代理SimpleExecutor。handler为Plugin,Plugin的拦截器为PageIntercetor,target为SimpleExecutor。下面我们继续看看PageIntercetor是怎么处理的。

PageInterceptor#intercept

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) {
            boundSql = ms.getBoundSql(parameter);
            cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
        } else {
            cacheKey = (CacheKey)args[4];
            boundSql = (BoundSql)args[5];
        }

        List resultList;
        if (this.dialect.skip(ms, parameter, rowBounds)) {
            resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
        } else {
            // msId这里为com.whf.spring.dao.UserDao.selectList
            String msId = ms.getId();
            // configuraction为mybatis plus的MybatisConfiguration
            Configuration configuration = ms.getConfiguration();
            Map<String, Object> additionalParameters = (Map)this.additionalParametersField.get(boundSql);
            if (this.dialect.beforeCount(ms, parameter, rowBounds)) {
                String countMsId = msId + this.countSuffix;
                MappedStatement countMs = this.getExistedMappedStatement(configuration, countMsId);
                Long count;
                if (countMs != null) {
                    count = this.executeManualCount(executor, countMs, parameter, boundSql, resultHandler);
                } else {
                    countMs = (MappedStatement)this.msCountMap.get(countMsId);
                    if (countMs == null) {
                        countMs = MSUtils.newCountMappedStatement(ms, countMsId);
                        this.msCountMap.put(countMsId, countMs);
                    }
                    // countMs的msId为com.whf.spring.dao.UserDao.selectList_COUNT,这里会执行count语句获得count
                    count = this.executeAutoCount(executor, countMs, parameter, boundSql, rowBounds, resultHandler);
                }

                if (!this.dialect.afterCount(count, parameter, rowBounds)) {
                    Object var24 = this.dialect.afterPage(new ArrayList(), parameter, rowBounds);
                    return var24;
                }
            }

            if (!this.dialect.beforePage(ms, parameter, rowBounds)) {
                resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
            } else {
                parameter = this.dialect.processParameterObject(ms, parameter, boundSql, cacheKey);
                //获得分页sql"SELECT  id AS id,create_time AS createTime,update_time AS updateTime,is_delete AS isDelete,phone,user_name AS userName,head  FROM user LIMIT ? "
                String pageSql = this.dialect.getPageSql(ms, boundSql, parameter, rowBounds, cacheKey);
                BoundSql pageBoundSql = new BoundSql(configuration, pageSql, boundSql.getParameterMappings(), parameter);
                Iterator var17 = additionalParameters.keySet().iterator();

                while(true) {
                    if (!var17.hasNext()) {
                        //这里执行分页sql
                        resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, pageBoundSql);
                        break;
                    }

                    String key = (String)var17.next();
                    pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
                }
            }
        }

        Object var22 = this.dialect.afterPage(resultList, parameter, rowBounds);
        return var22;
    } finally {
        this.dialect.afterAll();
    }
}

拦截方法主要做了两件事,一件执行countBoundsql获得count,一件执行pageBoundSql获得resultList。

同时需要注意的是:PageInterceptor的intercept方法只是获得了invocation的属性自己执行了并没有invocation.proceed()所以PageInterceptor不会传播拦截器链,如果想自定义拦截器需要比PageInterceptor后添加至MybatisConfiguration,生成拦截代理是按照添加顺序嵌套的,如果后面添加的拦截器不进行传播前面添加的拦截器就无法得到执行。下面我们来看看。

PageInterceptor使自定义拦截器失效

自定义拦截器如下:

package com.whf.spring.aspect;

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.util.Properties;

@Slf4j
@Intercepts(
        @Signature(method = "query",
                type = Executor.class,
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
        ))
public class MybatisInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        log.info("自定义拦截开始执行");

        Object proceed = invocation.proceed();
        
        log.info("自定义拦截结束执行");
        return proceed;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
}
package com.whf.spring.config;


import com.github.pagehelper.autoconfigure.PageHelperAutoConfiguration;
import com.whf.spring.aspect.MybatisInterceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import java.util.List;

@Configuration
@AutoConfigureAfter(PageHelperAutoConfiguration.class)
public class MybatisConfiguration {
    @Autowired
    private List<SqlSessionFactory> sqlSessionFactoryList;

    @PostConstruct
    public void addMyInterceptor() {
        MybatisInterceptor e = new MybatisInterceptor();
        for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
            sqlSessionFactory.getConfiguration().addInterceptor(e);
        }

    }
}

我们现在来看看生成的executor。

image.png 可以看到executor的handler的target也为代理对象,代理对象的handler的inrerceptor为笔者自定义的MybatisInterceptor。但是因为在《PageInterceptor#intercept》章节提到了注意点,所以是不会执行的,读者可以自行尝试。

值得注意的是:我们已经加了@AutoConfigureAfter(PageHelperAutoConfiguration.class)意思是希望比PageHelperAutoConfiguration后装配但是结果还是先装配了,原因是因为本启动项目目录下的会被先扫描到。

解决方法是把MybatisConfiguration加到resources/META-INF/spring.factories中进行自动装配,这样就是同等的。

解决

这样@AutoConfigureAfter(PageHelperAutoConfiguration.class)就正真生效了。 image.png

效果如图

image.png

image.png

总结

  • pagehelper的实现原理就是使用了mybatis提供的拦截器扩展,会代理executor,使用PageHelper#start中开启&设置的分页存放于在PageMethod中的ThreadLocal属性LOCAL_PAGE中,便于在后面线程安全的进行分页查询。

  • 同时PageInterceptor不会执行Invocation#proceed所以不会传播代理链,因此自定义拦截器不想被影响的话可以使用@AutoConfigureAfter(PageHelperAutoConfiguration.class)+把自定义拦截器加到resources/META-INF/spring.factories中进行自动装配。

    至于整个mybatis的执行流程可以参考笔者之前的《Mybatis(Plus)源码阅读路径》