Mybatis源码学习(三):映射文件中sql的解析

概述

根据上一篇文章,我们了解了 Mybatis 如何在加载配置文件后,根据指定配置方式寻找接口并且完成映射文件信息与接口的绑定的。在本篇文章,我们将延续上文,结合源码阐述映射文件中方法声明里的 sql 被解析为 java 对象的过程。

这是关于 Mybatis 的第三篇文章,前文:

Mybatis源码学习(一):配置文件的加载

Mybatis源码学习(二):Mapper接口的绑定

一、sql解析

image-20210711012449848

1.XMLMapperBuilder

Sql 解析与 MappedStatement的生成都在 XMLMapperBuilder 进行。根据上文可知,在 XMLMapperBuilderparsePendingStatements()方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void parsePendingStatements() {
// 获取所有映射文件中的方法声明
Collection<XMLStatementBuilder> incompleteStatements = configuration.getIncompleteStatements();
synchronized (incompleteStatements) {
Iterator<XMLStatementBuilder> iter = incompleteStatements.iterator();
while (iter.hasNext()) {
try {
// 遍历并转换为Statement对象
iter.next().parseStatementNode();
iter.remove();
} catch (IncompleteElementException e) {
// Statement is still missing a resource...
}
}
}
}

其中,Configuration类中的IncompleteStatements是在XMLMapperBuilder.parse()时添加进去的,我们可以理解他是一个刚从配置文件中根据<select><delete><update><insert>标签名拿到的 XML 节点,还没有做任何解析。

等到完成了接口与映射文件信息的绑定以后,再遍映射文件中的方法声明依次解析。

2.parseStatementNode方法

这个方法比较长,但是主要作用就是解析一个方法声明中的属性与子标签:

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
66
67
68
69
70
71
72
73
74
75
76
77
78
public void parseStatementNode() {
// 获取id属性
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");

if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}

// 获取标签节点名称
String nodeName = context.getNode().getNodeName();
// 判断sql类型
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
// 如果是select就判断是否需要获取/清空缓存
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

// 将include节点转为相应的sql节点
// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());

// 获取入参类型
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);

// 获取lang
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);

// 获取selectKey节点,解析完成后删除
// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);

// 解析sql
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
KeyGenerator keyGenerator; // 生成主键
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
// 如果设置了selectKey,则在插入获取主键生成器生成的主键
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}

// 创建对应的SqlSource对象
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
// 获取Statement类型,默认为prepared
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
// 获取一些连接参数
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");

// 入参和返回值类型
String parameterMap = context.getStringAttribute("parameterMap");
String resultType = context.getStringAttribute("resultType");
Class<?> resultTypeClass = resolveClass(resultType);
String resultMap = context.getStringAttribute("resultMap");
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");

// 构建MappedStatement并添加到配置类
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

3.SqlSource

仍然还是在上文的 parseStatementNode()方法中,暂且忽略缓存与各种返回值类型的处理,我们关注一下addMappedStatement()中传入的变量 sqlSource ,它本身是一个接口,跟DataSource的功能一样,通过SqlSource接口的实现类,可以获取 sql 对象:

1
2
3
public interface SqlSource {
BoundSql getBoundSql(Object parameterObject);
}

他具有的唯一一个抽象方法即为getBoundSql(),这个方法获取的 BoundSql类就是实际上的我们认为的方法声明中的那个 sql 对象:

1
2
3
4
5
6
7
public class BoundSql {
private final String sql;
private final List<ParameterMapping> parameterMappings;
private final Object parameterObject;
private final Map<String, Object> additionalParameters;
private final MetaObject metaParameters;
}

里面包含有带占位符和动态标签的原始 sql 语句,以及方法声明上相关的入参。

4.LanguageDriver

LanguageDriver 本身也是一个接口,他的实现类用于解析参数以及注解和映射文件中的 sql,并最终根据此生成 sql 数据源,我们可以简单的理解为针对指定格式 sql 语句的解析器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface LanguageDriver {

/**
* 创建一个参数处理器,将处理完参数后得到实际参数传递给JDBC语句。
*/
ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);

/**
* 解析XML并创建SqlSource
*/
SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);

/**
* 解析注解并创建SqlSource
*/
SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);

}

SqlSource 通过 LanguageDriver.createSqlSource()创建:

1
2
3
4
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
... ...
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

而这里的getLanguageDriver()方法,最终会回到Configuration.getLanguageDriver()中:

