9000文字+代码详细讲解SQL执行流程(一)!干货到底!建议收藏!

本次更新一下SQL执行流程,本篇为上集,喜欢的朋友们可以三连支持一下哦!这才是博主更新的动力~

一、SQL 执⾏⼊口

在单独使用 MyBatis 进行数据库操作时,我们通常都会先调用 SqlSession 接口的
getMapper方法为我们的Mapper接口生成实现类。然后就可以通过Mapper进行数据库操作。
比如像下面这样:

ArticleMapper articleMapper = session.getMapper(ArticleMapper.class);
Article article = articleMapper.findOne(1);

如果大家对 MyBatis 较为了解,会知道 SqlSession 是通过 JDK 动态代理的方式为接口
生成代理对象的。在调用接口方法时,相关调用会被代理逻辑拦截。在代理逻辑中可根据方
法名及方法归属接口获取到当前方法对应的 SQL 以及其他一些信息,拿到这些信息即可进
行数据库操作。
以上是一个简版的 SQL 执行过程,省略了很多细节。下面我们先按照这个简版的流程
进行分析,首先来看一下 Mapper 接口的代理对象创建过程。

1.1 为 Mapper 接⼜创建代理对象

本节,我们从 DefaultSqlSession 的 getMapper 方法开始看起,如下:

// -☆- DefaultSqlSession
public <T> T getMapper(Class<T> type) {
return configuration.<T>getMapper(type, this);
}
// -☆- Configuration
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
// -☆- MapperRegistry
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 从 knownMappers 中获取与 type 对应的 MapperProxyFactory
final MapperProxyFactory<T> mapperProxyFactory = 
(MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("……");
 }
try {
// 创建代理对象
return mapperProxyFactory.newInstance(sqlSession);
 } catch (Exception e) {
throw new BindingException("……");
 } }

如上,经过连续的调用,Mapper 接口代理对象的创建逻辑初现端倪。如果大家没分析过
MyBatis配置文件的解析过程,那么可能不知道knownMappers集合中的元素是何时存入的,
这 里简 单说 明一 下。MyBatis 在解析配置文件的节点的过程中,会调用MapperRegistry 的 addMapper 方法将 Class 到 MapperProxyFactory 对象的映射关系存入到knownMappers。具体的代码就不分析了,大家可以阅读我之前写的文章,或者自行分析相关的代码。
在获取到 MapperProxyFactory 对象后,即可调用工厂方法为 Mapper 接口生成代理对象
了。相关逻辑如下:

// -☆- MapperProxyFactory
public T newInstance(SqlSession sqlSession) {
// 创建 MapperProxy 对象,MapperProxy 实现了 InvocationHandler 接口,
// 代理逻辑封装在此类中
final MapperProxy<T> mapperProxy =
new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
protected T newInstance(MapperProxy<T> mapperProxy) {
// 通过 JDK 动态代理创建代理对象
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), 
new Class[]{mapperInterface}, mapperProxy);
}

上面的代码首先创建了一个 MapperProxy 对象,该对象实现了 InvocationHandler 接口。
然后将对象作为参数传给重载方法,并在重载方法中调用 JDK 动态代理接口为 Mapper 生成
代理对象。代理对象已经创建完毕,下面就可以调用接口方法进行数据库操作了。由于接口
方法会被代理逻辑拦截,所以下面我们把目光聚焦在代理逻辑上面,看看代理逻辑会做哪些
事情。

1.2 执⾏代理逻辑

Mapper 接口方法的代理逻辑首先会对拦截的方法进行一些检测,以决定是否执行后续
的数据库操作。对应的代码如下:

public Object invoke(Object proxy, 
Method method, Object[] args) throws Throwable {
try {
// 如果方法是定义在 Object 类中的,则直接调用
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
/*
* 下面的代码最早出现在 mybatis-3.4.2 版本中,用于支持 JDK 1.8 中的
* 新特性 - 默认方法。这段代码的逻辑就不分析了,有兴趣的同学可以
* 去 Github 上看一下相关的相关的讨论(issue #709),链接如下:
* 
* https://github.com/mybatis/mybatis-3/issues/709
*/
 } else if (isDefaultMethod(method)) {
 return invokeDefaultMethod(proxy, method, args);
 }
 } catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
 }
// 从缓存中获取 MapperMethod 对象,若缓存未命中,则创建 MapperMethod 对象
final MapperMethod mapperMethod = cachedMapperMethod(method);
// 调用 execute 方法执行 SQL
return mapperMethod.execute(sqlSession, args);
}

如上,代理逻辑会首先检测被拦截的方法是不是定义在 Object 中的,比如 equals、
hashCode 方法等。对于这类方法,直接执行即可。除此之外,MyBatis 从 3.4.2 版本开始,
对 JDK1.8 接口的默认方法提供了支持,具体就不分析了。完成相关检测后,紧接着从缓存
中获取或者创建 MapperMethod 对象,然后通过该对象中的 execute 方法执行 SQL。在分析execute 方法之前,我们先来看一下 MapperMethod 对象的创建过程。MapperMethod 的创建过程看似普通,但却包含了一些重要的逻辑,所以不能忽视。

1. 创建 MapperMethod 对象

本节来分析一下 MapperMethod 的构造方法,看看它的构造方法中都包含了哪些逻
辑。如下:

