MyBatis 初始化之XML解析详解

MyBatis 是现在比较流行的 ORM 框架,得益于它简单易用,虽然增加了开发者的一些操作,但是带来了设计和使用上的灵活,得到广泛的使用。之前的一篇文章MyBatis 初始化原理中我们已经知道了 MyBatis 的初始化流程,其实就是构造 Configuration 对象的过程,主要分为系统环境参数的初始化和 Mapper 映射的初始化,然后以 Configuration 对象作为 SqlSessionFactory 的参数,最终获取到 SqlSession 进行数据库的操作。流程如下图所示。
Myabtis初始化构造流程
从上图我们可以看到最主要的就是 XMLConfigBuilder 创建,以及调用 XMLConfigBuilder 的 parse() 方法得到 Configuration 对象,本文针对这两个过程,对照 MyBatis 源码进行详细分析。

构造 XMLConfigBuilder 对象

我们已经知道了 SqlSessionFactoryBuilder 的 build 函数内先创建 XMLConfigBuilder 对象,利用其 parse() 方法返回的 Configuration 对象,构建得到 DefaultSqlSessionFactory 对象。我们就仔细来看 XMLConfigBuilder 对象是如何构造的。 XMLConfigBuilder 的继承自 BaseBuilder, BaseBuilder 有 XMLxxxBuilder 子类,这些 XMLxxxBuilder 是用来解析 XML 配置文件的,不同类型 XMLxxxBuilder 用来解析 MyBatis 配置文件的不同部位。比如:XMLConfigBuilder 用来解析 MyBatis 的配置文件, XMLMapperBuilder 用来解析MyBatis中的映射文件(如上文提到的 UserMapper.xml),XMLStatementBuilder用来解析映射文件中的SQL语句。

当创建 XMLConfigBuilder 对象时,就会初始化 Configuration 对象,并且在初始化 Configuration 对象的时候,一些别名会被注册到 Configuration 的 typeAliasRegistry 容器中。具体代码如下。

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;
}
public Configuration() {
	typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
	typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
	typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
	typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
	typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
	typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
	typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
	typeAliasRegistry.registerAlias("LRU", LruCache.class);
	typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
	typeAliasRegistry.registerAlias("WEAK", WeakCache.class);
	……
}
parse 函数解析配置文件

当有了 XMLConfigBuilder 对象之后,接下来就可以用它来解析配置文件了,具体就看 parse() 方法,parse() 方法调用 parseConfiguration(XNode root) 方法解析,最后返回 Configuration 对象。

public Configuration parse() {
	if (parsed) {
		throw new BuilderException("Each XMLConfigBuilder can only be used once.");
	}
	parsed = true;
	parseConfiguration(parser.evalNode("/configuration"));
	return configuration;
}
private void parseConfiguration(XNode root) {
	try {
		// 解析<properties>节点
		propertiesElement(root.evalNode("properties"));
		// 解析<settings>节点
		Properties settings = settingsAsProperties(root.evalNode("settings"));
  		loadCustomVfs(settings);
		// 解析<typeAliases>节点
		typeAliasesElement(root.evalNode("typeAliases"));
		// 解析<plugins>节点
		pluginElement(root.evalNode("plugins"));
		// 解析<objectFactory>节点
		objectFactoryElement(root.evalNode("objectFactory"));
		objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
		// 解析<reflectorFactory>节点
		reflectorFactoryElement(root.evalNode("reflectorFactory"));
		settingsElement(settings);
		// 解析<environments>节点
		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);
	}
}

从上述代码中可以看到,XMLConfigBuilder 会依次解析配置文件中的 properties、settings、environments、typeAliases、plugins、mappers 等属性。
MyBatis 在初始化过程中处理 mybatis-config.xml 配置文件以及映射文件时,使用的是 DOM解析方式,并结合使用 XPath 解析 XML 配置文件。XPath 是一种为查询 XML 文档而设计的语言,它可以与 DOM 解析方式配合使用,实现对 XML 文档的解析。XPath 之于 XML 就好比 SQL 语言之于数据库。
从上面的源码中我们可以看到 MyBatis 使用 XPathParser 解析 XML文件,得到 XNode 对象,然后继续解析。而 XPathParser 类封装了 XPath、Document 和 EntityResolver,如图
XNode

