掘金 后端 ( ) • 2024-06-26 10:12

在MyBatis中,#{}${}是用于在SQL语句中嵌入参数的两种不同方式。它们的核心区别在于预处理和潜在的SQL注入风险。

#{}(预处理)

#{}用于预处理参数(prepared statement),也就是说,参数占位符会被替换为?,然后参数值会在执行时绑定到SQL语句中。这样做的好处是可以防止SQL注入,因为MyBatis会对参数进行适当的转义处理。

以下是使用#{}的代码示例:

@Select("SELECT * FROM user WHERE id = #{id}")
User getUserById(@Param("id") Integer id);

MyBatis会将上面的SQL语句转换为一个预处理语句(prepared statement),大致相当于:

SELECT * FROM user WHERE id = ?

然后,MyBatis会将id参数的值安全地绑定到问号(?)位置。

${}(直接替换)

${}进行的是直接字符串替换。你提供的字符串会在MyBatis创建SQL语句之前就被替换到SQL中。这种方式允许你动态地插入表名、列名或者是动态的SQL片段。但由于这种方式可能会导致SQL注入风险,它的使用需要非常小心。

以下是使用${}的代码示例:

@Select("SELECT * FROM ${tableName} WHERE id = ${id}")
User getUserById(@Param("tableName") String tableName, @Param("id") Integer id);

如果tableName是"user",id是1,那么最终的SQL将会是:

SELECT * FROM user WHERE id = 1

源码分析

当MyBatis解析#{}${}时,它使用了不同的解析器。对于#{},MyBatis使用ParameterMapping来处理每一个参数,将其转换为一个预处理的参数。

对于${},MyBatis将参数的实际值直接拼接到SQL字符串中,这就意味着如果参数包含特殊字符,它们将直接嵌入到SQL中,可能引起安全问题。

在MyBatis的源码中,SqlSourceBuilder 类处理带有#{}的表达式:

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处理的:

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注入攻击。