public class MapperMethod {
private final SqlCommand command;
private final MethodSignature method;
public MapperMethod(Class<?> mapperInterface,
Method method, Configuration config) {
// 创建 SqlCommand 对象,该对象包含一些和 SQL 相关的信息
this.command = new SqlCommand(config, mapperInterface, method);
// 创建 MethodSignature 对象,由类名可知,该对象包含了被拦截方法的一些信息
this.method = new MethodSignature(config, mapperInterface, method);
 } }

MapperMethod 构造方法的逻辑很简单,主要是创建 SqlCommand 和 MethodSignature 对
象。这两个对象分别记录了不同的信息,这些信息在后续的方法调用中都会被用到。下面我
们深入到这两个类的构造方法中,探索它们的初始化逻辑。

  • 创建 SqlCommand 对象

前面说了 SqlCommand 中保存了一些和 SQL 相关的信息,那具体有哪些信息呢?答案
在下面的代码中。

public static class SqlCommand {
private final String name;
private final SqlCommandType type;
public SqlCommand(Configuration configuration, 
Class<?> mapperInterface, Method method) {
final String methodName = method.getName();
final Class<?> declaringClass = method.getDeclaringClass();
// 解析 MappedStatement
MappedStatement ms = resolveMappedStatement(
mapperInterface, methodName, declaringClass, configuration);
// 检测当前方法是否有对应的 MappedStatement
if (ms == null) {
// 检测当前方法是否有 @Flush 注解
if (method.getAnnotation(Flush.class) != null) {
// 设置 name 和 type 遍历
name = null;
type = SqlCommandType.FLUSH;
 } else {
// 若 ms == null 且方法无 @Flush 注解,此时抛出异常。
// 这个异常比较常见,大家应该眼熟吧
throw new BindingException("……");
 }
 } else {
// 设置 name 和 type 变量
name = ms.getId();
type = ms.getSqlCommandType();
if (type == SqlCommandType.UNKNOWN) {
throw new BindingException("……");
 }
 }
 } }

SqlCommand 的构造方法主要用于初始化它的两个成员变量。代码不是很长,
逻辑也不难理解,就不多说了。继续往下看。

  • 创建 MethodSignature 对象

MethodSignature 即方法签名,顾名思义,该类保存了一些和目标方法相关的信息。比如
目标方法的返回类型,目标方法的参数列表信息等。下面,我们来分析一下 MethodSignature
的构造方法。

public static class MethodSignature {
private final boolean returnsMany;
private final boolean returnsMap;
private final boolean returnsVoid;
private final boolean returnsCursor;
private final Class<?> returnType;
private final String mapKey;
private final Integer resultHandlerIndex;
private final Integer rowBoundsIndex;
private final ParamNameResolver paramNameResolver;
public MethodSignature(Configuration configuration, 
Class<?> mapperInterface, Method method) {
// 通过反射解析方法返回类型
Type resolvedReturnType = TypeParameterResolver
.resolveReturnType(method, mapperInterface);
if (resolvedReturnType instanceof Class<?>) {
this.returnType = (Class<?>) resolvedReturnType;
 } else if (resolvedReturnType instanceof ParameterizedType) {
this.returnType = (Class<?>) (
(ParameterizedType) resolvedReturnType).getRawType();
 } else {
this.returnType = method.getReturnType();
 }
// 检测返回值类型是否是 void、集合或数组、Cursor、Map 等
this.returnsVoid = void.class.equals(this.returnType);
this.returnsMany = configuration.getObjectFactory()
.isCollection(this.returnType) || this.returnType.isArray();
this.returnsCursor = Cursor.class.equals(this.returnType);
// 解析 @MapKey 注解,获取注解内容
this.mapKey = getMapKey(method);
this.returnsMap = this.mapKey != null;
// 获取 RowBounds 参数在参数列表中的位置,如果参数列表中
// 包含多个 RowBounds 参数,此方法会抛出异常
this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
// 获取 ResultHandler 参数在参数列表中的位置
this.resultHandlerIndex =
getUniqueParamIndex(method, ResultHandler.class);
// 解析参数列表
this.paramNameResolver =
new ParamNameResolver(configuration, method);
 } }

上面的代码用于检测目标方法的返回类型,以及解析目标方法参数列表。其中,检测返
回类型的目的是为避免查询方法返回错误的类型。比如我们要求接口方法返回一个对象,结
果却返回了对象集合,这会导致类型转换错误。关于返回值类型的解析过程先说到这,下面
分析参数列表的解析过程。

public class ParamNameResolver {
private static final String GENERIC_NAME_PREFIX = "param";
private final SortedMap<Integer, String> names;
public ParamNameResolver(Configuration config, Method method) {
// 获取参数类型列表
final Class<?>[] paramTypes = method.getParameterTypes();
// 获取参数注解
final Annotation[][] paramAnnotations =
method.getParameterAnnotations();
final SortedMap<Integer, String> map =
new TreeMap<Integer, String>();
int paramCount = paramAnnotations.length;
for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
// 检测当前的参数类型是否为 RowBounds 或 ResultHandler
if (isSpecialParameter(paramTypes[paramIndex])) {
continue;
 }
String name = null;
for (Annotation annotation : paramAnnotations[paramIndex]) {
if (annotation instanceof Param) {
hasParamAnnotation = true;
// 获取 @Param 注解内容
name = ((Param) annotation).value();
break;
 }
 }
// name 为空,表明未给参数配置 @Param 注解
if (name == null) {
// 检测是否设置了 useActualParamName 全局配置
if (config.isUseActualParamName()) {
// 通过反射获取参数名称。此种方式要求 JDK 版本为 1.8+,
// 且要求编译时加入 -parameters 参数,否则获取到的参数名
// 仍然是 arg1, arg2, ..., argN
name = getActualParamName(method, paramIndex);
 }
if (name == null) {
/*
* 使用 map.size() 返回值作为名称,思考一下为什么不这样写:
* name = String.valueOf(paramIndex);
* 因为如果参数列表中包含 RowBounds 或 ResultHandler,这两个
* 参数会被忽略掉,这样将导致名称不连续。
*
* 比如参数列表 (int p1, int p2, RowBounds rb, int p3)
* - 期望得到名称列表为 ["0", "1", "2"]
* - 实际得到名称列表为 ["0", "1", "3"]
*/
name = String.valueOf(map.size());
 }
 }
// 存储 paramIndex 到 name 的映射
map.put(paramIndex, name);
 }
names = Collections.unmodifiableSortedMap(map);
 } }

