MyBatis 学习笔记(八)---源码分析篇--SQL 执行过程详细分析
前言
在面试中我们经常会被到MyBatis中 #{} 占位符与${}
占位符的区别。大多数的小伙伴都可以脱口而出#{} 会对值进行转义,防止SQL注入。而${}则会原样输出传入值,不会对传入值做任何处理。本文将通过源码层面分析为啥#{} 可以防止SQL注入。
源码解析
首先我们来看看MyBatis 中SQL的解析过程,MyBatis 会将映射文件中的SQL拆分成一个个SQL分片段,然后在将这些分片段拼接起来。
例如:在映射文件中有如下SQL
SELECT * FROM student <where> <if test="id!=null"> id=${id} </if> <if test="name!=null"> AND name =${name} </if> </where>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
MyBatis 会将该SQL 拆分成如下几部分进行解析
第一部分 SELECT * FROM Author
由StaticTextSqlNode存储
第二部分 <where>
由WhereSqlNode 存储
第三部分 <if></if>
由IfSqlNode存储
第四部分 ${id} ${name}
占位符里的文本由TextSqlNode存储。
获取BoundSql
BoundSql 是用来存储一个完整的SQL 语句,存储参数映射列表以及运行时参数
public class BoundSql { /** * 一个完整的SQL语句,可能会包含问号?占位符 */
private String sql;
/** * 参数映射列表,SQL中的每个#{xxx} * 占位符都会被解析成相应的ParameterMapping对象 */
private List<ParameterMapping> parameterMappings;
/** * 运行时参数,即用户传入的参数,比如Article对象, * 或是其他的参数 */
private Object parameterObject; /** * 附加参数集合,用户存储一些额外的信息,比如databaseId等 */
private Map<String, Object> additionalParameters;
/** * additionalParameters的元信息对象 */
private MetaObject metaParameters; .... 省略部分代码
}
- 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
分析SQL的解析,首先从获取BoundSql说起。其代码源头在MappedStatement。
public BoundSql getBoundSql(Object parameterObject) {
//其实就是调用sqlSource.getBoundSql BoundSql boundSql = sqlSource.getBoundSql(parameterObject); //剩下的可以暂时忽略,故省略代码 return boundSql;
}
- 1
- 2
- 3
- 4
- 5
- 6
如上,可以看出其内部就是调用的sqlSource.getBoundSql。 而我们sqlSource 接口又有如下几个实现类。
DynamicSqlSource
RawSqlSource
StaticSqlSource
ProviderSqlSource
VelocitySqlSource
其中DynamicSqlSource 是对动态SQL进行解析,当SQL配置中包含${}
或者<if>
,<set>
等标签时,会被认定为是动态SQL,此时使用 DynamicSqlSource 存储 SQL 片段,而RawSqlSource 是对原始的SQL 进行解析,而StaticSqlSource 是对静态SQL进行解析。这里我们重点介绍下DynamicSqlSource。话不多说,直接看源码。
public BoundSql getBoundSql(Object parameterObject) { //生成一个动态上下文 DynamicContext context = new DynamicContext(configuration, parameterObject);
//这里SqlNode.apply只是将${}这种参数替换掉,并没有替换#{}这种参数 rootSqlNode.apply(context);
//调用SqlSourceBuilder SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
//SqlSourceBuilder.parse,注意这里返回的是StaticSqlSource,解析完了就把那些参数都替换成?了,也就是最基本的JDBC的SQL写法 SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
//看似是又去递归调用SqlSource.getBoundSql,其实因为是StaticSqlSource,所以没问题,不是递归调用 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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
如上,该方法主要有如下几个过程:
- 生成一个动态上下文
- 解析SQL片段,替换${}类型的参数
- 解析SQL语句,并将参数都替换成?
- 调用StaticSqlSource的getBoundSql获取BoundSql
- 将DynamicContext的ContextMap中的内容拷贝到BoundSql中。
下面通过两个单元测试用例理解下。
@Test
public void shouldMapNullStringsToNotEmptyStrings() { final String expected = "id=${id}"; final MixedSqlNode sqlNode = mixedContents(new TextSqlNode(expected)); final DynamicSqlSource source = new DynamicSqlSource(new Configuration(), sqlNode); String sql = source.getBoundSql(new Bean("12")).getSql(); Assert.assertEquals("id=12", sql);
} @Test
public void shouldMapNullStringsToJINHAOEmptyStrings() { final String expected = "id=#{id}"; final MixedSqlNode sqlNode = mixedContents(new TextSqlNode(expected)); final DynamicSqlSource source = new DynamicSqlSource(new Configuration(), sqlNode); String sql = source.getBoundSql(new Bean("12")).getSql(); Assert.assertEquals("id=?", sql);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
如上,${} 占位符经过DynamicSqlSource的getBoundSql 方法之后直接替换成立用户传入值,而#{} 占位符则仅仅只是只会被替换成?号,不会被设值。
DynamicContext
DynamicContext 是SQL语句的上下文,每个SQL片段解析完成之后会存入DynamicContext中。让我们来看看DynamicContext的相关代码。
public DynamicContext(Configuration configuration, Object parameterObject) {
//绝大多数调用的地方parameterObject为null if (parameterObject != null && !(parameterObject instanceof Map)) { //如果不是map型 MetaObject metaObject = configuration.newMetaObject(parameterObject); bindings = new ContextMap(metaObject); } else { bindings = new ContextMap(null); }
//存储额外信息,如databaseId bindings.put(PARAMETER_OBJECT_KEY, parameterObject); bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
如上,在DynamicContext的构造函数中,根据传入的参数对象是否是Map类型,有两个不同构造ContextMap的方式,而ContextMap作为一个继承了HashMap的对象,作用就是用于统一参数的访问方式:用Map接口方法来访问数据。具体磊说,当传入的参数对象不是Map类型时,MyBatis会将传入的POJO对象用MetaObject 对象来封装,当动态计算sql过程需要获取数据时,用Map 接口的get方法包装 MetaObject对象的取值过程。
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; //先去map里找 if (super.containsKey(strKey)) { return super.get(strKey); } //如果没找到,再用ognl表达式去取值 //如person[0].birthdate.year if (parameterMetaObject != null) { return parameterMetaObject.getValue(strKey); } return null; }
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
DynamicContext 的解析到此完成。
解析SQL片段
正如前面所说,一个包含了${}
, <if>
,<where>
等标签的SQL 会被分成很多SQL片段。由SqlNode 的子类进行存储。
如图所示,
- StaticTextSqlNode 用于存储静态文本
- TextSqlNode用于存储带有${}占位符的文本
- ifSqlNode则用于存储
<if>
节点的内容 - WhereSqlNode 用于增加WHERE 前缀,然后替换掉AND 和OR 等前缀。
- MixedSqlNode内部维护了一个SqlNode集合,用于存储各种
各样的SqlNode。
首先我们来看看,MixedSqlNode 的很合SQL节点。
public class MixedSqlNode implements SqlNode {
//组合模式,拥有一个SqlNode的List
private List<SqlNode> contents; public MixedSqlNode(List<SqlNode> contents) { this.contents = contents;
} @Override
public boolean apply(DynamicContext context) { //依次调用list里每个元素的apply for (SqlNode sqlNode : contents) { sqlNode.apply(context); } return true;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
如上,从构造函数中可以看出,MixedSqlNode 拥有一个SqlNode的集合。这里利用了组合模式。在此处我重点介绍下TextSqlNode 文本SQL节点 和IfSqlNode if SQL节点。
//***TextSqlNode
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);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
在TextSqlNode 类的内部持有了一个绑定记号解析器BindingTokenParser,用于解析标记内容,并将结果返回给GenericTokenParser。核心代码如下:
//***TextSqlNode.BindingTokenParser 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)); // issue #274 return "" instead of "null"
// 通过正则表达式检测setValue有效性 checkInjection(srtValue); return srtValue; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
而GenericTokenParser 则是一个通用的记号解析器,用户处理#{}
和${}
参数。核心代码如下:
//*GenericTokenParser
public String parse(String text) { StringBuilder builder = new StringBuilder(); if (text != null && text.length() > 0) { char[] src = text.toCharArray(); int offset = 0; int start = text.indexOf(openToken, offset); //#{favouriteSection,jdbcType=VARCHAR} //这里是循环解析参数,参考GenericTokenParserTest,比如可以解析${first_name} ${initial} ${last_name} reporting.这样的字符串,里面有3个 ${} while (start > -1) { //判断一下 ${ 前面是否是反斜杠,这个逻辑在老版的mybatis中(如3.1.0)是没有的 if (start > 0 && src[start - 1] == '\\') { // the variable is escaped. remove the backslash. //新版已经没有调用substring了,改为调用如下的offset方式,提高了效率 //issue #760 builder.append(src, offset, start - offset - 1).append(openToken); offset = start + openToken.length(); } else { int end = text.indexOf(closeToken, start); if (end == -1) { builder.append(src, offset, src.length - offset); offset = src.length; } else { builder.append(src, offset, start - offset); offset = start + openToken.length(); String content = new String(src, offset, end - offset); //得到一对大括号里的字符串后,调用handler.handleToken,比如替换变量这种功能 builder.append(handler.handleToken(content)); offset = end + closeToken.length(); } } start = text.indexOf(openToken, offset); } if (offset < src.length) { builder.append(src, offset, src.length - offset); } } return builder.toString();
}
- 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
接着我们来看看IfSqlNode,该Sql节点主要是判断<if></if>
的条件是否成立,成立的话则调用其他节点的apply方法进行解析,并返回true。不成立的话则直接返回false。
public boolean apply(DynamicContext context) { //通过ONGL评估test 表达式的结果 if (evaluator.evaluateBoolean(test, context.getBindings())) {
// 若test表达式中的条件成立,则调用其它节点的apply方法进行解析。 contents.apply(context); return true; } return false;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
解析#{}占位符
经过前面的解析,我们已经能够从DynamicContext 中获取到完整的SQL语句了。但是这并不意味着解析工作就结束了。我们还有#{}
占位符没有处理。#{}
占位符不同于${}
占位符的处理方式。MyBatis 并不会直接将#{}
占位符替换成相应的参数值。
#{}
的解析过程封装在SqlSourceBuilder 的parse方法中。解析后的结果交给StaticSqlSource处理。话不多说,来看看源码吧。
//*SqlSourceBuilder
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
// 创建#{} 占位符处理器 ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters); //替换#{}中间的部分,如何替换,逻辑在ParameterMappingTokenHandler GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
// 解析#{}占位符,并返回解析结果 String sql = parser.parse(originalSql); //封装解析结果到StaticSqlSource中,并返回 return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
如上源码,该解析过程主要有四部,核心步骤就是解析#{}
占位符,并返回结果。GenericTokenParser 类在前面已经解析过了,下面我们重点看看SqlSourceBuilder的内部类ParameterMappingTokenHandler。该类的核心方法是handleToken方法。该方法的主要作用是将#{}
替换成?
并返回。然后就是构建参数映射。ParameterMappingTokenHandler 该类同样实现了TokenHandler 接口,所以GenericTokenParser 类的parse方法可以调用到。
//参数映射记号处理器,静态内部类
private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler { private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>(); private Class<?> parameterType; private MetaObject metaParameters; public ParameterMappingTokenHandler(Configuration configuration, Class<?> parameterType, Map<String, Object> additionalParameters) { super(configuration); this.parameterType = parameterType; this.metaParameters = configuration.newMetaObject(additionalParameters); } public List<ParameterMapping> getParameterMappings() { return parameterMappings; } @Override public String handleToken(String content) { //获取context的对应的ParameterMapping parameterMappings.add(buildParameterMapping(content)); //如何替换很简单,永远是一个问号,但是参数的信息要记录在parameterMappings里面供后续使用 return "?"; } //构建参数映射 private ParameterMapping buildParameterMapping(String content) { //#{favouriteSection,jdbcType=VARCHAR} //先解析参数映射,就是转化成一个hashmap /* * parseParameterMapping 内部依赖 ParameterExpression 对字符串进行解析,ParameterExpression 的 */ Map<String, String> propertiesMap = parseParameterMapping(content); String property = propertiesMap.get("property"); Class<?> propertyType; // metaParameters 为 DynamicContext 成员变量 bindings 的元信息对象 if (metaParameters.hasGetter(property)) { /* * parameterType 是运行时参数的类型。如果用户传入的是单个参数,比如 Article 对象,此时 * parameterType 为 Article.class。如果用户传入的多个参数,比如 [id = 1, author = "coolblog"], * MyBatis 会使用 ParamMap 封装这些参数,此时 parameterType 为 ParamMap.class。如果 * parameterType 有相应的 TypeHandler,这里则把 parameterType 设为 propertyType */ propertyType = metaParameters.getGetterType(property); } 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) { MetaClass metaClass = MetaClass.forClass(parameterType); if (metaClass.hasGetter(property)) { propertyType = metaClass.getGetterType(property); } else { // 如果 property 为空,或 parameterType 是 Map 类型,则将 propertyType 设为 Object.class propertyType = Object.class; } } 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 { throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + "}. Valid properties are " + parameterProperties); } } //#{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler} if (typeHandlerAlias != null) { builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias)); } return builder.build(); }
- 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
如上,buildParameterMapping方法,主要做了如下三件事
- 解析content,
- 解析propertyType,对应分割线上面的代码
- 构建ParameterMapping,对应分割下下面的代码。
最终的结果是将 #{xxx} 占位符中的内容解析成 Map。
例如:
上面占位符中的内容最终会被解析成如下的结果:
{ "property": "age", "typeHandler": "MyTypeHandler", "jdbcType": "NUMERIC", "javaType": "int" }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
BoundSql的创建过程就此结束了。我们接着往下看。
创建StatementHandler
StatementHandler 是非常核心的接口,从代码分词的角度来说,StatementHandler是MyBatis源码的边界,再往下层就是JDBC层面的接口了。StatementHandler需要和JDBC层面的接口打交道。它要做的事情有很多,在执行SQL之前,StatementHandler 需要创建合适的Statement对象。然后填充参数值到Statement对象中,最后通过Statement 对象执行SQL。待SQL执行完毕,还需要去处理查询结果。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0cMuXOsM-1582632260584)(./images/1559481523011.png)]
设置运行时参数到SQL中
JDBC 提供了三种 Statement 接口,分别是 Statement、PreparedStatement 和 CallableStatement。他们的关系如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-25GvS01Y-1582632260586)(./images/1559481899968.png)]
上面三个接口的层级分明,其中 Statement 接口提供了执行 SQL,获取执行结果等基本功能。PreparedStatement 在此基础上,对 IN 类型的参数提供了支持。使得我们可以使用运行时参数替换 SQL 中的问号 ? 占位符,而不用手动拼接 SQL。CallableStatement 则是 在 PreparedStatement 基础上,对 OUT 类型的参数提供了支持,该种类型的参数用于保存存储过程输出的结果。
本节,我将分析 PreparedStatement 的创建,以及设置运行时参数到 SQL 中的过程。其他两种 Statement 的处理过程,大家请自行分析。Statement 的创建入口是在 SimpleExecutor 的 prepareStatement 方法中,下面从这个方法开始进行分析。
//*SimpleExecutor
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { Statement stmt;
// 获取数据库连接 Connection connection = getConnection(statementLog); //创建Statement stmt = handler.prepare(connection); //为Statement设置IN参数 handler.parameterize(stmt); return stmt;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
如上,上面代码的逻辑不复杂,总共包含三个步骤。如下:
获取数据库连接
创建 Statement
为 Statement 设置 IN 参数
上面三个步骤看起来并不难实现,实际上如果大家愿意写,也能写出来。不过 MyBatis 对着三个步骤进行拓展,实现上也相对复杂一下。以获取数据库连接为例,MyBatis 并未没有在 getConnection 方法中直接调用 JDBC DriverManager 的 getConnection 方法获取获取连接,而是通过数据源获取获取连接。MyBatis 提供了两种基于 JDBC 接口的数据源,分别为 PooledDataSource 和 UnpooledDataSource。创建或获取数据库连接的操作最终是由这两个数据源执行。限于篇幅问题,本节不打算分析以上两种数据源的源码,相关分析会在下一篇文章中展开。
接下来,我将分析 PreparedStatement 的创建,以及 IN 参数设置的过程。按照顺序,先来分析 PreparedStatement 的创建过程。如下:
//*PreparedStatementHandler
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException { Statement statement = null; try { // 创建 Statement statement = instantiateStatement(connection); // 设置超时和 FetchSize setStatementTimeout(statement, transactionTimeout); setFetchSize(statement); return statement; } catch (SQLException e) { closeStatement(statement); throw e; } catch (Exception e) { closeStatement(statement); throw new ExecutorException("Error preparing statement. Cause: " + e, e); }
}
protected Statement instantiateStatement(Connection connection) throws SQLException { //调用Connection.prepareStatement String sql = boundSql.getSql(); if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) { String[] keyColumnNames = mappedStatement.getKeyColumns(); if (keyColumnNames == null) { return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS); } else { return connection.prepareStatement(sql, keyColumnNames); } } else if (mappedStatement.getResultSetType() != null) { return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY); } else { return connection.prepareStatement(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
如上,PreparedStatement 的创建过程没什么复杂的地方,就不多说了。下面分析运行时参数是如何被设置到 SQL 中的过程。
public void parameterize(Statement statement) throws SQLException { //通过参数处理器ParameterHandler设置运行时参数到PreparedStatement中 parameterHandler.setParameters((PreparedStatement) statement);
}
- 1
- 2
- 3
- 4
地方
public void setParameters(PreparedStatement ps) throws SQLException { ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId()); /* * 从BoundSql中获取ParameterMapping列表,每个ParameterMapping * 与原始SQL中的#{xxx} 占位符一一对应 * */ List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); if (parameterMappings != null) { //循环设参数 for (int i = 0; i < parameterMappings.size(); i++) { ParameterMapping parameterMapping = parameterMappings.get(i);
// 检测参数类型,排除掉mode为OUT类型的parameterMapping if (parameterMapping.getMode() != ParameterMode.OUT) { //如果不是OUT,才设进去 Object value;
// 获取属性名 String propertyName = parameterMapping.getProperty();
// 检测BoundSql的additionalParameter是否包含propertyName if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params //若有额外的参数, 设为额外的参数 value = boundSql.getAdditionalParameter(propertyName); } else if (parameterObject == null) { //若参数为null,直接设null value = null; } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { //若参数有相应的TypeHandler,直接设object value = parameterObject; } else { //除此以外,MetaObject.getValue反射取得值设进去 MetaObject metaObject = configuration.newMetaObject(parameterObject); value = metaObject.getValue(propertyName); }
// 之上,获取#{xxx}占位符属性所对应的运行时参数
// -------------------分割线-----------------------
// 之下,获取#{xxx}占位符属性对应的TypeHandler,并在最后通过TypeHandler将运行时参数值设置到
// PreparedStatement中。 TypeHandler typeHandler = parameterMapping.getTypeHandler(); JdbcType jdbcType = parameterMapping.getJdbcType(); if (value == null && jdbcType == null) { //不同类型的set方法不同,所以委派给子类的setParameter方法 jdbcType = configuration.getJdbcTypeForNull(); }
// 由类型处理器typeHandler向ParameterHandler设置参数 typeHandler.setParameter(ps, i + 1, value, jdbcType); } } }
}
- 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
如上代码,分割线以上的大段代码用于获取 #{xxx} 占位符属性所对应的运行时参数。分割线以下的代码则是获取 #{xxx} 占位符属性对应的 TypeHandler,并在最后通过 TypeHandler 将运行时参数值设置到 PreparedStatement 中。关于 TypeHandler 的用途。
文章来源: feige.blog.csdn.net,作者:码农飞哥,版权归原作者所有,如需转载,请联系作者。
原文链接:feige.blog.csdn.net/article/details/90744187
- 点赞
- 收藏
- 关注作者
评论(0)