主要是 XMLConfigBuilder 将 XML 配置文件的信息转换为 Document 对象,而 XML 配置定义文件 DTD 转换成 XMLMapperEntityResolver 对象,然后将封装到 XpathParser 对象中,XpathParser 的作用是提供根据 Xpath 表达式获取基本的 DOM 节点 Node 信息的操作,而解析 mappers 节点还使用到了前面提到了 XMLMapperBuilder。
代码如下:

public XMLConfigBuilder(Reader reader, String environment, Properties props) {
	this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props);
}

public XPathParser(Reader reader, boolean validation, Properties variables, EntityResolver entityResolver) {
	commonConstructor(validation, variables, entityResolver);
	this.document = createDocument(new InputSource(reader));
}

private Document createDocument(InputSource inputSource) {
	// important: this must only be called AFTER common constructor
	try {
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		factory.setValidating(validation);
		factory.setNamespaceAware(false);
		factory.setIgnoringComments(true);
		factory.setIgnoringElementContentWhitespace(false);
		factory.setCoalescing(false);
		factory.setExpandEntityReferences(true);

		DocumentBuilder builder = factory.newDocumentBuilder();
		builder.setEntityResolver(entityResolver);
		builder.setErrorHandler(new ErrorHandler() {
			@Override
			public void error(SAXParseException exception) throws SAXException {
				throw exception;
			}

			@Override
			public void fatalError(SAXParseException exception) throws SAXException {
				throw exception;
			}

			@Override
			public void warning(SAXParseException exception) throws SAXException {
			}
		});
	return builder.parse(inputSource);
	} catch (Exception e) {
		throw new BuilderException("Error creating document instance.  Cause: " + e, e);
	}
}

private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) {
	this.validation = validation;
	this.entityResolver = entityResolver;
	this.variables = variables;
	//利用XPathFactory创建一个新的xpath对象
	XPathFactory factory = XPathFactory.newInstance();
	this.xpath = factory.newXPath();
}
mapperElement 函数解析<mappers> 节点

我们可以看到解析的时候都是调用 XNode.eval*()方法,入参为 XPath 查询需要的信息,而XNode.eval*()系列方法是通过调用其封装的 XPathParser 对象的 eval*()方法实现的。这里我们再对解析 <mappers> 节点的mapperElement 方法深入分析一下。代码如下:

private void mapperElement(XNode parent) throws Exception {
	if (parent != null) {
		for (XNode child : parent.getChildren()) {
			//1.<package name="org.mybatis.builder"/> Register all interfaces in a package as mappers
			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");
			//2.<mapper resource="org/mybatis/builder/AuthorMapper.xml"/> Using classpath relative resources
			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());
				mapperParser.parse();
			//3.<mapper url="file:///var/mappers/AuthorMapper.xml"/> Using url fully qualified paths
			} 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();
			//4.<mapper class="org.mybatis.builder.AuthorMapper"/> Using mapper interface classes
			} 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.");
			}
		}
	}
}

从代码中我们可以看到 MyBatis 支持四种方式加载 mappers,首先解析 package 节点,若有则添加,没有则扫描mapper 节点, 获取 resource/url/mapperClass 属性,三个值只能有一个值是有值的,优先级 resource > url > mapperClass。如果 mapper 节点中的 resource 不为空,那么直接加载 resource 指向的 XXXMapper.xml 文件为字节流,通过 XMLMapperBuilder 解析 XXXMapper.xml,可以看到这里构建的 XMLMapperBuilde 还传入了 configuration ,所以之后肯定是会将 mapper 封装到 configuration 对象中去的。如果 url!=null ,那么通过 url 解析流程与上面 resource 一致。如果 mapperClass!=null ,那么通过加载类获取 mapper,添加到 configuration 中。最后如果都不满足,则直接抛异常,如果配置了两个或三个,直接抛异常。配置方式如下:

