问题
现象: 在测试$和#区别时, 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");
}