mybatis在mapper.xml使用$符号结合IDEA调试难发现的坑

这篇博客介绍了在使用Mybatis时遇到的问题,当在mapper.xml中使用${username}时,由于配置文件的解析机制,可能导致与预期不符的情况。通过分析Mybatis的源码,揭示了${}如何被解析,并从`properties`到`mappers`标签的解析过程。同时,博主还提醒开发者,在IDEA中调试时,XNode对象的toString方法可能会误导认为SQL已经解析,实际上${}还未被替换。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

问题

现象: 在测试$和#区别时, mapper.xml使用${username}, 发现始终都被赋值root(即连接用户名)
原因: 在xml配置文件中, ${xxx}占位符, 会被已经存在的键值对优先解析, 如jdbc.properties
解决: Xxxmapper.xml中使用${xxx}时, 避免和其他键值对重名

UserMapper.xml配置

<insert id="insertUser" parameterType="User" useGeneratedKeys="true" keyColumn="id" keyProperty="id">
   INSERT INTO user(username, password) VALUES(${username}, #{password})
</insert>

开启剖析之路

类比似先查看mybatis-config.xml配置的${driver}, ${url}, ${username}, ${password}如何被解析?
查看源码:

// 输入流
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// sqlSessionFactory
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

查看build方法, 一层层点进去, 进到下面这个build, 核心方法是parser.parse方法

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
  try {
    XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
    
    // 核心在于parser.parse()
    return build(parser.parse());
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error building SqlSession.", e);
  } finally {
    ErrorContext.instance().reset();
    try {
      inputStream.close();
    } catch (IOException e) {
      // Intentionally ignore. Prefer previous error.
    }
  }
}

查看parser.parse方法

public Configuration parse() {
  if (parsed) {
    throw new BuilderException("Each XMLConfigBuilder can only be used once.");
  }
  parsed = true;
  
  // 核心, 解析configuration配置信息
  parseConfiguration(parser.evalNode("/configuration"));
  return configuration;
}

查看parseConfiguration方法, 关注三个方法
propertiesElement(root.evalNode(“properties”));
environmentsElement(root.evalNode(“environments”));
mapperElement(root.evalNode(“mappers”));