1
2
3
4
5
if (langClass == null) {
return languageRegistry.getDefaultDriver();
}
languageRegistry.register(langClass);
return languageRegistry.getDriver(langClass);

而映射文件里方法声明中 lang 属性我们一般不会刻意设置,按上述逻辑,获取的是默认的语言驱动,这里的语言驱动来自于 Configuration 初始化时候注册的 XMLLanguageDriverRawLanguageDriver

1
2
3
4
5
public Configuration() {
... ...
languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
languageRegistry.register(RawLanguageDriver.class);
}

5.XMLLanguageDriver

XMLLanguageDriverLanguageDriver接口的实现,它用于解析 xml 格式的 sql 语句——或者更准确点说,是 mybatis 特定的方法声明语法。

RawLanguageDriver继承了XMLLanguageDriver,而XMLLanguageDriver又实现了LanguageDriver接口:

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 class XMLLanguageDriver implements LanguageDriver {

// 创建参数处理器
@Override
public ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
return new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);
}

// 解析映射文件的sql
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}

// 解析有复杂参数的sql
@Override
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
// issue #3
// 解析使用@Select这类sql标签上的sql
if (script.startsWith("<script>")) {
XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
// 先转为xml再按照映射文件的方式解析
return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
} else {

// issue #127
// 替换占位符中的变量,转为sql语法节点
script = PropertyParser.parse(script, configuration.getVariables());
TextSqlNode textSqlNode = new TextSqlNode(script);
// 如果含有动态标签
if (textSqlNode.isDynamic()) {
return new DynamicSqlSource(configuration, textSqlNode);
} else {
// 不含动态标签
return new RawSqlSource(configuration, script, parameterType);
}
}
}

}

基于XMLLanguageDriverRawLanguageDriver进一步完善了方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class RawLanguageDriver extends XMLLanguageDriver {

@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
SqlSource source = super.createSqlSource(configuration, script, parameterType);
checkIsNotDynamic(source);
return source;
}

@Override
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
SqlSource source = super.createSqlSource(configuration, script, parameterType);
checkIsNotDynamic(source);
return source;
}

private void checkIsNotDynamic(SqlSource source) {
if (!RawSqlSource.class.equals(source.getClass())) {
throw new BuilderException("Dynamic content is not allowed when using RAW language");
}
}

}

主要是再做检查,防止非静态的 sql 使用 RawSqlSource

对于 Myabtis 来说,sql 中带有 ${}<where>这类动态标签的都认为是动态 sql,反之则是静态 sql

7.XMLScriptBuilder

XMLScriptBuilder这个类用于对 Mybatis 的映射文件中的方法声明里的动态标签做解析。它的内部有一个 NodeHandle接口实现类集合,用于处理专门的动态标签:

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
private void initNodeHandlerMap() {
nodeHandlerMap.put("trim", new TrimHandler());
nodeHandlerMap.put("where", new WhereHandler());
nodeHandlerMap.put("set", new SetHandler());
nodeHandlerMap.put("foreach", new ForEachHandler());
nodeHandlerMap.put("if", new IfHandler());
nodeHandlerMap.put("choose", new ChooseHandler());
nodeHandlerMap.put("when", new IfHandler());
nodeHandlerMap.put("otherwise", new OtherwiseHandler());
nodeHandlerMap.put("bind", new BindHandler());
}

public SqlSource parseScriptNode() {
// 解析动态标签
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
if (isDynamic) {
// 是否带有动态标签的非静态sql
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
// 静态sql
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}

protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<>();
// 解析方法声明中的节点
NodeList children = node.getNode().getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
// 获取sql语句
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
// 是带有${}的动态sql
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
// 是静态sql
contents.add(new StaticTextSqlNode(data));
}

} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
// sql中带有动态标签
String nodeName = child.getNode().getNodeName();
// 获取对应的拦截器
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
handler.handleNode(child, contents);
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}

8.SqlNode

parseDynamicTags()最终返回的 MixedSqlNode 是一个 SqlNode 实现类,SqlNode 是一个表示节点的特殊接口:

1
2
3
public interface SqlNode {
boolean apply(DynamicContext context);
}

他的方法及其简单,每个 SqlNode 实现类都会实现apply()方法,当调用以后,sql 节点会被解析为正常的 sql 语句放入 DynamicContext上下文中。

所有方法声明中的 sql 节点都实现类这个接口:

image-20210622225424211

