插件即plugin,本质是一系列拦截器,便于用户自定义扩展。
预留了四处扩展点:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
接下来以Pagehelper为例,看看它怎么实现的Executor扩展。
一、startPage做了什么
PageHelper.startPage(1,20);
这是PageHelper推荐的使用方式,后续的第一个查询方法会分页。
观察这个方法的内部逻辑
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count) {
// 封装成page对象
Page<E> page = new Page<E>(pageNum, pageSize, count);
// == 将page对象存在`PageMethod#LOCAL_PAGE`——这是一个ThreadLocal
setLocalPage(page);
return page;
}
二、PageHelper如何与Mybatis建立联系的?
1.配置
mybatis的xml中增加配置
<plugins>
<!-- com.github.pagehelper为PageHelper类所在包名 -->
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 使用下面的方式配置参数,后面会有所有的参数介绍 -->
<property name="param1" value="value1"/>
</plugin>
</plugins>
增加插件配置后,会在xml解析时放入configuration中
org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration{
// 解析插件
pluginElement(root.evalNode("plugins"));
}
org.apache.ibatis.builder.xml.XMLConfigBuilder#pluginElement{
for (XNode child : parent.getChildren()) {
// 根据配置创建Interceptor对象
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
// 执行setProperties方法设置属性
interceptorInstance.setProperties(properties);
// == 将拦截器存入configuration中
configuration.addInterceptor(interceptorInstance);
}
}
看看Interceptor最终存放到了哪里?
org.apache.ibatis.session.Configuration#addInterceptor
org.apache.ibatis.plugin.InterceptorChain#addInterceptor
class InterceptorChain {
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
// == 最终存放位置
private final List<Interceptor> interceptors = new ArrayList<>();
结构
观察PageInterceptor的结构
// == 被@Intercepts注解修饰
@Intercepts(
{
// == 通过@Signature指定被拦截的方法
@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}),
}
)
// == 实现Interceptor接口
public class PageInterceptor implements Interceptor
Interceptor接口有三个方法:
// == 出发拦截后,会执行此方法逻辑
Object intercept(Invocation invocation) throws Throwable;
// == 默认的包装
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
// 初始化时设置属性
default void setProperties(Properties properties) {}
触发
PageInterceptor的入口在executor创建部分。
org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSession()
org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSessionFromDataSource
org.apache.ibatis.session.Configuration#newExecutor(org.apache.ibatis.transaction.Transaction, org.apache.ibatis.session.ExecutorType){
// == 过滤器链对executor做封装
executor = (Executor) interceptorChain.pluginAll(executor);
}
org.apache.ibatis.plugin.InterceptorChain#pluginAll{
for (Interceptor interceptor : interceptors) {
// == 对目标对象进行包装
target = interceptor.plugin(target);
}
return target;
}
查看具体的包装方法
default Object plugin(Object target) {
return Plugin.wrap(target, this);
⬇⬇⬇⬇⬇⬇
// == 核心逻辑是动态代理
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
既然是动态代理,直接观察invokcationHandler(Plugin)的invoke方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 执行拦截器的拦截方法
return interceptor.intercept(new Invocation(target, method, args));
}
观察PageInterceptor#intercept实现:
com.github.pagehelper.PageInterceptor#intercept{
// == 判断是否需要进行分页,如果不需要,直接返回结果
if (!dialect.skip(ms, parameter, rowBounds)) {
// -- 判断是否需要进行分页查询,page.getPageSize>0
if (dialect.beforePage(ms, parameter, rowBounds)) {
// -- 调用方言获取分页sql,添加limit语句
String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
//执行分页查询
resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
}
}
}
- 怎么判断是否需要分页?
com.github.pagehelper.PageHelper#skip
public boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {
// == PageMethod#LOCAL_PAGE中是否有值
Page page = pageParams.getPage(parameterObject, rowBounds);
// -- PageMethod#LOCAL_PAGE有值情况,初始化方言
autoDialect.initDelegateDialect(ms);
return false;
}
这里就和第一节连上了。
PageHelper.startPage向ThreadLocal中存放了page对象
此处skip()方法判断:如果有值返回false,需要分页,同时初始化方言dialect;无值返回true,不必分页。
- 方言dialect的初始化逻辑
通过数据库链接url中截取的数据库别名,来选择具体的方言类,并用反射进行初始化
com.github.pagehelper.page.PageAutoDialect#initDelegateDialect{
this.delegate = getDialect(ms);
}
com.github.pagehelper.page.PageAutoDialect#getDialect{
// 通过数据库链接截取别名
String dialectStr = fromJdbcUrl(url);
// -- 通过别名初始化
AbstractHelperDialect dialect = initDialect(dialectStr, properties);
}
com.github.pagehelper.page.PageAutoDialect#initDialect{
// 解析方言的class
Class sqlDialectClass = resloveDialectClass(dialectClass);
⬇⬇⬇⬇⬇
// == 通过dialectAliasMap获取
dialectAliasMap.get(className.toLowerCase());
// 通过反射创建
dialect = (AbstractHelperDialect) sqlDialectClass.newInstance();
}
我们看看dialectAliasMap中存了什么
com.github.pagehelper.page.PageAutoDialect#dialectAliasMap
static {
//初始化时注册别名
dialectAliasMap.put("hsqldb", HsqldbDialect.class);
dialectAliasMap.put("h2", HsqldbDialect.class);
dialectAliasMap.put("postgresql", HsqldbDialect.class);
dialectAliasMap.put("mysql", MySqlDialect.class);
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。