本文已参与「新人创作礼」活动, 一起开启掘金创作之路。
1 简介
日常开发过程中基于mybatis的sql查询,会存在分页查询场景,可通过手动写limit实现分页,同时可依赖分页插件进行实现。pageHeler是一款基于mybatis的插件实现分页的插件,无需自己手动实现分页。
2 pageHelper的实现
2.1 pageHelper的使用
下面是基于spring+mybatis的实现
mybtais的config.xml引入plugins标签
<plugins>
<!-- com.github.pagehelper为PageHelper类所在包名 -->
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 4.0.0以后版本可以不设置该参数 -->
<property name="helperDialect" value="mysql"/>
<!-- 该参数默认为false -->
<!-- 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用 -->
<!-- 和startPage中的pageNum效果一样-->
<property name="offsetAsPageNum" value="true"/>
<!-- 该参数默认为false -->
<!-- 设置为true时,使用RowBounds分页会进行count查询 -->
<property name="rowBoundsWithCount" value="false"/>
<!-- 设置为true时,如果pageSize=0或者RowBounds.limit = 0就会查询出全部的结果 -->
<!-- (相当于没有执行分页查询,但是返回结果仍然是Page类型)-->
<property name="pageSizeZero" value="true"/>
<!-- 3.3.0版本可用 - 分页参数合理化,默认false禁用 -->
<!-- 启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页 -->
<!-- 禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据 -->
<property name="reasonable" value="true"/>
<!-- 3.5.0版本可用 - 为了支持startPage(Object params)方法 -->
<!-- 增加了一个`params`参数来配置参数映射,用于从Map或ServletRequest中取值 -->
<!-- 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,orderBy,不配置映射的用默认值 -->
<!-- 不理解该含义的前提下,不要随便复制该配置 -->
<property name="params" value="pageNum=start;pageSize=limit;"/>
<!-- 支持通过Mapper接口参数来传递分页参数 -->
<property name="supportMethodsArguments" value="true"/>
<!-- always总是返回PageInfo类型,check检查返回类型是否为PageInfo,none返回Page -->
<property name="returnPageInfo" value="check"/>
</plugin>
</plugins>
代码引用实现(官网提供)
//第一种,RowBounds方式的调用
List<Country> list = sqlSession.selectList("x.y.selectIf", null, new RowBounds(0, 10));
//第二种,Mapper接口方式的调用,推荐这种使用方式。
PageHelper.startPage(1, 10);
List<Country> list = countryMapper.selectIf(1);
//第三种,Mapper接口方式的调用,推荐这种使用方式。
PageHelper.offsetPage(1, 10);
List<Country> list = countryMapper.selectIf(1);
//第四种,参数方法调用
//存在以下 Mapper 接口方法,你不需要在 xml 处理后两个参数
public interface CountryMapper {
List<Country> selectByPageNumSize(
@Param("user") User user,
@Param("pageNum") int pageNum,
@Param("pageSize") int pageSize);
}
//配置supportMethodsArguments=true
//在代码中直接调用:
List<Country> list = countryMapper.selectByPageNumSize(user, 1, 10);
//第五种,参数对象
//如果 pageNum 和 pageSize 存在于 User 对象中,只要参数有值,也会被分页
//有如下 User 对象
public class User {
//其他fields
//下面两个参数名和 params 配置的名字一致
private Integer pageNum;
private Integer pageSize;
}
//存在以下 Mapper 接口方法,你不需要在 xml 处理后两个参数
public interface CountryMapper {
List<Country> selectByPageNumSize(User user);
}
//当 user 中的 pageNum!= null && pageSize!= null 时,会自动分页
List<Country> list = countryMapper.selectByPageNumSize(user);
//第六种,ISelect 接口方式
//jdk6,7用法,创建接口
Page<Country> page = PageHelper.startPage(1, 10).doSelectPage(new ISelect() {
@Override
public void doSelect() {
countryMapper.selectGroupBy();
}
});
//jdk8 lambda用法
Page<Country> page = PageHelper.startPage(1, 10).doSelectPage(()-> countryMapper.selectGroupBy());
//也可以直接返回PageInfo,注意doSelectPageInfo方法和doSelectPage
pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(new ISelect() {
@Override
public void doSelect() {
countryMapper.selectGroupBy();
}
});
//对应的lambda用法
pageInfo = PageHelper.startPage(1, 10).doSelectPageInfo(() -> countryMapper.selectGroupBy());
//count查询,返回一个查询语句的count数
long total = PageHelper.count(new ISelect() {
@Override
public void doSelect() {
countryMapper.selectLike(country);
}
});
//lambda
total = PageHelper.count(()->countryMapper.selectLike(country));
2.2 实现分析
下面使用Iselect实现的方式进行
首先分析PageHelper#startPage方法,该方法有多重重载,下面是最终实现
/**
* 开始分页
*
* @param pageNum 页码
* @param pageSize 每页显示数量
* @param count 是否进行count查询
* @param reasonable 分页合理化,null时用默认配置
* @param pageSizeZero true且pageSize=0时返回全部结果,false时分页,null时用默认配置
*/
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
//构造返回对象,最终调用iselect对象后返回page对象,该对象继承ArrayList
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());
}
//将page对象设置到thradLocal中,后续会使用
setLocalPage(page);
return page;
}
再iselect得实现其实是执行mapper得方法,就是正常执行mybatis得流程,再mybatis流程中会被pageHelper实现得拦截器拦截住,所以直接看PageInterceptor
/**
*实现mybatis的Interceptor接口
*Intercepts注解释义
* Signature type 需要拦截的类
* method 拦截的方法
* args 调用query方法的参数类型
* 该注解定义是为了再执行各种sql的时候,让mybatis知道需要对哪些方法在什么时候进行拦截
* Signature注解可以确定描述为一个java接口,类型,方法名,参数
*/
@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 volatile Dialect dialect;
//进行count的前缀
private String countSuffix = "_COUNT";
//缓存
protected Cache<String, MappedStatement> msCountMap = null;
//全路径类名,是为了初始化使用
private String default_dialect_class = "com.github.pagehelper.PageHelper";
/**
* @Description: mybatis的拦截器执行方法
* @param Invocation 包含三个参数 taget代理的真实对象 method 反射方法 args方法参数
* @return
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
//获取调用参数类型
Object[] args = invocation.getArgs();
//mybatis的映射信息封装
MappedStatement ms = (MappedStatement) args[0];
//获取真正的参数
Object parameter = args[1];
//获取分页信息
RowBounds rowBounds = (RowBounds) args[2];
//获取结果集处理器
ResultHandler resultHandler = (ResultHandler) args[3];
//获取sql执行器
Executor executor = (Executor) invocation.getTarget();
CacheKey cacheKey;
BoundSql boundSql;
//由于逻辑关系,只会进入一次
if (args.length == 4) {
//4 个参数时
//获取boundSql,执行语句信息
boundSql = ms.getBoundSql(parameter);
//创建缓存
cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
} else {
//6 个参数时
cacheKey = (CacheKey) args[4];
boundSql = (BoundSql) args[5];
}
//判断方言是否存在
checkDialectExists();
List resultList;
//调用方法判断是否需要进行分页,如果不需要,直接返回结果
if (!dialect.skip(ms, parameter, rowBounds)) {
//判断是否需要进行 count 查询
if (dialect.beforeCount(ms, parameter, rowBounds)) {
//查询总数,主要得重新构造count语句
Long count = count(executor, ms, parameter, rowBounds, resultHandler, boundSql);
//处理查询总数,返回 true 时继续分页查询,false 时直接返回
//这边的判断主要是用来判断传入的分页参数是否超过整个count的值
if (!dialect.afterCount(count, parameter, rowBounds)) {
//当查询总数为 0 时,直接返回空的结果
return dialect.afterPage(new ArrayList(), parameter, rowBounds);
}
}
//执行分页查询,
//这边语句的修改则是来处理一个点,就是我们的执行语句是非分页的,需要通过以下的操作来处理
//分页参数,就是重盖mMappedStatement里存储的sql语句和传入分页参数
//语句修改则是需要根据不同的数据库方言进行不同的处理
resultList = ExecutorUtil.pageQuery(dialect, executor,
ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
} else {
//rowBounds用参数值,不使用分页插件处理时,仍然支持默认的内存分页
resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}
//该地方获取了结果集,将结果封装到page对象中
return dialect.afterPage(resultList, parameter, rowBounds);
} finally {
//移除使用的threadlocal的对象
if(dialect != null){
dialect.afterAll();
}
}
}
/**
* Spring bean 方式配置时,如果没有配置属性就会执行下面的 setProperties 方法,就不会初始化
* <p>
* 因此这里会出现 null 的情况 fixed #26
*/
private void checkDialectExists() {
if (dialect == null) {
synchronized (default_dialect_class) {
if (dialect == null) {
setProperties(new Properties());
}
}
}
}
//需要执行count查询,则就需要构造一个重新得mybatis执行的对象
private Long count(Executor executor, MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler,
BoundSql boundSql) throws SQLException {
String countMsId = ms.getId() + countSuffix;
Long count;
//先判断是否存在手写的 count 查询
MappedStatement countMs = ExecutorUtil.getExistedMappedStatement(ms.getConfiguration(), countMsId);
if (countMs != null) {
count = ExecutorUtil.executeManualCount(executor, countMs, parameter, boundSql, resultHandler);
} else {
countMs = msCountMap.get(countMsId);
//自动创建
if (countMs == null) {
//根据当前的 ms 创建一个返回值为 Long 类型的 ms
countMs = MSUtils.newCountMappedStatement(ms, countMsId);
msCountMap.put(countMsId, countMs);
}
count = ExecutorUtil.executeAutoCount(dialect, executor, countMs, parameter, boundSql, rowBounds, resultHandler);
}
return count;
}
/**
* @Description: mybatis的拦截器代理包装方法
* @param target 真实对象,通过这个方法进行动态代理的包装
* @return
*/
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
/**
* @Description: mybatis的拦截器针对加载的拦截器获取的所有属性设置
* @param properties 针对该拦截器所有设置的属性
* @return
*/
@Override
public void setProperties(Properties properties) {
//缓存实现,如果不配置,则默认实现guava的cache
msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties);
//获取分页逻辑,如果不实现,则使用默认的pageHelper的分页
//如果想自己实现,则需要对提供的接口Dialect进行自己的实现
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.setProperties(properties);
//获取count的前缀
String countSuffix = properties.getProperty("countSuffix");
if (StringUtil.isNotEmpty(countSuffix)) {
this.countSuffix = countSuffix;
}
}
}
2.3实现流程
3 mybatis对插件的实现
3.1mybatis的框架相关
3.1.1 mybatis的架构
3.1.2 mybatis的流程
3.1.3 myabtis的源码包结构
| 模块 | 分层 | 定义 | 备注 |
|---|---|---|---|
| session | 接口层 | 会话模块 | 工厂模式 |
| mapping | 核心处理层 | 映射 | |
| builder | 核心处理层 | 用来各种配置解析 | 建造者模式 |
| scripting | 核心处理层 | sql解析 | |
| plugin | 核心处理层 | 扩展插件 | 责任链模式,代理模式 |
| cursor | 核心处理层 | 游标执行 | |
| excutor | 核心处理层 | sql执行 | 模板模式 |
| datasource | 基础支持层 | 数据源模块 | |
| cache | 基础支持层 | 缓存模块 | 装饰者模式 |
| parsing | 基础支持层 | XML解析模块 | |
| reflection | 基础支持层 | 反射模块 | |
| logging | 基础支持层 | 日志模块 | 适配器模式 |
| binding | 基础支持层 | mapper的绑定 | 代理模式 |
| io | 基础支持层 | 资源加载模块 | |
| type | 基础支持层 | 类型转换模块 | |
| annotations | 基础支持层 | 注解模块 | |
| exceptions | 基础支持层 | 异常模块 |
3.2 拦截器实现流程
3.2.1 初始化加载过程
首先在加载mybatis的配置文件来构建sqlSessionFactory,再加载配置文件的时候就会读取plugins的节点。
//sqlSessionFactoryBuilder类中,其中一种构建sqlSessionFactory的方式
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
//委托XMLConfigBuilder来解析xml文件,并构建
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
//主要再parse方法中进行XML解析
return build(parser.parse());
} catch (Exception e) {
//这里是捕获异常,包装成自己的异常并抛出的idiom?,最后还要reset ErrorContext
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
//代码跟读到XMLConfigBuilder#parseConfiguration用来解析mybatis的配置文件
private void parseConfiguration(XNode root) {
try {
//分步骤解析
//1.properties
propertiesElement(root.evalNode("properties"));
//2.类型别名
typeAliasesElement(root.evalNode("typeAliases"));
//3.插件
pluginElement(root.evalNode("plugins"));
//4.对象工厂
objectFactoryElement(root.evalNode("objectFactory"));
//5.对象包装工厂
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
//6.设置
settingsElement(root.evalNode("settings"));
// read it after objectFactory and objectWrapperFactory issue #631
//7.环境
environmentsElement(root.evalNode("environments"));
//8.databaseIdProvider
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
//9.类型处理器
typeHandlerElement(root.evalNode("typeHandlers"));
//10.映射器
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
//解析插件的节点方法
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
//获取拦截器的包路径
String interceptor = child.getStringAttribute("interceptor");
//读取拦截器配置的各种属性,就是再插件中配置的各种值,配置到properties里
Properties properties = child.getChildrenAsProperties();
//myabis使用Resource封装的类加载器进行类加载,获取实列
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
//调用setProperties的方法,将值传给插件方,让插件方去处理需要的各种个性化的配置
interceptorInstance.setProperties(properties);
//调用InterceptorChain.addInterceptor
configuration.addInterceptor(interceptorInstance);
}
}
}
//configuration.addInterceptor该方法就是将拦截器加入拦截的链中
public class InterceptorChain {
//内部就是一个拦截器的List
private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
//对原始对象进行动态代理封装
public Object pluginAll(Object target) {
//循环调用每个Interceptor.plugin方法
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);
}
}
3.2.2 执行过程
加载过程从上面的流程图首先执行的方法是sqlSessionFactory#openSession方法
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
//获取事务相关
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
//通过事务工厂来产生一个事务
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//生成一个执行器(事务包含在执行器里),执行器主要用来进行JDBC的内部封装
final Executor executor = configuration.newExecutor(tx, execType);
//然后产生一个DefaultSqlSession,sqlSession主要用来定义各种sql接口,通过构造器参数可以看出来,mybatis的配置,执行器,事务提交
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
//如果打开事务出错,则关闭它
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
//最后清空错误上下文
ErrorContext.instance().reset();
}
}
//生成执行
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
//防止执行为空
executorType = executorType == null ? defaultExecutorType : executorType;
//方式将defaultExecutorType再次设置为空?
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
//然后就是简单的3个分支,产生3种执行器BatchExecutor/ReuseExecutor/SimpleExecutor
//批处理执行器
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
//重复执行器,缓存statement
executor = new ReuseExecutor(this, transaction);
} else {
//常规执行器最常用
executor = new SimpleExecutor(this, transaction);
}
//如果要求缓存,生成另一种CachingExecutor(默认就是有缓存),装饰者模式,所以默认都是返回CachingExecutor
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
//加载插件,插件通过动态代理的方式加载
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
//interceptorChain.pluginAll最终是对executor的增强实现,会调用Plugin#wrap方法
public static Object wrap(Object target, Interceptor interceptor) {
//取得签名Map
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
//取得要改变行为的类(ParameterHandler|ResultSetHandler|StatementHandler|Executor)
Class<?> type = target.getClass();
//取得接口
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
//产生代理
if (interfaces.length > 0) {
//进行动态代理
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
//取得签名Map
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
//取Intercepts注解,例子可参见ExamplePlugin.java
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
//必须得有Intercepts注解,没有报错
if (interceptsAnnotation == null) {
throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
}
//value是数组型,Signature的数组
Signature[] sigs = interceptsAnnotation.value();
//每个class里有多个Method需要被拦截,所以这么定义
Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
for (Signature sig : sigs) {
Set<Method> methods = signatureMap.get(sig.type());
if (methods == null) {
methods = new HashSet<Method>();
signatureMap.put(sig.type(), methods);
}
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<Class<?>>();
//循环调用符合的拦截器的方法
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()]);
}
//调用查询方法,以sqlSession#selectList
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
//根据statement id找到对应的MappedStatement
MappedStatement ms = configuration.getMappedStatement(statement);
//转而用执行器来查询结果,注意这里传入的ResultHandler是null
//pageHelper中的注解标识需要executor,query的方法,此方法正好合适,其实这里的executor
//已经增强实现了,所以此处调用query首先执行的Plugin#invoke方法
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();
}
}
//plugin#invoke方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//获取所有定义拦截的method
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
//如果需要拦截,则进行拦截
if (methods != null && methods.contains(method)) {
//调用Interceptor.intercept,也即实现插件逻辑,分页插件就是再次进行sql改写
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
//使用simpleExecutor为列,最终调用simpleExecutor#doQuery方法,主要是对JDBC的查询流程封装
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
//新建一个StatementHandler
//这里看到ResultHandler传入了
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//准备语句
stmt = prepareStatement(handler, ms.getStatementLog());
//StatementHandler.query
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
String sql = boundSql.getSql();
statement.execute(sql);
//先执行Statement.execute,然后resultSetHandler处理结果集
return resultSetHandler.<E>handleResultSets(statement);
}
4 思考
上面是mybatis针对拦截器通过责任链和动态代理的方式进行,我们常用的servlet和dubbo也有拦截器,他们是如何实现的。后续可继续进行相关源码学习内容。