方法参数列表解析完毕后,可得到参数下标与参数名的映射关系,这些映射关系最终存
储在 ParamNameResolver 的 names 成员变量中。这些映射关系将会在后面的代码中被用到,大家留意一下。下面写点代码测试一下 ParamNameResolver 的解析逻辑。如下:

public class ParamNameResolverTest {
@Test
public void test() throws NoSuchMethodException, 
NoSuchFieldException, IllegalAccessException {
Configuration config = new Configuration();
config.setUseActualParamName(false);
Method method = ArticleMapper.class.getMethod("select",
Integer.class, String.class, RowBounds.class, Article.class);
ParamNameResolver resolver = new ParamNameResolver(config, method);
Field field = resolver.getClass().getDeclaredField("names");
field.setAccessible(true);
// 通过反射获取 ParamNameResolver 私有成员变量 names
Object names = field.get(resolver);
System.out.println("names: " + names);
 }
 class ArticleMapper {
public void select(@Param("id") Integer id, 
@Param("author") String author, RowBounds rb, Article article) {}
 } }

测试结果如下
在这里插入图片描述
参数索引与名称映射图如下
在这里插入图片描述
到此,关于 MapperMethod 的初始化逻辑就分析完了,继续往下分析。

2. 执⾏ execute ⽅法

前面已经分析了 MapperMethod 的初始化过程,现在 MapperMethod 创建好了。那么,
接下来要做的事情是调用 MapperMethod 的 execute 方法,执行 SQL。代码如下:

// -☆- MapperMethod
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
// 根据 SQL 类型执行相应的数据库操作
switch (command.getType()) {
case INSERT: {
// 对用户传入的参数进行转换,下同
Object param = method.convertArgsToSqlCommandParam(args);
// 执行插入操作,rowCountResult 方法用于处理返回值
result = rowCountResult(s
qlSession.insert(command.getName(), param));
break;
 }
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
// 执行更新操作
result = rowCountResult(
sqlSession.update(command.getName(), param));
break;
 }
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
// 执行删除操作
result = rowCountResult(
sqlSession.delete(command.getName(), param));
break;
 }
case SELECT:
// 根据目标方法的返回类型进行相应的查询操作
if (method.returnsVoid() && method.hasResultHandler()) {
// 如果方法返回值为 void,但参数列表中包含 ResultHandler,表明
// 使用者想通过 ResultHandler 的方式获取查询结果,而非通过返回值
// 获取结果
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
// 执行查询操作,并返回多个结果
result = executeForMany(sqlSession, args);
 } else if (method.returnsMap()) {
// 执行查询操作,并将结果封装在 Map 中返回
result = executeForMap(sqlSession, args);
 } else if (method.returnsCursor()) {
// 执行查询操作,并返回一个 Cursor 对象
result = executeForCursor(sqlSession, args);
 } else {
Object param = method.convertArgsToSqlCommandParam(args);
// 执行查询操作,并返回一个结果
result = sqlSession.selectOne(command.getName(), param);
 }
break;
case FLUSH:
// 执行刷新操作
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("……");
 }
// 如果方法的返回值为基本类型,而返回值却为 null,此种情况下应抛出异常
if (result == null && method.getReturnType().isPrimitive() 
&& !method.returnsVoid()) {
throw new BindingException("……");
 }
return result; }

如上,execute 方法主要由一个 switch 语句组成,用于根据 SQL 类型执行相应的数据库
操作。该方法的逻辑清晰,不需 要太多的分析。不过在上面 代 码 中convertArgsToSqlCommandParam 方法出现次数比较频繁,这里分析一下:

// -☆- MapperMethod
public Object convertArgsToSqlCommandParam(Object[] args) {
return paramNameResolver.getNamedParams(args);
}
public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
if (args == null || paramCount == 0) {
return null;
 } else if (!hasParamAnnotation && paramCount == 1) {
/*
* 如果方法参数列表无 @Param 注解,且仅有一个非特别参数,则返回该
* 参数的值。比如如下方法:
* List findList(RowBounds rb, String name)
* names 如下:
* names = {1 : "0"}
* 此种情况下,返回 args[names.firstKey()],即 args[1] -> name
*/
return args[names.firstKey()];
 } else {
final Map<String, Object> param = new ParamMap<Object>();
int i = 0;
for (Map.Entry<Integer, String> entry : names.entrySet()) {
// 添加 <参数名, 参数值> 键值对到 param 中
param.put(entry.getValue(), args[entry.getKey()]);
// genericParamName = param + index。比如 param1, param2,... paramN
final String genericParamName =
GENERIC_NAME_PREFIX + String.valueOf(i + 1);
// 检测 names 中是否包含 genericParamName,什么情况下会包含?
// 答案如下:
// 使用者显式将参数名称配置为 param1,即 @Param("param1")
if (!names.containsValue(genericParamName)) {
// 添加 <param*, value> 到 param 中
param.put(genericParamName, args[entry.getKey()]);
 }i++;
 }
 return param;
 } }

