mybatis源码(八) Mybatis中的#{} 和${} 占位符的区别

mybatis源码(八) Mybatis中的#{} 和${} 占位符的区别

使用#{} 参数占位符时,占位符内容会被替换成 “?” 然后通过PreparedStatement 对象的setXxx()方法为参数占位符设置值;能够有效避免SQL注入的问题,所以应优先使用#{},当#{}无法满足时,在考虑用${}

而${} 参数占位符内容会被直接替换为参数值.

  1.${} 参数占位符的解析过程是在TextSqlNode类的apply()中完成的

  TextSqlNode部分源码如下:

public class TextSqlNode implements SqlNode {
  private final String text;
  private final Pattern injectionFilter;
  @Override
  public boolean apply(DynamicContext context) {
    // 通过GenericTokenParser对象解析${}参数占位符,使用BindingTokenParser对象处理参数占位符内容
    GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
    // 调用GenericTokenParser对象的parse()方法解析
    context.appendSql(parser.parse(text));
    return true;
  }

  private GenericTokenParser createParser(TokenHandler handler) {
    return new GenericTokenParser("${", "}", handler);
  }
}

这里的GenericTokenParser的parse(text)方法里完成的

 public String parse(String text) {
    if (text == null || text.isEmpty()) {
      return "";
    }
    // 获取第一个openToken在SQL中的位置
    int start = text.indexOf(openToken, 0);
    // start为-1说明SQL中不存在任何参数占位符
    if (start == -1) {
      return text;
    }
    // 將SQL转换为char数组
    char[] src = text.toCharArray();
    // offset用于记录已解析的#{或者}的偏移量,避免重复解析
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    // expression为参数占位符中的内容
    StringBuilder expression = null;
    // 遍历获取所有参数占位符的内容,然后调用TokenHandler的handleToken()方法替换参数占位符
    while (start > -1) {
      if (start > 0 && src[start - 1] == '\') {
        // this open token is escaped. remove the backslash and continue.
        builder.append(src, offset, start - offset - 1).append(openToken);
        offset = start + openToken.length();
      } else {
        // found open token. let's search close token.
        if (expression == null) {
          expression = new StringBuilder();
        } else {
          expression.setLength(0);
        }
        builder.append(src, offset, start - offset);
        offset = start + openToken.length();
        int end = text.indexOf(closeToken, offset);
        while (end > -1) {
          if (end > offset && src[end - 1] == '\') {
            // this close token is escaped. remove the backslash and continue.
            expression.append(src, offset, end - offset - 1).append(closeToken);
            offset = end + closeToken.length();
            end = text.indexOf(closeToken, offset);
          } else {
            expression.append(src, offset, end - offset);
            offset = end + closeToken.length();
            break;
          }
        }
        if (end == -1) {
          // close token was not found.
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
          // 调用TokenHandler的handleToken()方法替换参数占位符
          builder.append(handler.handleToken(expression.toString()));
          offset = end + closeToken.length();
        }
      }
      start = text.indexOf(openToken, offset);
    }
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }

    return builder.toString();
  }

上面代码的核心内容是遍历获取所有${}参数占位符的内容,然后调用BindingTokenParser类的handleToken()方法对参数占位符内容进行替换。BindingTokenParser类的handleToken()方法实现如下:

  private static class BindingTokenParser implements TokenHandler {

    private DynamicContext context;
    private Pattern injectionFilter;

    public BindingTokenParser(DynamicContext context, Pattern injectionFilter) {
      this.context = context;
      this.injectionFilter = injectionFilter;
    }

    @Override
    public String handleToken(String content) {
      // 获取Mybatis内置参数_parameter,_parameter属性中保存所有参数信息
      Object parameter = context.getBindings().get("_parameter");
      if (parameter == null) {
        context.getBindings().put("value", null);
      } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
        // 將参数对象添加到ContextMap对象中
        context.getBindings().put("value", parameter);
      }
      // 通过OGNL表达式获取参数值
      Object value = OgnlCache.getValue(content, context.getBindings());
      String srtValue = (value == null ? "" : String.valueOf(value)); // issue #274 return "" instead of "null"
      checkInjection(srtValue);
      // 返回参数值
      return srtValue;
    }