private void parseConfiguration(XNode root) {
  try {
    // properties标签解析, 把properties文件里面的键值对存到variables
    propertiesElement(root.evalNode("properties"));
    Properties settings = settingsAsProperties(root.evalNode("settings"));
    loadCustomVfs(settings);
    loadCustomLogImpl(settings);
    typeAliasesElement(root.evalNode("typeAliases"));
    pluginElement(root.evalNode("plugins"));
    objectFactoryElement(root.evalNode("objectFactory"));
    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
    reflectorFactoryElement(root.evalNode("reflectorFactory"));
    settingsElement(settings);
    
    // 解析environments, 这里面带有${driver}, ${url}, ${username}, ${password}
    environmentsElement(root.evalNode("environments"));
    databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    typeHandlerElement(root.evalNode("typeHandlers"));

    // 解析mappers标签
    mapperElement(root.evalNode("mappers"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  }
}

查看environmentsElement方法

private void environmentsElement(XNode context) throws Exception {
  if (context != null) {
    if (environment == null) {
      environment = context.getStringAttribute("default");
    }
    for (XNode child : context.getChildren()) {
      String id = child.getStringAttribute("id");
      if (isSpecifiedEnvironment(id)) {
        TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
        // 核心解析dataSource, 查看dataSourceElement
        DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
        DataSource dataSource = dsFactory.getDataSource();
        Environment.Builder environmentBuilder = new Environment.Builder(id)
            .transactionFactory(txFactory)
            .dataSource(dataSource);
        configuration.setEnvironment(environmentBuilder.build());
      }
    }
  }
}

查看dataSourceElement方法

private DataSourceFactory dataSourceElement(XNode context) throws Exception {
  if (context != null) {
    String type = context.getStringAttribute("type");
    // 核心方法
    Properties props = context.getChildrenAsProperties();
    DataSourceFactory factory = (DataSourceFactory) resolveClass(type).getDeclaredConstructor().newInstance();
    factory.setProperties(props);
    return factory;
  }
  throw new BuilderException("Environment declaration requires a DataSourceFactory.");
}

查看context.getChildrenAsProperties()方法

public Properties getChildrenAsProperties() {
  Properties properties = new Properties();
  // 核心getChildren
  for (XNode child : getChildren()) {
    String name = child.getStringAttribute("name");
    String value = child.getStringAttribute("value");
    if (name != null && value != null) {
      properties.setProperty(name, value);
    }
  }
  return properties;
}

查看getChildren()方法

public List<XNode> getChildren() {
  List<XNode> children = new ArrayList<>();
  NodeList nodeList = node.getChildNodes();
  if (nodeList != null) {
    for (int i = 0, n = nodeList.getLength(); i < n; i++) {
      Node node = nodeList.item(i);
      if (node.getNodeType() == Node.ELEMENT_NODE) {
          // 核心方法为构造器, new XNode(xpathParser, node, variables)
        children.add(new XNode(xpathParser, node, variables));
      }
    }
  }
  return children;
}

查看new XNode(xpathParser, node, variables)

public XNode(XPathParser xpathParser, Node node, Properties variables) {
  this.xpathParser = xpathParser;
  this.node = node;
  this.name = node.getNodeName();
  
  // 这个variables里面装着从properties里面解析出来的键值对
  this.variables = variables;
  
  // 解析节点的属性, 这里会使用variables的键值去替换所有属性${xxx}占位符
  this.attributes = parseAttributes(node);
  
  // 解析当前节点的CDATA_SECTION_NODE或者TEXT_NODE即文本内容, 非节点, 或者无文本时, 解析第一个子节点的CDATA_SECTION_NODE或者TEXT_NODE
  // 这里会使用variables的键值去替换所有文本${xxx}占位符
  this.body = parseBody(node);
}

查看parseAttributes(node)

private Properties parseAttributes(Node n) {
  Properties attributes = new Properties();
  NamedNodeMap attributeNodes = n.getAttributes();
  if (attributeNodes != null) {
    for (int i = 0; i < attributeNodes.getLength(); i++) {
      Node attribute = attributeNodes.item(i);
      // 通过PropertyParser.parse替换属性占位符
      String value = PropertyParser.parse(attribute.getNodeValue(), variables);
      attributes.put(attribute.getNodeName(), value);
    }
  }
  return attributes;
}

查看PropertyParser.parse(attribute.getNodeValue(), variables);

public static String parse(String string, Properties variables) {
  VariableTokenHandler handler = new VariableTokenHandler(variables);
  GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
  
  // 解析的规则, 大概是截取${和}之间的字符串, 去variables找键值替换
  return parser.parse(string);
}

到此为止, ${driver}, ${url}, ${username}, ${password}均从variables里面取到值

除此之外, 我们可以得出结论:
在创建XNode对象时, 会使用PropertyParser.parse去解析当前节点的属性和文本(但是不会解析子节点属性), 把${xxx}替换为variables里面对应的值

再来看mappers标签的解析

// 解析mappers标签
mapperElement(root.evalNode("mappers"));

查看mapperElement方法

private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      if ("package".equals(child.getName())) {
        String mapperPackage = child.getStringAttribute("name");
        configuration.addMappers(mapperPackage);
      } else {
        String resource = child.getStringAttribute("resource");
        String url = child.getStringAttribute("url");
        String mapperClass = child.getStringAttribute("class");
        if (resource != null && url == null && mapperClass == null) {
          ErrorContext.instance().resource(resource);
          InputStream inputStream = Resources.getResourceAsStream(resource);
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
          // 查看parse方法
          mapperParser.parse();
        } else if (resource == null && url != null && mapperClass == null) {
          ErrorContext.instance().resource(url);
          InputStream inputStream = Resources.getUrlAsStream(url);
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
          mapperParser.parse();
        } else if (resource == null && url == null && mapperClass != null) {
          Class<?> mapperInterface = Resources.classForName(mapperClass);
          configuration.addMapper(mapperInterface);
        } else {
          throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
        }
      }
    }
  }
}

查看mapperParser.parse()

