zoukankan      html  css  js  c++  java
  • 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();
        }
    

      

  • 相关阅读:
    AndroidStudio开发体温上报系统------问题总结
    AndroidStudio--app是如何运行的
    sqlite操作
    sqlite数据库
    Android Service
    echart自定义主题
    vue监听数组变化
    Django:数据库驱动安装
    pycharm链接mysql报错: Server returns invalid timezone. Go to 'Advanced' tab and set 'serverTimezone' property manually.
    Django2.2:UnicodeDecodeError: 'gbk' codec can't decode byte 0xa6 in position 9737: illegal multibyte sequence
  • 原文地址:https://www.cnblogs.com/yingxiaocao/p/13698084.html
Copyright © 2011-2022 走看看