通过前面两篇文章,我们完成了对 MyBatis 所有配置文件(包括配置文件和映射文件)解析过程的分析。回忆一下我们最开始给出的小示例(如下),经过前面的跋山涉水,我们终于完成了第一行代码的 99% (手动滑稽),这最后的 1% 就是创建 SqlSessionFactory 对象。所有的配置解析最后都会封装到 Configuration 对象中,接下去就是调用 SqlSessionFactoryBuilder#build
方法创建 SqlSessionFactory 对象,实际使用的是 DefaultSqlSessionFactory 实现类进行实例化。
1 2 3 4 5 6 7 SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder() .build(Resources.getResourceAsStream("mybatis-config.xml" )); try (SqlSession sqlSession = sessionFactory.openSession()) { UserMapper mapper = sqlSession.getMapper(UserMapper.class); User user = mapper.selectByName("zhenchao" ); }
SqlSessionFactory 是一个工厂类,用于创建 SqlSession 对象。按照官方文档的说明,SqlSessionFactory 对象一旦被创建就应该在应用的运行期间一直存在,不应该在运行期间对其进行清除或重建。调用该工厂的 SqlSessionFactory#openSession
方法可以开启一次会话,即创建一个 SqlSession 对象。SqlSession 封装了面向数据库执行 SQL 的所有 API,它不是线程安全的,因此不能被共享,所以该对象的最佳作用域是请求或方法作用域。在上面的示例中,我们用 SqlSession 拿到相应的 Mapper 接口对象(更准确的说是一个动态代理对象),然后执行指定的数据库操作,最后关闭此次会话。
上面这张时序图我们在本系列开篇的文章中已经引用过,描绘了 MyBatis 在一次会话生命周期内执行数据库操作的交互时序。下面对这幅图中所描绘的执行过程中类之间的交互时序关系作进一步说明,稍后我们会对图中涉及到的类和接口从源码层面进行分析,执行时序如下:
调用 SqlSessionFactory#openSession
方法创建 SqlSession 对象,开启一次会话;
调用 SqlSession#getMapper
方法获取指定的 Mapper 接口对象,这里实际上将请求委托给 Configuration#getMapper
方法执行,由前面分析映射文件解析过程时我们知道所有的 Mapper 接口都会注册到全局唯一的配置对象 Configuration 的 MapperRegistry 类型属性中;
MapperRegistry 在执行 MapperRegistry#getMapper
操作时会反射创建 Mapper 接口的动态代理对象并返回;
执行对应的数据库操作方法(例如 UserMapper#selectByName
),即调用 Mapper 接口动态代理对象的 MapperProxy#invoke
方法,在该方法中会获取封装执行方法的 MapperMethod 对象;
执行 MapperMethod#execute
方法,该方法会判定当前数据库操作类型(例如 SELECT),依据类型选择执行 SqlSession 对应的数据库操作方法;
SqlSession 会将数据库操作委托给具体的 Executor 执行。对于动态 SQL 语句而言,在这里会依据参数执行解析;对于查询语句而言,Executor 在条件允许的情况下会尝试先从缓存中进行查询,缓存不命中才会操作具体的数据库并更新缓存。MyBatis 强大的结果集映射操作也在这里完成;
返回查询结果;
调用当前会话的 SqlSession#close
方法关闭本次会话。
整个过程围绕一次查询操作展开,虽然不能覆盖 MyBatis 执行 SQL 语句的各个方面,但主线上还是能够说明白 MyBatis 针对一次 SQL 执行的大概过程。在下面的篇幅中,我们将一起分析这一整套时序背后的实现机制。
SQL 会话管理
SqlSession 接口是 MyBatis 对外提供的数据库操作 API,是 MyBatis 的核心接口之一,用于管理一次数据库会话。围绕 SqlSession 接口的类继承关系如下图所示,其中 DefaultSqlSession 是默认的 SqlSession 实现。SqlSessionFactory 是一个工厂接口,其功能是用来创建 SqlSession 对象,该接口中声明了多个重载版本的 SqlSessionFactory#openSession
方法,DefaultSqlSessionFactory 是该接口的默认实现。上述示例程序中 SqlSessionFactoryBuilder#build
方法就是基于该实现类创建的 SqlSessionFactory 对象。SqlSessionManager 类实现了这两个接口,所以具备创建、使用,以及管理 SqlSession 对象的能力,后面会详细说明。
SqlSession 接口中声明的方法都比较直观,感兴趣的读者可以自行阅读源码。我们来看一下针对该接口的默认实现类 DefaultSqlSession,该类的属性定义如下:
1 2 3 4 5 6 7 8 9 10 private final Configuration configuration;private final Executor executor;private final boolean autoCommit;private boolean dirty;private List<Cursor<?>> cursorList;
DefaultSqlSession 中的方法实现基本上都是对 Executor 接口方法的封装,实现上都比较简单。这里解释一下 DefaultSqlSession#cursorList
这个属性,在 DefaultSqlSession#selectCursor
方法中会记录查询返回的游标(Cursor)对象,并在关闭 SqlSession 会话时遍历集合逐一关闭,从而防止打开的游标没有被关闭的现象。
DefaultSqlSessionFactory 是 SqlSessionFactory 接口的默认实现,用于创建 SqlSession 对象。该实现类提供了两种创建 SqlSession 对象的方式,分别是基于当前数据源创建会话和基于当前数据库连接创建会话,对应的实现如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private SqlSession openSessionFromDataSource ( ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null ; try { final Environment environment = configuration.getEnvironment(); final TransactionFactory transactionFactory = this .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) { this .closeTransaction(tx); throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 private SqlSession openSessionFromConnection (ExecutorType execType, Connection connection) { try { boolean autoCommit; try { autoCommit = connection.getAutoCommit(); } catch (SQLException e) { autoCommit = true ; } final Environment environment = configuration.getEnvironment(); final TransactionFactory transactionFactory = this .getTransactionFactoryFromEnvironment(environment); final Transaction tx = transactionFactory.newTransaction(connection); final Executor executor = configuration.newExecutor(tx, execType); return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
两种创建会话的方式在执行流程上基本一致,具体细节如上述代码注释。
SqlSessionManager 同时实现了 SqlSessionFactory 和 SqlSession 两个接口,所以具备这两个接口全部的功能。该实现类的属性定义如下:
1 2 3 4 5 6 private final SqlSessionFactory sqlSessionFactory;private final SqlSession sqlSessionProxy;private final ThreadLocal<SqlSession> localSqlSession = new ThreadLocal<SqlSession>();
针对 SqlSessionFactory 接口中声明的方法,SqlSessionManager 均委托给持有的 SqlSessionFactory 对象完成。对于 SqlSession 接口中声明的方法,SqlSessionManager 提供了两种实现方式:如果当前线程已经绑定了一个 SqlSession 对象,那么只要未主动调用 SqlSessionManager#close
方法,就会一直复用该线程私有的 SqlSession 对象;否则会在每次执行数据库操作时创建一个新的 SqlSession 对象,并在使用完毕之后关闭会话。相关逻辑位于 SqlSessionInterceptor 类中,这是一个定义在 SqlSessionManager 中的内部类,属性 SqlSessionManager#sqlSessionProxy
是基于该类实现的动态代理对象:
1 2 this .sqlSessionProxy = (SqlSession) Proxy.newProxyInstance( SqlSessionFactory.class.getClassLoader(), new Class[]{SqlSession.class}, new SqlSessionInterceptor());
SqlSessionInterceptor 类实现自 InvocationHandler 接口,对应的 SqlSessionInterceptor#invoke
方法实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { final SqlSession sqlSession = localSqlSession.get(); if (sqlSession != null ) { try { return method.invoke(sqlSession, args); } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } } else { try (SqlSession autoSqlSession = SqlSessionManager.this .openSession()) { try { final Object result = method.invoke(autoSqlSession, args); autoSqlSession.commit(); return result; } catch (Throwable t) { autoSqlSession.rollback(); throw ExceptionUtil.unwrapThrowable(t); } } } }
SqlSessionInterceptor 首先会尝试获取线程私有的 SqlSession 对象,对于未绑定的线程来说会创建一个新的 SqlSession 对象,并在使用完毕之后立刻关闭。
动态代理 Mapper 接口
MyBatis 要求所有的 Mapper 都定义成接口的形式,这主要是为了配合 JDK 内置的动态代理机制,JDK 内置的动态代理要求被代理的类必须抽象出一个接口。常用的动态代理除了 JDK 内置的方式,还有基于 CGlib 等第三方组件的方式,MyBatis 采用了 JDK 内置的方式创建 Mapper 接口的动态代理对象。
我们先来复习一下 JDK 内置的动态代理机制,假设现在有一个接口 Mapper 及其实现类如下:
1 2 3 4 5 6 7 8 9 10 11 public interface Mapper { int select () ; } public class MapperImpl implements Mapper { @Override public int select () { System.out.println("do select." ); return 0 ; } }
现在我们希望在方法执行之前打印一行调用日志,基于动态代理的实现方式如下。我们需要定义一个实现了 InvocationHandler 接口的代理类,然后在其 InvocationHandler#invoke
方法中实现增强逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class MapperProxy implements InvocationHandler { private Mapper mapper; public MapperProxy (Mapper mapper) { this .mapper = mapper; } @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { System.out.println("before invoke." ); return method.invoke(this .mapper, args); } }
客户端调用代码:
1 2 3 4 Mapper mapper = new MapperImpl(); Mapper mapperProxy = (Mapper) Proxy.newProxyInstance( mapper.getClass().getClassLoader(), mapper.getClass().getInterfaces(), new MapperProxy(mapper)); mapperProxy.select();
回到 MyBatis 框架本身,我们在执行目标数据库操作时,一般会直接调用目标 Mapper 接口的相应方法,这里框架返回给我们的实际上是 Mapper 接口的动态代理类对象。MyBatis 基于 JDK 的动态代理机制实现了 Mapper 接口中声明的方法,这其中包含了 获取 SQL 语句、参数绑定、缓存操作、数据库操作,以及结果集映射处理 等步骤,下面就 Mapper 接口动态代理机制涉及到的相关类和方法进行分析。
上一篇在分析映射文件时我们介绍了在 MapperRegistry#knownMappers
属性中记录了 Mapper 接口与 MapperProxyFactory 的映射关系,MapperProxyFactory 顾名思义是 MapperProxy 的工厂类,其中定义了创建 Mapper 接口代理对象的方法,如下:
1 2 3 4 5 6 7 8 9 10 11 12 public T newInstance (SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache); return this .newInstance(mapperProxy); } protected T newInstance (MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance( mapperInterface.getClassLoader(), new Class[] {mapperInterface}, mapperProxy); }
来看一下 MapperProxy 实现,该类实现了 InvocationHandler 接口,对应的 InvocationHandler#invoke
实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { try { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this , args); } else { return this .cachedInvoker(method).invoke(proxy, method, args, sqlSession); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } }
上述方法的核心逻辑在于获取当前执行方法 Method 对象对应的 MapperMethodInvoker 方法调用器,并执行 MapperMethodInvoker#invoke
方法触发对应的数据库操作。
围绕 MapperMethodInvoker 接口,MyBatis 提供了两种实现,即 DefaultMethodInvoker 和 PlainMethodInvoker,其中前者用于支持 JDK 7 引入的动态类型语言特性,后者则是对 MapperMethod 的封装。MapperMethod 中主要定义两个内部类:
SqlCommand :用于封装方法关联的 SQL 语句名称和类型。
MethodSignature :用来封装方法相关的签名信息。
先来看一下 SqlCommand 的具体实现,该类定义了 SqlCommand#name
和 SqlCommand#type
两个属性,分别用于记录对应 SQL 语句的名称和类型,并在构造方法中实现了相应的解析逻辑和初始化操作,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 public SqlCommand (Configuration configuration, Class<?> mapperInterface, Method method) { final String methodName = method.getName(); final Class<?> declaringClass = method.getDeclaringClass(); MappedStatement ms = this .resolveMappedStatement(mapperInterface, methodName, declaringClass, configuration); if (ms == null ) { if (method.getAnnotation(Flush.class) != null ) { name = null ; type = SqlCommandType.FLUSH; } else { throw new BindingException( "Invalid bound statement (not found): " + mapperInterface.getName() + "." + methodName); } } else { name = ms.getId(); type = ms.getSqlCommandType(); if (type == SqlCommandType.UNKNOWN) { throw new BindingException("Unknown execution method for: " + name); } } } private MappedStatement resolveMappedStatement ( Class<?> mapperInterface, String methodName, Class<?> declaringClass, Configuration configuration) { String statementId = mapperInterface.getName() + "." + methodName; if (configuration.hasStatement(statementId)) { return configuration.getMappedStatement(statementId); } else if (mapperInterface.equals(declaringClass)) { return null ; } for (Class<?> superInterface : mapperInterface.getInterfaces()) { if (declaringClass.isAssignableFrom(superInterface)) { MappedStatement ms = this .resolveMappedStatement( superInterface, methodName, declaringClass, configuration); if (ms != null ) { return ms; } } } return null ; }
SqlCommand 在实例化时所做的主要工作就是解析当前 Mapper 方法关联的 SQL 对应的 MappedStatement 对象,并初始化记录的 SQL 语句名称和类型,整个解析过程如上述代码注释。
再来看一下 MethodSignature 类,该类用于封装一个具体 Mapper 方法的相关签名信息,其中定义的方法实现都比较简单,这里列举一下其属性定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private final boolean returnsMany;private final boolean returnsMap;private final boolean returnsVoid;private final boolean returnsCursor;private final boolean returnsOptional;private final Class<?> returnType;private final String mapKey;private final Integer resultHandlerIndex;private final Integer rowBoundsIndex;private final ParamNameResolver paramNameResolver;
上面属性中重点介绍一下 ParamNameResolver 这个类,它的作用在于解析 Mapper 方法的参数列表,以便于在方法实参和方法关联的 SQL 语句的参数之间建立映射关系。其中一个比较重要的属性是 ParamNameResolver#names
,定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 private final SortedMap<Integer, String> names;
我把它的英文注释和我的理解都写在代码注释中,应该可以清楚理解该属性的作用。至于为什么需要跳过 RowBounds 和 ResultHandler 这两个类型的参数,是因为前者用于设置 LIMIT 参数,后者用于设置结果集处理器,所以都不是真正意义上的参数,按照我的话说这两种类型的参数都是功能型的参数。
ParamNameResolver 在构造方法中实现了对参数列表的解析,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public ParamNameResolver (Configuration config, Method method) { final Class<?>[] paramTypes = method.getParameterTypes(); final Annotation[][] paramAnnotations = method.getParameterAnnotations(); final SortedMap<Integer, String> map = new TreeMap<>(); int paramCount = paramAnnotations.length; for (int paramIndex = 0 ; paramIndex < paramCount; paramIndex++) { if (isSpecialParameter(paramTypes[paramIndex])) { continue ; } String name = null ; for (Annotation annotation : paramAnnotations[paramIndex]) { if (annotation instanceof Param) { hasParamAnnotation = true ; name = ((Param) annotation).value(); break ; } } if (name == null ) { if (config.isUseActualParamName()) { name = this .getActualParamName(method, paramIndex); } if (name == null ) { name = String.valueOf(map.size()); } } map.put(paramIndex, name); } names = Collections.unmodifiableSortedMap(map); }
整个过程概括来说就是遍历处理指定方法的参数列表,忽略 RowBounds 和 ResultHandler 类型的参数,并判断参数前面是否有 @Param
注解,如果有则尝试以注解指定的字符串作为参数名称,否则基于配置决定是否采用参数的真实名称作为这里的参数名,再不济就采用下标值作为参数名称。
考虑到会忽略 RowBounds 和 ResultHandler 两种类型的参数,但是属性 ParamNameResolver#names
对应的 key 又是递增的,所以就可能出现在以下标值作为参数名称时,参数名称与对应下标值不一致的情况。例如,假设有一个方法的参数列表为 (int a, RowBounds rb, int b)
,因为有 RowBounds 类型夹在中间,如果以下标值作为参数名称的最终解析结果就是 {0, "0"}, {2, "1"}
,下标与具体的参数名称不一致。
ParamNameResolver 中还有一个比较重要的方法 ParamNameResolver#getNamedParams
,用于关联实参和形参列表,其中 args 参数是用户传递的实参数组,方法基于前面的参数列表解析结果将传递的实现与对应的方法参数进行关联,最终记录到 Object 对象中进行返回,实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public Object getNamedParams (Object[] args) { final int paramCount = names.size(); if (args == null || paramCount == 0 ) { return null ; } else if (!hasParamAnnotation && paramCount == 1 ) { return args[names.firstKey()]; } else { final Map<String, Object> param = new ParamMap<>(); int i = 0 ; for (Map.Entry<Integer, String> entry : names.entrySet()) { param.put(entry.getValue(), args[entry.getKey()]); final String genericParamName = GENERIC_NAME_PREFIX + (i + 1 ); if (!names.containsValue(genericParamName)) { param.put(genericParamName, args[entry.getKey()]); } i++; } return param; } }
做了这么多的铺垫,是时候回来继续分析 MapperMethod 的核心方法 MapperMethod#execute
了。该方法的作用在于委托 SqlSession 对象执行方法对应的 SQL 语句,实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 public Object execute (SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) { case INSERT: { Object param = method.convertArgsToSqlCommandParam(args); result = this .rowCountResult(sqlSession.insert(command.getName(), param)); break ; } case UPDATE: { Object param = method.convertArgsToSqlCommandParam(args); result = this .rowCountResult(sqlSession.update(command.getName(), param)); break ; } case DELETE: { Object param = method.convertArgsToSqlCommandParam(args); result = this .rowCountResult(sqlSession.delete(command.getName(), param)); break ; } case SELECT: if (method.returnsVoid() && method.hasResultHandler()) { this .executeWithResultHandler(sqlSession, args); result = null ; } else if (method.returnsMany()) { result = this .executeForMany(sqlSession, args); } else if (method.returnsMap()) { result = this .executeForMap(sqlSession, args); } else if (method.returnsCursor()) { result = this .executeForCursor(sqlSession, args); } else { Object param = method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(command.getName(), param); if (method.returnsOptional() && (result == null || !method.getReturnType().equals(result.getClass()))) { result = Optional.ofNullable(result); } } break ; case FLUSH: result = sqlSession.flushStatements(); break ; default : throw new BindingException("Unknown execution method for: " + command.getName()); } if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) { throw new BindingException("Mapper method '" + command.getName() + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ")." ); } return result; }
上述方法会依据具体的 SQL 语句类型分而治之。对于 INSERT、UPDATE,以及 DELETE 类型而言,会先调用 MethodSignature#convertArgsToSqlCommandParam
方法关联实参与方法形参,本质上是调用前面介绍的 ParamNameResolver#getNamedParams
方法。然后就是调用 SqlSession 对应的方法执行数据库操作,并通过方法 MapperMethod#rowCountResult
对结果进行类型转换。关于 SqlSession 相关方法的具体实现留到下一节针对性介绍。对于 SELECT 类型而言,则需要考虑不同的返回类型,分为 void、Collection、数组、Map、Cursor,以及对象几类情况,这里所做的都是对于参数或返回结果的处理,核心逻辑也都位于 SqlSession 中,在这一层面的实现都比较简单,就不再一一展开。对于 FLUSH 类型来说,官方文档的说明如下:
如果使用了这个注解,它将调用定义在 Mapper 接口中的 SqlSession#flushStatements
方法。
具体的实现也就位于这里。
SQL 语句执行器
Executor 接口声明了基本的数据库操作,前面在介绍 SqlSession 时曾描述 SqlSession 是 MyBatis 框架对外提供的 API 接口,其中声明了对数据库的基本操作方法,而这些操作方法基本上都是对 Executor 方法的封装。Executor 接口定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 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) ; }
围绕 Executor 接口的类继承关系如下图,其中 CachingExecutor 实现类用于为 Executor 提供二级缓存支持。
BaseExecutor 抽象类实现了 Executor 接口中声明的所有方法,并抽象了 4 个模板方法交由子类实现,这 4 个方法分别是:doUpdate、doFlushStatements、doQuery,以及 doQueryCursor。SimpleExecutor 派生自 BaseExecutor 抽象类,并为这 4 个模板方法提供了最简单的实现。ReuseExecutor 如其名,提供了重用的特性,提供对 Statement 对象的重用,以减少 SQL 预编译,以及创建和销毁 Statement 对象的开销。BatchExecutor 实现类则提供了对 SQL 语句批量执行的特性,也是针对提升性能的一种优化实现。
缓存结构设计
考虑到 Executor 在执行数据库操作时与缓存操作存在密切联系,所以在具体介绍 Executor 的实现之前我们先来了解一下 MyBatis 的缓存机制。
在谈论数据库架构设计时往往需要引入缓存的概念,数据库是相对脆弱且耗时的,所以需要尽量避免请求落库。在实际项目架构设计中,我们一般会引入 Redis、Memcached 这一类的组件对数据进行缓存,MyBatis 作为一个强大的 ORM 框架,也为缓存提供了内建的实现。前面我们在分析配置文件加载与解析时曾介绍过 MyBatis 缓存组件的具体实现,MyBatis 在数据存储上采用 HashMap 作为基本存储结构,并提供了多种装饰器从多个方面为缓存增加相应的特性。
本小节我们关注的是 MyBatis 在缓存结构方面的设计,MyBatis 缓存从结构上可以分为 一级缓存 和 二级缓存 ,一级缓存相对于二级缓存在粒度上更细,生命周期也更短。
上图描绘了 MyBatis 缓存的结构设计,当我们发起一次数据库查询时,如果启用了二级缓存的话,MyBatis 首先会从二级缓存中检索查询结果,如果缓存不命中则会继续检索一级缓存,只有在这两层缓存都不命中的情况下才会查询数据库,最后会以数据库返回的结果更新一级缓存和二级缓存。
MyBatis 的 一级缓存是会话级别的缓存(生命周期与本次会话相同) ,当我们开启一次数据库会话时,框架默认会为本次会话绑定一个一级缓存对象。此类缓存主要应对在一个会话范围内的冗余查询操作,比如使用同一个 SqlSession 对象同时连续执行多次相同的查询语句。这种情况下每次查询都落库是没有必要的,因为短时间内数据库变化的可能性会很小,但是每次都落库却是一笔不必要的开销。一级缓存默认是开启的,且无需进行配置,即一级缓存对开发者是透明的,如果确实希望干预一级缓存的内在运行,可以借助于插件来实现。
对于二级缓存而言,默认也是开启的,MyBatis 提供了相应的治理选项,具体可以参考官方文档。 二级缓存是应用级别的缓存 ,随着服务的启动而存在,并随着服务的关闭消亡。前面我们在分析 <cache/>
和 <cache-ref/>
标签时介绍了一个二级缓存会与一个具体的 namespace 绑定,并且支持引用一个已定义 namespace 缓存,即多个 namespace 可以共享同一个缓存。
本小节从整体结构上对 MyBatis 的缓存实现机制进行说明,目的在于对 MyBatis 的缓存有一个整体感知,关于一级缓存和二级缓存的具体实现,留到下面介绍分析 Executor 接口具体实现时穿插说明。
Statement 处理器
StatementHandler 接口及其实现类是 Executor 实现的基础,可以将其看作是 MyBatis 与数据库操作之间的纽带,实现了对 java.sql.Statement
对象的获取,以及 SQL 语句参数绑定与执行的逻辑。StatementHandler 接口及其实现类的类继承关系如下图所示:
其中 BaseStatementHandler 中实现了一些公共的逻辑;SimpleStatementHandler、PreparedStatementHandler,以及 CallableStatementHandler 实现类分别对应 Statement、PreparedStatement 和 CallableStatement 的相关实现;RoutingStatementHandler 并没有添加新的实现,而是对前面三种 StatementHandler 实现类的封装,它会在构造方法中依据当前传递的 Statement 类型创建对应的 StatementHandler 实现类对象。
StatementHandler 接口定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public interface StatementHandler { Statement prepare (Connection connection, Integer transactionTimeout) throws SQLException ; void parameterize (Statement statement) throws SQLException ; void batch (Statement statement) throws SQLException ; int update (Statement statement) throws SQLException ; <E> List<E> query (Statement statement, ResultHandler resultHandler) throws SQLException ; <E> Cursor<E> queryCursor (Statement statement) throws SQLException ; BoundSql getBoundSql () ; ParameterHandler getParameterHandler () ; }
首先来看一下 BaseStatementHandler 实现,该类中主要实现了获取 Statement 对象的逻辑,该类的属性定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 protected final Configuration configuration;protected final ObjectFactory objectFactory;protected final TypeHandlerRegistry typeHandlerRegistry;protected final ResultSetHandler resultSetHandler;protected final ParameterHandler parameterHandler;protected final Executor executor;protected final MappedStatement mappedStatement;protected final RowBounds rowBounds;protected BoundSql boundSql;
BaseStatementHandler 之于 StatementHandler#prepare
方法的实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public Statement prepare (Connection connection, Integer transactionTimeout) throws SQLException { ErrorContext.instance().sql(boundSql.getSql()); Statement statement = null ; try { statement = this .instantiateStatement(connection); this .setStatementTimeout(statement, transactionTimeout); this .setFetchSize(statement); return statement; } catch (SQLException e) { this .closeStatement(statement); throw e; } catch (Exception e) { this .closeStatement(statement); throw new ExecutorException("Error preparing statement. Cause: " + e, e); } }
上述方法首先会调用 BaseStatementHandler#instantiateStatement
方法获取一个 Statement 对象,这是一个模板方法交由子类实现;然后对拿到的 Statement 对象设置超时时间和返回的行数属性。
BaseStatementHandler 中定义了 ParameterHandler 类型的属性,主要用于为包含 ?
占位符的 SQL 语句绑定实参。ParameterHandler 接口定义如下:
1 2 3 4 5 6 7 8 9 public interface ParameterHandler { Object getParameterObject () ; void setParameters (PreparedStatement ps) throws SQLException ; }
其中,方法 ParameterHandler#getParameterObject
与存储过程相关,下面主要分析一下 ParameterHandler#setParameters
方法的实现。该方法用来为 SQL 语句绑定实参,具体操作等同于我们在直接使用 PreparedStatement 对象时注入相应类型的参数填充 SQL 语句。DefaultParameterHandler 是目前该接口的唯一实现,其 DefaultParameterHandler#setParameters
方法实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 public void setParameters (PreparedStatement ps) { ErrorContext.instance().activity("setting parameters" ).object(mappedStatement.getParameterMap().getId()); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); if (parameterMappings != null ) { for (int i = 0 ; i < parameterMappings.size(); i++) { ParameterMapping parameterMapping = parameterMappings.get(i); if (parameterMapping.getMode() != ParameterMode.OUT) { Object value; String propertyName = parameterMapping.getProperty(); if (boundSql.hasAdditionalParameter(propertyName)) { value = boundSql.getAdditionalParameter(propertyName); } else if (parameterObject == null ) { value = null ; } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { value = parameterObject; } else { MetaObject metaObject = configuration.newMetaObject(parameterObject); value = metaObject.getValue(propertyName); } TypeHandler typeHandler = parameterMapping.getTypeHandler(); JdbcType jdbcType = parameterMapping.getJdbcType(); if (value == null && jdbcType == null ) { jdbcType = configuration.getJdbcTypeForNull(); } try { typeHandler.setParameter(ps, i + 1 , value, jdbcType); } catch (TypeException | SQLException e) { throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e); } } } } }
上述实现整体上就是获取 BoundSql 对象记录的参数名称与 SQL 语句中参数的映射关系,然后获取参数名称对应的用户传递的实参设置到 PreparedStatement 对象中。如果使用过原生 JDBC 操作过数据库,对往 PreparedStatement 中填充实参的过程应该不难理解。
关于其余几个 StatementHandler 实现类的都比较简单,就不再展开。
结果集映射
结果集映射是 MyBatis 提供的一个强大且易用的特性,标签 <resultMap/>
用于配置数据库返回的结果集 ResultSet 与实体类属性之间的映射关系。前面我们分析了该标签的解析过程,本小节一起来探究一下 MyBatis 是如何基于这些配置执行结果集映射操作。
SQL 语句执行器 Executor 在调用具体的 StatementHandler 执行数据库查询操作时会针对数据库返回的结果集调用 ResultSetHandler 中相应方法执行结果集到实体类对象的映射处理。例如下面的代码块是 PreparedStatementHandler 在执行 PreparedStatementHandler#query
时的具体逻辑:
1 2 3 4 5 6 7 public <E> List<E> query (Statement statement, ResultHandler resultHandler) throws SQLException { PreparedStatement ps = (PreparedStatement) statement; ps.execute(); return resultSetHandler.handleResultSets(ps); }
ResultSetHandler 接口定义了结果集映射所需要的方法,具体如下:
1 2 3 4 5 6 7 8 9 10 11 12 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 是目前 ResultSetHandler 接口的唯一实现。MyBatis 为结果集映射提供了灵活的配置,灵活的背后是强(复)大(杂)的映射解析过程,尤其是对于嵌套映射配置的情况。本小节力图对整个映射过程做一个比较详细的介绍,不过还是建议读者自己亲自 debug 跟踪一下整个执行过程。接下来我们围绕 ResultSetHandler#handleResultSets
方法对结果集映射处理过程进行分析,该方法实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 public List<Object> handleResultSets (Statement stmt) throws SQLException { ErrorContext.instance().activity("handling results" ).object(mappedStatement.getId()); final List<Object> multipleResults = new ArrayList<>(); int resultSetCount = 0 ; ResultSetWrapper rsw = this .getFirstResultSet(stmt); List<ResultMap> resultMaps = mappedStatement.getResultMaps(); int resultMapCount = resultMaps.size(); this .validateResultMapsCount(rsw, resultMapCount); while (rsw != null && resultMapCount > resultSetCount) { ResultMap resultMap = resultMaps.get(resultSetCount); this .handleResultSet(rsw, resultMap, multipleResults, null ); rsw = this .getNextResultSet(stmt); this .cleanUpAfterHandlingResultSet(); resultSetCount++; } String[] resultSets = mappedStatement.getResultSets(); if (resultSets != null ) { while (rsw != null && resultSetCount < resultSets.length) { ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]); if (parentMapping != null ) { String nestedResultMapId = parentMapping.getNestedResultMapId(); ResultMap resultMap = configuration.getResultMap(nestedResultMapId); this .handleResultSet(rsw, resultMap, null , parentMapping); } rsw = this .getNextResultSet(stmt); this .cleanUpAfterHandlingResultSet(); resultSetCount++; } } return this .collapseSingleResultList(multipleResults); }
上述方法的执行过程可以分为两部分:普通结果集映射和多结果集映射。其中,多结果集映射一般用于存储过程,这是一个小众化的需求,所以大部分时候上述方法仅执行第一部分的逻辑。这一部分的执行过程如代码注释,其核心在于 DefaultResultSetHandler#handleResultSet
方法,该方法在第二部分中也会被调用,后面会针对该方法进行专门说明。
下面就第二部分的触发机制举例说明,能够执行到这里一般都伴随着存储过程,这里以 MySQL 数据库为例创建一个可以返回多结果集的存储过程,其中 t_blog 表和 t_post 表的定义参考官方文档示例:
1 2 3 4 5 CREATE PROCEDURE usp_demo(IN ID INT ) BEGIN SELECT * FROM t_blog WHERE id = ID; SELECT * FROM t_post WHERE id = ID; END ;
对应的映射配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <resultMap id ="usp_demo_result_map" type ="org.zhenchao.mybatis.entity.Blog" > <constructor > <idArg column ="id" javaType ="int" /> </constructor > <result property ="title" column ="title" /> <collection property ="posts" ofType ="org.zhenchao.mybatis.entity.Post" resultSet ="posts" > <id property ="id" column ="id" /> <result property ="subject" column ="subject" /> </collection > </resultMap > <select id ="uspDemo" resultSets ="blogs,posts" resultMap ="usp_demo_result_map" statementType ="CALLABLE" > {CALL usp_demo(#{id, jdbcType=INTEGER, mode=IN})} </select >
上述配置中,我们基于 resultSets 属性分别为对应的结果集命名,在执行该存储过程时会先映射 t_blog 数据表对应的结果集,映射的过程中遇到名为 posts 的结果集时,MyBatis 不会转去解析该结果集,而是会将该结果集记录到 DefaultResultSetHandler#nextResultMaps
属性中,等运行到第二部分时再对这些未解析的结果集统一进行映射处理。
上述过程中处理结果集映射的核心逻辑均位于 DefaultResultSetHandler#handleResultSet
方法中。该方法执行的主要逻辑在于判断当前是否指定了结果集处理器(即前面介绍过的 ResultHandler),如果没有指定则会创建一个默认的结果集处理器(默认采用 DefaultResultHandler 实现),然后调用 DefaultResultSetHandler#handleRowValues
方法执行映射逻辑。方法 DefaultResultSetHandler#handleResultSet
的实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 private void handleResultSet (ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException { try { if (parentMapping != null ) { this .handleRowValues(rsw, resultMap, null , RowBounds.DEFAULT, parentMapping); } else { if (resultHandler == null ) { DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory); this .handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null ); multipleResults.add(defaultResultHandler.getResultList()); } else { this .handleRowValues(rsw, resultMap, resultHandler, rowBounds, null ); } } } finally { this .closeResultSet(rsw.getResultSet()); } }
方法 DefaultResultSetHandler#handleRowValues
会判断当前映射配置中是否存在嵌套映射的情况,如果存在嵌套映射则执行方法 DefaultResultSetHandler#handleRowValuesForNestedResultMap
方法处理嵌套结果集映射,否则执行 DefaultResultSetHandler#handleRowValuesForSimpleResultMap
方法处理简单的结果集映射。下面以简单结果集映射的过程进行分析,对于嵌套结果集映射的过程还是强烈建议大家去 debug 跟踪理解,单凭静态文字很难说清楚。
方法 DefaultResultSetHandler#handleRowValuesForSimpleResultMap
实现了对简单(相对于嵌套而言)结果集映射的处理逻辑。首先会基于 RowBounds 设置定位具体的处理行,MyBatis 对于 LIMIT 分页的处理是逻辑分页,而不是物理分页,即将符合条件的记录全部载入内存,然后在内存中进行截取,如果希望执行物理分页,可以自己编码插件,或者使用第三方插件,然后会遍历结果集中目标记录行对其逐一映射。
方法 DefaultResultSetHandler#handleRowValuesForSimpleResultMap
实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 private void handleRowValuesForSimpleResultMap (ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException { DefaultResultContext<Object> resultContext = new DefaultResultContext<>(); ResultSet resultSet = rsw.getResultSet(); this .skipRows(resultSet, rowBounds); while (this .shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) { ResultMap discriminatedResultMap = this .resolveDiscriminatedResultMap(resultSet, resultMap, null ); Object rowValue = this .getRowValue(rsw, discriminatedResultMap, null ); this .storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet); } }
针对记录行的映射处理,方法首先会获取记录行对应的真正 ResultMap 映射配置对象,因为可能存在配置了 <discriminator/>
标签执行条件映射的情况,如果没有配置该标签则会使用当前实参对应的 ResultMap 对象。标签 <discriminator/>
的处理过程位于 DefaultResultSetHandler#resolveDiscriminatedResultMap
方法中,对照配置应该比较容易理解。获取到 ResultMap 映射配置对象之后,下一步就可以调用 DefaultResultSetHandler#getRowValue
方法对当前记录行执行映射处理,该方法实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 private Object getRowValue (ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException { final ResultLoaderMap lazyLoader = new ResultLoaderMap(); Object rowValue = this .createResultObject(rsw, resultMap, lazyLoader, columnPrefix); if (rowValue != null && !this .hasTypeHandlerForResultObject(rsw, resultMap.getType())) { final MetaObject metaObject = configuration.newMetaObject(rowValue); boolean foundValues = this .useConstructorMappings; if (this .shouldApplyAutomaticMappings(resultMap, false )) { foundValues = this .applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues; } foundValues = this .applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues; foundValues = lazyLoader.size() > 0 || foundValues; rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null ; } return rowValue; }
方法首先会调用 DefaultResultSetHandler#createResultObject
方法创建实体结果对象,然后为该对象执行属性映射注入。对于未配置映射关系的属性会基于配置决定是否执行自动映射,对于明确指定映射关系的属性,则调用 DefaultResultSetHandler#applyPropertyMappings
方法执行映射处理,该方法的具体实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 private boolean applyPropertyMappings (ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException { final List<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix); boolean foundValues = false ; final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings(); for (ResultMapping propertyMapping : propertyMappings) { String column = this .prependPrefix(propertyMapping.getColumn(), columnPrefix); if (propertyMapping.getNestedResultMapId() != null ) { column = null ; } if (propertyMapping.isCompositeResult() || (column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH))) || propertyMapping.getResultSet() != null ) { Object value = this .getPropertyMappingValue( rsw.getResultSet(), metaObject, propertyMapping, lazyLoader, columnPrefix); final String property = propertyMapping.getProperty(); if (property == null ) { continue ; } else if (value == DEFERRED) { foundValues = true ; continue ; } if (value != null ) { foundValues = true ; } if (value != null || (configuration.isCallSettersOnNulls() && !metaObject.getSetterType(property).isPrimitive())) { metaObject.setValue(property, value); } } } return foundValues; }
方法首先会获取当前结果集对应的映射关系配置和列名集合,然后遍历处理映射配置。针对嵌套查询、多结果集映射,以及普通映射的情况分而治之,这一过程位于 DefaultResultSetHandler#getPropertyMappingValue
方法中(实现如下)。针对嵌套查询的情况我们后面专门进行分析;对于多结果集的情况会将对应的结果集配置对象记录到 DefaultResultSetHandler#nextResultMaps
属性中,稍后会专门处理(即前面的第二部分代码);针对普通的映射则会基于 TypeHandler 获取属性对应的 java 类型值,也就是我们期望的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private Object getPropertyMappingValue (ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException { if (propertyMapping.getNestedQueryId() != null ) { return this .getNestedQueryMappingValue(rs, metaResultObject, propertyMapping, lazyLoader, columnPrefix); } else if (propertyMapping.getResultSet() != null ) { this .addPendingChildRelation(rs, metaResultObject, propertyMapping); return DEFERRED; } else { final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler(); final String column = this .prependPrefix(propertyMapping.getColumn(), columnPrefix); return typeHandler.getResult(rs, column); } }
最后会调用 DefaultResultSetHandler#storeObject
方法将实体结果对象记录到 DefaultResultHandler#list
属性中,并在 DefaultResultSetHandler#handleResultSet
方法中调用 DefaultResultHandler#getResultList
方法拿到这些结果对象。
执行器实现
前面已经介绍了 Executor 接口以及相关的实现类继承关系,本小节将对这些执行器实现类逐一展开分析。
BaseExecutor
BaseExecutor 是一个抽象类,实现了 Executor 接口中声明的所有方法,并采用模板方法模式抽象出 4 个模板方法交由子类实现。需要强调的一点是,BaseExecutor 抽象类引入了一级缓存支持,在相应方法实现中增加了对一级缓存的操作,因此该抽象类的所有实现类都具备一级缓存的特性。BaseExecutor 抽象类的属性定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 protected Transaction transaction;protected Executor wrapper;protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;protected PerpetualCache localCache;protected PerpetualCache localOutputParameterCache;protected Configuration configuration;protected int queryStack;private boolean closed;
下面针对一些比较复杂的方法实现展开说明,首先来看一下 BaseExecutor#update
方法实现(如下)。需要注意的是这里的 update 并不等同于 SQL 语句中的 UPDATE 操作,对于 Executor 而言数据库操作只包含 query 和 update 两大类,这里的 query 可以理解为 SQL 语句的 SELECT 操作,而 update 则对应着 SQL 语句中的 INSERT、UPDATE、DELECT 三类操作。
1 2 3 4 5 6 7 8 9 10 public int update (MappedStatement ms, Object parameter) throws SQLException { ErrorContext.instance().resource(ms.getResource()).activity("executing an update" ).object(ms.getId()); if (closed) { throw new ExecutorException("Executor was closed." ); } this .clearLocalCache(); return this .doUpdate(ms, parameter); }
上述方法首先会判定当前 Executor 是否已被关闭,对于没有关闭的 Executor 会首先清空一级缓存,然后调用 BaseExecutor#doUpdate
模板方法,该方法由子类实现。
再来看一下 BaseExecutor#query
方法,该方法用于执行数据库查询操作。因为引入了一级缓存,所以这里的查询不是简单的直接查询数据库,而是会先查询一级缓存,只有在缓存不命中的情况下才会查询数据库,并利用数据库返回的结果对象更新一级缓存。该方法的实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 public <E> List<E> query (MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameter); CacheKey key = this .createCacheKey(ms, parameter, rowBounds, boundSql); return this .query(ms, parameter, rowBounds, resultHandler, key, boundSql); } 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()) { this .clearLocalCache(); } List<E> list; try { queryStack++; list = resultHandler == null ? (List<E>) localCache.getObject(key) : null ; if (list != null ) { this .handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else { list = this .queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); } } finally { queryStack--; } if (queryStack == 0 ) { for (DeferredLoad deferredLoad : deferredLoads) { deferredLoad.load(); } deferredLoads.clear(); if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { this .clearLocalCache(); } } return list; }
整个 BaseExecutor#query
方法的执行过程如上述代码注释。
方法 BaseExecutor#queryCursor
与 BaseExecutor#query
都是提供数据库查询操作,区别在于前者返回的是一个游标(Cursor)对象,而后者返回的是已经完成结果集映射的结果对象,游标需要等待用户真正操作时才会执行结果集映射的过程。
SimpleExecutor
SimpleExecutor 提供了对 Executor 的简单实现,针对每一次数据库操作都会创建一个新的 Statement 对象,并在操作完毕之后进行关闭。SimpleExecutor 针对抽象类 BaseExecutor 中声明的方法实现流程都相同,下面以 SimpleExecutor#doQuery
方法为例进行分析,该方法的实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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 handler = configuration.newStatementHandler( wrapper, ms, parameter, rowBounds, resultHandler, boundSql); stmt = this .prepareStatement(handler, ms.getStatementLog()); return handler.query(stmt, resultHandler); } finally { this .closeStatement(stmt); } }
方法首先会基于 Configuration#newStatementHandler
创建对应的 StatementHandler 对象,这里实际上是采用了前面介绍的 RoutingStatementHandler 实现类依据入参进行创建。然后会调用 SimpleExecutor#prepareStatement
方法创建 Statement 对象,并为 SQL 语句绑定实参。接着执行具体的数据库查询操作,对于查询操作此时会执行结果集映射处理。最后关闭 Statement 对象。
方法 SimpleExecutor#prepareStatement
实现如下:
1 2 3 4 5 6 7 8 9 10 private Statement prepareStatement (StatementHandler handler, Log statementLog) throws SQLException { Statement stmt; Connection connection = this .getConnection(statementLog); stmt = handler.prepare(connection, transaction.getTimeout()); handler.parameterize(stmt); return stmt; }
其中 StatementHandler#prepare
方法的运行逻辑前面已经分析过。方法 StatementHandler#parameterize
执行了参数绑定的操作,该方法在 SimpleStatementHandler 中为空实现,毕竟对于 Statement 来说不支持设置参数;而对于 PreparedStatementHandler 和 CallableStatementHandler 而言都是调用了 ParameterHandler#setParameters
方法,该方法在前面已经专门分析过,不再重复介绍。
ReuseExecutor 提供了对 Statement 对象重用的机制,以减少该对象创建和销毁,以及 SQL 预编译所带来的开销。ReuseExecutor 类中定义了一个 ReuseExecutor#statementMap
属性(如下),其中 key 为 SQL 语句,value 为对应的 Statement 对象,以此实现对 Statement 对象的复用。
1 2 private final Map<String, Statement> statementMap = new HashMap<String, Statement>();
ReuseExecutor 中的方法实现也基本上沿用了同一套思路,仍然以 ReuseExecutor#doQuery
为例进行说明,该方法实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 public <E> List<E> doQuery (MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { Configuration configuration = ms.getConfiguration(); StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); Statement stmt = this .prepareStatement(handler, ms.getStatementLog()); return handler.query(stmt, resultHandler); }
上述方法与 SimpleExecutor#doQuery
的区别在于在获取 Statement 对象时会先尝试从本地缓存中获取,如果缓存不命中则会创建一个新的 Statement 对象,并更新缓存,实现位于 ReuseExecutor#prepareStatement
方法中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 private Statement prepareStatement (StatementHandler handler, Log statementLog) throws SQLException { Statement stmt; BoundSql boundSql = handler.getBoundSql(); String sql = boundSql.getSql(); if (this .hasStatementFor(sql)) { stmt = this .getStatement(sql); this .applyTransactionTimeout(stmt); } else { Connection connection = this .getConnection(statementLog); stmt = handler.prepare(connection, transaction.getTimeout()); this .putStatement(sql, stmt); } handler.parameterize(stmt); return stmt; }
BatchExecutor
BatchExecutor 用于批量执行 SQL 语句。通常应用程序都是单行的执行 SQL 语句,但是某些场景下单行执行数据库操作是比较耗时的,比如需要远程执行数据库操作。因此,JDBC 针对 INSERT、UPDATE,以及 DELETE 操作提供了批量执行的支持。
BatchExecutor 是批量 SQL 语句执行器,其属性定义如下:
1 2 3 4 5 6 7 8 private final List<Statement> statementList = new ArrayList<>();private final List<BatchResult> batchResultList = new ArrayList<>();private String currentSql;private MappedStatement currentStatement;
下面探究一下 BatchExecutor 的批处理执行过程。首先来看一下 BatchExecutor#doUpdate
方法实现,该方法用于添加批处理 SQL 语句:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public int doUpdate (MappedStatement ms, Object parameterObject) throws SQLException { final Configuration configuration = ms.getConfiguration(); final StatementHandler handler = configuration.newStatementHandler( this , ms, parameterObject, RowBounds.DEFAULT, null , null ); final BoundSql boundSql = handler.getBoundSql(); final String sql = boundSql.getSql(); final Statement stmt; if (sql.equals(currentSql) && ms.equals(currentStatement)) { int last = statementList.size() - 1 ; stmt = statementList.get(last); this .applyTransactionTimeout(stmt); handler.parameterize(stmt); BatchResult batchResult = batchResultList.get(last); batchResult.addParameterObject(parameterObject); } else { Connection connection = this .getConnection(ms.getStatementLog()); stmt = handler.prepare(connection, transaction.getTimeout()); handler.parameterize(stmt); currentSql = sql; currentStatement = ms; statementList.add(stmt); batchResultList.add(new BatchResult(ms, sql, parameterObject)); } handler.batch(stmt); return BATCH_UPDATE_RETURN_VALUE; }
上述方法中会判断当前执行的 SQL 模式(包含 ?
占位符的 SQL 语句)是否与前一次执行的相同,如果相同就会获取上次执行的 Statement 对象,并为之绑定实参;否则就会创建一个新的 Statement 对象,并记录本次执行的 SQL 模式,最后基于底层的数据库批处理方法 Statement#addBatch
添加批量 SQL 语句。由上述方法我们可以知道,对于连续同模式的批处理 SQL 操作会共享同一个 Statement 对象。
那么这些添加的批量 SQL 又是如何被执行的呢?这个过程位于 BatchExecutor#doFlushStatements
方法中,方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 public List<BatchResult> doFlushStatements (boolean isRollback) throws SQLException { try { List<BatchResult> results = new ArrayList<>(); if (isRollback) { return Collections.emptyList(); } for (int i = 0 , n = statementList.size(); i < n; i++) { Statement stmt = statementList.get(i); this .applyTransactionTimeout(stmt); BatchResult batchResult = batchResultList.get(i); try { batchResult.setUpdateCounts(stmt.executeBatch()); MappedStatement ms = batchResult.getMappedStatement(); List<Object> parameterObjects = batchResult.getParameterObjects(); KeyGenerator keyGenerator = ms.getKeyGenerator(); if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) { Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator; jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects); } else if (!NoKeyGenerator.class.equals(keyGenerator.getClass())) { for (Object parameter : parameterObjects) { keyGenerator.processAfter(this , ms, stmt, parameter); } } this .closeStatement(stmt); } catch (BatchUpdateException e) { } results.add(batchResult); } return results; } finally { for (Statement stmt : statementList) { this .closeStatement(stmt); } currentSql = null ; statementList.clear(); batchResultList.clear(); } }
方法会遍历我们在 BatchExecutor#doUpdate
中构造的 Statement 集合,分别执行集合中蕴含的 Statement 对象,并将执行的结果记录到 BatchResult 对象中(说明:在 BatchExecutor#doUpdate
方法中已经为每个 Statement 对象构造好了一个空的 BatchResult 对象,记录在 BatchExecutor#batchResultList
集合中),最后将 BatchResult 对象封装到集合中返回。因为都是数据库更新一类的操作,所以这里没有复杂的结果集映射,只需要记录每一条 SQL 语句执行所影响的行数即可。
CachingExecutor
由前面 Executor 的继承关系我们可以看到,CachingExecutor 相对于其它 Executor 实现来说似乎有其特别之处。CachingExecutor 直接实现了 Executor 接口,实际上它是一个 Executor 装饰器, 用于为 Executor 提供二级缓存支持 。该接口的属性定义如下:
1 2 3 4 private final Executor delegate;private final TransactionalCacheManager tcm = new TransactionalCacheManager();
其中第一个属性就是 CachingExecutor 具体修饰的 Executor 对象。我们来看一下第二个属性,TransactionalCacheManager 用来管理当前 CachingExecutor 对应的二级缓存对象,它的方法实现都比较简单,其中相对让人疑惑的是它的唯一一个属性:
1 2 private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
该属性的 key 就是当前对应的二级缓存,而 value 则是对于该二级缓存对象采用 TransactionalCache 装饰后的对象。所以 key 和 value 本质上都映射到同一个缓存对象,只是 value 采用了 TransactionalCache 进行增强。TransactionalCache 也是一个缓存装饰器,在前面介绍缓存装饰器实现时特意留着没有说明,这里一起来分析一下。该装饰器的属性定义如下:
1 2 3 4 5 6 7 8 private final Cache delegate;private boolean clearOnCommit;private final Map<Object, Object> entriesToAddOnCommit;private final Set<Object> entriesMissedInCache;
对应的读缓存和写缓存操作,以及事务提交方法实现比较简单,读者可以自行阅读源码。
继续回来看 CachingExecutor 的实现,所有实现方法中只有 CachingExecutor#query
方法稍微复杂一些,该方法的实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public <E> List<E> query (MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameterObject); CacheKey key = this .createCacheKey(ms, parameterObject, rowBounds, boundSql); return this .query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); } public <E> List<E> query (MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { Cache cache = ms.getCache(); if (cache != null ) { this .flushCacheIfRequired(ms); if (ms.isUseCache() && resultHandler == null ) { this .ensureNoOutParams(ms, boundSql); @SuppressWarnings("unchecked") List<E> list = (List<E>) tcm.getObject(cache, key); if (list == null ) { list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); tcm.putObject(cache, key, list); } return list; } } return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
正如我们一开始对于 MyBatis 缓存结构设计描绘的那样,上述方法首先在二级缓存中进行检索,如果二级缓存不命中则会执行被装饰的 Executor 对象的 Executor#query
方法。由前面的分析我们知道,Executor 的实现都自带一级缓存特性,所以接下去会查询一级缓存。只有在一级缓存也不命中的情况下,请求才会落库,并由数据库返回的结果对象更新一级缓存和二级缓存。
那么这里使用的二级缓存对象是在哪里创建的呢?实际上前面我们就定义说二级缓存是应用级别的,所以当应用启动时二级缓存就已经被创建了,这个过程发生在对映射文件进行解析时。在映射文件中我们会按照需要配置一定的 <cache/>
和 <cache-ref>
标签,而在解析 <cache/>
标签时会调用 MapperBuilderAssistant#useNewCache
方法创建对应的二级缓存对象。
总结
本文对 MyBatis 执行 SQL 语句所涉及到的各个方面做了一个比较详细的分析。当我们基于 MyBatis 触发一次数据库操作时,首先需要开启一次数据库会话,然后获取目标 Mapper 接口,并调用相应的 Mapper 方法执行数据库操作,最后拿到操作结果。MyBatis 在这中间基于动态代理机制实现了 SQL 语句的检索、参数绑定、数据库操作,以及结果集映射等一系列操作,并引入了缓存机制优化数据库查询性能。
回过头来看,MyBatis 的整体设计还是非常巧妙的,却也很是直观且简单,是对动态代理机制的典型应用,其设计思想和对于设计模式的应用值得我们在实际开发中借鉴。
本文是 MyBatis 源码解析系列的最后一篇文章,由于时间仓促,再加上作者水平有限,整个系列的文章中不免有错误之处,还望批评指正!
参考
MyBatis 官方文档
MyBatis 技术内幕