在执行 SQL 之前,需要将 SQL 语句完整的解析出来。我们都知道 SQL 是配置在映射文
件中的,但由于映射文件中的 SQL 可能会包含占位符#{},以及动态 SQL 标签,比如、
等。

因此,我们并不能直接使用映射文件中配置的 SQL。MyBatis 会将映射文件中的 SQL解析成一组 SQL 片段。如果某个片段中也包含动态SQL相关的标签,那么,MyBatis会对该片段再次进行分片。最终,一个 SQL 配置将会被解析成一个 SQL 片段树。

image.png
需要对片段树进行解析,以便从每个片段对象中获取相应的内容。然后将这些内容
组合起来即可得到一个完成的 SQL 语句,这个完整的 SQL 以及其他的一些信息最终会存储
在 BoundSql 对象中。

成员变量信息

1
2
3
4
5
private final String sql;
private final List<ParameterMapping> parameterMappings;
private final Object parameterObject;
private final Map<String, Object> additionalParameters;
private final MetaObject metaParameters;

含义

变量名 类型 用途
sql String 一个完整的 SQL 语句,可能会包含问号 ? 占位符
parameterMappings List 参数映射列表,SQL 中的每个 #{xxx} 占位符都会被解析成相应的 ParameterMapping 对象
parameterObject Object 运行时参数,即用户传入的参数,比如 Article对象,或是其他的参数
additionalParameters Map 附加参数集合,用于存储一些额外的信息,比如 datebaseId 等
metaParameters MetaObject additionalParameters 的元信息对象

第一站是 MappedStatement 的 getBoundSql 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// -☆- 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 是一个接口,它有如下几个实现类:

  • DynamicSqlSource
  • RawSqlSource
  • StaticSqlSource
  • ProviderSqlSource
  • VelocitySqlSource

首先我们把最后两个排除掉,不常用。剩下的三个实现类中,仅前两个实现类会在映射文件解析的过程中被使用。

  • 当SQL 配置中包含${}(不是#{})占位符,或者包含、等标签时,会被认为是
    动态 SQL,此时使用 DynamicSqlSource 存储 SQL 片段。
  • 否则,使用 RawSqlSource 存储 SQL配置信息。

相比之下 DynamicSqlSource存储的SQL片段类型较多,解析起来也更为复杂一些。因此下面我将分析 DynamicSqlSource 的 getBoundSql 方法。弄懂这个,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
// -☆- 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 中

DynamicContext

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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());
}
}
  • sqlBuilder 变量用于存放 SQL 片段的解析结果
  • bindings 则用于存储一些额外的信息,比如运行时参数和 databaseId 等。
    • bindings类型为 ContextMap,ContextMap 定义在 DynamicContext 中
    • 是一个静态内部类。该类继承自 HashMap,并覆写了 get 方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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。

1
2
3
4
5
6
7
8
public void appendSql(String sql) {
sqlBuilder.append(sql);
sqlBuilder.append(" ");
}

public String getSql() {
return sqlBuilder.toString().trim();
}

解析 SQL ⽚段

对于一个包含了${}占位符,或、等标签的 SQL,在解析的过程中,会被分解
成多个片段。每个片段都有对应的类型,每种类型的片段都有不同的解析逻辑。

在源码中,片段这个概念等价于sql节点,即SqlNode。SqlNode是一个接口,它有众多的实现类。其继承体系如下:
image.png

  • StaticTextSqlNode 用于存储静态文本
  • TextSqlNode 用于存储带有${}占位符的文本
  • IfSqlNode 则用于存储节点的内容
  • MixedSqlNode 内部维护了一个 SqlNode集合,用于存储各种各样的 SqlNode

MixedSqlNode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 集合,并调用其他 SqlNode 实现类对象的 apply 方法解析 sql。

StaticTextSqlNode

1
2
3
4
5
6
7
8
9
10
11
12
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 中即可。

TextSqlNode

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 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 是一个通用的标记解析器,用于解析形如${xxx},#{xxx}等标记 。GenericTokenParser负责将标记中的内容抽取出来,并将标记内容交给相应的TokenHandler 去处理。
  • BindingTokenParser 负责解析标记内容,并将解析结果返回给GenericTokenParser,用于替换${xxx}标记。

实例
1
2
3
SELECT * FROM article WHERE author = '${author}'

SELECT * FROM article WHERE author = 'tianxiaobo' // 解析后