convertArgsToSqlCommandParam 是一个空壳方法,该方法最终调用了
ParamNameResolver 的 getNamedParams 方法。getNamedParams 方法的主要逻辑是根据条件返回不同的结果,该方法的代码不是很难理解,我也进行了比较详细的注释,就不多说了。
分析完 convertArgsToSqlCommandParam 的逻辑,接下来说说 MyBatis 对哪些 SQL 指令提供了支持,如下:

  • 查询语句:SELECT
  • 更新语句:INSERT/UPDATE/DELETE
  • 存储过程:CALL

在上面的列表中,我刻意对 SELECT/INSERT/UPDATE/DELETE 等指令进行了分类,分
类依据指令的功能以及 MyBatis 执行这些指令的过程。这里把 SELECT 称为查询语句,
INSERT/UPDATE/DELETE 等称为更新语句。下来按照顺序对着两种语句执行过程进行分析,
先来分析查询语句的执行过程。

二、 查询语句的执⾏过程

查询语句对应的方法比较多,有如下几种:

  • executeWithResultHandler
  • executeForMany
  • executeForMap
  • executeForCursor

这些方法在内部调用了 SqlSession 中的一些 select方法,比如 selectList、selectMap、
selectCursor 等。这些方法的返回值类型是不同的,因此对于每种返回类型,需要有专门的处理方法。以 selectList 方法为例,该方法的返回值类型为 List。但如果我们的 Mapper 或 Dao的接口方法返回值类型为数组,或者 Set,直接将 List 类型的结果返回给 Mapper/Dao 就不合适了。execute
等方法只是对 select等方法做了一层简单的封装,因此接下来我们应们应该把目光放在这些 select方法上。

2.1 selectOne ⽅法分析

本节选择分析 selectOne 方法,而不是其他的方法,大家或许会觉得奇怪。前面提及了
selectList、selectMap、selectCursor 等方法,这里却分析一个未提及的方法。这样做并没什么
特别之处,主要原因是 selectOne 在内部会调用 selectList 方法。这里分析 selectOne 方法是
为了告知大家,selectOne 和 selectList 方法是有联系的,同时分析 selectOne 方法等同于分析
selectList 方法。如果你不信的话,那我们看源码吧,源码面前了无秘密。

// -☆- DefaultSqlSession
public <T> T selectOne(String statement, Object parameter) {
// 调用 selectList 获取结果
List<T> list = this.<T>selectList(statement, parameter);
if (list.size() == 1) {
// 返回结果
return list.get(0);
 } else if (list.size() > 1) {
// 如果查询结果大于 1 则抛出异常,这个异常也是很常见的
throw new TooManyResultsException("……");
 } else {
return null;
 } }

如上,selectOne 方法在内部调用 selectList 了方法,并取 selectList 返回值的第 1 个元素
作为自己的返回值。如果 selectList 返回的列表元素大于 1,则抛出异常。上面代码比较易懂,就不多说了。下面我们来看看 selectList 方法的实现。

// -☆- DefaultSqlSession
public <E> List<E> selectList(String statement, Object parameter) {
// 调用重载方法
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
private final Executor executor;
public <E> List<E> selectList(String statement, Object parameter, RowBounds
rowBounds) {
try {
// 获取 MappedStatement
MappedStatement ms = configuration.getMappedStatement(statement);
// 调用 Executor 实现类中的 query 方法
return executor.query(ms, wrapCollection(parameter), 
rowBounds, Executor.NO_RESULT_HANDLER);
 } catch (Exception e) {
throw ExceptionFactory.wrapException("……");
 } finally {
ErrorContext.instance().reset();
 } }

如上,这里要来说说 executor 变量,该变量类型为 Executor。Executor 是一个接口,它
的实现类如下:
在这里插入图片描述
Executor 有这么多的实现类,大家猜一下 executor 变量对应哪个实现类。要弄清楚这个
问题,需要大家到源头去查证。这里提示一下,大家可以跟踪一下 DefaultSqlSessionFactory的openSession 方法,很快就能发现 executor 变量创建的踪迹。限于篇幅原因,本文就不分析 openSession 方法的源码了。默认情况下,executor 的类型为 CachingExecutor,该类是一个装饰器类,用于给目标 Executor 增加二级缓存功能。那目标 Executor 是谁呢?默认情况下是 SimpleExecutor。
现在大家搞清楚 executor 变量的身份了,接下来继续分析 selectOne 方法的调用栈。先
来看看 CachingExecutor 的 query 方法是怎样实现的。如下:

// -☆- CachingExecutor
public <E> List<E> query(MappedStatement ms, Object parameterObject, 
RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 获取 BoundSql
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 创建 CacheKey
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
// 调用重载方法
return query(ms, parameterObject, 
rowBounds, resultHandler, key, boundSql);
}

上面的代码用于获取 BoundSql 对象,创建 CacheKey 对象,然后再将这两个对象传给重
载方法。BoundSql 的获取过程较为复杂,我将在下一节进行分析。CacheKey 以及接下来即
将出现的一二级缓存将会独立成章分析。上面的方法等代码和 SimpleExecutor 父类 BaseExecutor 中的实现没什么区别,有区别的地方在于这个方法所调用的重载方法。继续往下看。

// -☆- CachingExecutor
public <E> List<E> query(MappedStatement ms, Object parameterObject, 
RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, 
BoundSql boundSql) throws SQLException {
// 从 MappedStatement 中获取缓存
Cache cache = ms.getCache();
// 若映射文件中未配置缓存或参照缓存,此时 cache = null
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 若缓存未命中,则调用被装饰类的 query 方法
list = delegate.<E>query(ms, parameterObject, 
rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
 }
return list;
 } }
