MyBatis插件的用法与源码逻辑及PageHelper相关源码(万字长文干货)
Posted 守夜人爱吃兔子
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MyBatis插件的用法与源码逻辑及PageHelper相关源码(万字长文干货)相关的知识,希望对你有一定的参考价值。
一 . 前言
今天主要带大家了解 MyBatis 插件的用法与主要的源码逻辑,和PageHelper 的相关源码,本文是一篇概括性文档 , 用于后续快速使用相关功能 , 整体难度较低。文章很长,干货很多,一时半会看不完的建议先一键三连。
二 . 流程
2.1 基础用法
基础拦截器类:
@Intercepts(
{@org.apache.ibatis.plugin.Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class DefaultInterceptor implements Interceptor {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public Object intercept(Invocation invocation) throws Throwable {
logger.info("------> this is in intercept <-------");
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
logger.info("------> this is in plugin <-------");
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
logger.info("------> this is in setProperties <-------");
}
}
基础配置类:
@Configuration
public class PluginsConfig {
@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;
@PostConstruct
public void addPageInterceptor() {
DefaultInterceptor interceptor = new DefaultInterceptor();
// 此处往 SqlSessionFactory 中添加
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
}
}
}
这里可以看到 , 拦截到的参数如下 :
2.2 功能详解
整个拦截过程有几个主要的组成部分 :
Interceptor 拦截器接口:
M- Interceptor#intercept(Invocation invocation) : 拦截方法
M- plugin : 调用 Plugin#wrap(Object target, Interceptor interceptor) 方法,执行代理对象的创建
M- setProperties : 从 properties 获取一些需要的属性值
// 这里能看到 , 其中强制实现的方法只有 intercept
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
default void setProperties(Properties properties) {
}
}
InterceptorChain 拦截器链:
InterceptorChain 是一个拦截器链 , 用于执行相关的Interceptor 操作 , 其中有一个拦截器集合
// 主要的大纲如此所示
F- List<Interceptor> ArrayList
M- addInterceptor : 添加拦截器
- 在 Configuration 的 #pluginElement(XNode parent) 方法中被调用
- 创建 Interceptor 对象,并调用 Interceptor#setProperties(properties) 方法
- 调用 Configuration#addInterceptor(interceptorInstance) 方法
- 添加到 Configuration.interceptorChain 中
M- pluginAll : 应用所有拦截器到指定目标对象
注解@Signature 和 @Intercepts:
这是过程中主要涉及的2个注解 :
- @Signature : 定义类型
- @Intercepts : 定义拦截器 , Intercepts 中可以包含一个 Signature 数组
- type : 拦截器处理的类
- args : 方法参数 (重载的原因)
- method : 拦截的方法
2.3 源码跟踪
Step 1 : 资源的加载:
资源的加载主要是对对应方法的代理逻辑 , 一个 plugin 操作 , 主要包含2个步骤 :
// 步骤一 : 声明拦截器对象
@Intercepts(
{@org.apache.ibatis.plugin.Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
// 步骤二 : sqlSessionFactory 中添加拦截器对象
sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
来跟进一下相关的操作 :
C01- Configuration
F01_01- InterceptorChain interceptorChain
M01_01- addInterceptor
- interceptorChain.addInterceptor(interceptor)
?- 可以看到 , 这里往 InterceptorChain 添加了 interceptor
// M01_01 源代码
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
// 这里将拦截器加到了拦截器链中
// PS : 但是此处未完全完成 , 仅仅只是添加 , 具体的操作会在上面 pluginAll 中完成
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
Step 2 : plugin 的构建:
Step 1 中已经完成了相关 interceptors 的添加 , 这个环节需要通过 Interceptor 构建对应的 Plugin
先来看一下调用链 :
- C- SqlSessionTemplate # selectList : 发起 Select 请求
- C- SqlSessionInterceptor # invoke : 构建一个 Session 代理
- C- SqlSessionUtils # getSqlSession : 获取 Session 对象
- C- DefaultSqlSessionFactory # openSessionFromDataSource
- C- Configuration # newExecutor
整体来说 ,从 getSqlSession 开始关注即可
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
return session;
}
// 核心节点 , 开启 Session
session = sessionFactory.openSession(executorType);
registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
return session;
}
DefaultSqlSessionFactory 构建一个 Session , 同时调用 Plugin 生成逻辑:
此处构建 Session 的同时 , 最终调用 Plugin warp 构建 Plugin
C- DefaultSqlSessionFactory
// 打开的起点 : 打开 session 的时候 ,
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);
// 此处构建 Executor 并且放入 Session
final Executor executor = configuration.newExecutor(tx, execType);
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();
}
}
// 调用拦截链
C- Configuration
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 = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
// 拦截链处理
C- InterceptorChain
public Object pluginAll(Object target) {
// 此处是在构建拦截器链 , 返回的是最后的拦截器处理类
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
C- Plugin
// 调用 Plugin wrap , 生成了一个新的 plugin , 该 plugin 包含对应的 interceptor 的 拦截方法
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
// 核心 , 对方法做了代理 , 同时为代理类传入了 Plugin
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
Step 3 : invoke 执行:
拦截器是基于 Proxy 代理实现的 , 在这里看一下代理的调用 :
以 Query 为例 :
当调用 Executor 中 Query 方法时,会默认调用代理类 , 那么他在整个逻辑中是作为什么角色的?
Executor 作用 : Executor 是 Mybatis 中的顶层接口 , 定义了主要的数据库操作方法
public interface Executor {
ResultHandler NO_RESULT_HANDLER = null;
int update(MappedStatement ms, Object parameter) throws SQLException;
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
<E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
List<BatchResult> flushStatements() throws SQLException;
void commit(boolean required) throws SQLException;
void rollback(boolean required) throws SQLException;
CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);
boolean isCached(MappedStatement ms, CacheKey key);
void clearLocalCache();
void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);
Transaction getTransaction();
void close(boolean forceRollback);
boolean isClosed();
void setExecutorWrapper(Executor executor);
}
execute 的调用 :
之前构建 Session 的时候 , 已经为其定义了Plugin , 以下为 Plugin 的主要流程
// 1 . 选择合适的 plugin
C- DefaultSqlSession
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
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();
}
}
// 2 . 中间通过 proxy 代理
// 3 . plugin 中调用拦截器
C05- Plugin
F05_01- private final Interceptor interceptor;
M05_01- invoke(Object proxy, Method method, Object[] args)
- 获取可以拦截的 method
- 判断当前的 method 是否在可拦截的
// M05_01 : invoke@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);
}
}
// 4 . 调用 interceptor
public Object intercept(Invocation invocation) throws Throwable {
// 此处调用代理方法的实际逻辑
return invocation.proceed();
}
三 . 扩展 PageHelper
以下为 PageHelper 中如何使用相关的拦截器方法的 :
PageHelper 中主要有 2个拦截器 :
- PageInterceptor : 分页操作插件
- QueryInterceptor : 查询操作插件
我们这次主要看一下 PageInterceptor 拦截器 :
拦截器代码:
@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;
private String countSuffix = "_COUNT";
protected Cache<String, MappedStatement> msCountMap = null;
private String default_dialect_class = "com.github.pagehelper.PageHelper";
@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 个参数时
// 获取绑定 SQL : select id, type_code, type_class, type_policy, type_name, supplier_id, supplier_name from sync_type
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)) {
//查询总数
Long count = count(executor, ms, parameter, rowBounds, resultHandler, 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 {
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());
}
}
}
}
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;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
//缓存 count ms
msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", 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.setProperties(properties);
String countSuffix = properties.getProperty("countSuffix");
if (StringUtil.isNotEmpty(countSuffix)) {
this.countSuffix = countSuffix;
}
}
}
补充 :ExecutorUtil.executeAutoCount 相关逻辑
/**
* 执行自动生成的 count 查询
**/
public static Long executeAutoCount(Dialect dialect, Executor executor, MappedStatement countMs,
Object parameter, BoundSql boundSql,
RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
Map<String, Object> additionalParameters = getAdditionalParameter(boundSql);
//创建 count 查询的缓存 key
CacheKey countKey = executor.createCacheKey(countMs, parameter, RowBounds.DEFAULT, boundSql);
// 调用方言获取 count sql
// SELECT count(0) FROM sync_type
String countSql = dialect.getCountSql(countMs, boundSql, parameter, rowBounds, countKey);
//countKey.update(countSql);
BoundSql countBoundSql = new BoundSql(countMs.getConfiguration(), countSql, boundSql.getParameterMappings(), parameter);
//当使用动态 SQL 时,可能会产生临时的参数,这些参数需要手动设置到新的 BoundSql 中
for (String key : additionalParameters.keySet()) {
countBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
}
//执行 count 查询
Object countResultList = executor.query(countMs, parameter, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql);
Long count = (Long) ((List) countResultList).get(0);
return count;
}
补充 : pageQuery 查询
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));
}
//执行分页查询
return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
} else {
//不执行分页的情况下,也不执行内存分页
return executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
}
}
因为源码的注释程度很高 , 所以基本上不需要做额外的标注了 , 整体的流程就是 :
- Step 1 : 拦截器 intercept方法定义整体逻辑
- Step 2 : count 方法决定他是否分页
- Step 3 :pageQuery 调用方言进行事情SQL的拼接
整体中有几点值得关注 :
- 只生成分页的 row 和 pageKey 等 , 在最终通过方言组合 , 以适应多种数据库结构
- 核心还是调用 executor.query 原生方法
简述一下 PageHepler 的绑定流程:
核心处理类为 AbstractHelperDialect , 先从创建开始 :
PageHelper.startPage(page, size);
List<SyncType> allOrderPresentList = syncTypeDAO.findAll();
// Step 1 : startPage 核心代码
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;
}
此处最核心的就是 setLocalPage , 会使用 ThreadLocal 保持线程参数
protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
// Step 2 : 拦截器中的参数获取
// 可以看到第一句就是从 getLocalPage - ThreadLocal 中获取
C- AbstractHelperDialect # processParameterObject
public 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>();
//动态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;
}
}
}
}
}
return processPageParameter(ms, paramMap, page, boundSql, pageKey);
}
总结
拦截器的使用 :
- 准备拦截器类
- sqlSessionFactory.getConfiguration().addInterceptor(interceptor) 添加拦截器
PageHelper 核心 :
- parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey) : 获取分页的参数
- dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey) : 参数解析为 SQL
- ThreadLocal 保存参数
这应该是写的最简单的一篇源码分析了 , 除了本身结构不复杂以外 , 相关源码的注释也很清晰 , 基本上没有什么分析的需求。如果觉得文章对你有帮助,可以点下一键三连,后续我会持续更新更多java相关干货文章。祝大家万事胜意!
以上是关于MyBatis插件的用法与源码逻辑及PageHelper相关源码(万字长文干货)的主要内容,如果未能解决你的问题,请参考以下文章