Mybatis插件Plugin
在对数据库操作时,免不了要用到JDBC,但是这部分代码写起来非常的冗长繁琐。于是乎,“懒惰”的人们为了避免做重复相同的事情,持久层框架应运而生。Mybatis便是其中一款优秀的框架,使用非常广泛,免除了我们几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。我们不探讨Mybatis的基本使用,因为从它的官方文档就能获取。本文将从Mybatis Plugin为切入点,从而大致了解一下Mybatis的工作原理。不过一个完整的框架,不是一篇文章就能讲完的,因此本文也只是做到“抛砖引玉”作用,希望能梳理出一个大致的脉络,让大家在此基础上能更清晰地阅读Mybatis源码。
Mybatis Plugin介绍
Plugin即插件,这里我们可以理解成拦截器。这是Mybatis提供的开放功能,它允许我们对SQL执行过程中的某一些点进行拦截调用。因此我们把插件也可以理解成一种扩展,官方文档中提到四个核心接口:
Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) ParameterHandler (getParameterObject, setParameters) ResultSetHandler (handleResultSets, handleOutputParameters) StatementHandler (prepare, parameterize, batch, update, query)
接口后面将能够拦截的方法全部列举了出来,这里并不直接代入性地介绍它们各自的作用,首先我们看下官方给的使用插件的例子:
@Intercepts({@Signature( type= Executor.class, method = "update", args = {MappedStatement.class,Object.class})}) public class ExamplePlugin implements Interceptor { public Object intercept(Invocation invocation) throws Throwable { return invocation.proceed(); } public Object plugin(Object target) { return Plugin.wrap(target, this); } public void setProperties(Properties properties) { } } // 在配置文件中配置上就行 <plugins> <plugin interceptor="org.mybatis.example.ExamplePlugin"> <property name="someProperty" value="100"/> </plugin> </plugins>
使用起来还是很简单的,只需要实现Interceptor 接口,然后用注解@Intercepts为它打上需要拦截的方法、类名的标记,然后配置文件丽注册上它即可。
Plugin注册
既然我们声明了一个自定义的插件类,这个类实现了Interceptor 接口,那总该框架要将它应用进去。肯定有一个地方将它注册进去了。
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).newInstance(); interceptorInstance.setProperties(properties); // 将解析出来的Interceptor放入到configruation中 configuration.addInterceptor(interceptorInstance); } } }
我们看到,这里将配置文件中的插件节点解析出来放入到configuration配置中。
// Configuration的添加其实就是将插件放入插件链中。 public void addInterceptor(Interceptor interceptor) { interceptorChain.addInterceptor(interceptor); }
从它取得名字interceptorChain,我们大致能猜出来插件的链式调用:
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; } public void addInterceptor(Interceptor interceptor) { interceptors.add(interceptor); } public List<Interceptor> getInterceptors() { return Collections.unmodifiableList(interceptors); } }
接下来,我们通过查找调用关系,看下pluginAll函数在哪里被调用:
public class Configuration { ...... public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql); // 装载ParameterHandler插件 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 = (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 = (StatementHandler) interceptorChain.pluginAll(statementHandler); return statementHandler; } // 该方法会被SqlSessionFactory调用,用来构造SqlSession对象。 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); } // 这里缓存默认开启,也就是常说的二级缓存的代理执行器,这个是二级缓存的全局开关,二级缓存也是依赖该CacheExecutor的实现。 if (cacheEnabled) { executor = new CachingExecutor(executor); } // 装载Executor插件 executor = (Executor) interceptorChain.pluginAll(executor); return executor; } ...... }
在创建上述解释的几种核心类型时候,开始对插件进行了装载。代码还是比较简单的,因此解析.xml配置文件时候对插件进行解析并保存到InterceptorChain中,然后对各个核心接口进行对象创建时,对插件进行装载注入。接下来就是再看下对插件注入的关键代码 Plugin.wrap(target, this):
public class Plugin implements InvocationHandler { ...... public static Object wrap(Object target, Interceptor interceptor) { // 通过扫描自定义插件类上的注解@Intercepts,@Signature来获取需要代理的核心接口及其相应的方法 Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); 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; } ...... }
熟悉动态代理的同学应该是不陌生的,因此configuration中,创建各个核心接口的方法中其实返回的是一个代理对象,其中已经装载了我们自定义的插件,只等待一个合适机会去触发!接下来我们就可以对还没有介绍的四个核心接口—Executor,ParameterHandler,ResultSetHandler,StatementHandler该出场了。
Executor-执行器
我们都知道,SqlSession是Mybatis对外暴露提供的一个接口,调用它来进行数据库的相关操作。它有一个默认实现DefaultSqlSession,比如当我们查询数据时,实际上它是将请求转交给Executor处理:
public class DefaultSqlSession{ ...... @Override public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { try { // 根据id找出配置中对应的Mapper。 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(); } } ...... }
其实在executor执行器中也定义了很多基本的数据库操作方法。因此基本上来说,SqlSession其实把相应的请求转交给Executor执行。我们对Executor中想要拦截的方法,其实就是在这里进行执行的,应该来说这一步是整个请求过程中比较开始一步。接下来就简单介绍下它里面的方法:
// 更新操作 int update(MappedStatement ms, Object parameter) throws SQLException; // 查询。还有一个query返回的是游标,此处暂不列出来 <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException; //批量执行SQL时调用 List<BatchResult> flushStatements() throws SQLException; //提交事务 void commit(boolean required) throws SQLException; //回滚事务 void rollback(boolean required) throws SQLException; //获取事务对象 Transaction getTransaction(); //关闭Executor void close(boolean forceRollback); //判断当前Executor是否关闭 boolean isClosed();
我们可以根据需要对上述的方法进行拦截,定制出属于自己的功能!而对于Executor,MyBatis本身就提供一个公共抽象类来实现一些公共的方法:BaseExecutor。我们还是以查询入手,继续往下探究一下。
public abstract class BaseExecutor{ ...... @Override public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameter); // 创建缓存键。以便相同的查询不再需要查库。此处即一级缓存 CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); return query(ms, parameter, rowBounds, resultHandler, key, boundSql); } @SuppressWarnings("unchecked") @Override public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId()); if (closed) { throw new ExecutorException("Executor was closed."); } if (queryStack == 0 && ms.isFlushCacheRequired()) { clearLocalCache(); } List<E> list; try { // 这个是记录下嵌套查询的层数。 queryStack++; list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; if (list != null) { handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else { // 没有缓存就从数据库里查询 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); } } finally { queryStack--; } if (queryStack == 0) { // 这个用于延迟加载。嵌套查询情况下,有可能内部查询结果并没有处理完成,此时可以先加载外部查询结果,对嵌套查询延迟处理。 for (DeferredLoad deferredLoad : deferredLoads) { deferredLoad.load(); } // issue #601 deferredLoads.clear(); // 一级缓存默认是SESSION级别,如果设置的是STATMENT级别,则清除缓存 if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { // issue #482 clearLocalCache(); } } return list; } ...... }
从上面的代码中,我们可以很清楚看到localCache其实就是一个缓存,它内部其实就是利用HashMap来实现的。这个缓存其实就是一级缓存,它将整个SqlSession查询的结果进行短暂的缓存,该缓存会在update或者事务提交、回滚时进行清除。
我们再看下BaseExecutor中queryFromDatabase的简单实现:
public abstract class BaseExecutor{ ....... private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { List<E> list; localCache.putObject(key, EXECUTION_PLACEHOLDER); try { // doQuery是一个抽象方法,它由子类实现。 list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); } finally { localCache.removeObject(key); } // 缓存结果 localCache.putObject(key, list); if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } return list; } ...... } // 子类SimpleExecutor的doQuery方法。 @Override 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(); // 上述已经分析的创建statmentHandler, 并将插件内容已经注入进来。 StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); stmt = prepareStatement(handler, ms.getStatementLog()); return handler.<E>query(stmt, resultHandler); } finally { closeStatement(stmt); } }
从数据库中查询还是比较简单的,核心的还是doQuery方法。最终doQuery由各自的子类实现,上面列出来的doQuery的实现,我选用的是默认的SimpleExecutor。从代码中看出来,executor又将查询交给StatementHandler执行。到这里发现又有一个核心接口暴露了出来:StatementHandler。
StatmentHandler–Statment处理器
从这个接口的名称我们也能看出,该处理器处理的对象已经开始围绕statment展开,这个就是平时我们写JDBC代码时绕不开的接口,在这里我们看到了它的身影,首先我们把该处理器接口方法简单列举一下:
public interface StatementHandler { // 从connection中获取statment对象 Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException; // 绑定参数 void parameterize(Statement statement) throws SQLException; // 批量执行SQL void batch(Statement statement) throws SQLException; // 以下就是query delete update相关操作 int update(Statement statement) throws SQLException; <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException; <E> Cursor<E> queryCursor(Statement statement) throws SQLException; // 相关的sql对象,sql中可能包含 “?” BoundSql getBoundSql(); // ParameterHandler 参数处理器,该接口后续会提及 ParameterHandler getParameterHandler(); }
Mybatis默认使用的是RoutingStatementHandler,它实际上是一种策略模式方法,根据MappedStatement的类型来选择具体使用哪种StatmentHandler,其中包括SimpleStatementHandler、PreparedStatementHandler、CallableStatementHandler,它们分别对应JDBC中的Statment、PreparedStatement和CallableStatement。从插件角度来看,我们可以通过拦截该StatmentHandler接口, 来对statment做一些自定义处理。
我们简单看下SimpleStatmentHandler的query方法实现:
@Override public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException { String sql = boundSql.getSql(); statement.execute(sql); // 结果集映射处理 return resultSetHandler.<E>handleResultSets(statement); }
上面其实就是调用statment来执行查询请求,接着又碰到了一个核心接口resultSetHandler,用来处理映射结果。因此从这里开始,请求才被发送出去。上面我们提及了下,boundSql包含了需要执行的SQL,但是有可能包含”?”,所以在PreparedStatementHandler中,需要对参数进行处理:
@Override public void parameterize(Statement statement) throws SQLException { parameterHandler.setParameters((PreparedStatement) statement); }
此时我们又看到了一个核心接口parameterHandler。parameterize方法会在执行之前,对SQL中绑定的参数进行处理。
ParameterHandler–参数处理器
public interface ParameterHandler { Object getParameterObject(); void setParameters(PreparedStatement ps) throws SQLException; }
该接口就比较简单,主要就是两个方法对参数处理。如果我们通过插件来拦截这个接口,我们可以对整个参数处理进行自定义的处理。它有一个默认实现DefaultParameterHandler。其中对参数的转换,又涉及到TypeHandler,ParameterMapping一些关键类,有兴趣同学可以自己阅读相关源码。
到这里我们可以稍微总结下,对于Executor,Parameterhandler的拦截,此时我们的查询请求依然在预处理的时机,statmentHandler则是真正请求发出的时机,接下来出场的ResultSetHandler则是请求返回之后的处理流程了。
ResultSetHandler–结果集处理器
当我们使用ResultMap或是ResultType配置时,其实就是配置映射规则,ResultSetHandler则会对我们配置的规则将结果集映射成相应的对象。
public interface ResultSetHandler { // 结果集映射成相应的对象集合 <E> List<E> handleResultSets(Statement stmt) throws SQLException; // 结果集映射成游标对象 <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException; // 存储过程参数处理 void handleOutputParameters(CallableStatement cs) throws SQLException; }
该接口也有一个默认实现:DefaultResultSetHandler。对结果集的映射可以说是Mybatis相对来说比较复杂的一个过程,因为它需要对各种场景结果进行处理,其中还有嵌套映射,延迟加载,游标处理等等一些场景,本片介于篇幅并不打算一一展开,点到为止,同样的有兴趣同学可以自己阅读相关源码。
总结
对于插件我们梳理了它的使用注册过程,紧接着我们对插件拦截的核心接口进行了同样的简单介绍,我们能清楚明白我们拦截的接口具体能做什么,只有明白了这些,我们才好定义自己想要的功能。