// 调用被装饰类的 query 方法
return delegate.<E>query(
ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

以上代码涉及到了二级缓存,若二级缓存为空,或未命中,则调用被装饰类的 query 方
法。下面来看一下 BaseExecutor 的中签名相同的 query 方法是如何实现的。

// -☆- BaseExecutor
public <E> List<E> query(MappedStatement ms, Object parameter, 
RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, 
BoundSql boundSql) throws SQLException {
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();
 }
deferredLoads.clear();
if (configuration.getLocalCacheScope()==LocalCacheScope.STATEMENT) {
clearLocalCache();
 }
 }
return list; }

上面的方法主要用于从一级缓存中查找查询结果,若缓存未命中,再向数据库进行查询。
在上面的代码中,出现了一个新的类 DeferredLoad,这个类用于延迟加载。该类的实现并不
复杂,但是具体用途让我有点疑惑。这个我目前也未完全搞清楚,就不分析了。接下来,我
们来看一下 queryFromDatabase 方法的实现。

// -☆- 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; }

上面的代码仍然不是 selectOne 方法调用栈的终点,抛开缓存操作,queryFromDatabase
最终还会调用 doQuery 进行查询。所以下面我们继续进行跟踪。

// -☆- SimpleExecutor
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
StatementHandler handler = configuration.newStatementHandler(
wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 创建 Statement
stmt = prepareStatement(handler, ms.getStatementLog());
// 执行查询操作
return handler.<E>query(stmt, resultHandler);
 } finally {
// 关闭 Statement
closeStatement(stmt);
 } }

doQuery 方法中仍然有不少的逻辑,完全看不到即将要到达终点的趋势,不过这离终点
又近了一步。接下来,我们先跳过 StatementHandler 和 Statement 创建过程,这两个对象的创建过程会在后面进行说明。这里,我们以 PreparedStatementHandler 为例,看看它的 query 方法是怎样实现的。如下:

// -☆- PreparedStatementHandler
public <E> List<E> query(Statement statement, ResultHandler resultHandler) 
throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
// 执行 SQL
ps.execute();
// 处理执行结果
return resultSetHandler.<E>handleResultSets(ps);
}

到这里似乎看到了希望,整个调用过程总算要结束了。不过先别高兴的太早,SQL 执行
结果的处理过程也很复杂,稍后将会专门拿出一节内容进行分析。
以上就是 selectOne 方法的执行过程,尽管我已经简化了代码分析,但是整个过程看起来还是很复杂的。查询过程涉及到了很多方法调用,不把这些调用方法搞清楚,很难对
MyBatis 的查询过程有深入的理解。所以在接下来的章节中,我将会对一些重要的调用进行
分析。如果大家不满足于泛泛而谈,那么接下来咱们一起进行更深入的探索吧。

2.2 获取 BoundSql

在执行 SQL 之前,需要将 SQL 语句完整的解析出来。我们都知道 SQL 是配置在映射文
件中的,但由于映射文件中的 SQL 可能会包含占位符#{},以及动态 SQL 标签,比如、
等。因此,我们并不能直接使用映射文件中配置的 SQL。MyBatis 会将映射文件中
的 SQL 解析成一组 SQL 片段。如果某个片段中也包含动态 SQL 相关的标签,那么,MyBatis会对该片段再次进行分片。最终,一个 SQL 配置将会被解析成一个 SQL 片段树。形如下面的图片:
在这里插入图片描述
我们需要对片段树进行解析,以便从每个片段对象中获取相应的内容。然后将这些内容
组合起来即可得到一个完成的 SQL 语句,这个完整的 SQL 以及其他的一些信息最终会存储
在 BoundSql 对象中。下面我们来看一下 BoundSql 类的成员变量信息,如下:

private final String sql;
private final List<ParameterMapping> parameterMappings;
private final Object parameterObject;
private final Map<String, Object> additionalParameters;
private final MetaObject metaParameters;

下面用一个表格列举各个成员变量的含义
在这里插入图片描述
以上对 BoundSql 的成员变量做了简要的说明,部分参数的用途大家现在可能不是很明
白。不过不用着急,这些变量在接下来的源码分析过程中会陆续的出现。到时候对着源码多
思考,或是写点测试代码调试一下,即可弄懂。
好了,现在准备工作已经做好。接下来,开始分析 BoundSql 的构建过程。我们源码之
旅的第一站是 MappedStatement 的 getBoundSql 方法,代码如下:

// -☆- MappedStatement
public BoundSql getBoundSql(Object parameterObject) {
// 调用 sqlSource 的 getBoundSql 获取 BoundSql
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
List<ParameterMapping> parameterMappings =
boundSql.getParameterMappings();
if (parameterMappings == null || parameterMappings.isEmpty()) {
// 创建新的 BoundSql,这里的 parameterMap 是 ParameterMap 类型。
// 由<ParameterMap> 节点进行配置,该节点已经废弃,不推荐使用。
// 默认情况下,parameterMap.getParameterMappings() 返回空集合
boundSql = new BoundSql(configuration, boundSql.getSql(), 
parameterMap.getParameterMappings(), parameterObject);
 }
// 省略不重要的逻辑
return boundSql; }

