mybatis中${}和#{}源码分析
源码视角搞清楚为什么#{}可以防止sql注入
1.参考示例
2.${}和#{}解析以及赋值对比分析
2.1 sql解析对比
2.2 sql赋值分析
3.关于${}和#{}模糊查询方式梳理说明
源码视角搞清楚为什么#{}可以防止sql注入,直接进入主题!
示例方法:
// 根据标题模糊查询资讯信息
List<News> findNews(String title);
配置文件:
<!--${}模糊查询-->
<select id="findNews" resultType="com.it.txm.demo.controller.News">
select id,title from find_news where title like '%${title1}%'
</select>
<!--#{}模糊查询-->
<select id="findNews" resultType="com.it.txm.demo.controller.News">
select id,title from find_news
where title like "%"#{title}"%"
</select>
测试案例:
List<News> news = newsMapper.findNews("abc");
System.out.println(news);
${}和#{}解析以及赋值对比分析
mybatis配置文件解析赋值流程
GenericTokenParser.java中parse方法执行解析处理(mybatis配置文件加载,具体sql执行之前两者都会执行此方法,含有${}的sql执行赋值操作也会执行此方法).
public String parse(String text) {
if (text == null || text.isEmpty()) {
return "";
}
//判断sql中是否包含${}或#{},如果没有则结束,继续解析下一个节点
int start = text.indexOf(openToken);
if (start == -1) {
return text;
}
// 省略部分代码
// sql中含有${}或#{}处理方式不同.前者使用PropertyParser解析,后者使用SqlSourceBuilder解析
builder.append(handler.handleToken(expression.toString()));
// 省略部分代码
return builder.toString();
}
对于#{}处理,会将#{title}替换成?,对应源码:
SqlSourceBuilder.java中handleToken
public String handleToken(String content) {
parameterMappings.add(buildParameterMapping(content));
return "?";
}
对于${}解析处理,最终只会进行参数拼接:
PropertyParser.java中handleToken
public String handleToken(String content) {
if (variables != null) {
String key = content;
if (enableDefaultValue) {
final int separatorIndex = content.indexOf(defaultValueSeparator);
String defaultValue = null;
if (separatorIndex >= 0) {
key = content.substring(0, separatorIndex);
defaultValue = content.substring(separatorIndex + defaultValueSeparator.length());
}
if (defaultValue != null) {
return variables.getProperty(key, defaultValue);
}
}
if (variables.containsKey(key)) {
return variables.getProperty(key);
}
}
return "${" + content + "}";
}
}
${}赋值处理,获取值具体对应源码:
TextSqlNode.java
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);
}
Object value = OgnlCache.getValue(content, context.getBindings());
String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null"
checkInjection(srtValue);
return srtValue;
}
获取值之后重新走GenericTokenParser.java中parse进行参数拼接.拼接后sql如下:
#{}赋值处理:
按照 string类型进行赋值处理,i表示第几个参数,将?替换成对应的实参.具体对应源码:
StringTypeHandler.java中setNonNullParameter
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
throws SQLException {
ps.setString(i, parameter);
}
需要注意的地方是对于传递的参数参数是字符串时,会将双引号替换为单引号.
ClientPreparedQueryBindings.java执行上面具体的setString逻辑
public void setString(int parameterIndex, String x) {
// 省略部分代码,下面是对特殊字符进行转义处理
for (int i = 0; i < stringLength; ++i) {
char c = x.charAt(i);
switch (c) {
case 0: /* Must be escaped for 'mysql' */
buf.append('\\');
buf.append('0');
break;
case '\n': /* Must be escaped for logs */
buf.append('\\');
buf.append('n');
break;
case '\r':
buf.append('\\');
buf.append('r');
break;
case '\\':
buf.append('\\');
buf.append('\\');
break;
case '\'':
buf.append('\'');
buf.append('\'');
break;
case '"': /* Better safe than sorry */
if (this.session.getServerSession().useAnsiQuotedIdentifiers()) {
buf.append('\\');
}
buf.append('"');
break;
case '\032': /* This gives problems on Win32 */
buf.append('\\');
buf.append('Z');
break;
case '\u00a5':
case '\u20a9':
// escape characters interpreted as backslash by mysql
if (this.charsetEncoder != null) {
CharBuffer cbuf = CharBuffer.allocate(1);
ByteBuffer bbuf = ByteBuffer.allocate(1);
cbuf.put(c);
cbuf.position(0);
this.charsetEncoder.encode(cbuf, bbuf, true);
if (bbuf.get(0) == '\\') {
buf.append('\\');
}
}
buf.append(c);
break;
default:
buf.append(c);
}
}
buf.append('\'');
parameterAsString = buf.toString();
}
// 将双引号参数替换成单引号
byte[] parameterAsBytes = this.isLoadDataQuery ? StringUtils.getBytes(parameterAsString)
: (needsQuoted ? StringUtils.getBytesWrapped(parameterAsString, '\'', '\'', this.charEncoding)
: StringUtils.getBytes(parameterAsString, this.charEncoding));
setValue(parameterIndex, parameterAsBytes, MysqlType.VARCHAR);
}
}
赋值完成之后的sql如下
至此,${}和#{}sql解析以及参数赋值梳理完毕.后面查询以及结果集封装操作相同,在此不再展开.
${}和#{}模糊查询方式梳理说明
${}模糊查询,无论单引还是双引,执行结果相同
<select id="findNews" resultType="com.it.txm.demo.controller.News">
select id,title from find_news
where title like "%${title1}%"
</select>
<select id="findNews" resultType="com.it.txm.demo.controller.News">
select id,title from find_news
where title like '%${title1}%'
</select>
#{}模糊查询,下面两种执行正常
<select id="findNews" resultType="com.it.txm.demo.controller.News">
select id,title from find_news
where title like concat('%',#{title},'%')
</select>
<select id="findNews" resultType="com.it.txm.demo.controller.News">
select id,title from find_news
where title like "%"#{title}"%"
</select>
如果单引的方式会由于解析问题导致查询不到指定内容(本例中查询结果为空),平常注意一下即可.
<select id="findNews" resultType="com.it.txm.demo.controller.News">
select id,title from find_news
where title like '%'#{title}'%'
</select>
欢迎小伙伴评论区留言,共同探讨,相互学习!
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)