PageHelper源码分析
PageHelper源码分析
分析版本:5.2.0
项目地址:https://github.com/pagehelper/Mybatis-PageHelper
可以看下作者写的关于拦截器的文章。
分页的原理,就是根据Mybatis提供的拦截器机制,来对Executor执行SQL语句时做一个拦截,并替换掉原来的SQL语句。
核心代码都在com.github.pagehelper.PageInterceptor
,主要逻辑是先在分页前执行count语句(各个不同的类型的数据库可能会有差异,通过实现不同dialect来屏蔽掉),然后改写sql语句注入分页逻辑的语句块。
@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 {
@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();
//对 boundSql 的拦截处理
if (dialect instanceof BoundSqlInterceptor.Chain) {
boundSql = ((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.ORIGINAL, boundSql, cacheKey);
}
List resultList;
//调用方法判断是否需要进行分页,如果不需要,直接返回结果
if (!dialect.skip(ms, parameter, rowBounds)) {
//判断是否需要进行 count 查询
if (dialect.beforeCount(ms, parameter, rowBounds)) {
//查询总数
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);
}
}
// ‼️注入分页SQL
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 {
if(dialect != null){
dialect.afterAll();
}
}
}
}
ExceutorUtil.pageQuery
静态方法
中间会调用dialect来生成分页的sql。
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));
}
//对 boundSql 的拦截处理
if (dialect instanceof BoundSqlInterceptor.Chain) {
pageBoundSql = ((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.PAGE_SQL, pageBoundSql, pageKey);
}
//执行分页查询
return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
} else {
//不执行分页的情况下,也不执行内存分页
return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
}
}
MySQLDialect
(MySQL方言)
MySQLDialect用两个方法:
processPageParameter
和getPageSql
,前者生成占位符对于的参数映射关系,后者生成分页SQL语句。
public class MySqlDialect extends AbstractHelperDialect {
// 注入参数,对应占位符?
@Override
public Object processPageParameter(MappedStatement ms, Map<String, Object> paramMap, Page page, BoundSql boundSql, CacheKey pageKey) {
paramMap.put(PAGEPARAMETER_FIRST, page.getStartRow());
paramMap.put(PAGEPARAMETER_SECOND, page.getPageSize());
//处理pageKey
pageKey.update(page.getStartRow());
pageKey.update(page.getPageSize());
//处理参数配置
if (boundSql.getParameterMappings() != null) {
List<ParameterMapping> newParameterMappings = new ArrayList<ParameterMapping>(boundSql.getParameterMappings());
if (page.getStartRow() == 0) {
newParameterMappings.add(new ParameterMapping.Builder(ms.getConfiguration(), PAGEPARAMETER_SECOND, int.class).build());
} else {
newParameterMappings.add(new ParameterMapping.Builder(ms.getConfiguration(), PAGEPARAMETER_FIRST, long.class).build());
newParameterMappings.add(new ParameterMapping.Builder(ms.getConfiguration(), PAGEPARAMETER_SECOND, int.class).build());
}
MetaObject metaObject = MetaObjectUtil.forObject(boundSql);
metaObject.setValue("parameterMappings", newParameterMappings);
}
return paramMap;
}
// 生成SQL语句
@Override
public String getPageSql(String sql, Page page, CacheKey pageKey) {
StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
sqlBuilder.append(sql);
if (page.getStartRow() == 0) {
sqlBuilder.append("\n LIMIT ? ");
} else {
sqlBuilder.append("\n LIMIT ?, ? ");
}
return sqlBuilder.toString();
}
}
PageHelper如何执行这个过程?
在使用PageHelper.startPage
静态方法时,会自动注入一个新的Page对象到PageMethod
类中的LOCAL_PAGE
静态属性,LOCAL_PAGE是一个线程本地变量,类型为ThreadLocal<Page>
,执行sql语句时,mybatis会保持一个线程执行一条SQL语句,不存在多线程竞争,因此,在调用分页方法时,会拦截之后第一次执行的SQL语句,并通过mybatis的拦截器机制实现改写sql语句。(⚠️注意:调用PageHelper.startPage方法不需要实例化PageHelper,因为是静态方法)
public abstract class PageMethod {
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
Page<E> page = new Page<E>(pageNum, pageSize, count);
page.setReasonable(reasonable);
page.setPageSizeZero(pageSizeZero);
//当已经执行过orderBy的时候
Page<E> oldPage = getLocalPage();
if (oldPage != null && oldPage.isOrderByOnly()) {
page.setOrderBy(oldPage.getOrderBy());
}
setLocalPage(page);
return page;
}
}
public class PageHelper extends PageMethod implements Dialect, BoundSqlInterceptor.Chain {
// ...
}
PageInterceptor拦截器,在拦截到Executor的query方法时,会在首次执行时实例化PageHelper。
public class PageInterceptor implements Interceptor {
private String default_dialect_class = "com.github.pagehelper.PageHelper";
private void checkDialectExists() {
if (dialect == null) {
synchronized (default_dialect_class) {
if (dialect == null) {
setProperties(new Properties());
}
}
}
}
@Override
public void setProperties(Properties properties) {
// ...
String dialectClass = properties.getProperty("dialect");
if (StringUtil.isEmpty(dialectClass)) {
dialectClass = default_dialect_class;
}
try {
// 使用默认无参构造器创建
Class<?> aClass = Class.forName(dialectClass);
dialect = (Dialect) aClass.newInstance();
} catch (Exception e) {
throw new PageException(e);
}
// ...
}
}
无论如何,最后到要走到dialect(PageHelper)的注入分页SQL的逻辑。在PageInterceptor中调用dialect的就是ExecutorUtil.pageQuery
方法。这个方法最终又会走到PageHelper上,但是PageHelper并没有实现任何特定数据库的方言处理逻辑,而是把这些逻辑委托给PageAutoDialect
。这个类会自动识别当前的数据库类型并执行对应的方言SQL。

