阅读 MyBatis 源码(插件开发)

上一篇文章介绍了 MyBatis 执行 SQL 查询的流程,对源码中的关键类如 Configuration、Executor、StatementHandler 有了一定认识之后,本篇聊聊 MyBatis 的插件机制。
1. 简介 MyBatis 插件,简单理解为拦截器,它采用动态代理的方式,实现对目标方法的拦截,在前后做一些操作。
基于插件机制,基本上可以控制 SQL 执行的各个阶段,如执行阶段,参数处理阶段,语法构建阶段,结果集处理阶段,具体可以根据项目业务来实现对应业务逻辑。
支持拦截的方法:

  1. 执行器 Executor:update、query、commit、rollback 等方法;
  2. 参数处理器 ParameterHandler:getParameterObject、setParameters 等方法;
  3. 结果集处理器 ResultSetHandler:handleResultSets、handleOutputParameters 等方;
  4. SQL语法构建器 StatementHandler:prepare、parameterize、batch、update、query 等方法;
2. 插件示例 MyBatis XML 配置文件中定义分页插件。

自定义分页插件 PageInterceptor 代码如下,用于拦截 Executor#query 方法,修改 MappedStatement 对象中的 SQL 语句。
package com.sumkor.plugin; import com.sumkor.plugin.page.BoundSqlSqlSource; import com.sumkor.plugin.page.Page; import com.sumkor.plugin.page.PageUtil; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.plugin.*; import org.apache.ibatis.reflection.DefaultReflectorFactory; import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.reflection.factory.DefaultObjectFactory; import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import java.util.Properties; import java.util.StringJoiner; /** * 拦截 Executor#query 方法 * * @author Sumkor * @since 2021/7/26 */ @Intercepts({ @Signature( type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} ) }) @Slf4j public class PageInterceptor implements Interceptor {private static final int MAPPED_STATEMENT_INDEX = 0; private static final int PARAMETER_INDEX = 1; private static final int ROW_BOUNDS_INDEX = 2; /** * 通过反射工具类 MetaObject 来修改 MappedStatement 对象中的 SQL 语句 */ @Override public Object intercept(Invocation invocation) throws Throwable { log.info("------------------PageInterceptor#intercept 开始------------------"); final Object[] queryArgs = invocation.getArgs(); final MappedStatement ms = (MappedStatement) queryArgs[MAPPED_STATEMENT_INDEX]; final Object parameter = queryArgs[PARAMETER_INDEX]; // 获取分页参数 Page pagingParam = PageUtil.getPagingParam(); try { if (pagingParam != null) { // 构造新的分页查询 SQL 字符串 final BoundSql boundSql = ms.getBoundSql(parameter); String pagingSql = getPagingSql(boundSql.getSql(), pagingParam.getOffset(), pagingParam.getLimit()); BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), pagingSql, boundSql.getParameterMappings(), boundSql.getParameterObject()); // 通过反射工具类,重置 MappedStatement 中的 SQL 语句 // MetaObject metaObject = MetaObject.forObject(ms, new DefaultObjectFactory(), new DefaultObjectWrapperFactory(), new DefaultReflectorFactory()); MetaObject metaObject = SystemMetaObject.forObject(ms); metaObject.setValue("sqlSource", new BoundSqlSqlSource(newBoundSql)); // 重置 RowBound queryArgs[ROW_BOUNDS_INDEX] = new RowBounds(RowBounds.NO_ROW_OFFSET, RowBounds.NO_ROW_LIMIT); } } catch (Exception e) { log.error("PageInterceptor#intercept 异常", e); } finally { log.info("------------------PageInterceptor#intercept 结束------------------"); PageUtil.removePagingParam(); } return invocation.proceed(); }/** * 使得当前插件生效 */ @Override public Object plugin(Object o) { return Plugin.wrap(o, this); }/** * 构造新的 sql: select xxx from xxx where yyy limit offset,limit */ public String getPagingSql(String sql, int offset, int limit) { StringBuilder result = new StringBuilder(sql.length() + 100); result.append(sql).append(" limit "); if (offset > 0) { result.append(offset).append(",").append(limit); }else{ result.append(limit); } return result.toString(); } }

通过反射工具类 MetaObject 来修改 MappedStatement 对象中的 SQL 语句,加上 limit m,n 的分页条件。
阅读 MyBatis 源码(插件开发)
文章图片

3. 源码分析 3.1 解析插件配置 通过 SqlSessionFactoryBuilder 来解析 mybatis-config.xml 文件:
Reader reader = Resources.getResourceAsReader("mybatis-config.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);

其中会解析 plugins 标签:
org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration
org.apache.ibatis.builder.xml.XMLConfigBuilder#pluginElement
private void pluginElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { String interceptor = child.getStringAttribute("interceptor"); Properties properties = child.getChildrenAsProperties(); Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance(); interceptorInstance.setProperties(properties); configuration.addInterceptor(interceptorInstance); } } }

这里会将 Interceptor 的实现类进行实例化,并注册到 Configuration 对象之中的 InterceptorChain 成员变量之中。
org.apache.ibatis.session.Configuration#addInterceptor
protected final InterceptorChain interceptorChain = new InterceptorChain(); public void addInterceptor(Interceptor interceptor) { interceptorChain.addInterceptor(interceptor); }

InterceptorChain 类完整内容如下,其中有个 List 集合存储注册给 Configuration 对象的插件实例。
org.apache.ibatis.plugin.InterceptorChain
public class InterceptorChain {private final List 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 getInterceptors() { return Collections.unmodifiableList(interceptors); }}

3.2 插件实现机制 MyBatis 中的 Executor、ParameterHandler、ResultSetHandler、StatementHandler 类均支持插件扩展,而插件的实现机制主要是基于动态代理实现的。
由 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) { return newExecutor(transaction, defaultExecutorType); }public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; // 该类型的执行器会批量执行所有更新语句,如果 SELECT 在多个更新中间执行,将在必要时将多条更新语句分隔开来,以方便理解。 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; }