<mappers>
	<!-- 通过配置文件路径 -->
	<mapper resource="mapper/UserMapper.xml" ></mapper>
	<!-- 通过Java全限定类名 -->
	<mapper class="com.hycjack.mapper.UserMapper"/>
	<!-- 通过url 通常是mapper不在本地时用 -->
	<mapper url=""/>
	<!-- 通过包名 -->
	<package name="com.hycjack.mapper"/>
</mappers>

我们的配置文件会通过 XMLMapperBuilder 来进行解析,进入 XMLMapperBuilder 的构造函数和 parse 方法中看一下:

public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
	this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver()),
	configuration, resource, sqlFragments);
}

private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
	super(configuration);
	this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
	this.parser = parser;
	this.sqlFragments = sqlFragments;
	this.resource = resource;
}

public void parse() {
	//判断文件是否之前解析过
	if (!configuration.isResourceLoaded(resource)) {
		//解析mapper文件节点
		configurationElement(parser.evalNode("/mapper"));
		configuration.addLoadedResource(resource);
		//绑定Namespace里面的Class对象
		bindMapperForNamespace();
	}
	//重新解析之前解析不了的节点
	parsePendingResultMaps();
	parsePendingCacheRefs();
	parsePendingStatements();
}
XmlMapperBuilder 解析 mapper

可以看到最核心的就是 configurationElement 方法,入参为一个XNode, 而 XMLMapperBuilder 本身构造了一个参数 XPathParser,从这里我们可以发现 MyBatis 的 XML 解析离不开 XPathParser 和 XNode 这两个类,这两个类构成了 MyBatis 解析模块的核心,具体代码在 parsing 包下。而 configurationElement 方法具体代码如下:

/**
  * 解析mapper文件里面的节点,拿到里面配置的配置项 最终封装成一个MapperedStatemanet
 **/
private void configurationElement(XNode context) {
	try {
		//获取命名空间 namespace
		String namespace = context.getStringAttribute("namespace");
		if (namespace == null || namespace.equals("")) {
			//如果namespace为空则抛出异常
			throw new BuilderException("Mapper's namespace cannot be empty");
		}
		builderAssistant.setCurrentNamespace(namespace);
		//解析缓存节点
		cacheRefElement(context.evalNode("cache-ref"));
		cacheElement(context.evalNode("cache"));
        
		//解析parameterMap和resultMap
		parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      		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);
	}
 }

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);
		}
	}
}

这里找了一张图,便于对照代码和 XML 配置。如下所示:
Mapper

XMLStatementBuilder 的 parseStatementNode 方法我们不在这篇中展开,我们只需知道 MyBatis 做的就是,先判断这个节点是用来干什么的,然后再获取这个节点的 id、parameterType、resultType 等属性,封装成一个MappedStatement 对象,由于这个对象很复杂,所以 MyBatis 使用了构造者模式来构造这个对象,最后当 MappedStatement 对象构造完成后,将其封装到 Configuration 对象中。代码执行至此,基本就结束了对 Configuration 对象的构建。

总结

从上面的分析我们了解了 MyBatis 加载 mappers 的流程,知道其支持四种方式加载 mappers 的配置方式,以及对其解析 XML 的解析器模块有了一定了解,主要是 XPathParser 和 XNode 这两个类的使用,也知道了 MyBatis 是使用DOM 和 XPath 方式解析XML的。

参考:

更新时间:2020-12-13 13:33:14

本文由 HycJack 创作,如果您觉得本文不错,请随意赞赏
采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
原文链接:https://www.hycjack.cn/archives/myabtis02md
最后更新:2020-12-13 13:33:14

评论

Your browser is out of date!

Update your browser to view this website correctly. Update my browser now

×