前言
本篇主要讲述Mybatis-PageHelper分页插件使用和其底层原理。
Spring 集成Mybatis-PageHelper
在Mybatis集成篇(一)篇幅中讲了Spring集成Mybatis,在这个基础上,我们把Mybatis-PageHelper也集成进去
案例:
在原有的pom文件中增加Mybatis-PageHelper依赖
<!--分页插件--><dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper</artifactId><version>5.0.0</version></dependency>
在原有的MybatisConfig文件中增加几行代码
MybatisConfig
@Beanpublic SqlSessionFactory getSqlSessionFactory() throws Exception {org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();configuration.setLogImpl(StdOutImpl.class);SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();factoryBean.setDataSource(dataSource());factoryBean.setConfiguration(configuration);// 分页拦截器PageInterceptor pageInterceptor = new PageInterceptor();Properties properties = new Properties();// 这里的需要根据数据库类型设置properties.setProperty("helperDialect","mysql");pageInterceptor.setProperties(properties);// 添加拦截器configuration.addInterceptor(pageInterceptor);return factoryBean.getObject();}
Main类改造
public class Main {public static void main(String[] args) throws Exception {AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MybatisConfig.class);UserMapper userMapper = applicationContext.getBean(UserMapper.class);// PageHelper打开方式 startPageHelper.startPage(1,10);List<User> users = userMapper.selectUser(1);PageInfo<User> userPageInfo = new PageInfo<>(users);// PageHelper打开方式 enduserPageInfo.getList().forEach(System.out::println);users.forEach(System.out::println);}
}
结果:
Springboot 集成Mybatis-Pagehelper
在Mybatis集成篇(而)篇幅中讲了Spring集成Mybatis,在这个基础上,我们把Mybatis-PageHelper也集成进去
案例:
在原有的pom文件中增加Mybatis-PageHelper依赖
<!--分页插件--><dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId><version>2.1.0</version></dependency>
ApplicationTest.test2()改造
@Testpublic void test2() {PageHelper.startPage(1,10);List<User> users = userMapper.selectList(new LambdaQueryWrapper<User>());PageInfo pageInfo = new PageInfo(users);pageInfo.getList().forEach(System.out::println);}
结果:
原理
从第一个案例的可以看出, configuration.addInterceptor(pageInterceptor)才使得分页插件功能生效。同理,从第二个案例分析自动转配类PageHelperAutoConfiguration
也是增加了分页插件的PageInterceptor是,所以我们主要的入口是PageInterceptor类
PageInterceptor
@Intercepts({@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 static final Log log = LogFactory.getLog(PageInterceptor.class);private static boolean debug = false;protected Cache<String, MappedStatement> msCountMap = null;protected CountMsIdGen countMsIdGen = CountMsIdGen.DEFAULT;private volatile Dialect dialect;private String countSuffix = "_COUNT";private String default_dialect_class = "com.github.pagehelper.PageHelper";public PageInterceptor() {String bannerEnabled = System.getProperty("pagehelper.banner");if (StringUtil.isEmpty(bannerEnabled)) {bannerEnabled = System.getenv("PAGEHELPER_BANNER");}//默认 TRUEif (StringUtil.isEmpty(bannerEnabled) || Boolean.parseBoolean(bannerEnabled)) {log.debug("\n\n" +",------. ,--. ,--. ,--. \n" +"| .--. ' ,--,--. ,---. ,---. | '--' | ,---. | | ,---. ,---. ,--.--. \n" +"| '--' | ' ,-. | | .-. | | .-. : | .--. | | .-. : | | | .-. | | .-. : | .--' \n" +"| | --' \\ '-' | ' '-' ' \\ --. | | | | \\ --. | | | '-' ' \\ --. | | \n" +"`--' `--`--' .`- / `----' `--' `--' `----' `--' | |-' `----' `--' \n" +" `---' `--' is intercepting.\n");}}public static boolean isDebug() {return debug;}/*** 输出启用分页方法时的调用堆栈信息*/protected void debugStackTraceLog() {if (isDebug()) {Page<Object> page = PageMethod.getLocalPage();log.debug(page.getStackTrace());}}@Overridepublic 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();//对 boundSql 的拦截处理if (dialect instanceof BoundSqlInterceptor.Chain) {boundSql = ((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.ORIGINAL, boundSql, cacheKey);}List resultList;//调用方法判断是否需要进行分页,如果不需要,直接返回结果// dialect默认是PageHelper,PageHelper类型会根据jdbc的url找到对应的处理类// 例如jdbc:mysql://116.198.234.105:3306/mybatis?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true这个url,会根据url中:xx:这样的标识找到对应的处理类,类似例子中,会找到MySqlDialect处理类if (!dialect.skip(ms, parameter, rowBounds)) {//开启debug时,输出触发当前分页执行时的PageHelper调用堆栈// 如果和当前调用堆栈不一致,说明在启用分页后没有消费,当前线程再次执行时消费,调用堆栈显示的方法使用不安全debugStackTraceLog();Future<Long> countFuture = null;//判断是否需要进行 count 查询if (dialect.beforeCount(ms, parameter, rowBounds)) {if (dialect.isAsyncCount()) {countFuture = asyncCount(ms, boundSql, parameter, rowBounds);} else {//查询总数Long count = count(executor, ms, parameter, rowBounds, null, 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);if (countFuture != null) {Long count = countFuture.get();dialect.afterCount(count, parameter, rowBounds);}} else {//rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);}return dialect.afterPage(resultList, parameter, rowBounds);} finally {if (dialect != null) {dialect.afterAll();}}}/*** 异步查询总数*/private Future<Long> asyncCount(MappedStatement ms, BoundSql boundSql, Object parameter, RowBounds rowBounds) {Configuration configuration = ms.getConfiguration();//异步不能复用 BoundSql,因为分页使用时会添加分页参数,这里需要复制一个新的BoundSql countBoundSql = new BoundSql(configuration, boundSql.getSql(), new ArrayList<>(boundSql.getParameterMappings()), parameter);Map<String, Object> additionalParameter = ExecutorUtil.getAdditionalParameter(boundSql);if (additionalParameter != null){for (String key : additionalParameter.keySet()) {countBoundSql.setAdditionalParameter(key, additionalParameter.get(key));}}//异步想要起作用需要新的数据库连接,需要独立的事务,创建新的Executor,因此异步查询只适合在独立查询中使用,如果混合增删改操作,不能开启异步Environment environment = configuration.getEnvironment();TransactionFactory transactionFactory = null;if (environment == null || environment.getTransactionFactory() == null) {transactionFactory = new ManagedTransactionFactory();} else {transactionFactory = environment.getTransactionFactory();}//创建新的事务Transaction tx = transactionFactory.newTransaction(environment.getDataSource(), null, false);//使用新的 Executor 执行 count 查询,这里没有加载拦截器,避免递归死循环Executor countExecutor = new CachingExecutor(new SimpleExecutor(configuration, tx));return dialect.asyncCountTask(() -> {try {return count(countExecutor, ms, parameter, rowBounds, null, countBoundSql);} finally {tx.close();}});}/*** Spring bean 方式配置时,如果没有配置属性就不会执行下面的 setProperties 方法,就不会初始化* <p>* 因此这里会出现 null 的情况 fixed #26*/private void checkDialectExists() {if (dialect == null) {synchronized (default_dialect_class) {if (dialect == null) {setProperties(new Properties());}}}}private Long count(Executor executor, MappedStatement ms, Object parameter,RowBounds rowBounds, ResultHandler resultHandler,BoundSql boundSql) throws SQLException {String countMsId = countMsIdGen.genCountMsId(ms, parameter, boundSql, countSuffix);Long count;//先判断是否存在手写的 count 查询MappedStatement countMs = ExecutorUtil.getExistedMappedStatement(ms.getConfiguration(), countMsId);if (countMs != null) {count = ExecutorUtil.executeManualCount(executor, countMs, parameter, boundSql, resultHandler);} else {if (msCountMap != null) {countMs = msCountMap.get(countMsId);}//自动创建if (countMs == null) {//根据当前的 ms 创建一个返回值为 Long 类型的 mscountMs = MSUtils.newCountMappedStatement(ms, countMsId);if (msCountMap != null) {msCountMap.put(countMsId, countMs);}}count = ExecutorUtil.executeAutoCount(this.dialect, executor, countMs, parameter, boundSql, rowBounds, resultHandler);}return count;}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {//缓存 count msmsCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties);String dialectClass = properties.getProperty("dialect");if (StringUtil.isEmpty(dialectClass)) {dialectClass = default_dialect_class;}Dialect tempDialect = ClassUtil.newInstance(dialectClass, properties);tempDialect.setProperties(properties);String countSuffix = properties.getProperty("countSuffix");if (StringUtil.isNotEmpty(countSuffix)) {this.countSuffix = countSuffix;}// debug模式,用于排查不安全分页调用debug = Boolean.parseBoolean(properties.getProperty("debug"));// 通过 countMsId 配置自定义类String countMsIdGenClass = properties.getProperty("countMsIdGen");if (StringUtil.isNotEmpty(countMsIdGenClass)) {countMsIdGen = ClassUtil.newInstance(countMsIdGenClass, properties);}// 初始化完成后再设置值,保证 dialect 完成初始化dialect = tempDialect;}
}
PageHelper
/** The MIT License (MIT)** Copyright (c) 2014-2023 abel533@gmail.com** Permission is hereby granted, free of charge, to any person obtaining a copy* of this software and associated documentation files (the "Software"), to deal* in the Software without restriction, including without limitation the rights* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell* copies of the Software, and to permit persons to whom the Software is* furnished to do so, subject to the following conditions:** The above copyright notice and this permission notice shall be included in* all copies or substantial portions of the Software.** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN* THE SOFTWARE.*/package com.github.pagehelper;import com.github.pagehelper.dialect.AbstractHelperDialect;
import com.github.pagehelper.page.PageAutoDialect;
import com.github.pagehelper.page.PageBoundSqlInterceptors;
import com.github.pagehelper.page.PageMethod;
import com.github.pagehelper.page.PageParams;
import com.github.pagehelper.parser.CountSqlParser;
import com.github.pagehelper.util.StringUtil;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.RowBounds;import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinWorkerThread;
import java.util.concurrent.Future;/*** Mybatis - 通用分页拦截器<br/>* 项目地址 : http://git.oschina.net/free/Mybatis_PageHelper** @author liuzh/abel533/isea533* @version 5.0.0*/
public class PageHelper extends PageMethod implements Dialect, BoundSqlInterceptor.Chain {private PageParams pageParams;private PageAutoDialect autoDialect;private PageBoundSqlInterceptors pageBoundSqlInterceptors;private ForkJoinPool asyncCountService;@Overridepublic boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {Page page = pageParams.getPage(parameterObject, rowBounds);if (page == null) {return true;} else {//设置默认的 count 列if (StringUtil.isEmpty(page.getCountColumn())) {page.setCountColumn(pageParams.getCountColumn());}//设置默认的异步 count 设置if (page.getAsyncCount() == null) {page.setAsyncCount(pageParams.isAsyncCount());}autoDialect.initDelegateDialect(ms, page.getDialectClass());return false;}}@Overridepublic boolean isAsyncCount() {return getLocalPage().asyncCount();}@Overridepublic <T> Future<T> asyncCountTask(Callable<T> task) {//异步执行时需要将ThreadLocal值传递,否则会找不到AbstractHelperDialect dialectThreadLocal = autoDialect.getDialectThreadLocal();Page<Object> localPage = getLocalPage();String countId = UUID.randomUUID().toString();return asyncCountService.submit(() -> {try {//设置 ThreadLocalautoDialect.setDialectThreadLocal(dialectThreadLocal);setLocalPage(localPage);return task.call();} finally {autoDialect.clearDelegate();clearPage();}});}@Overridepublic boolean beforeCount(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {return autoDialect.getDelegate().beforeCount(ms, parameterObject, rowBounds);}@Overridepublic String getCountSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey countKey) {return autoDialect.getDelegate().getCountSql(ms, boundSql, parameterObject, rowBounds, countKey);}@Overridepublic boolean afterCount(long count, Object parameterObject, RowBounds rowBounds) {return autoDialect.getDelegate().afterCount(count, parameterObject, rowBounds);}@Overridepublic Object processParameterObject(MappedStatement ms, Object parameterObject, BoundSql boundSql, CacheKey pageKey) {return autoDialect.getDelegate().processParameterObject(ms, parameterObject, boundSql, pageKey);}@Overridepublic boolean beforePage(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {return autoDialect.getDelegate().beforePage(ms, parameterObject, rowBounds);}@Overridepublic String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {return autoDialect.getDelegate().getPageSql(ms, boundSql, parameterObject, rowBounds, pageKey);}public String getPageSql(String sql, Page page, RowBounds rowBounds, CacheKey pageKey) {return autoDialect.getDelegate().getPageSql(sql, page, pageKey);}@Overridepublic Object afterPage(List pageList, Object parameterObject, RowBounds rowBounds) {//这个方法即使不分页也会被执行,所以要判断 nullAbstractHelperDialect delegate = autoDialect.getDelegate();if (delegate != null) {return delegate.afterPage(pageList, parameterObject, rowBounds);}return pageList;}@Overridepublic void afterAll() {//这个方法即使不分页也会被执行,所以要判断 nullAbstractHelperDialect delegate = autoDialect.getDelegate();if (delegate != null) {delegate.afterAll();autoDialect.clearDelegate();}clearPage();}@Overridepublic BoundSql doBoundSql(BoundSqlInterceptor.Type type, BoundSql boundSql, CacheKey cacheKey) {Page<Object> localPage = getLocalPage();BoundSqlInterceptor.Chain chain = localPage != null ? localPage.getChain() : null;if (chain == null) {BoundSqlInterceptor boundSqlInterceptor = localPage != null ? localPage.getBoundSqlInterceptor() : null;BoundSqlInterceptor.Chain defaultChain = pageBoundSqlInterceptors != null ? pageBoundSqlInterceptors.getChain() : null;if (boundSqlInterceptor != null) {chain = new BoundSqlInterceptorChain(defaultChain, Arrays.asList(boundSqlInterceptor));} else if (defaultChain != null) {chain = defaultChain;}if (chain == null) {chain = DO_NOTHING;}if (localPage != null) {localPage.setChain(chain);}}return chain.doBoundSql(type, boundSql, cacheKey);}@Overridepublic void setProperties(Properties properties) {setStaticProperties(properties);pageParams = new PageParams();autoDialect = new PageAutoDialect();pageBoundSqlInterceptors = new PageBoundSqlInterceptors();pageParams.setProperties(properties);autoDialect.setProperties(properties);pageBoundSqlInterceptors.setProperties(properties);//20180902新增 aggregateFunctions, 允许手动添加聚合函数(影响行数)CountSqlParser.addAggregateFunctions(properties.getProperty("aggregateFunctions"));// 异步 asyncCountService 并发度设置,这里默认为应用可用的处理器核心数 * 2,更合理的值应该综合考虑数据库服务器的处理能力int asyncCountParallelism = Integer.parseInt(properties.getProperty("asyncCountParallelism","" + (Runtime.getRuntime().availableProcessors() * 2)));asyncCountService = new ForkJoinPool(asyncCountParallelism,pool -> {final ForkJoinWorkerThread worker = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool);worker.setName("pagehelper-async-count-" + worker.getPoolIndex());return worker;}, null, true);}
}
那本篇到这里就结束了。