简而言之,他们的实现类很多,但是实际上就分四类:

  1. StaticTextSqlNode:顾名思义,普通的静态 sql 语句部分,只带有#{}表达式而不含有动态标签和${}赋值表达式;
  2. TextSqlNode:带有${}赋值表达式的 sql 语句;
  3. 动态标签节点:除了TextSqlNodeMixedSqlNodeStaticTextSqlNode的所有其他实现类,即处理 Mybatis 动态标签的特殊节点;
  4. MixedSqlNode:如parseDynamicTags()所示,这是一个混合了所有类型标签的节点,也是实际上的根节点;

我们以 StaticTextSqlNode为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class StaticTextSqlNode implements SqlNode {
private final String text;

public StaticTextSqlNode(String text) {
this.text = text;
}

@Override
public boolean apply(DynamicContext context) {
// 拼接sql
context.appendSql(text);
return true;
}

}

普通静态 sql 节点 apply()以后就把当前的节点内的 sql 拼接到上下文的 sql 中,同理,其他的实现类也差不多。由于节点之间还有嵌套关系,因此有些节点还会自己维护一个独立的上下文,内部处理的时候先把 sql 拼到独立上下文里面,等自己的节点处理完再把独立上下文中的 sql 拼接到父目录的上下文中。

image-20210622235212852

根据各自的逻辑处理后,最终都会把节点代表的 sql 拼接到上下文里,最终所有节点逻辑处理完,就会在上下文中拼接处一条完整的 sql。

而起到这个作用的,就是根节点 MixedSqlNode

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MixedSqlNode implements SqlNode {
private final List<SqlNode> contents;

public MixedSqlNode(List<SqlNode> contents) {
this.contents = contents;
}

@Override
public boolean apply(DynamicContext context) {
contents.forEach(node -> node.apply(context));
return true;
}
}

MixedSqlNode内部有所有的 SqlNode,它的 apply()方法就是遍历节点然后把每个节点的 apply()方法都执行一遍。

这里另外提到一点,对于动态标签里的语法,比如 <if test="...">里的 test,这里的表达式实际上是 Ognl 表达式,这在 jsp 中也有使用,我们可以简单理解为一个脚本语言,通过表达式,我们可以实现一些简单的功能,比如取值或者比较等等。

8.RawSqlSource与DynamicSqlSource

XMLLanguageDriver.createSqlSource()最终会根据textSqlNode.isDynamic()的结果——也就是方法声明的 sql 中是否含有动态标签——区分要创建哪一种 SqlSource,带${}和动态标签的用DynamicSqlSource,不带的用 RawSqlSource

我们以比较简单的 RawSqlSource 为例:

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
public class RawSqlSource implements SqlSource {

private final SqlSource sqlSource;

public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
this(configuration, getSql(configuration, rootSqlNode), parameterType);
}

public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> clazz = parameterType == null ? Object.class : parameterType;
// 将#{}表达式替换为?占位符
sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
}

// 从上下文中获取根标签,也就是之前提到的MixedSqlNode
private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
DynamicContext context = new DynamicContext(configuration, null);
// 解析处理所有的SqlNode,并拼接到context里
rootSqlNode.apply(context);
// 获取拼接好的sql
return context.getSql();
}

@Override
public BoundSql getBoundSql(Object parameterObject) {
// 获取最终的BoundSql
return sqlSource.getBoundSql(parameterObject);
}

}

可以看到,getSql()本质上就是拿到处理好的 sql 节点,然后调用他们的 apply()方法,最终拼接到上下文中,然后再返回这个拼好的 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
public class DynamicSqlSource implements SqlSource {

private final Configuration configuration;
private final SqlNode rootSqlNode;

public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
this.configuration = configuration;
this.rootSqlNode = rootSqlNode;
}

@Override
public BoundSql getBoundSql(Object parameterObject) {
// 获取上下文并解析根标签
DynamicContext context = new DynamicContext(configuration, parameterObject);
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
// 将#{}表达式替换为?占位符,获取最终的BoundSql
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
// 添加参数到boundSql中
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}

}

DynamicSqlSourceRawSqlSource没有什么区别,只不过由于存在动态标签与${},相比简单的静态 sql,需要先将这两者解析为 sql。

9.SqlSourceBuilder