3.2.1 原始对象的生成
探究插件的动态代理机制之前,回顾一下需要被代理的原始对象的生成流程:
【阅读 MyBatis 源码(插件开发)】Executor
开启 SqlSession 会话的时候创建:
org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSession
org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSessionFromDataSource
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); 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(); } }

StatementHandler
SqlSession#selectList 执行 SQL 的时候创建:
org.apache.ibatis.session.defaults.DefaultSqlSession#selectList
org.apache.ibatis.executor.CachingExecutor#query
org.apache.ibatis.executor.BaseExecutor#query
org.apache.ibatis.executor.BaseExecutor#queryFromDatabase
org.apache.ibatis.executor.SimpleExecutor#doQuery
@Override public List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { Statement stmt = null; try { Configuration configuration = ms.getConfiguration(); StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); // 创建 stmt = prepareStatement(handler, ms.getStatementLog()); return handler.query(stmt, resultHandler); } finally { closeStatement(stmt); } }

ParameterHandler、ResultSetHandler
在 StatementHandler 的构造函数中创建:
org.apache.ibatis.session.Configuration#newStatementHandler
org.apache.ibatis.executor.statement.RoutingStatementHandler#RoutingStatementHandler
org.apache.ibatis.executor.statement.PreparedStatementHandler#PreparedStatementHandler
org.apache.ibatis.executor.statement.BaseStatementHandler#BaseStatementHandler
protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { this.configuration = mappedStatement.getConfiguration(); this.executor = executor; this.mappedStatement = mappedStatement; this.rowBounds = rowBounds; this.typeHandlerRegistry = configuration.getTypeHandlerRegistry(); this.objectFactory = configuration.getObjectFactory(); if (boundSql == null) { // issue #435, get the key before calculating the statement generateKeys(parameterObject); boundSql = mappedStatement.getBoundSql(parameterObject); }this.boundSql = boundSql; this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql); // 创建 ParameterHandler,支持插件扩展 this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql); // 创建 ResultSetHandler,支持插件扩展 }

3.2.2 代理对象的生成
在 Configuration 类中创建 Executor、ParameterHandler、ResultSetHandler、StatementHandler 对象的时候,都会调用 InterceptorChain#pluginAll 方法来进行插件扩展。
org.apache.ibatis.plugin.InterceptorChain#pluginAll
public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; }

Interceptor 接口中的 plugin 方法默认实现如下:
org.apache.ibatis.plugin.Interceptor#plugin
default Object plugin(Object target) { return Plugin.wrap(target, this); }

Plugin#wrap 方法的两个入参:
  • target 表示 Executor、ParameterHandler、ResultSetHandler、StatementHandler 这些原始对象。
  • interceptor 是自定义的插件类。
代码流程:
  1. 解析 Interceptor 插件类上的 @Intercepts @Signature 注解。
  2. 根据注解,判断是否是对 target 原始对象的拦截,是则为 target 原始对象生成代理对象。
org.apache.ibatis.plugin.Plugin#wrap
public static Object wrap(Object target, Interceptor interceptor) { Map, Set> signatureMap = getSignatureMap(interceptor); // 解析插件类上的 @Intercepts @Signature 注解 Class type = target.getClass(); Class[] interfaces = getAllInterfaces(type, signatureMap); if (interfaces.length > 0) { // 满足条件的,说明 target 类需要经过插件 interceptor 来拦截,因此为 target 生成代理 return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); // 返回代理对象 } return target; // 返回原始对象 }

3.2.3 代理对象的使用
以上述的自定义分页插件为例,com.sumkor.plugin.PageInterceptor 类注解声明了对 Executor#query 方法的拦截,是对 Executor 对象生成动态代理。
org.apache.ibatis.session.defaults.DefaultSqlSession#selectList
阅读 MyBatis 源码(插件开发)
文章图片
因此,对 Executor#query 方法的调用,实际是执行代理对象的方法:
org.apache.ibatis.plugin.Plugin#invoke
public class Plugin implements InvocationHandler {private final Object target; private final Interceptor interceptor; private final Map, Set> signatureMap; private Plugin(Object target, Interceptor interceptor, Map, Set> signatureMap) { this.target = target; this.interceptor = interceptor; this.signatureMap = signatureMap; }@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { Set 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); } } }

再进一步调用到自定义的插件类的方法:
com.sumkor.plugin.PageInterceptor#intercept
4. 总结 MyBatis 插件对 Executor、ParameterHandler、ResultSetHandler、StatementHandler 这四个接口上的方法进行拦截,利用 JDK 动态代理机制,为这些接口的实现类创建代理对象。
在执行方法时,先去执行代理对象的方法,从而执行自己编写的拦截逻辑。
值得注意的是,Executor、ParameterHandler、ResultSetHandler、StatementHandler 这些对象的生命周期都是 Session 范围的,每一次开启 SqlSession 会话都会创建新的代理对象。
作者:Sumkor
链接:https://segmentfault.com/a/11...

    推荐阅读