关于MyBatis的xml解析
MyBatis作为一个ORM框架,xml的解析是其核心部分之一,可以说也是MyBatis运行流程的一部分,所以有必要深究一下。
MyBatis中解析xml配置文件的技术用到了XPath,所以先来说一下XPath是如何解析xml的,直接看代码。有一个xml文件如下:
<root>
<book year="2018">
<title>book01</title>
<author>zhangsan</author>
<publisher>hhhhh</publisher>
</book>
<book year="2017">
<title>book02</title>
<author>lisi</author>
<publisher>aaaaa</publisher>
</book>
<book year="2016">
<title>book03</title>
<author>wangwu</author>
<publisher>lllll</publisher>
</book>
</root>
XPath是解析xml的一种语法,比如:“/” 表是从根节点开始,“//”表是从某一节点开始,“[]”表示条件,“@”表示属性,“node()”表示节点,“text()”表示文本节点,“.”表示当前节点,“* ..”表示当前节点的父节点,所以比如://book表示book节点,//book[author=’zhangsan’]表示book节点的author节点为zhangsan,解析xml的步骤共有7步骤:
* 1、生成DocumentBuilderFactory 目的是获取DocumentBuilder
* 2、由DoumentBuilderFactory获取DocumentBuilder
* 3、DocumentBuilder用来加载整个XML文件,builder.parse(xml文件路径)
* 4、加载完整个xml就可以生成XPathFactory
* 5、由XPathFactory来获取XPath对象
* 6、获取XPath的表达式就是对象XPathExpression(表达式)
* 7、exp来求值,参数doc和XPath语法,是个固定值
示例类:
public class XPathTest {
public static void main(String[] args) throws Exception {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = documentBuilderFactory.newDocumentBuilder();
Document doc=builder.parse("src\\main\\resources\\test.xml");
XPathFactory xPathFactory = XPathFactory.newInstance();
XPath xpath = xPathFactory.newXPath();
XPathExpression exp = xpath.compile("//book[author='zhangsan']/title/text()");
Object result=exp.evaluate(doc,XPathConstants.NODESET);
System.out.println("查询作者为zhangsan的图书标题:");
NodeList nodes = (NodeList)result;
for (int i = 0; i < nodes.getLength(); i++) {
System.out.println(nodes.item(i).getNodeValue());
}
}
}
由此来完成xml的解析。
那么MyBatis是怎么做的呢?首先还记得在生成SqlSessionFactory类的时候调用的代码吧?
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
build的时候new了一个XMLConfigBuilder,代码XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
在这个里面,构造方法是:
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}
所以XPathParser来了,这个MyBatis内部的一个XPath的解析类,我们来看一下它的一部分代码:
public class XPathParser {
private Document document;
private boolean validation;
private EntityResolver entityResolver;
private Properties variables;
private XPath xpath;
.................
public XPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver) {
this.commonConstructor(validation, variables, entityResolver);
this.document = this.createDocument(new InputSource(inputStream));
}
private Document createDocument(InputSource inputSource) {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setValidating(this.validation);
factory.setNamespaceAware(false);
factory.setIgnoringComments(true);
factory.setIgnoringElementContentWhitespace(false);
factory.setCoalescing(false);
factory.setExpandEntityReferences(true);
DocumentBuilder builder = factory.newDocumentBuilder();
builder.setEntityResolver(this.entityResolver);
builder.setErrorHandler(new ErrorHandler() {
public void error(SAXParseException exception) throws SAXException {
throw exception;
}
public void fatalError(SAXParseException exception) throws SAXException {
throw exception;
}
public void warning(SAXParseException exception) throws SAXException {
}
});
return builder.parse(inputSource);
} catch (Exception var4) {
throw new BuilderException("Error creating document instance. Cause: " + var4, var4);
}
}
private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) {
this.validation = validation;
this.entityResolver = entityResolver;
this.variables = variables;
XPathFactory factory = XPathFactory.newInstance();
this.xpath = factory.newXPath();
}
}
XPathFactory factory = XPathFactory.newInstance();
this.xpath = factory.newXPath();
这个就是我们刚才XPtahTest中的创建XPathFactory的代码,有了XPath也就是说我们要进行XPath语法的解析了,再往下看,创建了一个Document,代码:
this.document = this.createDocument(new InputSource(inputStream));
干嘛呢,我们看一下,
private Document createDocument(InputSource inputSource) {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setValidating(this.validation);
factory.setNamespaceAware(false);
factory.setIgnoringComments(true);
factory.setIgnoringElementContentWhitespace(false);
factory.setCoalescing(false);
factory.setExpandEntityReferences(true);
DocumentBuilder builder = factory.newDocumentBuilder();
builder.setEntityResolver(this.entityResolver);
builder.setErrorHandler(new ErrorHandler() {
public void error(SAXParseException exception) throws SAXException {
throw exception;
}
public void fatalError(SAXParseException exception) throws SAXException {
throw exception;
}
public void warning(SAXParseException exception) throws SAXException {
}
});
return builder.parse(inputSource);
} catch (Exception var4) {
throw new BuilderException("Error creating document instance. Cause: " + var4, var4);
}
}
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();觉不觉着很眼熟,没错就是我们刚才XPathTest中生成DocumentBuilderFactory 目的是获取DocumentBuilder,DocumentBuilder builder = factory.newDocumentBuilder();
有了DocumentBuilder就可以装在xml了,到此时XPathParser就准备好了,然后我们再回到SqlSessionFactory类的这段代码中,
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
SqlSessionFactory var5;
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
var5 = this.build(parser.parse());
} catch (Exception var14) {
throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
}
SqlSessionFactory 中的build方法中的参数传入了XMLConfigBuilder的parse()这个方法返回的是MyBatis的类Configuration也就是配置信息的存储类,那么我们先不看这个类,再看XMLConfigBuilder的parse(),
public Configuration parse() {
if (this.parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
} else {
this.parsed = true;
this.parseConfiguration(this.parser.evalNode("/configuration"));
return this.configuration;
}
}
this.parseConfiguration(this.parser.evalNode(“/configuration”))这个东西就一目了然了,相当于传入了XPath的表达式获取结果,那么它是从configuration开始获取的,而configuration就是我们mybatis-config.xml的根节点,也就是从这里开始MyBatis开始解析配置文件。我们来看parseConfiguration这个方法的实现:
private void parseConfiguration(XNode root) {
try {
//1.properties
propertiesElement(root.evalNode("properties"));
//2.类型别名
typeAliasesElement(root.evalNode("typeAliases"));
//3.插件
pluginElement(root.evalNode("plugins"));
//4.对象工厂
objectFactoryElement(root.evalNode("objectFactory"));
//5.对象包装工厂
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
//6.设置
settingsElement(root.evalNode("settings"));
// read it after objectFactory and objectWrapperFactory issue #631
//7.环境
environmentsElement(root.evalNode("environments"));
//8.databaseIdProvider
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
//9.类型处理器
typeHandlerElement(root.evalNode("typeHandlers"));
//10.映射器
mapperElement(root.evalNode("mappers"));
}
它为每一个xml里的配置节点都写了一个专门的处理方法,如果你对MyBatis的配置项比较熟悉,一看就能明白,properties、typeAliases、objectFactory、settings还有我们最最熟悉的mappers,也就是说MyBatis会专门去查找这些元素,然后做各自的处理,那么我们就看看我们必不可少的配置元素,mapper吧,代码:
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
Iterator var2 = parent.getChildren().iterator();
while(true) {
while(var2.hasNext()) {
XNode child = (XNode)var2.next();
String resource;
if ("package".equals(child.getName())) {
resource = child.getStringAttribute("name");
this.configuration.addMappers(resource);
} else {
resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
XMLMapperBuilder mapperParser;
InputStream inputStream;
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
inputStream = Resources.getResourceAsStream(resource);
mapperParser = new XMLMapperBuilder(inputStream, this.configuration, resource, this.configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
inputStream = Resources.getUrlAsStream(url);
mapperParser = new XMLMapperBuilder(inputStream, this.configuration, url, this.configuration.getSqlFragments());
mapperParser.parse();
} else {
if (resource != null || url != null || mapperClass == null) {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
Class<?> mapperInterface = Resources.classForName(mapperClass);
this.configuration.addMapper(mapperInterface);
}
}
}
return;
}
}
}
我们来看这一段:
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
inputStream = Resources.getResourceAsStream(resource);
mapperParser = new XMLMapperBuilder(inputStream, this.configuration, resource, this.configuration.getSqlFragments());
mapperParser.parse();
} else
实例化了一个类XMLMapperBuilder,在MyBatis里所有带Builder的类都继承自BaseBuilder,所有的Builder类的构造方法也都类似,这是XMLMapperBuilder的构造方法:
public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver()), configuration, resource, sqlFragments);
}
从中我们可以看出,也是实例化了一个XPathParser这个类,在XMLMapperBuilder类中做了什么事呢?new 完XMLMapperBuilder之后调用 mapperParser.parse();在parse()里开始解析mapper.xml,我们来看一下,
public void parse() {
if (!this.configuration.isResourceLoaded(this.resource)) {
this.configurationElement(this.parser.evalNode("/mapper"));
this.configuration.addLoadedResource(this.resource);
this.bindMapperForNamespace();
}
this.parsePendingResultMaps();
this.parsePendingChacheRefs();
this.parsePendingStatements();
}
this.configurationElement(this.parser.evalNode("/mapper"));
就是用来解析mapper的,去解析各种mapper的配置属性,由于我们示例里的xml比较简单只写了select|insert|update|delete所以我们来看一下代码片段:
//1.配置namespace
String namespace = context.getStringAttribute("namespace");
if (namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
//2.配置cache-ref
cacheRefElement(context.evalNode("cache-ref"));
//3.配置cache
cacheElement(context.evalNode("cache"));
//4.配置parameterMap(已经废弃,老式风格的参数映射)
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
//5.配置resultMap(高级功能)
resultMapElements(context.evalNodes("/mapper/resultMap"));
//6.配置sql(定义可重用的 SQL 代码段)
sqlElement(context.evalNodes("/mapper/sql"));
//7.配置select|insert|update|delete
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
this.buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
解析select标签:
private void buildStatementFromContext(List<XNode> list) {
if (this.configuration.getDatabaseId() != null) {
this.buildStatementFromContext(list, this.configuration.getDatabaseId());
}
this.buildStatementFromContext(list, (String)null);
}
这个方法buildStatementFromContext里面就是循环遍历,然后新生成了一个XMLStatementBuilder类:
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
//构建所有语句,一个mapper下可以有很多select
//语句比较复杂,核心都在这里面,所以调用XMLStatementBuilder
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
//核心XMLStatementBuilder.parseStatementNode
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
//如果出现SQL语句不完整,把它记下来,塞到configuration去
configuration.addIncompleteStatement(statementParser);
}
}
}
new完以后调用parseStatementNode()方法,其实就是用来挨个解析select标签里的各种属性的:
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
//如果databaseId不匹配,退出
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
//驱动程序每次批量返回的结果行数
Integer fetchSize = context.getIntAttribute("fetchSize");
//超时时间
Integer timeout = context.getIntAttribute("timeout");
//引用外部 parameterMap,已废弃
String parameterMap = context.getStringAttribute("parameterMap");
//参数类型
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
//引用外部的 resultMap(高级功能)
String resultMap = context.getStringAttribute("resultMap");
//结果类型
String resultType = context.getStringAttribute("resultType");
//脚本语言,mybatis3.2的新功能
String lang = context.getStringAttribute("lang");
//得到语言驱动
LanguageDriver langDriver = getLanguageDriver(lang);
Class<?> resultTypeClass = resolveClass(resultType);
//结果集类型,FORWARD_ONLY|SCROLL_SENSITIVE|SCROLL_INSENSITIVE 中的一种
String resultSetType = context.getStringAttribute("resultSetType");
//语句类型, STATEMENT|PREPARED|CALLABLE 的一种
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
//获取命令类型(select|insert|update|delete)
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
//是否要缓存select结果
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
//仅针对嵌套结果 select 语句适用:如果为 true,就是假设包含了嵌套结果集或是分组了,这样的话当返回一个主结果行的时候,就不会发生有对前面结果集的引用的情况。
//这就使得在获取嵌套的结果集的时候不至于导致内存不够用。默认值:false。
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// Include Fragments before parsing
//解析之前先解析<include>SQL片段
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// Parse selectKey after includes and remove them.
//解析之前先解析<selectKey>
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
//解析成SqlSource,一般是DynamicSqlSource
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
String resultSets = context.getStringAttribute("resultSets");
//(仅对 insert 有用) 标记一个属性, MyBatis 会通过 getGeneratedKeys 或者通过 insert 语句的 selectKey 子元素设置它的值
String keyProperty = context.getStringAttribute("keyProperty");
//(仅对 insert 有用) 标记一个属性, MyBatis 会通过 getGeneratedKeys 或者通过 insert 语句的 selectKey 子元素设置它的值
String keyColumn = context.getStringAttribute("keyColumn");
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))
? new Jdbc3KeyGenerator() : new NoKeyGenerator();
}
最后添加进MappedStatement,这个类就是映射的语句,最后经过一系列复杂的处理会把这个mapper接口添加进Configuration类中的mapper注册器,
Class<?> mapperInterface = Resources.classForName(mapperClass);
this.configuration.addMapper(mapperInterface);
好吧,今天先写到这里。