public class PageHelper extends PageMethod implements Dialect, BoundSqlInterceptor.Chain {
@Override
public String getPageSql(
MappedStatement ms,
BoundSql boundSql,
Object parameterObject,
RowBounds rowBounds,
CacheKey pageKey
) {
return autoDialect.getDelegate()
.getPageSql(ms, boundSql, parameterObject, rowBounds, pageKey);
}
}
PageAutoDialec是如何判断数据库类型的?
很简单,通过jdbc的url来判断。
public class PageAutoDialect {
// 在类加载阶段,就注册号所有数据库对应的dialect
private static Map<String, Class<? extends Dialect>> dialectAliasMap =
new HashMap<String, Class<? extends Dialect>>();
public static void registerDialectAlias(
String alias,
Class<? extends Dialect> dialectClass
){
dialectAliasMap.put(alias, dialectClass);
}
static {
//注册别名
registerDialectAlias("hsqldb", HsqldbDialect.class);
registerDialectAlias("h2", HsqldbDialect.class);
registerDialectAlias("postgresql", HsqldbDialect.class);
registerDialectAlias("phoenix", HsqldbDialect.class);
registerDialectAlias("mysql", MySqlDialect.class);
registerDialectAlias("mariadb", MySqlDialect.class);
registerDialectAlias("sqlite", MySqlDialect.class);
registerDialectAlias("herddb", HerdDBDialect.class);
registerDialectAlias("oracle", OracleDialect.class);
registerDialectAlias("oracle9i", Oracle9iDialect.class);
registerDialectAlias("db2", Db2Dialect.class);
registerDialectAlias("informix", InformixDialect.class);
//解决 informix-sqli #129,仍然保留上面的
registerDialectAlias("informix-sqli", InformixDialect.class);
registerDialectAlias("sqlserver", SqlServerDialect.class);
registerDialectAlias("sqlserver2012", SqlServer2012Dialect.class);
registerDialectAlias("derby", SqlServer2012Dialect.class);
//达梦数据库,https://github.com/mybatis-book/book/issues/43
registerDialectAlias("dm", OracleDialect.class);
//阿里云PPAS数据库,https://github.com/pagehelper/Mybatis-PageHelper/issues/281
registerDialectAlias("edb", OracleDialect.class);
//神通数据库
registerDialectAlias("oscar", MySqlDialect.class);
registerDialectAlias("clickhouse", MySqlDialect.class);
}
// 获取方言
private AbstractHelperDialect getDialect(MappedStatement ms) {
//改为对dataSource做缓存
DataSource dataSource = ms.getConfiguration().getEnvironment().getDataSource();
String url = getUrl(dataSource);
if (urlDialectMap.containsKey(url)) {
return urlDialectMap.get(url);
}
try {
lock.lock();
if (urlDialectMap.containsKey(url)) {
return urlDialectMap.get(url);
}
if (StringUtil.isEmpty(url)) {
throw new PageException("无法自动获取jdbcUrl,请在分页插件中配置dialect参数!");
}
String dialectStr = fromJdbcUrl(url);
if (dialectStr == null) {
throw new PageException("无法自动获取数据库类型,请通过 helperDialect 参数指定!");
}
AbstractHelperDialect dialect = initDialect(dialectStr, properties);
urlDialectMap.put(url, dialect);
return dialect;
} finally {
lock.unlock();
}
}
}
count语句是如何实现的?
简单点来说,就是把原有的SQL封装为一个子查询,并执行count聚合函数。
伪代码如下:
String originalSql = "select * from xxx";
String countSql = String.format("select count(0) from (%s) as temp_table", originalSql);
当然,PageHelper代码逻辑更复杂,包含对一些特殊情况的处理、对SQL语句的解析等等。
mybatis拦截器实现原理
要想实现mybatis中的拦截器,得先实现Interceptor
接口,并在plugin
方法中确定是否返回代理类。
mybatis框架在InterceptorChain中实现了拦截器链。
public class InterceptorChain {
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
// 层层代理
target = interceptor.plugin(target);
}
return target;
}
}
可代理的目标有四类:ParameterHandler
、ResultSetHandler
、StatementHandler
、Executor
。
在Configuration类中调用newXxx方法生成对应的实例时,会调用InterceptorChain的pluginAll方法获取代理对象。
public class Configuration {
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
// 生成代理对象
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
ResultHandler resultHandler, BoundSql boundSql) {
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
// 生成代理对象
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
}
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
// 生成代理对象
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
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) interceptorChain.pluginAll(executor);
return executor;
}
}
当然,如果不想生成对应类型的对象,这需要在拦截器的plugin方法中返回target参数即可。
如果要生成该怎么做?
调用mybatis提供的工具类Plugin
的静态方法wrap
。并在拦截器上配置@Intercepts
和@Signature
注解。
这个类通过JDK自带的动态代理来生成代理对象。
public class Plugin implements InvocationHandler {
public static Object wrap(Object target, Interceptor interceptor) {
// 解析@Signature注解,获取接口信息和方法信息
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
// 获取需要代理的接口(从拦截器类上的@Signature注解获取需要被代理接口,并判断被代理的对象是否存在相应的接口,全都不存在则不会进行代理)
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
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)) {
// 命中🎯,则调用拦截器方法
return interceptor.intercept(new Invocation(target, method, args));
}
// 否则,执行真实对象的方法
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
// ...
}
那么Plugin又是如何知道拦截器要拦截对应接口的哪个方法呢?
通过拦截器的类上的注解Signature
,Plugin会在生成代理对象前就把需要代理方法对签名和信息封装到一个map中,并在invoke方法中进行判断和执行。
获取需要被拦截的接口的方法代码如下:
public class Plugin implements Invocation {
// 解析注解
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
// issue #251
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;
}
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
// 获取所有接口
Set<Class<?>> interfaces = new HashSet<>();
while (type != null) {
for (Class<?> c : type.getInterfaces()) {
// 判断是否与@Signature注解中的接口是否匹配
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
// 迭代查询父接口
type = type.getSuperclass();
}
return interfaces.toArray(new Class<?>[interfaces.size()]);
}
}
__EOF__

本文链接:https://www.cnblogs.com/yghr/p/16859579.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)