在我们使用mybatis的时候,有没有思考过mybatis中解析xml中动态sql的。
这里可以从mybatis的test中可以看到:xml映射构建测试和xml配置构建测试,这里以xml映射构建为例,来看一下它从解析中可以看到什么。
xml映射构建测试类
在mybatis中,我们经常会看到mybatis的xml中的sql带有if、choose…when、where等标签,那它们是怎样被解析的呢?
其追踪过程:
XMLMapperBuilder#parse->XMLMapperBuilder#configurationElement#buildStatementFromContext#parseStatementNode->XMLLanguageDriver#createSqlSource->XMLScriptBuilder#parseDynamicTags->XMLScriptBuilder#handleNode->执行解析动态节点标签if/where/choose等
测试解析入口:
//创建配置对象,获取xml映射构建,通过xml映射构建进行解析
@Test
void shouldSuccessfullyLoadXMLMapperFile() throws Exception {
Configuration configuration = new Configuration();
String resource = "org/apache/ibatis/builder/AuthorMapper.xml";
try (InputStream inputStream = Resources.getResourceAsStream(resource)) {
XMLMapperBuilder builder = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
builder.parse();
}
}
进行mapper进行构建,首先通过xpath进行解析:
public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver()),
configuration, resource, sqlFragments);
}
然后通过xml映射构建进行解析:
//执行解析操作
public void parse() {
//配置中如果不是资源加载
if (!configuration.isResourceLoaded(resource)) {
//配置元素 重要
configurationElement(parser.evalNode("/mapper"));
//将其添加到加载资源中
configuration.addLoadedResource(resource);
//为命名空间绑定映射
bindMapperForNamespace();
}
//解析等待的结果maps、缓存引用、语句
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
可以看到xpath的解析的结果是XNode对象:
//配置元素
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"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
//构建语句从上下文中
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);
}
}
构建语句从上下文,解析语句节点:
//构建语句从上下文
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
//构建语句从上下文
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) {
configuration.addIncompleteStatement(statementParser);
}
}
}
解析语句XMLStatementBuilder#parseStatementNode:
//解析语句节点
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
/**
* 重要 sql资源
*/
//创建sql资源 重要
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String resultType = context.getStringAttribute("resultType");
Class<?> resultTypeClass = resolveClass(resultType);
String resultMap = context.getStringAttribute("resultMap");
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");
/**
* 重要 添加映射语句
*/
//构建助手,添加映射语句
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
XMLLanguageDriver#createSqlSource创建sql资源:分为两种脚本节点:动态的和静态的
//创建sql资源
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
//创建xml脚本构建器对象
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
//xml脚本构建对象调用解析脚本节点
return builder.parseScriptNode();
}
解析脚本节点:
//解析脚本节点 重要
public SqlSource parseScriptNode() {
//解析动态tags
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
//判断是否是动态的
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
解析动态tags:
//解析动态tags
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);
//文本sql节点是否是动态的,如果是则将其添加到contents中,同时将isDynamic设置为true,否者将其添加为静态文本sql节点数据
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.");
}
//处理节点 重要
/**
* 从这里可以看到是这里调用了handleNode方法,从而解析里面的各个节点的动态sql 重要
*/
handler.handleNode(child, contents);
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
解析node:
//节点处理器
private interface NodeHandler {
void handleNode(XNode nodeToHandle, List<SqlNode> targetContents);
}
处理节点的截图:
动态sql处理节点
可以看到解析if的处理器:
//处理if节点
@Override
public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
//获取字符串属性test中的
String test = nodeToHandle.getStringAttribute("test");
//ifsql节点中的节点信息放入到最终的内容中
IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
targetContents.add(ifSqlNode);
}
可以节点解析标签:
sql节点解析处理,这里以if标签为例
解析if标签 :
@Select("<script>SELECT firstName <if test=\"includeLastName != null\">, lastName</if> FROM names WHERE lastName LIKE #{name}</script>")
进行解析:
//解析动态tags
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);
//文本sql节点是否是动态的,如果是则将其添加到contents中,同时将isDynamic设置为true,否者将其添加为静态文本sql节点数据
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.");
}
//处理节点 重要
/**
* 从这里可以看到是这里调用了handleNode方法,从而解析里面的各个节点的动态sql 重要
*/
handler.handleNode(child, contents);
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
可以看到属性就是我们经常看到的test的最终结果,可以看到满足条件不为空,进行拼接:
if标签解析
最终会将其解析成:
解析的sql语句
之后进行处理,变成mysql中可以执行的sql.