MyBatis 是现在比较流行的 ORM 框架,得益于它简单易用,虽然增加了开发者的一些操作,但是带来了设计和使用上的灵活,得到广泛的使用。之前的一篇文章MyBatis 初始化原理中我们已经知道了 MyBatis 的初始化流程,其实就是构造 Configuration 对象的过程,主要分为系统环境参数的初始化和 Mapper 映射的初始化,然后以 Configuration 对象作为 SqlSessionFactory 的参数,最终获取到 SqlSession 进行数据库的操作。流程如下图所示。
从上图我们可以看到最主要的就是 XMLConfigBuilder 创建,以及调用 XMLConfigBuilder 的 parse() 方法得到 Configuration 对象,本文针对这两个过程,对照 MyBatis 源码进行详细分析。
我们已经知道了 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);
……
}
当有了 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,如图
主要是 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();
}
我们可以看到解析的时候都是调用 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();
}
可以看到最核心的就是 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 配置。如下所示:
XMLStatementBuilder 的 parseStatementNode 方法我们不在这篇中展开,我们只需知道 MyBatis 做的就是,先判断这个节点是用来干什么的,然后再获取这个节点的 id、parameterType、resultType 等属性,封装成一个MappedStatement 对象,由于这个对象很复杂,所以 MyBatis 使用了构造者模式来构造这个对象,最后当 MappedStatement 对象构造完成后,将其封装到 Configuration 对象中。代码执行至此,基本就结束了对 Configuration 对象的构建。
从上面的分析我们了解了 MyBatis 加载 mappers 的流程,知道其支持四种方式加载 mappers 的配置方式,以及对其解析 XML 的解析器模块有了一定了解,主要是 XPathParser 和 XNode 这两个类的使用,也知道了 MyBatis 是使用DOM 和 XPath 方式解析XML的。
参考:
本文由 HycJack 创作,如果您觉得本文不错,请随意赞赏
采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
原文链接:https://www.hycjack.cn/archives/myabtis02md
最后更新:2020-12-13 13:33:14
Update your browser to view this website correctly. Update my browser now