在MyBatis中,#{}
和${}
是用于在SQL语句中嵌入参数的两种不同方式。它们的核心区别在于预处理和潜在的SQL注入风险。
#{}
(预处理)
#{}
用于预处理参数(prepared statement),也就是说,参数占位符会被替换为?
,然后参数值会在执行时绑定到SQL语句中。这样做的好处是可以防止SQL注入,因为MyBatis会对参数进行适当的转义处理。
以下是使用#{}
的代码示例:
java
代码解读
复制代码
@Select("SELECT * FROM user WHERE id = #{id}")
User getUserById(@Param("id") Integer id);
MyBatis会将上面的SQL语句转换为一个预处理语句(prepared statement),大致相当于:
sql
代码解读
复制代码
SELECT * FROM user WHERE id = ?
然后,MyBatis会将id
参数的值安全地绑定到问号(?)位置。
${}
(直接替换)
${}
进行的是直接字符串替换。你提供的字符串会在MyBatis创建SQL语句之前就被替换到SQL中。这种方式允许你动态地插入表名、列名或者是动态的SQL片段。但由于这种方式可能会导致SQL注入风险,它的使用需要非常小心。
以下是使用${}
的代码示例:
java
代码解读
复制代码
@Select("SELECT * FROM ${tableName} WHERE id = ${id}")
User getUserById(@Param("tableName") String tableName, @Param("id") Integer id);
如果tableName
是"user",id
是1,那么最终的SQL将会是:
sql
代码解读
复制代码
SELECT * FROM user WHERE id = 1
源码分析
当MyBatis解析#{}
和${}
时,它使用了不同的解析器。对于#{}
,MyBatis使用ParameterMapping
来处理每一个参数,将其转换为一个预处理的参数。
对于${}
,MyBatis将参数的实际值直接拼接到SQL字符串中,这就意味着如果参数包含特殊字符,它们将直接嵌入到SQL中,可能引起安全问题。
在MyBatis的源码中,SqlSourceBuilder
类处理带有#{}
的表达式:
java
代码解读
复制代码
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
TokenParser parser = new TokenParser("#{", "}", handler);
String sql = parser.parse(originalSql);
StaticSqlSource sqlSource = new StaticSqlSource(configuration, sql, handler.getParameterMappings());
return sqlSource;
}
这里,ParameterMappingTokenHandler
负责将#{}
标记的参数转换为预处理参数,并创建相应的ParameterMapping
。
而对于${}
的处理,简单的字符串替换是通过TextSqlNode
处理的:
java
代码解读
复制代码
public boolean apply(DynamicContext context) {
Matcher matcher = pattern.matcher(text);
StringBuilder builder = new StringBuilder();
while (matcher.find()) {
String replacement = getProperty(matcher.group(1), context.getBindings());
if (replacement != null) {
matcher.appendReplacement(builder, replacement);
} else {
// handle null value ...
}
}
matcher.appendTail(builder);
context.appendSql(builder.toString());
return true;
}
在这里,getProperty
方法直接从上下文中取出变量值并替换掉${}
标记的部分。
细节和最佳实践
- 应尽可能使用
#{}
来防止SQL注入攻击。 - 只有在需要动态替换表名、列名或者SQL片段时才考虑使用
${}
。 - 如果必须使用
${}
,确保参数值来自于信任的源,或者对参数值进行严格的验证和清理,以避免SQL注入风险。 - 在可能的情况下,考虑使用MyBatis的内置功能,如
<if>
标签和<choose>
标签等,来动态构建SQL语句,而不是依赖${}
。
总之,在编写安全的MyBatis应用时,理解#{}
和${}
的区别是至关重要的,以确保你的应用不容易受到SQL注入攻击。