MyBatis 原理浅析 2 ——配置解析

前言

在前文《MyBatis 原理浅析——基本原理》一文中,简要分析了 MyBatis 的技术原理,主要是 SqlSession 和 Mapper 的相关实现原理。本文重点分析 MyBatis 的配置解析过程,从 XML 文件提取配置到 Configuration 类。

XML解析涉及到的类

XML 解析主要涉及以下几个类:XMLConfigBuilder、XMLMapperBuilder、BaseBuilder、XNode、Configuration、XPathParser 和 Configuration 中的配置类。各个类型的关系可以简单用下图描述。在 SqlSessionFactoryBuilder 类中调用方法解析 XML 时使用的是 XMLConfigBuilder 类,XMLConfigBuilder 类的 parse 方法是 XML 解析的入口,解析完成后返回配置类型 Configuration。BaseBuilder 类是抽象类,提供了 typeAlias、typeHandler 配置的处理方法,也是 XMLConfigBuilder 类和 XMLMapperBuilder 类的父类。XNode 类用于存储解析过程中遇到的节点,XPathParser 类封装了 XPath 相关的操作,如 XML文件的载入、节点的解析等。Configuration 类存储了解析出来的所有属性。

MyBatis 原理浅析 2 ——配置解析

XML 解析准备阶段

XML 文件的解析通过在 SqlSessionFactoryBuilder 类中创建 XMLConfigBuilder 类并调用其 parse 方法开始。

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
   try {
     XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
     return build(parser.parse());
   } catch (Exception e) {
     throw ExceptionFactory.wrapException("Error building SqlSession.", e);
   } finally {
     ErrorContext.instance().reset();
     try {
       inputStream.close();
     } catch (IOException e) {
       // Intentionally ignore. Prefer previous error.
     }
   }
}

在 XMLConfigBuilder 初始化时,会创建 XPathParser 类的实例,然后创建 Configuration 类的实例,保存各个参数以供后续使用。

public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
   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;
}

在 XPathParser 类的初始化过程中,会创建 XPath 实例,然后读入 XML 文件创建 Document 对象,完成 XML 文件解析的准备工作。

public XPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver) {
   commonConstructor(validation, variables, entityResolver);
   this.document = createDocument(new InputSource(inputStream));
}
 
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();
}
 
private Document createDocument(InputSource inputSource) {    
   try {
     DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
     ...... //初始化 factory
     DocumentBuilder builder = factory.newDocumentBuilder();      
     ...... //初始化 builder
     return builder.parse(inputSource);
   } catch (Exception e) {
     throw new BuilderException("Error creating document instance.  Cause: " + e, e);
   }
}

XML 配置解析过程

XML 配置解析是从 XMLConfigBuilder 的 parse() 方法开始的。在 parse() 方法中,首先判断是否已经解析过,如果重复解析则抛出异常,然后解析出 XML 文件的根节点 configuration,再从根节点中依次解析出 properties、settings、typeAliases、plugins、objectFactory、objectWrapperFactory、reflectorFactory、environments、databaseIdProvider、typeHandlers、mappers节点的内容,解析完成后返回 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 {
     //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);
   }
}

解析过程可以分为以下几步:

1、解析 properties :读取 XML 中的名称键值对并保存,如果引用了 properties 文件或传入了 properties 配置,则会用新的 properties 值替换 XML 中的配置。

2、解析 settings:按照 Properties 类型存储。

3、解析 typeAliases:typeAliases 定义了类的别名,解析后需要根据 type 的值加载相应的 class,并注册到 typeAliasRegistry 中。

4、解析 plugins:加载 Interceptor 实现类,创建对象,并存入 configuration 中。

5、解析 objectFactory:加载 ObjectFactory 实现类,创建对象,并存入 configuration 中。

6、解析 objectWrapperFactory:加载 ObjectWrapperFactory 实现类,创建对象,并存入 configuration 中。

7、解析 reflectorFactory:加载 ReflectorFactory 实现类,创建对象,并存入 configuration 中。

8、解析 environments:根据配置的默认环境 ID,加载环境配置,创建 Environment 对象,并存入 configuration 中。Environment 对象包含了数据源和事务管理器对象。

9、解析 databaseIdProvider:加载 DatabaseIdProvider 实现类,读取属性配置,创建对象,获取数据源的 databaseId 并存入 configuration。

10、解析 typeHandlers:解析属性配置,加载实现类,创建对象,并注册到 typeHandlerRegistry 中。

11、解析 mappers:解析 mapper 节点,如果引用了 XML 文件则载入文件并创建 XMLMapperBuilder 对象进行解析,如果引用了包或类则添加到 configuration 中。注册 Mapper 到 MapperRegistry 时,会创建 Mapper 接口的代理实现工厂 MapperProxyFactory,扫描 Mapper 接口的注解并存入 configuration 。

Mapper 文件的解析

mapper XML 文件的解析在 XMLMapperBuilder 中实现。与 XMLConfigBuilder 一样,XMLMapperBuilder 初始化时也会创建 XPathParser 类的实例,但会使用参数传入的 configuration。

XML 文件的解析是在 parse 方法中完成的。首先判断是否已经加载过,如果没有加载过则获取 XML 的根节点 mapper 并开始解析,解析完成后根据命名空间的配置加载相应的 Mapper 接口并添加到 configuration 中,最后加载未完成加载的 ResultMap、CacheRef 和 Statement。

public void parse() {
   if (!configuration.isResourceLoaded(resource)) {
     configurationElement(parser.evalNode("/mapper"));
     configuration.addLoadedResource(resource);
     bindMapperForNamespace();
   }
   parsePendingResultMaps();
   parsePendingCacheRefs();
   parsePendingStatements();
}

configurationElement 方法实现了对 mapper 子节点的解析,如下所示,读取了 namespace、cache-ref、cache、parameterMap、resultMap、sql、select、insert、update、delete 等节点和属性。如果引用的类型不在此 XML 中,并且配置中也获取不到,则会加入未完成列表,在解析下个 XML 文件时再次尝试加载。

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. Cause: " + e, e);
   }
}

mapper 文件的解析细节比较复杂,后文再深入分析。

每周 3 篇学习笔记或技术总结,面向有一定基础的 Java 程序员,内容涉及 Java 进阶、虚拟机、MySQL、NoSQL、分布式计算、开源框架等多个领域。关注作者或微信公众号 backend-develop 第一时间获取最新内容。

MyBatis 原理浅析 2 --配置解析

相关推荐