1.参考示例
2.${}和#{}解析以及赋值对比分析
    2.1 sql解析对比
    2.2 sql赋值分析
3.关于${}和#{}模糊查询方式梳理说明
    源码视角搞清楚为什么#{}可以防止sql注入,直接进入主题!

1.参考示例

    示例方法:

// 根据标题模糊查询资讯信息
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);

2. ${}和#{}解析以及赋值对比分析

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();
  }

2.1 sql解析对比

    对于#{}处理,会将#{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 + "}";
    }
  }

2.2 sql赋值对比

    ${}赋值处理,获取值具体对应源码:
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解析以及参数赋值梳理完毕.后面查询以及结果集封装操作相同,在此不再展开.

3.关于 ${}和#{}模糊查询方式梳理说明

    ${}模糊查询,无论单引还是双引,执行结果相同

<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>

    欢迎小伙伴评论区留言,共同探讨,相互学习!

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