Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

0%

# mybatis 的 $ 和 # 是有啥区别

发表于 2020-09-06 更新于 2022-06-11 分类于 Java ， Mybatis ， Spring ， Mysql ， Sql注入 ， Mybatis 阅读次数： 阅读次数： Disqus：

这个问题也是面试中常被问到的，就抽空来了解下这个，跳过一大段前面初始化的逻辑，
对于一条`select * from t1 where id = #{id}`这样的 sql，在初始化扫描 mapper 的xml文件的时候会根据是否是 dynamic 来判断生成 DynamicSqlSource 还是 RawSqlSource，这里它是一条 RawSqlSource，
在这里做了替换，将`#{}`替换成了`?`
前面说的是否 dynamic 就是在这里进行判断

    // org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseScriptNode
    public SqlSource parseScriptNode() {
        MixedSqlNode rootSqlNode = parseDynamicTags(context);
        SqlSource sqlSource;
        if (isDynamic) {
          sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
        } else {
          sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
        }
        return sqlSource;
      }
    // org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseDynamicTags
    protected MixedSqlNode parseDynamicTags(XNode node) {
        List<SqlNode> contents = new ArrayList<>();
        NodeList children = node.getNode().getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
          XNode child = node.newXNode(children.item(i));
          if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
            String data = child.getStringBody("");
            TextSqlNode textSqlNode = new TextSqlNode(data);
            if (textSqlNode.isDynamic()) {
              contents.add(textSqlNode);
              isDynamic = true;
            } else {
              contents.add(new StaticTextSqlNode(data));
            }
          } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
            String nodeName = child.getNode().getNodeName();
            NodeHandler handler = nodeHandlerMap.get(nodeName);
            if (handler == null) {
              throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
            }
            handler.handleNode(child, contents);
            isDynamic = true;
          }
        }
        return new MixedSqlNode(contents);
      }
    // org.apache.ibatis.scripting.xmltags.TextSqlNode#isDynamic
      public boolean isDynamic() {
        DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
        GenericTokenParser parser = createParser(checker);
        parser.parse(text);
        return checker.isDynamic();
      }
        private GenericTokenParser createParser(TokenHandler handler) {
        return new GenericTokenParser("${", "}", handler);
      }

可以看到其中一个条件就是是否有`${}`这种占位符，假如说上面的 sql 换成 `${}`，那么可以看到它会在这里创建一个 dynamicSqlSource,

    // org.apache.ibatis.scripting.xmltags.DynamicSqlSource
    public class DynamicSqlSource implements SqlSource {

      private final Configuration configuration;
      private final SqlNode rootSqlNode;

      public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
        this.configuration = configuration;
        this.rootSqlNode = rootSqlNode;
      }

      @Override
      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, context.getBindings());
        BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
        context.getBindings().forEach(boundSql::setAdditionalParameter);
        return boundSql;
      }

    }

这里眼尖的同学可能就可以看出来了，RawSqlSource 在初始化的时候已经经过了 parse，把`#{}`替换成了`?`占位符，但是 DynamicSqlSource 并没有
再看这个图，我们发现在这的时候还没有进行替换
然后往里跟
好像是这里了
这里 rootSqlNode.apply 其实是一个对原来 sql 的解析结果的一个循环调用，不同类型的标签会构成不同的 node，像这里就是一个 textSqlNode
可以发现到这我们的 sql 已经被替换了，而且是直接作为 string 类型替换的，所以可以明白了这个问题所在，就是注入，不过细心的同学发现其实这里是有个
理论上还是可以做过滤的，不过好像现在没用起来。
我们前面可以发现对于`#{}`是在启动扫描 mapper的 xml 文件就替换成了 `?`,然后是在什么时候变成实际的值的呢
发现到这的时候还是没有替换，其实说白了也就是 prepareStatement 那一套，
在这里进行替换，会拿到 org.apache.ibatis.mapping.ParameterMapping，然后进行替换，因为会带着类型信息，所以不用担心注入咯