功能
XMLMapperBuilder 中会将 mapper 映射文件中除 CRUD 外的标签解析验证,轮到CRUD标签的时候,是交给专门的类去做处理的,也就是XMLStatementBuilder。XMLStatementBuilder的解析工作:
第一步:解析一些较基本的属性,比如 id、databaseId(多数据库配置)、useCache(是否启动二级缓存)、flushCache(只要语句调用就刷新缓存)等。
第二步;替换sql中的include标签,将复用其它地方的sql整合处理成为一个完整的sql
第三步:处理主键生成策略,mybatis 默认使用的是 NoKeyGenerator,如果设置了 useGeneratedKeys 会使用 Jdbc3KeyGenerator
第四步:mybatis到了这一步,会将sql处理成为sqlSource存储起来并且替换类似#{}这样的特殊字符,SqlSource是mybatis中比较重要的一个部分,所以这一步sql处理会比较复杂。
UML
代码解析
parseStatementNode() 是 XMLStatementBuilder 解析的入口,首先,他会去找CURD标签上的唯一id,然后就是databaseId,databaseId是数据库厂商标识,MyBatis 会加载所有不带 databaseId 或匹配当前 databaseId 的语句,如果带和不带的语句都有,则不带的会被忽略,一般选择databaseId的时候是需要配置多数据库厂商的时候。在解析完databaseId 之后就是解析flushCache、useCache等标签,默认情况下,如果是select语句,则是开启缓存的
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;
// 如果是select语句,则默认是 useCache 启动缓存
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
...
}
在CURD标签中,有时候为了方便是会配置一些 include标签的,复用其他sql,所以XMLStatementBuilder 在接下来的工作就是解析 include标签,解析 include标签的功能是由 XMLIncludeTransformer 这个类完成的,构造方法是将当前的configuration、builderAssistant传入进去,之所以传入这两个参数是为了在接下来的解析动作中可以加载其它的sqlSeqment。
// 解析 include标签
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
在XMLIncludeTransformer 的 applyIncludes()方法中,会去解析include标签,这一块的逻辑比较复杂,不过条例还算清晰,在这之前需要介绍一下mybatis的几个概念,我们在CRUD标签中所写的sql最终都会解析成为一个个sqlnode,比如 select * from 这种纯文本的sql会对应为一个静态文本sql,foreach这种动态sql会对应为一个foreachnode,详细细节在解析动态sql中会介绍。
首先进入到applyIncludes()方法中,是几个 条件case, 我们的CURD标签,根节点是一个element标签,仔细看这段逻辑,空标签会走第二个条件case,也就是 source.getNodeType() == Node.ELEMENT_NODE 这个条件,在这块代码块中会获取到这个节点下的所有的孩子节点,进行递归处理,其实这里就可以拿到 include标签了。进入递归,如果是include标签,则会走第一个条件节点,会去configuration中找到这个include包括的sql加载进来,并且进行递归去处理,处理完成后则会将处理完成的sql拼接到当前node后并且移除当前include,至于如果是静态文本sql的话,会做一个非常重要的处理,就是替换 $ {}这种格式的占位符,也就是预先替换。
private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
// 如果包括include标签则会走这里
if (source.getNodeName().equals("include")) {
Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
Properties toIncludeContext = getVariablesContext(source, variablesContext);
applyIncludes(toInclude, toIncludeContext, true);
if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
toInclude = source.getOwnerDocument().importNode(toInclude, true);
}
source.getParentNode().replaceChild(toInclude, source);
// 会将include包含的sql文本拼接到这里并且将自己给移除掉
while (toInclude.hasChildNodes()) {
toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
}
toInclude.getParentNode().removeChild(toInclude);
// 如果是element则是走一块逻辑,其实也就是 select insert等标签
} else if (source.getNodeType() == Node.ELEMENT_NODE) {
if (included && !variablesContext.isEmpty()) {
// replace variables in attribute values
NamedNodeMap attributes = source.getAttributes();
for (int i = 0; i < attributes.getLength(); i++) {
Node attr = attributes.item(i);
attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
}
}
//
NodeList children = source.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
applyIncludes(children.item(i), variablesContext, included);
}
// 如果是 select * from 这种的sql走得这里,TEXT_NODE
} else if (included && (source.getNodeType() == Node.TEXT_NODE || source.getNodeType() == Node.CDATA_SECTION_NODE)
&& !variablesContext.isEmpty()) {
// replace variables in text node
source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
}
}
替换 $ {}这种占位符是交给PropertyParser.parse()去完成,mybatis对于$ {}的处理是提前处理的,它会从环境变量中去取相对应key的值拿过来在这里替换掉,可以看他的处理逻辑,通过GenericTokenParser去匹配 $ {} ,如果匹配到的话就通过handler去环境变量中拿,variables就是configuration中配置的properties,至于parser.parse(string)的内容是一个替换算法,感兴趣的同学可以自己去看看。
其实从这段逻辑也可以看出,$ {} 是并不安全的,因为他会完全将符合key的值替换到sql中,会有sql注入的风险。
public static String parse(String string, Properties variables) {
VariableTokenHandler handler = new VariableTokenHandler(variables);
GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
return parser.parse(string);
}
经过incude标签的处理,sqlnode已经是粗略加工过的sql了,接下来就是解析一些标签,诸如selectKey 、根据selectKey去选择主键生成策略,通过LanguageDriver去加工处理前边初步加工过的sql为sqlsource,然后就是fetchSize、timeout、parameterMap、resultType、resultMap、resultSetType、keyProperty、keyColumn、resultSets标签。
// 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;
}
// 通过 langDriver 去解析sqlSource
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
// mybatus默认是PrepareStatement处理
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");
在这一步中,其实有个很重要的东西,就是 SqlSource 的解析,mybatis会将 CRUD标签中的sql解析成为 sqlSource存放起来(主要是解析动态sql),解析sql是由 LanguageDriver 去处理的,LanguageDriver 是个接口专门负责动态sql的解析接口,有几个实现类,一般mybatis解析是由XMLLanguageDriver去做的,当然你如果有需求也可以自己去配置driver
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
LanguageDriver 的实现类
在createSqlSource()中可以看到,解析动态sql是由 XMLScriptBuilder 去完成的
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
下一章介绍XMLScriptBuilder 的解析工作。