如上,MappedStatement 的 getBoundSql 在内部调用了 SqlSource 实现类的 getBoundSql
方法。处理此处的调用,余下的逻辑都不是重要逻辑,就不啰嗦了。接下来,我们把目光转
移到 SqlSource 实现类的 getBoundSql 方法上。SqlSource 是一个接口,它有如下几个实现类:

  • DynamicSqlSource
  • RawSqlSource
  • StaticSqlSource
  • ProviderSqlSource
  • VelocitySqlSource

在如上几个实现类中,我们应该选择分析哪个实现类的逻辑呢?首先我们把最后两个排
除掉,不常用。剩下的三个实现类中,仅前两个实现类会在映射文件解析的过程中被使用。
当 SQL 配置中包含${}(不是#{})占位符,或者包含、等标签时,会被认为是
动态 SQL,此时使用 DynamicSqlSource 存储 SQL 片段。否则,使用 RawSqlSource 存储 SQL配置信息。相比之下 DynamicSqlSource 存储的 SQL 片段类型较多,解析起来也更为复杂一些。因此下面我将分析 DynamicSqlSource 的 getBoundSql 方法。弄懂这个,RawSqlSource 也不在话下。

// -☆- DynamicSqlSource
public BoundSql getBoundSql(Object parameterObject) {
// 创建 DynamicContext
DynamicContext context =
new DynamicContext(configuration, parameterObject);
// 解析 SQL 片段,并将解析结果存储到 DynamicContext 中
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ?
Object.class : parameterObject.getClass();
// 构建 StaticSqlSource,在此过程中将 sql 语句中的占位符 #{} 替换为问号 ?,
// 并为每个占位符构建相应的 ParameterMapping
SqlSource sqlSource = sqlSourceParser.parse(
context.getSql(), parameterType, context.getBindings());
// 调用 StaticSqlSource 的 getBoundSql 获取 BoundSql
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
// 将 DynamicContext 的 ContextMap 中的内容拷贝到 BoundSql 中
for(Map.Entry<String, Object> entry : context.getBindings().entrySet()){
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
 }
return boundSql; }

如上,DynamicSqlSource 的 getBoundSql 方法的代码看起来不多,但是逻辑却并不简单。
该方法由数个步骤组成,这里总结一下:

  1. 创建 DynamicContext
  2. 解析 SQL 片段,并将解析结果存储到 DynamicContext 中
  3. 解析 SQL 语句,并构建 StaticSqlSource
  4. 调用 StaticSqlSource 的 getBoundSql 获取 BoundSql
  5. 将 DynamicContext 的 ContextMap 中的内容拷贝到 BoundSql 中

如上 5 个步骤中,第 5 步为常规操作,就不多说了,其他步骤将会在接下来章节中一一
进行分析。按照顺序,我们先来分析 DynamicContext 的实现。

1.DynamicContext

DynamicContext 是 SQL 语句构建的上下文,每个 SQL 片段解析完成后,都会将解析结
果存入 DynamicContext 中。待所有的 SQL 片段解析完毕后,一条完整的 SQL 语句就会出现在 DynamicContext 对象中。下面我们来看一下 DynamicContext 类的定义。

public class DynamicContext {
public static final String PARAMETER_OBJECT_KEY = "_parameter";
public static final String DATABASE_ID_KEY = "_databaseId";
private final ContextMap bindings;
private final StringBuilder sqlBuilder = new StringBuilder();
public DynamicContext(
Configuration configuration, Object parameterObject) {
// 创建 ContextMap
if (parameterObject != null && !(parameterObject instanceof Map)) {
MetaObject metaObject =
configuration.newMetaObject(parameterObject);
bindings = new ContextMap(metaObject);
 } else {
bindings = new ContextMap(null);
 }
// 存放运行时参数 parameterObject 以及 databaseId
bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
 } }

上面只贴了 DynamicContext 类的部分代码。其中 sqlBuilder 变量用于存放 SQL 片段的
解析结果,bindings 则用于存储一些额外的信息,比如运行时参数和 databaseId 等。bindings类型为 ContextMap,ContextMap 定义在 DynamicContext 中,是一个静态内部类。该类继承自 HashMap,并覆写了 get 方法。它的代码如下:

static class ContextMap extends HashMap<String, Object> {
private MetaObject parameterMetaObject;
public ContextMap(MetaObject parameterMetaObject) {
this.parameterMetaObject = parameterMetaObject;
 }
@Override
public Object get(Object key) {
String strKey = (String) key;
// 检查是否包含 strKey,若包含则直接返回
if (super.containsKey(strKey)) {
return super.get(strKey);
 }
if (parameterMetaObject != null) {
// 从运行时参数中查找结果
return parameterMetaObject.getValue(strKey);
 }
return null;
 } }

DynamicContext 对外提供了两个接口,用于操作 sqlBuilder。分别如下:

public void appendSql(String sql) {
sqlBuilder.append(sql);
sqlBuilder.append(" ");
}
public String getSql() {
return sqlBuilder.toString().trim();
}

以上就是对 DynamicContext 的简单介绍,DynamicContext 的源码不难理解,这里就不
多说了。继续往下分析。

2. 解析 SQL ⽚段

对于一个包含了${}占位符,或<if><where>等标签的 SQL,在解析的过程中,会被分解
成多个片段。每个片段都有对应的类型,每种类型的片段都有不同的解析逻辑。在源码中,
片段这个概念等价于 sql 节点,即 SqlNode。SqlNode 是一个接口,它有众多的实现类。其继
承体系如下:
在这里插入图片描述

上图只画出了部分的实现类,还有一小部分没画出来,不过这并不影响接下来的分析。
在众多实现类中,StaticTextSqlNode 用于存储静态文本,TextSqlNode 用于存储带有${}占位符的文本,IfSqlNode 则用于存储节点的内容。MixedSqlNode 内部维护了一个 SqlNode
集合,用于存储各种各样的 SqlNode。接下来,我将会对 MixedSqlNode、StaticTextSqlNode、TextSqlNode、IfSqlNode、WhereSqlNode 以及 TrimSqlNode 等进行分析,其他的实现类请大家自行分析。

public class MixedSqlNode implements SqlNode {
private final List<SqlNode> contents;
public MixedSqlNode(List<SqlNode> contents) {
this.contents = contents;
 }
 @Override
public boolean apply(DynamicContext context) {
// 遍历 SqlNode 集合
for (SqlNode sqlNode : contents) {
// 调用 salNode 对象本身的 apply 方法解析 sql
sqlNode.apply(context);
 }
return true;
 } }

MixedSqlNode 可以看做是 SqlNode 实现类对象的容器,凡是实现了 SqlNode 接口的类
都可以存储到 MixedSqlNode 中,包括它自己。MixedSqlNode 解析方法 apply 逻辑比较简单,即遍历 SqlNode 集合,并调用其他 SalNode 实现类对象的 apply 方法解析 sql。那下面我们来看看其他 SalNode 实现类的 apply 方法是怎样实现的。

public class StaticTextSqlNode implements SqlNode {
private final String text;
public StaticTextSqlNode(String text) {
this.text = text;
 }
@Override
public boolean apply(DynamicContext context) {
context.appendSql(text);
return true;
 } }

StaticTextSqlNode 用于存储静态文本,所以它不需要什么解析逻辑,直接将其存储的
SQL 片段添加到 DynamicContext 中即可。StaticTextSqlNode 的实现比较简单,看起来很轻
松。下面分析一下 TextSqlNode。

public class TextSqlNode implements SqlNode {
private final String text;
private final Pattern injectionFilter;
@Override
public boolean apply(DynamicContext context) {
// 创建 ${} 占位符解析器
GenericTokenParser parser = createParser(
new BindingTokenParser(context, injectionFilter));
// 解析 ${} 占位符,并将解析结果添加到 DynamicContext 中
context.appendSql(parser.parse(text));
return true;
 }
private GenericTokenParser createParser(TokenHandler handler) {
// 创建占位符解析器,GenericTokenParser 是一个通用解析器,
// 并非只能解析 ${} 占位符
return new GenericTokenParser("${", "}", handler);
 }
private static class BindingTokenParser implements TokenHandler {
private DynamicContext context;
private Pattern injectionFilter;
public BindingTokenParser(
DynamicContext context, Pattern injectionFilter) {
this.context = context;
this.injectionFilter = injectionFilter;
 }
@Override
public String handleToken(String content) {
Object parameter = context.getBindings().get("_parameter");
if (parameter == null) {
context.getBindings().put("value", null);
 }else if(SimpleTypeRegistry.isSimpleType(parameter.getClass())){
context.getBindings().put("value", parameter);
 }
// 通过 ONGL 从用户传入的参数中获取结果
Object value = OgnlCache
.getValue(content, context.getBindings());
String srtValue = (value == null ? "" : String.valueOf(value));
// 通过正则表达式检测 srtValue 有效性
checkInjection(srtValue);
return srtValue;
 }
 } }

如上,GenericTokenParser 是一个通用的标记解析器,用于解析形如KaTeX parse error: Expected 'EOF', got '#' at position 7: {xxx},#̲{xxx}等标 记 。Gene…{xxx}标记。举个例子说明一下吧,如下。我们有这样一个 SQL 语句,用于从 article 表中查询某个作者所写的文章。如下:
SELECT * FROM article WHERE author = '${author}'
假设我们我们传入的 author 值为 tianxiaobo,那么该 SQL 最终会被解析成如下的结果:
SELECT * FROM article WHERE author = 'tianxiaobo'
一般情况下,使用${author}接受参数都没什么问题。但是怕就怕在有人不怀好意,构建
了一些恶意的参数。当用这些恶意的参数替换${author}时就会出现灾难性问题——SQL 注
入。比如我们构建这样一个参数 author=tianxiaobo’;DELETE FROM article;#,然后我们把这个参数传给 TextSqlNode 进行解析。得到的结果如下
SELECT * FROM article WHERE author = 'tianxiaobo'; DELETE FROM article;#'
看到没,由于传入的参数没有经过转义,最终导致了一条 SQL 被恶意参数拼接成了两
条 SQL。更要命的是,第二天 SQL 会把 article 表的数据清空,这个后果就很严重了(从删
库到跑路)。这就是为什么我们不应该在 SQL 语句中是用${}占位符,风险太大。
分析完 TextSqlNode 的逻辑,接下来,分析 IfSqlNode 的实现。

public class IfSqlNode implements SqlNode {
private final ExpressionEvaluator evaluator;
private final String test;
private final SqlNode contents;
public IfSqlNode(SqlNode contents, String test) {
this.test = test;
this.contents = contents;
this.evaluator = new ExpressionEvaluator();
 }
@Override
public boolean apply(DynamicContext context) {
// 通过 ONGL 评估 test 表达式的结果
if (evaluator.evaluateBoolean(test, context.getBindings())) {
// 若 test 表达式中的条件成立,则调用其他节点的 apply 方法进行解析
contents.apply(context);
return true;
 }
return false;
 } }

IfSqlNode 对应的是<iftest=‘xxx’>节点,节点是日常开发中使用频次比较高的一个节
点。它的具体用法我想大家都很熟悉了,这里就不多啰嗦。IfSqlNode 的 apply 方法逻辑并不复杂,首先是通过 ONGL 检测 test 表达式是否为 true,如果为 true,则调用其他节点的 apply方法继续进行解析。需要注意的是节点中也可嵌套其他的动态节点,并非只有纯文本。
因此 contents 变量遍历指向的是 MixedSqlNode,而非 StaticTextSqlNode。
关于 IfSqlNode 就说到这,接下来分析 WhereSqlNode 的实现。

public class WhereSqlNode extends TrimSqlNode {
/** 前缀列表 */
private static List<String> prefixList = Arrays.asList(
"AND ", "OR ", "AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t");
public WhereSqlNode(Configuration configuration, SqlNode contents) {
// 调用父类的构造方法
super(configuration, contents, "WHERE", prefixList, null, null);
 } }

在 MyBatis 中,WhereSqlNode 和 SetSqlNode 都是基于 TrimSqlNode 实现的,所以上面
的代码看起来很简单。WhereSqlNode 对应于节点,关于该节点的用法以及它的应用
场景,大家请自行查阅资料。我在分析源码的过程中,默认大家已经知道了该节点的用途和
应用场景。
接下来,我们把目光聚焦在 TrimSqlNode 的实现上。

public class TrimSqlNode implements SqlNode {
private final SqlNode contents;
private final String prefix;
private final String suffix;
private final List<String> prefixesToOverride;
private final List<String> suffixesToOverride;
private final Configuration configuration;
@Override
public boolean apply(DynamicContext context) {
// 创建具有过滤功能的 DynamicContext
FilteredDynamicContext filteredDynamicContext =
new FilteredDynamicContext(context);
// 解析节点内容
boolean result = contents.apply(filteredDynamicContext);
// 过滤掉前缀和后缀
filteredDynamicContext.applyAll();
return result;
 } }

如上,apply 方法首选调用了其他 SqlNode 的 apply 方法解析节点内容,这步操作完成
后,FilteredDynamicContext 中会得到一条 SQL 片段字符串。接下里需要做的事情是过滤字
符串前缀后和后缀,并添加相应的前缀和后缀。这个事情由 FilteredDynamicContext 负责,
FilteredDynamicContext 是 TrimSqlNode 的私有内部类。我们去看一下它的代码。

private class FilteredDynamicContext extends DynamicContext {
private DynamicContext delegate;
/** 构造方法会将下面两个布尔值置为 false */
private boolean prefixApplied;
private boolean suffixApplied;
private StringBuilder sqlBuffer;
public void applyAll() {
sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
String trimmedUppercaseSql =
sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
if (trimmedUppercaseSql.length() > 0) {
// 引用前缀和后缀,也就是对 sql 进行过滤操作,移除掉前缀或后缀
applyPrefix(sqlBuffer, trimmedUppercaseSql);
applySuffix(sqlBuffer, trimmedUppercaseSql);
 }
// 将当前对象的 sqlBuffer 内容添加到代理类中
delegate.appendSql(sqlBuffer.toString());
}
private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql){
if (!prefixApplied) {
// 设置 prefixApplied 为 true,以下逻辑仅会被执行一次
prefixApplied = true;
if (prefixesToOverride != null) {
for (String toRemove : prefixesToOverride) {
// 检测当前 sql 字符串是否包含前缀,比如 'AND ', 'AND\t'等
if (trimmedUppercaseSql.startsWith(toRemove)) {
// 移除前缀
sql.delete(0, toRemove.trim().length());
break;
 }
 }
 }
// 插入前缀,比如 WHERE
if (prefix != null) {
sql.insert(0, " ");
sql.insert(0, prefix);
 }
 }
 }
// 该方法逻辑与 applyPrefix 大同小异,大家自行分析
private void applySuffix(
StringBuilder sql, String trimmedUppercaseSql){
} }

