分页插件Pagehelper源码分析
- 一、插件机制
- 二、Pagehelper源码分析
前文叙述过以下内容:Mybatis对动态代理的使用,一二级缓存和懒加载的原理。其中二级缓存解释了在分布式环境下可能出现缓存不一致问题,但没说解决方案。其实个人认为这种问题除非数据库集群等机制,不然个人认为一个服务大概率就对应的一个持久化层,很少会出现不一致的问题,如果有这边还是建议不使用二级缓存就是了,或者自己写个缓存解决我觉得挺好(没遇到过🤣🤣🤣)。
动态代理的使用(Javassist、CGLIB、JDK动态代理)
Mybatis查询流程(一级、二级缓存、懒加载原理)
Mybatis 除了前面源码分析到的那些核心部分,Mybatis 还提供了一强大的功能,即支持插件机制。Mybatis支持对Executor、StatementHandler、ParameterHandler和ResultSetHandler进行拦截,也就是说会对这4种对象进行代理,这就是插件的功能。针对插件机制的核心原理和我们常用的Pagehelper源码分析接下来由小编阐述一下。
一、插件机制
关于插件机制的概述,下面这文我觉得解释得很清楚了(看完下面这文的话可以直接跳到下面的Pagehelper源码分析去看)
MyBatis详解 - 插件机制
下面只解释核心部分(像Mybatis解析插件配置等等就不阐述了)
下面是Mybatis解析配置的插件后封装到的拦截器链,这后面插件机制的处理生成代理链的使用类。该拦截器链解析完配置后封装到了熟悉的 Configuration 中。
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<>();
// 这个用于后续生成代理链
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
// 在解析插件配置时,会把解析到的实例化拦截器然后封装到这过滤器链中
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
咱查看一下 pluginAll
方法在项目中的用法。
Mybatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,Mybatis 允许使用插件来拦截方法调用包括:
- Executor(update、query、flushStatement、commit、rollback、getTransaction、close、isClosed)拦截执行器的方法。PageInterceptor 就是对 query 方法进行拦截。
- ParameterHandler(getParameterObject,setParameters)拦截结果集的处理
- ResultSetHandler(handlerResults,handleOutputParameters)拦截结果集的处理。
- StatementHandler(prepare,parameterize、batch、update、query)拦截sql语法构建的处理。
Mybatis 采用责任链的模式,通过动态代理组织多个拦截器(插件),通过这些拦截器可以改变Mybatis的默认行为(诸如SQL重写之类的),由于插件会深入到Mybatis的核心,因此在编写自己的插件前最好了解下它的原理,以便写出高效的插件。
下面看看 Interceptor 的底层源码,其中 plugin 和 setProperties 是默认方法,注意这个 Plugin.wrap(target,this)
它底层用了动态代理,一个拦截接着一个拦截器代理,然后就产生了一个代理链,看完下面拦截器的源码,咱来看看 Plugin.wrap 源码实现。
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
default void setProperties(Properties properties) {
// NOP
}
}
下面是对 Plugin 的源码分析,实现了 InvocationHandler
接口。
//这个类是Mybatis拦截器的核心,大家可以看到该类继承了InvocationHandler
//又是JDK动态代理机制
public class Plugin implements InvocationHandler {
//目标对象
private Object target;
//拦截器
private Interceptor interceptor;
//记录需要被拦截的类与方法
private Map<Class<?>, Set<Method>> signatureMap;
private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
this.target = target;
this.interceptor = interceptor;
this.signatureMap = signatureMap;
}
//一个静态方法,对一个目标对象进行包装,生成代理类。
public static Object wrap(Object target, Interceptor interceptor) {
//首先根据interceptor上面定义的注解 获取需要拦截的信息
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
//目标对象的Class
Class<?> type = target.getClass();
//返回需要拦截的接口信息
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
//如果长度为>0 则返回代理类 否则不做处理
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
//代理对象每次调用的方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//通过method参数定义的类 去signatureMap当中查询需要拦截的方法集合
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);
}
}
//根据拦截器接口(Interceptor)实现类上面的注解获取相关信息
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
//获取注解信息
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
//为空则抛出异常
if (interceptsAnnotation == null) { // issue #251
throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
}
//获得Signature注解信息
Signature[] sigs = interceptsAnnotation.value();
Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
//循环注解信息
for (Signature sig : sigs) {
//根据Signature注解定义的type信息去signatureMap当中查询需要拦截方法的集合
Set<Method> methods = signatureMap.get(sig.type());
//第一次肯定为null 就创建一个并放入signatureMap
if (methods == null) {
methods = new HashSet<Method>();
signatureMap.put(sig.type(), methods);
}
try {
//找到sig.type当中定义的方法 并加入到集合
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;
}
//根据对象类型与signatureMap获取接口信息
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
Set<Class<?>> interfaces = new HashSet<Class<?>>();
//循环type类型的接口信息 如果该类型存在与signatureMap当中则加入到set当中去
while (type != null) {
for (Class<?> c : type.getInterfaces()) {
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
type = type.getSuperclass();
}
//转换为数组返回
return interfaces.toArray(new Class<?>[interfaces.size()]);
}
}
其中核心方法就是 invoke 方法,因为是代理吗,那它最后执行就是这个 invoke 方法咯,所以关注一下这个 invoke 方法,如果执行的方法是咱定义的话会走 Interceptor.interceptor
方法。return interceptor.intercept(new Invocation(target, method, args));
传的 Invocation 实例可以看见传了 target,method,args 方法。
interceptor.pluginAll 方法在以下地方被调用:
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, boolean autoCommit) {
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, autoCommit);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
现在的话可以梳理一下了:
interceptorChain.pluginAll
在Mybatis中四处被调用(上面指出了),它上面解释了是一个代理链对象,就是说一个插件就是写一个代理,一个插件的核心在它的 interceptor
方法中,参数是 Invocation 实例对象,从这个实例对象中可以获取 实例对象、参数、和对应的那个方法,然后自定义你想的操作。
代理时它会判断是否是你要执行的那个方法,如果不是的话会直接执行方法,也就是往下一个代理走。
二、Pagehelper源码分析
首先看 PageInterceptor 是对哪个进行拦截,来看看它的注解。可以看见是对 Executor.query 方法进行拦截。
@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}),
}
)
刚刚说了一个插件的形成就是它的核心方法 intercepor
@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;
//调用方法判断是否需要进行分页,如果不需要,直接返回结果
// dialect 其实就是对Pagehelper实例对象的封装
// Pagehelper 类是对分页 Page 的一些处理
// 在每个分页阶段都会去执行 Pagehelper 的一个回调
if (!dialect.skip(ms, parameter, rowBounds)) {
//开启debug时,输出触发当前分页执行时的PageHelper调用堆栈
// 如果和当前调用堆栈不一致,说明在启用分页后没有消费,当前线程再次执行时消费,调用堆栈显示的方法使用不安全
debugStackTraceLog();
//判断是否需要进行 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);
}
}
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) {
// 去调用 Pagehelper 中的 afterAll,把缓存的 Page 删掉
// Pagehelper 把 Page 对象放在 ThreadLocal 中
dialect.afterAll();
}
}
}
其中 Page 继承了 ArrayList ,到查询到结果集后,会调用 Pagehelper 中的 afterPage 方法,会对结果集的对象浅克隆到 Page 中(调用 System.arrayCopy 方法)。也就是说咱的 Page 就是结果。
简单看看其 afterPage 的核心代码:
大概知道了分页插件的核心原理后,咱来说说其流程吧。
-
在分页之前,会先去执行查询所有记录条数的sql,然后再判断给的分页参数是否合理什么的;
-
执行查询所有记录的sql,首先是去看看你有没有设有自己的MappedStatement,其id是
id+_COUNT
,比如我执行的分页方法是query
,那就判断你有没有query_COUNT
方法。否则的话Mybatis会为你创一个MappedStatement,这个MappedStatement大部分参数是继承这个查询对应的MappedStatement的参数,比如是否开启二级缓存,那么它会与那个查询对应的MappedStatement公用一个查询Cache
,即二级缓存。(有时候是应该自己是写COUNT方法,因为比如你用了软删除什么的字段,而它查询的是总数的) -
二级缓存这里是建议开的,因为如果表很多记录的话,这个查询COUNT的sql执行起来就挺耗时间的。
-
当然 PageInterceptor 会缓存这生成的 MappedStatement 的,不然反复创建相同的 MappedStatement 影响性能,PageInterceptor 中封装了一个
msCountMap
属性,该属性在 setProperties 方法中进行了实例化,其就是一个 SimpleCache。这样的话如果下次再在执行 count 的sql时,会先从这个缓存中拿这个 MappedStatement,如果没有的话就新建。
-
然后封装到分页Page数据中,然后去改装原sql为分页sql,即在后面加
limit
…然后执行这个sql,最后将结果集对象改装为Page对象返回。
这样的话我们得到的结果集就是 Page 了.
if(list instanceOf Page){Page page = (Page)list;}