2.#{} 的解析过程可参考SqlSourceBuilder.parse()方法

public class SqlSourceBuilder extends BaseBuilder {

  private static final String parameterProperties = "javaType,jdbcType,mode,numericScale,resultMap,typeHandler,jdbcTypeName";

  public SqlSourceBuilder(Configuration configuration) {
    super(configuration);
  }
  public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    // ParameterMappingTokenHandler为Mybatis参数映射处理器,用于处理SQL中的#{}参数占位符
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    // Token解析器,用于解析#{}参数
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    // 调用GenericTokenParser对象的parse()方法將#{}参数占位符转换为?
    String sql = parser.parse(originalSql);
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

同样的。这里使用ParameterMappingTokenHandler 处理器解析的。和${} 一样都是使用GenericTokenParser.parse方法进行解析的,只是处理器不一样。

#{} 占位符使用ParameterMappingTokenHandler 的部分源码如下:

  private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {

    private List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
    private Class<?> parameterType;
    private MetaObject metaParameters;

    public ParameterMappingTokenHandler(Configuration configuration, Class<?> parameterType, Map<String, Object> additionalParameters) {
      super(configuration);
      this.parameterType = parameterType;
      this.metaParameters = configuration.newMetaObject(additionalParameters);
    }

    public List<ParameterMapping> getParameterMappings() {
      return parameterMappings;
    }

    @Override
    public String handleToken(String content) {
      parameterMappings.add(buildParameterMapping(content));
      return "?";
    }

从上面的代码可以看出,SQL配置中的所有#{}参数占位符内容都被替换成了"?" 字符,为什么要替换成一一个"?" 字符呢?
因为MyBatis默认情况下会使用PreparedStatement对象与数据库进行交互,因此#{}参数占位符内容被替换成了问号,然后调用PreparedStatement对象的setXXX()方法为参数占位符设置值。

3.#{ } 和${} 的使用案例

  3.1 ${} 的使用:假设我们的sql如下:

    <select id="getUserByName" parameterType="java.lang.String"
            resultType="com.blog4java.mybatis.example.entity.User">
      select * from user where name = ${userName}
    </select>

  如果mapper调用的时候,传入的参数值如下:

    @Test
    public void testGetUserByName() {
        String userName = "Test4";
        UserEntity userEntity = userMapper.getUserByName(userName);
        System.out.println(userEntity);
    }

  就会抛出如下异常

org.apache.ibatis.exceptions.PersistenceException: 
### Error building SqlSession.
### The error may exist in SQL Mapper Configuration
### Cause: org.apache.ibatis.builder.BuilderException: Error parsing SQL Mapper Configuration. Cause: org.apache.ibatis.builder.BuilderException: Error registering typeAlias for 'velocityDriver'. Cause: java.lang.ClassNotFoundException: Cannot find class: org.mybatis.scripting.velocity.Driver

  

上面的Mapper调用将会抛出异常,原因是TextSqlNode类的apply()方法中解析${}参数占位符时,只是对参数占位符内容进行替换,将参数占位符替换为对应的参数值,因此SQL 配置解析后的内容如下:

select * from user where name = Test4

因此,语句不合法,正确的写法应该是,在参数前后,加入一个单引号,如下所示:

    @Test
    public void testGetUserByName() {
        String userName = "'Test4'";
        UserEntity userEntity = userMapper.getUserByName(userName);
        System.out.println(userEntity);
    }

  3.2 #{} 的使用:假设我们的sql如下:

select * from user where name = #{userName}

#{} 参数占位符会被解析成“?”  上面的Sql语句解析结果为

select * from user where name = ?

Mybatis会使用PreparedStatement对象与数据库进行交互,大致过程如下:

    @Test
    public void test001() throws SQLException {
        Connection connection = DriverManager.getConnection("xxx");
        PreparedStatement statement = connection.prepareStatement("select * from user where name = ? ");
        statement.setString(1,"Test");
        statement.execute();
    }

  

原文地址:https://www.cnblogs.com/yingxiaocao/p/13698084.html