在上面的代码中,我们重点关注 applyAll 和 applyPrefix 方法,其他的方法大家自行分
析。applyAll 方法的逻辑比较简单,首先从 sqlBuffer 中获取 SQL 字符串。然后调用 applyPrefix和 applySuffix 进行过滤操作。最后将过滤后的 SQL 字符串添加到被装饰的类中。applyPrefix方法会首先检测 SQL 字符串是不是以"AND",“OR”,或"AND\n","OR\n"等前缀开头,若是则将前缀从 sqlBuffer 中移除。然后将前缀插入到 sqlBuffer 的首部,整个逻辑就结束了。下面写点代码简单验证一下,如下:

public class SqlNodeTest {
@Test
public void testWhereSqlNode() throws IOException {
String sqlFragment = "AND id = #{id}";
MixedSqlNode msn = new MixedSqlNode(
Arrays.asList(new StaticTextSqlNode(sqlFragment)));
WhereSqlNode wsn = new WhereSqlNode(new Configuration(), msn);
DynamicContext dc = new DynamicContext(
new Configuration(), new ParamMap<>());
wsn.apply(dc);
System.out.println("解析前:" + sqlFragment);
System.out.println("解析后:" + dc.getSql());
 } }

测试结果如下
在这里插入图片描述

今天就先更到这,后续会继续更新!三连支持一下吧!

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值