一般情况下,使用{author}时就会出现灾难性问题——SQL 注入。比如我们构建这样一个参数 author=tianxiaobo’;DELETE FROM article;#,然后我们把这个参数传给 TextSqlNode 进行解析。得到的结果如下

1
SELECT * FROM article WHERE author = 'tianxiaobo'; DELETE FROM article;#'

由于传入的参数没有经过转义,最终导致了一条SQL被恶意参数拼接成了两条SQL。更要命的是,第二天SQL会把article表的数据清空,这个后果就很严重了(从删库到跑路)。这就是为什么我们不应该在 SQL 语句中是用${}占位符,风险太大。

IfSqlNode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 的 apply 方法逻辑并不复杂,首先是通过 ONGL 检测 test 表达式是否为 true,如果为 true,则调用其他节点的 apply方法继续进行解析。

需要注意的是节点中也可嵌套其他的动态节点,并非只有纯文本。因此 contents 变量遍历指向的是 MixedSqlNode,而非 StaticTextSqlNode。

WhereSqlNode

1
2
3
4
5
6
7
8
9
10
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);
}
}

WhereSqlNode 和 SetSqlNode 都是基于 TrimSqlNode 实现的

TrimSqlNode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 的私有内部类。

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
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) {
if (!suffixApplied) {
suffixApplied = true;
if (suffixesToOverride != null) {
for (String toRemove : suffixesToOverride) {
if (trimmedUppercaseSql.endsWith(toRemove) || trimmedUppercaseSql.endsWith(toRemove.trim())) {
int start = sql.length() - toRemove.trim().length();
int end = sql.length();
sql.delete(start, end);
break;
}
}
}
if (suffix != null) {
sql.append(" ");
sql.append(suffix);
}
}
}
}

applyAll 方法的逻辑比较简单,首先从 sqlBuffer 中获取 SQL 字符串。然后调用 applyPrefix和 applySuffix进行过滤操作。最后将过滤后的SQL字符串添加到被装饰的类中

applyPrefix方法会首先检测SQL字符串是不是以”AND”,”OR”,或”AND\n”,”OR\n”等前缀开头,若是则将前缀从 sqlBuffer中移除。然后将前缀插入到sqlBuffer的首部,整个逻辑就结束了。

解析#{}占位符

经过前面的解析,我们已经能从 DynamicContext 获取到完整的 SQL 语句了。但这并不
意味着解析过程就结束了,因为当前的SQL语句中还有一种占位符没有处理,即#{}。与${}占位符的处理方式不同,MyBatis 并不会直接将#{}占位符替换为相应的参数值。

#{}占位符的解析逻辑是包含在 SqlSourceBuilder 的 parse 方法中,该方法最终会将解析
后的 SQL以及其他的一些数据封装到StaticSqlSource 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// -☆- SqlSourceBuilder
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 = parser.parse(originalSql);
// 封装解析结果到 StaticSqlSource 中,并返回
return new StaticSqlSource(
configuration, sql, handler.getParameterMappings());
}

重点关注#{}占位符处理器 ParameterMappingTokenHandler 的逻辑。

1
2
3
4
5
6
public String handleToken(String content) {
// 获取 content 的对应的 ParameterMapping
parameterMappings.add(buildParameterMapping(content));
// 返回 ?
return "?";
}

ParameterMappingTokenHandler 的 handleToken 方法看起来比较简单,但实际上并非如
此。

