基于mybatis源码和PageHelper源码进行的测试
版本
mybatis3.5.0,pageHelper6.0.0
测试用例
依赖
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.15</version>
</dependency>
<dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.11</version><scope>test</scope>
</dependency>
<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper</artifactId><version>6.0.0</version>
</dependency>
<dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>3.5.0</version>
</dependency>
如果引入上面这些依赖运行报下面这样的错误
需要再引入两个依赖,这个两个依赖直接去mybatis源码的pom.xml中找即可
<dependency><groupId>ognl</groupId><artifactId>ognl</artifactId><version>3.2.10</version><scope>compile</scope><optional>true</optional>
</dependency>
<dependency><groupId>org.javassist</groupId><artifactId>javassist</artifactId><version>3.24.1-GA</version><scope>compile</scope><optional>true</optional>
</dependency>
如果还报错,是下面这种错误,也是一样的从mybatis源码的pom.xml中找到依赖引用到自己的项目中
<dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId><version>1.7.25</version><optional>true</optional>
</dependency>
<dependency><groupId>org.slf4j</groupId><artifactId>slf4j-log4j12</artifactId><version>1.7.25</version><optional>true</optional>
</dependency>
<dependency><groupId>log4j</groupId><artifactId>log4j</artifactId><version>1.2.17</version><optional>true</optional>
</dependency>
<dependency><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-core</artifactId><version>2.11.1</version><optional>true</optional>
</dependency>
<dependency><groupId>commons-logging</groupId><artifactId>commons-logging</artifactId><version>1.2</version><optional>true</optional>
</dependency>
日志文件
#将等级为DEBUG的日志信息输出到console和file这两个目的地,console和file的定义在下面的代码
log4j.rootLogger=DEBUG,console,file#控制台输出的相关设置
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.Target = System.out
log4j.appender.console.Threshold=DEBUG
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%m%n#文件输出的相关设置
log4j.appender.file = org.apache.log4j.RollingFileAppender
log4j.appender.file.File=./log/kuang.log
log4j.appender.file.MaxFileSize=10mb
log4j.appender.file.Threshold=DEBUG
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=[%d{yy-MM-dd}][%c]%m%n#日志输出级别
log4j.logger.org.mybatis=DEBUG
log4j.logger.java.sql=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.ResultSet=DEBUG
log4j.logger.java.sql.PreparedStatement=DEBUG
数据库配置文件
jdbc.type=mysql
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:6608/mybatis?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
jdbc.username=mybatis
jdbc.password=mybatis
mybatis配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configurationPUBLIC "-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-config.dtd">
<!-- 配置文件的根元素 -->
<configuration><!-- 属性:定义配置外在化 --><properties resource="jdbc.properties"/><settings><setting name="logImpl" value="LOG4J"/></settings>
<!--分页拦截器--><plugins><plugin interceptor="com.github.pagehelper.PageInterceptor"/></plugins>
<!--环境配置--><environments default="mysql"><environment id="mysql"><transactionManager type="JDBC" /><!-- 配置数据库连接信息 --><dataSource type="POOLED"><property name="driver" value="${jdbc.driver}" /><property name="url" value="${jdbc.url}" /><property name="username" value="${jdbc.username}" /><property name="password" value="${jdbc.password}" /></dataSource></environment></environments><!-- 映射器:指定映射文件或者映射类 --><mappers><mapper resource="CmpRecordMapper.xml"/></mappers></configuration>
映射文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.mybatis.domain.CmpRecordMapper"><select id="getCmpRecordBy" parameterType="java.util.Map" resultType="org.mybatis.domain.CmpRecord"><include refid="selectColumnSql"/><if test="createTimeBegin != null">and create_time >= #{createTimeBegin}</if><if test="createTimeEnd != null">and create_time <= #{createTimeEnd}</if></select><sql id="selectColumnSql">selectid as "id",create_time as "createTime",valid as "valid",create_org_id as "createOrgId",create_org_name as "createOrgName",create_dept_id as "createDeptId",create_dept_name as "createDeptName",creator_id as "creatorId",creator_name as "creatorName",limit_time as "limitTime",category_id as "categoryId",category_name as "categoryName",cmp_reason_id as "cmpReasonId",cmp_reason_name as "cmpReasonName",content_abstract as "contentAbstract"from t_cmp_record where valid = 1</sql>
</mapper>
实体模型
public class CmpRecord {private String id;private Date createTime;private int valid = 1;private String createOrgId;private String createOrgName;private String createDeptId;private String createDeptName;private String creatorId;private String creatorName;private Date limitTime;private String categoryId;private String categoryName;private String cmpReasonId;private String cmpReasonName;private String contentAbstract;public CmpRecord() {}public CmpRecord(String id, Date createTime, int valid, String createOrgId, String createOrgName, String createDeptId, String createDeptName, String creatorId, String creatorName, Date limitTime, String categoryId, String categoryName, String cmpReasonId, String cmpReasonName, String contentAbstract) {this.id = id;this.createTime = createTime;this.valid = valid;this.createOrgId = createOrgId;this.createOrgName = createOrgName;this.createDeptId = createDeptId;this.createDeptName = createDeptName;this.creatorId = creatorId;this.creatorName = creatorName;this.limitTime = limitTime;this.categoryId = categoryId;this.categoryName = categoryName;this.cmpReasonId = cmpReasonId;this.cmpReasonName = cmpReasonName;this.contentAbstract = contentAbstract;}//省略get/set方法@Overridepublic String toString() {return "CmpRecord{" +"id='" + id + '\'' +", valid=" + valid +", createOrgId='" + createOrgId + '\'' +", createOrgName='" + createOrgName + '\'' +", createDeptId='" + createDeptId + '\'' +", createDeptName='" + createDeptName + '\'' +", creatorId='" + creatorId + '\'' +", creatorName='" + creatorName + '\'' +", categoryId='" + categoryId + '\'' +", categoryName='" + categoryName + '\'' +", cmpReasonId='" + cmpReasonId + '\'' +", cmpReasonName='" + cmpReasonName + '\'' +", contentAbstract='" + contentAbstract + '\'' +'}';}
}
Mapper接口
public interface CmpRecordMapper {void saveCmpRecord(CmpRecord record);CmpRecord getCmpRecordId(String id);List<CmpRecord> getCmpRecordBy(Map<String, Object> params);
}
测试方法
public static void testPageHelper() throws IOException {InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);SqlSession sqlSession = sqlSessionFactory.openSession();CmpRecordMapper mapper = sqlSession.getMapper(CmpRecordMapper.class);Map<String,Object> params = new HashMap<>();params.put("createTimeBegin","2023-10-01 00:12:02");params.put("createTimeEnd","2023-11-01 00:12:02");PageHelper.startPage(1,2);List<CmpRecord> records = mapper.getCmpRecordBy(null);PageInfo<CmpRecord> datas = new PageInfo<>(records);System.out.println(datas.toString());
}public static void main(String[] args) throws IOException {testPageHelper();
}
PageHelper源码分析
拦截器PageInterceptor
//这里执行拦截Executor接口的两个query方法
@SuppressWarnings({"rawtypes", "unchecked"})
@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;//这里是方言 是 PageHelperprivate 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");}@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;//参数个数的不同对应上面不同的query方法//由于逻辑关系,只会进入一次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];}//这里会使用默认方言 PageHelpercheckDialectExists();//对 boundSql 的拦截处理if (dialect instanceof BoundSqlInterceptor.Chain) {//这里会调用PageHelper.doudSql方法,返回还是原来的BoundSql对象boundSql = ((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.ORIGINAL, boundSql, cacheKey);}List resultList;//调用方法判断是否需要进行分页,如果不需要,直接返回结果if (!dialect.skip(ms, parameter, rowBounds)) {Future<Long> countFuture = null;//判断是否需要进行 count 查询if (dialect.beforeCount(ms, parameter, rowBounds)) {//这里会判断是否异步进行count查询,一般都是同步count查询,所以会走elseif (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);//异步想要起作用需要新的数据库连接,需要独立的事务,创建新的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
public class PageHelper extends PageMethod implements Dialect, BoundSqlInterceptor.Chain {private PageAutoDialect autoDialect;//数据库方言 根据你配置的数据库类型选择对应的方言 这里选择的是MySqlDialect.classprivate ForkJoinPool asyncCountService;//需要异步执行sql的线程池 初始化在当前类的 setProperties方法中@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 <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 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 Object processParameterObject(MappedStatement ms, Object parameterObject, BoundSql boundSql, CacheKey pageKey) {return autoDialect.getDelegate().processParameterObject(ms, parameterObject, boundSql, pageKey);}//TODO 其他方法省略
}
AbstractHelperDialect
@Overridepublic Object processParameterObject(MappedStatement ms, Object parameterObject, BoundSql boundSql, CacheKey pageKey) {//处理参数Page page = getLocalPage();//如果只是 order by 就不必处理参数if (page.isOrderByOnly()) {return parameterObject;}Map<String, Object> paramMap = null;if (parameterObject == null) {paramMap = new HashMap<String, Object>();} else if (parameterObject instanceof Map) {//解决不可变Map的情况paramMap = new HashMap<String, Object>();paramMap.putAll((Map) parameterObject);} else {paramMap = new HashMap<String, Object>();// sqlSource为ProviderSqlSource时,处理只有1个参数的情况if (ms.getSqlSource() instanceof ProviderSqlSource) {String[] providerMethodArgumentNames = ExecutorUtil.getProviderMethodArgumentNames((ProviderSqlSource) ms.getSqlSource());if (providerMethodArgumentNames != null && providerMethodArgumentNames.length == 1) {paramMap.put(providerMethodArgumentNames[0], parameterObject);paramMap.put("param1", parameterObject);}}//动态sql时的判断条件不会出现在ParameterMapping中,但是必须有,所以这里需要收集所有的getter属性//TypeHandlerRegistry可以直接处理的会作为一个直接使用的对象进行处理boolean hasTypeHandler = ms.getConfiguration().getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass());MetaObject metaObject = MetaObjectUtil.forObject(parameterObject);//需要针对注解形式的MyProviderSqlSource保存原值if (!hasTypeHandler) {for (String name : metaObject.getGetterNames()) {paramMap.put(name, metaObject.getValue(name));}}//下面这段方法,主要解决一个常见类型的参数时的问题if (boundSql.getParameterMappings() != null && boundSql.getParameterMappings().size() > 0) {for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {String name = parameterMapping.getProperty();if (!name.equals(PAGEPARAMETER_FIRST)&& !name.equals(PAGEPARAMETER_SECOND)&& paramMap.get(name) == null) {if (hasTypeHandler|| parameterMapping.getJavaType().equals(parameterObject.getClass())) {paramMap.put(name, parameterObject);break;}}}}}//调用具体数据库方言进行处理 这里是调用的MySqlDialectreturn processPageParameter(ms, paramMap, page, boundSql, pageKey);}
项目中使用PageHelper遇到的问题
分页不正确
原因:PageMethod.LOCAL_PAGE没有及时删除
PageMethod
protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();protected static boolean DEFAULT_COUNT = true;/*** 设置 Page 参数** @param page*/public static void setLocalPage(Page page) {LOCAL_PAGE.set(page);}/*** 获取 Page 参数** @return*/public static <T> Page<T> getLocalPage() {return LOCAL_PAGE.get();}/*** 移除本地变量*/public static void clearPage() {LOCAL_PAGE.remove();}//TODO 省略其他方法
}
而且并不是每一次都有问题,这个其实取决于我们启动服务所使用的容器,比如tomcat,在其内部处理请求是通过线程池的方式。甚至现在的很多容器是基于netty的,都是通过线程池,复用线程来增加服务的并发量。
假设线程1持有没有被清除的page参数,不断调用同一个方法,后面两个请求使用的是线程2和线程3没有问题,再一个请求轮到线程1了,此时就会出现问题了。
解决方案
手动调用PageHelper.clearPage方法
public static void testPageHelper() throws IOException {InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);SqlSession sqlSession = sqlSessionFactory.openSession();CmpRecordMapper mapper = sqlSession.getMapper(CmpRecordMapper.class);Map<String,Object> params = new HashMap<>();params.put("createTimeBegin","2023-10-01 00:12:02");params.put("createTimeEnd","2023-11-01 00:12:02");PageHelper.startPage(1,2);List<CmpRecord> records = mapper.getCmpRecordBy(null);PageInfo<CmpRecord> datas = new PageInfo<>(records);//TODO 手动调用PageHelper.clearPage();System.out.println(datas.toString());}