SqlSourceBuilder.getBoundSql()的主要作用就是将#{}占位符替换为 jdbc 中的?占位符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
// 参数处理器
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
// #{}赋值表达式处理器
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql;
// 如果有额外的空格就先删除
if (configuration.isShrinkWhitespacesInSql()) {
// 将#{}表达式替换为具体的参数值
sql = parser.parse(removeExtraWhitespaces(originalSql));
} else {
sql = parser.parse(originalSql);
}
// 转为静态StaticSqlSource
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}

所有的 sql 最后都会完成解析,变为最终的自带?占位符的静态 sql 字符串,sql 与对应的入参、返回值类型等信息最终被封装为一个对象,这个对象就是 BoundSql

1
2
3
4
5
6
7
8
9
10
11
public class BoundSql {
// sql字符串
private final String sql;
// 与占位符对应的入参信息
private final List<ParameterMapping> parameterMappings;
// 参数
private final Object parameterObject;
// 附加参数
private final Map<String, Object> additionalParameters;
private final MetaObject metaParameters;
}

里面包含有带占位符和动态标签的原始 sql 语句,以及方法声明上相关的入参。

然后我们再回头看看DynamicSqlSource,我们知道由于存在动态标签,因此DynamicSqlSource会在创建了StaticSqlSource之后,通过StaticSqlSource拿到仍然带有动态标签数据的 BoundSql,接着在根据入参处理动态标签,最终再生成一个 BoundSql

二、总结

我们总结一下 Mapper 中方法声明被变成 sql 加载进 Mybatis 的过程:

  1. 方法声明的解析发生在一个 Mapper 文件被加载——也就是 XMLMapperBuilder构建——的时候,此时各个 statement 都还只是未被解析的 XML 节点;

  2. XMLMapperBuilder中,先通过parsePendingStatements()遍历节点,然后依次调用节点的 parseStatementNode()方法,在这一步主要一下几件事:

    • 获取 id、超时时间与缓存等相关的配置信息;
    • 获取方法入参与返回值类型;
    • 解析 <include>节点,并替换为相应的内容;
    • 解析 <selectKey>节点,根据配置设置相应的主键生成策略,完成后然后删除节点;
    • 解析sql,根据配置的LanguageDriver将 sql 解析为对应的 SqlSource
    • 获取 StatementTyoe,默认都为prepared

    然后根据以上的信息构建一个 MappedStatement对象,这个 MappedStatement即是后续方法实现的基础。

然后回头再看看步骤二,步骤二干了很多事,但是最重要的在于解析 statement 语法生成 SqlSource

  1. 获取 LanguageDriver 接口实现类,该实现类为来在配置文件加载时 Mybatis 默认提供的 XMLLanguageDriver,主要用来解析 Myabtis 特有的带有动态标签与参数标签的语法;

  2. LanguageDriver中创建XMLScriptBuilder用于解析 statement 中的语法,这里如果是类似 @Select这样注解形式的sql,就先解析为 xml 再用XMLScriptBuilder

  3. XMLScriptBuilder中将 statement 语法也解析为一串的 XML 节点,并根据节点对应的语法做了区分:

    • 如果是带有 ${}表达式的字符串节点被认为是动态 sql 节点,转为TextSqlNode
    • 如果是只带有#{}表达式的字符串节点或者干脆就是纯字符串的字符串节点,转为StaticTextSqlNode
    • 如果是动态标签,就通过节点解析器NodeHandler处理,最后转为类似 WhereSqlNode这样的动态标签节点;
  4. 还是在XMLScriptBuilder中,解析出来的各种 Node 都是 SqlNode 接口的实现类,他们都有 apply()方法,当调用的时候,会把自身对应的数据解析为 sql 并拼接到一个上下文对象的 sql 字符串中。

    最终这些节点都被添加到一个集合中,最终放入一个根节点 MixedSqlNode里,当调用MixedSqlNodeapply()方法时,就会调用所有节点的apply()方法民,最终就会得到一个完整的 sql。

  5. 回到LanguageDriver中,在它的createSqlSource()方法中,根据是否带有${}与动态标签区分为DynamicSqlSourceRawSqlSource两种 DataSource

    • RawSqlSource:调用MixedSqlNode.apply()拿到完整的 sql,然后把 #{}表达式替换为 ?占位符,接着根据 sql 创建一个StaticSqlSource,然后通过StaticSqlSource获取boundSql
    • DynamicSqlSource:跟RawSqlSource一样,先获得完整的 sql,并生成一个StaticSqlSource对象,然后通过StaticSqlSource拿到boundSql,接着再根据传入的参数处理 BoundSql中的动态标签,最终再生成一个 BoundSql
0%