深度解析Mybatis中#{}与${}的区别

Q:#$的区别是什么?
A:#会在sql中使用占位符,有效得防止了sql注入,$会把参数直接拼接到sql中可能会引发sql注入。
如果你只知道这些区别,或者想知道为什么两种写法会产生这些区别,那么你就可以静下来看看下面我写的。
DynamicSqlSource中有一个getBoundSql方法,如下:

public BoundSql getBoundSql(Object parameterObject) {
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    rootSqlNode.apply(context);
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType);
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    for (Map.Entry entry : context.getBindings().entrySet()) {
      boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
    }
    return boundSql;
  }

注意其中的apply方法,这将是$被解析的地方。其中rootSqlNode是通过构造函数传递过来的一般会是一个MixedSqlNode,看MixedSqlNodeapply方法:

public boolean apply(DynamicContext context) {
    for (SqlNode sqlNode : contents) {
      sqlNode.apply(context);
    }
    return true;
  }

继续对内部的SqlNode调用apply方法,其中文本类型的sql会被解析为TextSqlNode。下面我们看一下TextSqlNodeapply方法:

public boolean apply(DynamicContext context) {
    GenericTokenParser parser = new GenericTokenParser("${", "}", new BindingTokenParser(context));
    context.appendSql(parser.parse(text));
    return true;
  }
private static class BindingTokenParser implements TokenHandler {

    private DynamicContext context;

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

    public String handleToken(String content) {
      try {
        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());
        return (value == null ? "" : String.valueOf(value)); // issue #274 return "" instead of "null"
      } catch (OgnlException e) {
        throw new BuilderException("Error evaluating expression '" + content + "'. Cause: " + e, e);
      }
    }
  }

TextSqlNode中的apply所有需要的内部类BindingTokenParser也一并贴出来了。
看到这里我们应该先看看GenericTokenParser中怎么为我们解析的:

public String parse(String text) {
    StringBuilder builder = new StringBuilder();
    if (text != null) {
      String after = text;
      int start = after.indexOf(openToken);
      int end = after.indexOf(closeToken);
      while (start > -1) {
        if (end > start) {
          String before = after.substring(0, start);
          String content = after.substring(start + openToken.length(), end);
          String substitution;

          // check if variable has to be skipped
          if (start > 0 && text.charAt(start - 1) == '\\') {
            before = before.substring(0, before.length() - 1);
            substitution = new StringBuilder(openToken).append(content).append(closeToken).toString();
          } else {
            substitution = handler.handleToken(content);
          }

          builder.append(before);
          builder.append(substitution);
          after = after.substring(end + closeToken.length());
        } else if (end > -1) {
          String before = after.substring(0, end);
          builder.append(before);
          builder.append(closeToken);
          after = after.substring(end + closeToken.length());
        } else {
          break;
        }
        start = after.indexOf(openToken);
        end = after.indexOf(closeToken);
      }
      builder.append(after);
    }
    return builder.toString();
  }

不出意料,这个类只是将sql中被openTokencloseToken所包围的tokenTokenHandle类来解析,那我们就可以继续回到TextSqlNode中了,可以看到上面的BindingTokenParser中有一个handleToken方法这就是产生最开始Q&A区别的地方,这个方法会直接将${}所包围的token用传递进来的参数解析出来并返回解析之后的value,也就是这个BindingTokenParser会直接将sql中的${}部分用参数解析完并拼接回sql。如果你是一个老手,估计都不会滥用${},那让我们回到开始的DynamicSqlSource中吧,继续看DynamicSqlSource中下面的代码:

public BoundSql getBoundSql(Object parameterObject) {
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    rootSqlNode.apply(context);
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType);
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    for (Map.Entry entry : context.getBindings().entrySet()) {
      boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
    }
    return boundSql;
  }

rootSqlNode.apply(context)之后,还会有一个SqlSourceBuilder这个类也有一个parse方法(在这个地方不得不说下即使Mybatis现在是最流行的ORM框架之一但是它的设计上确实不怎么样,现在随便来一个类都有一个parse方法,为什么不直接抽象出一个接口来),这个parse方法就是处理#的地方了。下面我们来看看:

public SqlSource parse(String originalSql, Class parameterType) {
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType);
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    String sql = parser.parse(originalSql);
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

  private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {

    private List parameterMappings = new ArrayList();
    private Class parameterType;

    public ParameterMappingTokenHandler(Configuration configuration, Class parameterType) {
      super(configuration);
      this.parameterType = parameterType;
    }

    public List getParameterMappings() {
      return parameterMappings;
    }

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

    private ParameterMapping buildParameterMapping(String content) {
      Map propertiesMap = parseParameterMapping(content);
      String property = propertiesMap.get("property");
      String jdbcType = propertiesMap.get("jdbcType");
      Class propertyType;
      MetaClass metaClass = MetaClass.forClass(parameterType);
      if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
        propertyType = parameterType;
      } else if (JdbcType.CURSOR.name().equals(jdbcType)) {
        propertyType = java.sql.ResultSet.class;
      } else if (metaClass.hasGetter(property)) {
        propertyType = metaClass.getGetterType(property);
      } else {
        propertyType = Object.class;
      }
      ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
      if (jdbcType != null) {
        builder.jdbcType(resolveJdbcType(jdbcType));
      }
      Class javaType = null;
      String typeHandlerAlias = null;
      for (Map.Entry entry : propertiesMap.entrySet()) {
        String name = entry.getKey();
        String value = entry.getValue();
        if ("javaType".equals(name)) {
          javaType = resolveClass(value);
          builder.javaType(javaType);
        } else if ("jdbcType".equals(name)) {
          builder.jdbcType(resolveJdbcType(value));
        } else if ("mode".equals(name)) {
          builder.mode(resolveParameterMode(value));
        } else if ("numericScale".equals(name)) {
          builder.numericScale(Integer.valueOf(value));
        } else if ("resultMap".equals(name)) {
          builder.resultMapId(value);
        } else if ("typeHandler".equals(name)) {
          typeHandlerAlias = value;
        } else if ("jdbcTypeName".equals(name)) {
          builder.jdbcTypeName(value);
        }
      }
      if (typeHandlerAlias != null) {
        builder.typeHandler((TypeHandler) resolveTypeHandler(javaType, typeHandlerAlias));
      }
      return builder.build();
    }

老规矩先上代码再分析,上面是SqlSourceBuilderapply方法以及用到的TokenHandle的内部实现类ParameterMappingTokenHandle
先看最简单的产生#$区别的地方,ParameterMappingTokenHandlehandleToken方法中不管你传过来的是什么都是直接返回一个占位符?

接下来要介绍#$的第二个区别了##:看上面ParameterMappingTokenHandle类的parseParameterMapping方法我们可以发现在#{}中可以写一些其它的东西,比如javaTypejdbcTypetypeHandler等,所以我们可以写出类似这种的sql:#{id,javaType=String,jdbcType=VARCHAR,typeHandler=cn.fay.mybatis.MyStringTypeHandler},我们可以在sql中指定变量的类型以及设置这个变量时对应所需要用到的TypeHandler当然如果你用到了自定义的TypeHandler的话,你要在mybatis的配置中声明一下,如下:


        
    

这里需要注意的是mybatis的配置文件中对typeAliasestypeHandlersplugingsmappers等元素的顺序是有要求的不能乱,如果你在使用中遇到了问题解决不了,可以过来问我。
至此,#$的区别应该说得差不多了,有问题可以来沟通。

你可能感兴趣的:(深度解析Mybatis中#{}与${}的区别)