前言
在面试中我们经常会被到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>
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;
.... 省略部分代码
}
分析SQL的解析,首先从获取BoundSql说起。其代码源头在MappedStatement。
public BoundSql getBoundSql(Object parameterObject) {
//其实就是调用sqlSource.getBoundSql
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
//剩下的可以暂时忽略,故省略代码
return boundSql;
}
如上,可以看出其内部就是调用的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;
}
如上,该方法主要有如下几个过程:
- 生成一个动态上下文
- 解析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);
}
如上,${} 占位符经过DynamicSqlSource的getBoundSql 方法之后直接替换成立用户传入值,而#{} 占位符则仅仅只是只会被替换成?号,不会被设值。
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());
}
如上,在DynamicContext的构造函数中,根据传入的参数对象是否是Map类型,有两个不同构造ContextMap的方式,而ContextMap作为一个继承了HashMap的对象,作用就是用于统一参数的访问方式:用Map接口方法来访问数据。具体磊说,当传入的参数对象不是Map类型时,MyBatis会将传入的POJO对象用MetaObject 对象来封装,当动态计算sql过程需要获取数据时,用Map 接口的get方法包装 MetaObject对象的取值过程。
。。。。。。。。。。。。。。。。。
版权原因,完整文章,请参考如下: