从XML到Java对象
涉及的具体的解析可能不太详尽,但是可以自行debug进行回忆,这块代码不是很难!
首先在最初学习MyBatis的时候,我们需要配置一个mybatis-config.xml和若干XXXMapper.xml文件的,这两种形式的文件就构建出了MyBatis中最基础和重要的数据来源。那么现在就有问题了:
- 文件是存储在硬盘上的数据源,而程序是运行在内存中的,那么如何将这些文件读取到内存中。
- 我们使用Java语言进行程序的开发,在Java中有一个很著名的话就是“万物皆对象”,那么MyBatis是如何将些数据封装Java对象以便在JVM中使用的呢!
围绕这两点我们来讲一下,MyBatis是如何做的。
在使用MyBatis时需要一个SqlSession来获取XXXMapper,或者直接执行SQL操作,但是不管怎么样都得搞个SqlSession出来,而这个SqlSession是由SqlSessionFactory来创建的,所以在每次使用的使用总会有一下几行代码:
1. InputStream resourceAsStream = OriginMybatisTest.class.getClassLoader().getResourceAsStream("mybatis-config.xml");
2. SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsStream);
3. SqlSession sqlSession = build.openSession();
其实上面第一个问题的解决很简单,jdk本身乃至MyBatis框架都提供了快速读取文件为字节流或者字符流的方式,上面的代码就是通过类加载器读取一个mybatis-config.xml到字节流中。
当然MyBatis本身也提供了工具类:
InputStream resourseAsStream = Resources.getResourceAsStream("mybatis-config.xml");
解析和封装
那么最关键的就是第二个问题了, 即MyBatis是如何将XML进行解析并且封装中JVM中的一个个对象以供使用的。
XML的解析
一般对于XML的解析有三种方式:DOM、SAX、XPath。
XPath比前两者出现的时间要晚,但是使用起来却更加的简单,而MyBatis采用的也是这种方式,并且MyBatis还对XPath进行了一次封装,因此如果我们开发中有需要可以也可以去使用。
下面看一个XPath的一个简单Demo:
XPathParser parser = new XPathParser(Resources.getResourceAsReader(OriginMybatisTest.class.getClassLoader(),"mybatis-config.xml"));
获取/configuration标签下的所有子标签
XNode configuration = parser.evalNode("configuration");
for (XNode child : configuration.getChildren()) {
System.out.println(child);
}
将每个标签解析成一个XNode对象,依靠这个对象当前节点以及其子节点进行解析并且都会封装成XNode对象。MyBatis对XPath的封装都在org.apache.ibatis.parsing 中
MyBatis解析mybatis-config.xml
而MyBatis对于mybatis-config.xml的解析发生在:
new SqlSessionFactoryBuilder().build(resourceAsStream);
build方法的入参就是之前获取的xml文件的字节流对象:

对于上图中的第①点很容易解释,看一下它的构造方法就清楚了:
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
***1**
this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
**super(new Configuration());**
ErrorContext.instance().resource("SQL Mapper Configuration");
this.configuration.setVariables(props);
this.parsed = false;
this.environment = environment;
this.parser = parser;
}
在*1处,创建了一个XPathParser对象,并且还把inputStream,也就是xml文件的字节流传了进去,这就表明后续就可以同个这个parse文件的解析。并且在这里我们还可以看到一个非常重要的一个对象的初始化,就是Configuration!
重点我们需要关注图中的第二点:
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// 可以看到这里就可以解析根节点并且获取根节点的XNode对象,而“/configuration” config文件中的跟标签了
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
在parseConfiguration(root)标签中我们就可以很清晰的看到各种等待被解析的标签,这些名称都是在mybatis-config.xml文件中存在的标签。看了一下大部分的标签解析都不难,到时候想看了可以自行去debug,这些解析都发生在XMLConfigBuilder 中。
但是呢还是有必要看一下,对于mapper文件的解析的! 我将对下面的代码进行一下修剪,在else中下面有三个判断条件,大部分情况下我们只会写resource属性,所以下面的两个关于url和class属性的逻辑就先删掉了
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);
***1**
InputStream inputStream = Resources.getResourceAsStream(resource);
***2**
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
***3**
mapperParser.parse();
}’
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
在对于mappers标签解析的时候是要获取该标签下的所有**子标签,**并且进行一个遍历,因此就有了大致下面的一个解析流程
以第二种情况,mapper标签中不是package属性进行演示,对于mapper文件的解析和config的解析大同小异,基本上都是:
- 获取mapper文件的字节流
- 通过字节流构建一个XMLMapperBuilder,内部同样封装了XPathParse,并且据此进行标签的解析
- 解析mapper文件中的各个标签信息,封装进MappedStatement对象,并且将该对象放入到Configuration中
前两步都比较简单主要来分析一下第三步, mapperParser.parse() :
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
**真正的解析在这里,去获取XXXMapper.xml中的所有标签**
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
在这个方法中我们就可以看到一些属性的东西
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("**namespace**");
if (namespace == null || namespace.equals("")) {
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);
}
}
这里我们就可以看到一些熟悉的字符串了,其中最终要的肯定就是上面标红的那一行了。如果再看一下这个代码的话依旧会看到很多熟悉的属性
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
String resultMap = context.getStringAttribute("resultMap");
String resultType = context.getStringAttribute("resultType");
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
Class<?> resultTypeClass = resolveClass(resultType);
String resultSetType = context.getStringAttribute("resultSetType");
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
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());
// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
String resultSets = context.getStringAttribute("resultSets");
String keyProperty = context.getStringAttribute("keyProperty");
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))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
至此在属性解析的过程中,我们会发现MyBatis的一个手法,就是不管是哪一种属性标签都会为其创建一个XXXBuilderAssistant对象,在创建的时候会将configuration传入,在对应解析方法的最用会依靠这个builderAssistant将解析并且封装好的对象,添加进configuration中去。
比如解析mapper标签的过程中使用的是MapperBuilderAssistant ,然后解析出来的子标签对象都是通过assistant添加进的MappedStatement或者添加进configuration中。
在整个对config.xml解析中,在完成所有标签解析之后,整个大的这个解析也就结束了,最终就会返回一个
SqlSessionFactory并且在后面生产SqlSession进行SQL的使用。而SqlSessionFactory内部就封装了Configuration进一步封装了MappedStatement