public void parse() {
  // 资源未被加载过, 才加载
  if (!configuration.isResourceLoaded(resource)) {
      // 核心configurationElement
    configurationElement(parser.evalNode("/mapper"));
    configuration.addLoadedResource(resource);
    bindMapperForNamespace();
  }

  parsePendingResultMaps();
  parsePendingCacheRefs();
  parsePendingStatements();
}

查看configurationElement

// 最终封装成MappedStatement对象
private void configurationElement(XNode context) {
  try {
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.isEmpty()) {
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    builderAssistant.setCurrentNamespace(namespace);
    cacheRefElement(context.evalNode("cache-ref"));
    cacheElement(context.evalNode("cache"));
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));

    // 解析resultMap标签
    resultMapElements(context.evalNodes("/mapper/resultMap"));

    // 解析sql标签
    sqlElement(context.evalNodes("/mapper/sql"));

    // 解析select|insert|update|delete标签
    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
  }
}

查看buildStatementFromContext, 继续查看buildStatementFromContext方法

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
  for (XNode context : list) {
    final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
    try {
        // 核心
      statementParser.parseStatementNode();
    } catch (IncompleteElementException e) {

      // 在xml中有可能顺序问题, 导致有些标签依赖于其他标签, 那么就会出现解析不完全的情况, 就先放到这里, 后续再继续解析
      configuration.addIncompleteStatement(statementParser);
    }
  }
}

查看statementParser.parseStatementNode()

public void parseStatementNode() {
  // 这里省略了很多代码
  ...

  // 核心创建sql语句
  SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
  
  // 这里省略了很多代码
  ...
}

查看实现类XMLLanguageDriver的createSqlSource方法

@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
  XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
  // 查看parseScriptNode
  return builder.parseScriptNode();
}

查看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;
}

查看parseDynamicTags

protected MixedSqlNode parseDynamicTags(XNode node) {
  List<SqlNode> contents = new ArrayList<>();
  NodeList children = node.getNode().getChildNodes();
  for (int i = 0; i < children.getLength(); i++) {
    // node.newXNode(children.item(i)); 会创建新的XNode, 导致sql中的${username}被variables中的username键值对给替换了
    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);
}

我们可以看到XNode child = node.newXNode(children.item(i));
这句就是在创建XNode, 代表会解析当前节点属性和文本中的${xxx}, 替换为variables的键值

IDEA调试坑

在IDEA中调试的时候, XNode对象显示的时候, 会调用该对象toString方法, 但由于XNode对象的toString方法被重写了, 误以为此时已经解析SQL了, ${username}被赋值了, 其实还没有

@Override
public String toString() {
  StringBuilder builder = new StringBuilder();
  toString(builder, 0);
  return builder.toString();
}
// 重载的另一个toString
private void toString(StringBuilder builder, int level) {
  builder.append("<");
  builder.append(name);
  for (Map.Entry<Object, Object> entry : attributes.entrySet()) {
    builder.append(" ");
    builder.append(entry.getKey());
    builder.append("=\"");
    builder.append(entry.getValue());
    builder.append("\"");
  }
  
  // 这个getChildren方法, 会把所有的node转为XNode即会为每个子节点创建XNode, 所以都会解析${xxx}
  // 所以在IDEA中要是查看XNode对象信息的话, 会调用的toString方法, 误以为SQL中的${username}已经被解析了
  // 但实际上的话, SQL中的${username}是在parseDynamicTags方法中被解析的
  // toString解析后的结果只是作为一个返回值, 跟实际上的SQL并无关系
  List<XNode> children = getChildren();
  if (!children.isEmpty()) {
    builder.append(">\n");
    for (XNode child : children) {
      indent(builder, level + 1);
      // 递归调用子类的toString
      child.toString(builder, level + 1);
    }
    indent(builder, level);
    builder.append("</");
    builder.append(name);
    builder.append(">");
  } else if (body != null) {
    builder.append(">");
    builder.append(body);
    builder.append("</");
    builder.append(name);
    builder.append(">");
  } else {
    builder.append("/>");
    indent(builder, level);
  }
  builder.append("\n");
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值