GenericTokenParser 负责将#{}占位符中的内容抽取出来,并将抽取出的内容传给
handleToken 方法。handleToken 方法负责将传入的参数解析成对应的 ParameterMapping 对象,这步操作由 buildParameterMapping 方法完成。

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
private ParameterMapping buildParameterMapping(String content) {
/*
* 将 #{xxx} 占位符中的内容解析成 Map。大家可能很好奇一个普通的字符串是怎么
* 解析成 Map 的,举例说明一下。如下:
*
* #{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}
*
* 上面占位符中的内容最终会被解析成如下的结果:
*
* {
* "property": "age",
* "typeHandler": "MyTypeHandler",
* "jdbcType": "NUMERIC",
* "javaType": "int"
* }
*
* parseParameterMapping 内部依赖 ParameterExpression 对字符串进行解析,
* ParameterExpression 的逻辑不是很复杂,这里就不分析了。
*/
Map<String, String> propertiesMap = parseParameterMapping(content);
String property = propertiesMap.get("property");
Class<?> propertyType;

// metaParameters 为 DynamicContext 成员变量 bindings 的元信息对象
if (metaParameters.hasGetter(property)) {
propertyType = metaParameters.getGetterType(property);
/*
* parameterType 是运行时参数的类型。如果用户传入的是单个参数,比如 Article
* 对象,此时 parameterType 为 Article.class。如果用户传入的多个参数,比如
* [id = 1, author = "coolblog"],MyBatis 会使用 ParamMap 封装这些参数,
* 此时 parameterType 为 ParamMap.class。如果 parameterType 有相应的
* TypeHandler,这里则把 parameterType 设为 propertyType
*/
} else if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
propertyType = parameterType;
} else if(JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))){
propertyType = java.sql.ResultSet.class;
} else if (property == null||Map.class.isAssignableFrom(parameterType)){
// 如果 property 为空,或 parameterType 是 Map 类型,
// 则将 propertyType 设为 Object.class
propertyType = Object.class;
} else {
// 代码逻辑走到此分支中,表明 parameterType 是一个自定义的类,
// 比如 Article,此时为该类创建一个元信息对象
MetaClass metaClass = MetaClass.forClass(
parameterType, configuration.getReflectorFactory());
// 检测参数对象有没有与 property 想对应的 getter 方法
if (metaClass.hasGetter(property)) {
// 获取成员变量的类型
propertyType = metaClass.getGetterType(property);
} else {
propertyType = Object.class;
}
}

// -------------------------- 分割线 ---------------------------
ParameterMapping.Builder builder = new ParameterMapping.Builder(
configuration, property, propertyType);
// 将 propertyType 赋值给 javaType
Class<?> javaType = propertyType;
String typeHandlerAlias = null;

// 遍历 propertiesMap
for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
String name = entry.getKey();
String value = entry.getValue();
if ("javaType".equals(name)) {
// 如果用户明确配置了 javaType,则以用户的配置为准
javaType = resolveClass(value);
builder.javaType(javaType);
} else if ("jdbcType".equals(name)) {
// 解析 jdbcType
builder.jdbcType(resolveJdbcType(value));
} else if ("mode".equals(name)) {...}
else if ("numericScale".equals(name)) {...}
else if ("resultMap".equals(name)) {...}
else if ("typeHandler".equals(name)) {
typeHandlerAlias = value;
}
else if ("jdbcTypeName".equals(name)) {...}
else if ("property".equals(name)) {...}
else if ("expression".equals(name)) {
throw new BuilderException("……");
} else {
throw new BuilderException("……");
}
}
if (typeHandlerAlias != null) {
// 解析 TypeHandler
builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
}
// 构建 ParameterMapping 对象
return builder.build();
}

buildParameterMapping 代码很多,逻辑看起来很复杂。但是它做的事情却不是很
多,只有 3 件事情。如下:

  1. 解析 content
  2. 解析 propertyType,对应分割线之上的代码
  3. 构建 ParameterMapping 对象,对应分割线之下的代码

实例

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 SqlSourceBuilderTest {
@Test
public void test() {
// 带有复杂 #{} 占位符的参数,接下里会解析这个占位符
String sql = "SELECT * FROM Author WHERE age ="
+ "#{age,javaType=int,jdbcType=NUMERIC}";
SqlSourceBuilder sqlSourceBuilder =
new SqlSourceBuilder(new Configuration());
SqlSource sqlSource = sqlSourceBuilder.parse(
sql, Author.class, new HashMap<>());
BoundSql boundSql = sqlSource.getBoundSql(new Author());
System.out.println(String.format("SQL: %s\n", boundSql.getSql()));
System.out.println(String.format(
"ParameterMappings: %s", boundSql.getParameterMappings()));
}
}

public class Author {
private Integer id;
private String name;
private Integer age;
// 省略 getter/setter
}

SQL 中的#{age,…}占位符被替换成了问号?。#{age,…}也被解析成了一个 ParameterMapping 对象。

StaticSqlSource

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class StaticSqlSource implements SqlSource {
private final String sql;
private final List<ParameterMapping> parameterMappings;
private final Configuration configuration;
public StaticSqlSource(Configuration configuration, String sql) {
this(configuration, sql, null);
}
public StaticSqlSource(Configuration configuration, String sql,
List<ParameterMapping> parameterMappings) {
this.sql = sql;
this.parameterMappings = parameterMappings;
this.configuration = configuration;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
// 创建 BoundSql 对象
return new BoundSql(
configuration, sql, parameterMappings, parameterObject);
}
}