coding……
但行好事 莫问前程

Mybatis源码解读『配置文件解析』

上篇文章中,我们介绍了如何通过JDBC和Mybatis来连接并操作数据库。通过对Mybatis连接数据库的介绍,我们发现,Mybatis使用的核心是那份配置文件mybatis-config.xml,这是我们使用Mybatis的入口。本篇文章,我们就来看一下,该配置文件是如何解析并生效的。

还有一点,我们需要提前明确。就是我们使用JDBC时,大致存在如下步骤:

  1. 加载驱动程序
  2. 获得数据库连接
  3. 创建Statement
  4. 执行Statement

而使用Mybatis时,对于开发人员来说就不用关心获取数据库连接以及通过连接获取Statement等操作了。那么是不是Mybatis废弃了这些操作,使用了其它“高端”的操作呢?

答案是否定的,Mybatis本质上说就是对JDBC的包装。通过Mybatis可以让开发人员不用过多关注与数据库连接,以及一些结果映射的工作上,而是让开发人员可以集中于SQL本身,简化JDBC操作,Mybatis本质上也是使用JDBC操作数据库的。之后的文章,我们会陆续去介绍Mybatis使用JDBC操作数据库的原理。

1. 配置文件解析入口XMLConfigBuilder

我们上篇文章中,我们是通过如下方式使用的Mybatis。

public static void main(String[] args) throws IOException {
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    SqlSession sqlSession = sqlSessionFactory.openSession();
    try {
        UserInfoMapper userInfoMapper = sqlSession.getMapper(UserInfoMapper.class);
        // 使用Mapper中的方法操作数据库
    } finally {
        sqlSession.close();
    }
}

不难发现,对于我们的配置文件mybatis-config.xml,我们首先将配置文件转换成InputStream,然后将InputStream作为参数传给了SqlSessionFactoryBuilder的build方法。通过调用SqlSessionFactoryBuilder的build方法,我们得到了一个SqlSessionFactory对象(生产SqlSession的Factory)。所以配置文件的解析肯定在SqlSessionFactoryBuilder中。我们继续跟进来看一下SqlSessionFactoryBuilder的build方法。

public SqlSessionFactory build(InputStream inputStream) {
  return build(inputStream, null, null);
}

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
  try {
    // 1. 创建一个配置文件解析器XMLConfigBuilder
    XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
    // 2. 调用配置文件解析器的parse方法,将配置文件解析为Configuration对象
    // 调用build方法,通过Configuration对象构建DefaultSqlSessionFactory
    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.
    }
  }
}

public SqlSessionFactory build(Configuration config) {
  return new DefaultSqlSessionFactory(config);
}

所以配置文件解析,其实就是使用XMLConfigBuilder将配置文件解析为Configuration对象。XMLConfigBuilder就是用于解析Mybatis配置文件的,继承于BaseBuilder。BaseBuilder有很多子类实现,每一种子类实现都对应一种xml或者xml中的节点解析。

比如XMLConfigBuilder用于解析解析Mybatis配置文件生成Configuration对象,XMLMapperBuilder用于解析Mybatis配置文件中的”/mapper”节点,并将”/mapper”节点的子节点解析到Configuration对应的成员变量中。

首先来看一下XMLConfigBuilder的构造方法。

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

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

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

最终XMLConfigBuilder的parser成员变量类型为XPathParser,通过调用XPathParser构造函数生成的。同时为其父类BaseBuilder的configuration成员变量传入一个通过调用Configuration默认构造函数获得的对象。

public Configuration(Environment environment) {
  this();
  this.environment = environment;
}

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

  typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);

  typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
  typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);

  typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
  typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
  typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
  typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
  typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
  typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
  typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);

  typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
  typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);

  languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
  languageRegistry.register(RawLanguageDriver.class);
}

Configuration类的默认构造函数,核心就是注册一些别名,这些别名可以再mapper.xml配置文件中使用,我们也可以在mapper.xml中自定义一些别名,这部分以及typeAliasRegistry的作用我们后面再介绍。

这个图可以比较清晰地说明这三个类之间的关系,Configuration其实就是Mybatis配置文件最终解析生成对象的类型,所以包含了很多成员变量,这些成员变量都在Mybatis配置文件中有对应的标签;而XMLConfigBuilder是用来生成Configuration对象的,继承自BaseBuilder。

介绍完XMLConfigBuilder的构造函数后,我们进入核心方法的介绍——parse()方法,我们上面介绍过XMLConfigBuilder的parse方法,完成了xml配置文件到Configuration对象的解析。所以接下来,我们先来看一下一份完整的需要解析的Mybatis配置文件中有哪些内容:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <!-- 引入外部配置文件,也可以自定义一些属性 -->
    <properties resource="dataSource.properties">
        <property name="pKey" value="pValue"/>
    </properties>

    <!-- 类型别名 -->
    <typeAliases>
       <!-- 在用到UserInfo类型的时候,可以直接使用别名,不需要再输入UserInfo类的全路径 -->
       <typeAlias type="com.zhuoli.service.mybatis.explore.model.UserInfo" alias="userInfo"/>
    </typeAliases>

    <!-- 类型处理器 -->
    <typeHandlers>
        <!-- 类型处理器的作用是完成JDBC类型和java类型的转换,mybatis默认已经定义了很多类型处理器,正常无需自定义-->
    </typeHandlers>
    
    <!-- 对象工厂 -->
    <!-- mybatis创建结果对象的新实例时,会通过对象工厂来完成,mybatis中定义了默认的对象工厂,正常无需配置 -->
    <objectFactory type=""></objectFactory>
    
    <!-- 插件 -->
    <plugins>
        <!-- 可以通过plugin标签,添加拦截器,比如分页拦截器 -->
       <plugin interceptor="com.github.pagehelper.PageInterceptor">
            <!-- 分页合理化参数,默认文false;pageNum <= 0,查询第一页;pageNum > 总页数,查询最后一页-->
            <property name="reasonable" value="true"/>
        </plugin>
    </plugins>
    
    <!-- 全局配置参数,Mybatis中定义了默认值,这里无定制需求,也可以不定义 -->
    <settings>   
        <setting name="cacheEnabled" value="false" />   
        <setting name="useGeneratedKeys" value="true" /> <!-- 是否自动生成主键 -->
        <setting name="defaultExecutorType" value="REUSE" />   
        <setting name="lazyLoadingEnabled" value="true"/> <!-- 延迟加载标识 -->
        <setting name="aggressiveLazyLoading" value="true"/> <!--有延迟加载属性的对象是否延迟加载 -->
        <setting name="multipleResultSetsEnabled" value="true"/> <!-- 是否允许单个语句返回多个结果集 -->
        <setting name="useColumnLabel" value="true"/> <!-- 使用列标签而不是列名 -->
        <setting name="autoMappingBehavior" value="PARTIAL"/> <!-- 指定mybatis如何自动映射列到字段属性; NONE:自动映射;PARTIAL:只会映射结果没有嵌套的结果;FULL:可以映射任何复杂的结果 -->
        <setting name="defaultExecutorType" value="SIMPLE"/> <!-- 默认执行器类型 -->
        <setting name="defaultFetchSize" value=""/>
        <setting name="defaultStatementTimeout" value="5"/> <!-- 驱动等待数据库相应的超时时间,单位是秒-->
        <setting name="safeRowBoundsEnabled" value="false"/> <!-- 是否允许使用嵌套语句RowBounds -->
        <setting name="safeResultHandlerEnabled" value="true"/>
        <setting name="mapUnderscoreToCamelCase" value="false"/> <!-- 下划线列名是否自动映射到驼峰属性:如user_id映射到userId -->
        <setting name="localCacheScope" value="SESSION"/> <!-- 本地缓存(session是会话级别) -->
        <setting name="jdbcTypeForNull" value="OTHER"/> <!-- 数据为空值时,没有特定的JDBC类型的参数的JDBC类型 -->
        <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/> <!-- 指定触发延迟加载的对象的方法 -->
        <setting name="callSettersOnNulls" value="false"/> <!--如果setter方法或map的put方法,如果检索到的值为null时,数据是否有用  -->
        <setting name="logPrefix" value="XXXX"/> <!-- mybatis日志文件前缀字符串 -->
        <setting name="logImpl" value="SLF4J"/> <!-- mybatis日志的实现类 -->
        <setting name="proxyFactory" value="CGLIB"/> <!-- mybatis代理工具 -->
    </settings>  

    <!-- 环境配置 -->
    <environments default="default">
        <environment id="default">
            <transactionManager type="JDBC"></transactionManager>
            <dataSource type="POOLED">
                <property name="driver" value="${dataSource.driver}"/>
                <property name="url" value="${dataSource.url}"/>
                <property name="username" value="${dataSource.username}"/>
                <property name="password" value="${dataSource.password}"/>
            </dataSource>
        </environment>
    </environments>

    <!-- mapper文件映射配置 -->
    <mappers>
        <mapper resource="mapper/user_info.xml"/>
    </mappers>
</configuration>

可以看到,根节点是<configuration>标签,标签下存在很多子标签。接下来开始介绍XMLConfigBuilder的parse方法。

public Configuration parse() {
  if (parsed) {
    throw new BuilderException("Each XMLConfigBuilder can only be used once.");
  }
  parsed = true;
  parseConfiguration(parser.evalNode("/configuration"));
  return configuration;
}

方法中会使用上面初始化的XPathParser的evalNode方法获取配置文件中<configuration>节点对应的XNode,其实也比较简单,我们先来介绍一下XPathParser取xml节点的逻辑。

public XNode evalNode(String expression) {
  return evalNode(document, expression);
}

public XNode evalNode(Object root, String expression) {
  Node node = (Node) evaluate(expression, root, XPathConstants.NODE);
  if (node == null) {
    return null;
  }
  return new XNode(this, node, variables);
}

private Object evaluate(String expression, Object root, QName returnType) {
  try {
    return xpath.evaluate(expression, root, returnType);
  } catch (Exception e) {
    throw new BuilderException("Error evaluating XPath.  Cause: " + e, e);
  }
}

这里的document其实是XPathParser的成员变量,通过xml配置文件流创建的,类型为org.w3c.dom.Document。而最终调用的xpath.evaluate,其实就是使用JDK中自带的工具类XPath解析xml的指定节点到具体Java类型,这里解析为XNode类型。所以我们通过XPathParser的evalNode方法,传入了”configuration”,获取到<configuration>节点对应的XNode。

接下来继续来看parseConfiguration方法。

private void parseConfiguration(XNode root) {
  try {
    
    // 1. 解析<configuration>节点下<properties>配置
    propertiesElement(root.evalNode("properties"));
    // 2. 解析<configuration>节点下<settings>配置
    Properties settings = settingsAsProperties(root.evalNode("settings"));
    // 从settings中,加载vfs
    loadCustomVfs(settings);
    // 从settings中,加载logImpl属性
    loadCustomLogImpl(settings);
    // 3. 解析<configuration>节点下<typeAliases>配置
    typeAliasesElement(root.evalNode("typeAliases"));
    // 4. 解析<configuration>节点下<plugins>配置
    pluginElement(root.evalNode("plugins"));
    // 5. 解析<configuration>节点下<objectFactory>配置
    objectFactoryElement(root.evalNode("objectFactory"));
    // 6. 解析<configuration>节点下<objectWrapperFactory>配置
    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
    // 7. 解析<configuration>节点下<reflectorFactory>配置
    reflectorFactoryElement(root.evalNode("reflectorFactory"));
    // 8. 将settings中的内容设置到Configuration对象中
    settingsElement(settings);
    // 9. 解析<configuration>节点下<environments>配置
    environmentsElement(root.evalNode("environments"));
    // 10. 解析<configuration>节点下<databaseIdProvider>配置
    databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    // 11. 解析<configuration>节点下<typeHandlers>配置
    typeHandlerElement(root.evalNode("typeHandlers"));
    // 12. 解析<configuration>节点下<mappers>配置
    mapperElement(root.evalNode("mappers"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  }
}

不难发现,该方法其实就是Mybatis配置文件解析的入口。下面我们就来重点介绍一下如上parseConfiguration中调用的一些方法。

2. 配置文件解析

2.1 properties解析

<configuration>节点下的<properties>节点,主要用于配置一些KV对,如下:

<!-- 引入外部配置文件,也可以自定义一些属性 -->
<properties resource="dataSource.properties">
    <property name="pKey" value="pValue"/>
</properties>

解析上述这段<properties>内容的方法是propertiesElement。

private void propertiesElement(XNode context) throws Exception {
  if (context != null) {
    // 1. 获取<properties>节点下子节点<property>配置的KV对
    Properties defaults = context.getChildrenAsProperties();

    // 2. 获取<properties>节点的resource|url属性
    String resource = context.getStringAttribute("resource");
    String url = context.getStringAttribute("url");
    if (resource != null && url != null) {
      throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.");
    }

    // 3. 从resource|url中获取KV对,并添加到defaults中
    if (resource != null) {
      defaults.putAll(Resources.getResourceAsProperties(resource));
    } else if (url != null) {
      defaults.putAll(Resources.getUrlAsProperties(url));
    }

    // 4. 从configuration中获取KV对存储的成员variables值
    Properties vars = configuration.getVariables();
    // 如果configuration中已经存在KV对,则合并到<properties>解析的KV结果中
    if (vars != null) {
      defaults.putAll(vars);
    }
    // 5. 将合并后的结果设置到XPathParser和Configuration的variables成员中
    parser.setVariables(defaults);
    configuration.setVariables(defaults);
  }
}
public Properties getChildrenAsProperties() {
  Properties properties = new Properties();
  for (XNode child : getChildren()) {
    String name = child.getStringAttribute("name");
    String value = child.getStringAttribute("value");
    if (name != null && value != null) {
      properties.setProperty(name, value);
    }
  }
  return properties;
}

public List<XNode> getChildren() {
  List<XNode> children = new ArrayList<>();
  NodeList nodeList = node.getChildNodes();
  if (nodeList != null) {
    for (int i = 0, n = nodeList.getLength(); i < n; i++) {
      Node node = nodeList.item(i);
      if (node.getNodeType() == Node.ELEMENT_NODE) {
        children.add(new XNode(xpathParser, node, variables));
      }
    }
  }
  return children;
}

解析properties主要分三个步骤:

  1. 解析properties节点的子节点,并将解析结果设置到Properties对象中
  2. <properties>节点的resource和url属性不为空,则从文件系统或网络中读取属性配置
  3. 将解析出的Properties对象设置到XPathParser和Configuration对象中

需要注意的是,propertiesElement方法是先解析properties节点的子节点内容,再从文件系统或者网络读取属性配置,会存在同名属性覆盖的问题,也就是从文件系统,或者网络中读取到的属性及属性值会覆盖掉properties子节点中同名的属性和及值。

2.2 settings解析

<configuration>节点下的<settings>节点,主要用于对Mybatis进行一些设置,如下:

<!-- 全局配置参数,Mybatis中定义了默认值,这里无定制需求,也可以不定义 -->
<settings>   
    <setting name="cacheEnabled" value="false" />   
    <setting name="useGeneratedKeys" value="true" /> <!-- 是否自动生成主键 -->
    <setting name="defaultExecutorType" value="REUSE" />   
    <setting name="lazyLoadingEnabled" value="true"/> <!-- 延迟加载标识 -->
    <setting name="aggressiveLazyLoading" value="true"/> <!--有延迟加载属性的对象是否延迟加载 -->
    <setting name="multipleResultSetsEnabled" value="true"/> <!-- 是否允许单个语句返回多个结果集 -->
    <setting name="useColumnLabel" value="true"/> <!-- 使用列标签而不是列名 -->
    <setting name="autoMappingBehavior" value="PARTIAL"/> <!-- 指定mybatis如何自动映射列到字段属性; NONE:自动映射;PARTIAL:只会映射结果没有嵌套的结果;FULL:可以映射任何复杂的结果 -->
    <setting name="defaultExecutorType" value="SIMPLE"/> <!-- 默认执行器类型 -->
    <setting name="defaultFetchSize" value=""/>
    <setting name="defaultStatementTimeout" value="5"/> <!-- 驱动等待数据库相应的超时时间,单位是秒-->
    <setting name="safeRowBoundsEnabled" value="false"/> <!-- 是否允许使用嵌套语句RowBounds -->
    <setting name="safeResultHandlerEnabled" value="true"/>
    <setting name="mapUnderscoreToCamelCase" value="false"/> <!-- 下划线列名是否自动映射到驼峰属性:如user_id映射到userId -->
    <setting name="localCacheScope" value="SESSION"/> <!-- 本地缓存(session是会话级别) -->
    <setting name="jdbcTypeForNull" value="OTHER"/> <!-- 数据为空值时,没有特定的JDBC类型的参数的JDBC类型 -->
    <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/> <!-- 指定触发延迟加载的对象的方法 -->
    <setting name="callSettersOnNulls" value="false"/> <!--如果setter方法或map的put方法,如果检索到的值为null时,数据是否有用  -->
    <setting name="logPrefix" value="XXXX"/> <!-- mybatis日志文件前缀字符串 -->
    <setting name="logImpl" value="SLF4J"/> <!-- mybatis日志的实现类 -->
    <setting name="proxyFactory" value="CGLIB"/> <!-- mybatis代理工具 -->
</settings>

解析上述<settings>节点的方法是settingsAsProperties。

private Properties settingsAsProperties(XNode context) {
  if (context == null) {
    return new Properties();
  }
  // 1. 获取子节点的name -> value对,设置到Properties对象中
  Properties props = context.getChildrenAsProperties();
  // 2. 创建Configuration类的“元信息”对象
  MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
  // 3. 遍历上述第1步获取的Properties对象,检查Configuration类中是否存在相关set方法,如果不存在抛异常
  for (Object key : props.keySet()) {
    if (!metaConfig.hasSetter(String.valueOf(key))) {
      throw new BuilderException("The setting " + key + " is not known.  Make sure you spelled it correctly (case sensitive).");
    }
  }
  // 4. 返回<settings>解析出的Properties对象
  return props;
}

关于MetaClass检查,其实就是检查<setttings>子节点<setting>的name属性对应的值,必须在Configuration类有相应的Setter,比如设置了一个属性useGenerateKeys,那么必须在Configuration类中有setUseGenerateKeys方法才行。

settingsAsProperties方法,只是将xml配置文件中<settings>节点解析为Properties对象,最终起作用,肯定还需要将该Properties对象设置回Configuration对象中,而设置回Configuration的过程就在settingsElement方法中。

private void settingsElement(Properties props) {
  configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
  configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
  configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
  configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
  configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
  configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), false));
  configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true));
  configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true));
  configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false));
  configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
  configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null));
  configuration.setDefaultFetchSize(integerValueOf(props.getProperty("defaultFetchSize"), null));
  configuration.setDefaultResultSetType(resolveResultSetType(props.getProperty("defaultResultSetType")));
  configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
  configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false));
  configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
  configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
  configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
  configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
  configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
  configuration.setDefaultEnumTypeHandler(resolveClass(props.getProperty("defaultEnumTypeHandler")));
  configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
  configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"), true));
  configuration.setReturnInstanceForEmptyRow(booleanValueOf(props.getProperty("returnInstanceForEmptyRow"), false));
  configuration.setLogPrefix(props.getProperty("logPrefix"));
  configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
  configuration.setShrinkWhitespacesInSql(booleanValueOf(props.getProperty("shrinkWhitespacesInSql"), false));
  configuration.setDefaultSqlProviderType(resolveClass(props.getProperty("defaultSqlProviderType")));
}

可以看到,设置每个属性时,都有默认值。所以我们的xml配置文件中没有自定义配置<settings>节点,在settingsAsProperties方法中解析出一个空的Properties对象,Mybatis也可以进行默认设置。

2.3 typeAliases解析

MyBatis中,可以为我们自定义的类定义一个别名。这样在使用的时候,我们只需要用别名即可,无需再把全限定的类名写出来。

在MyBatis中,有两种方式进行别名配置。第一种是仅配置包名,让MyBatis去扫描包中的类型,并根据类型得到相应的别名,如下:

<typeAliases>
    <package name="com.zhuoli.service.mybatis.explore.model"/>
</typeAliases>

第二种方式是通过手动的方式,明确为某个类型配置别名,如下:

<!-- 类型别名 -->
<typeAliases>
   <!-- 在用到UserInfo类型的时候,可以直接使用别名,不需要再输入UserInfo类的全路径 -->
   <typeAlias type="com.zhuoli.service.mybatis.explore.model.UserInfo" alias="userInfo"/>
</typeAliases>

解析上述<typeAliases>节点的方法是typeAliasesElement。

private void typeAliasesElement(XNode parent) {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      // 1. 从指定的包中解析别名和类型的映射
      if ("package".equals(child.getName())) {
        String typeAliasPackage = child.getStringAttribute("name");
        configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
      } else {
        //2. 从typeAlias节点中解析别名和类型的映射
        // 获取<typeAlias>节点alias和type属性值,alias不是必填项,可为空
        String alias = child.getStringAttribute("alias");
        String type = child.getStringAttribute("type");
        try {
          // 3. 加载type对应的类型
          Class<?> clazz = Resources.classForName(type);
          // 4. 注册别名到类型的映射
          if (alias == null) {
            typeAliasRegistry.registerAlias(clazz);
          } else {
            typeAliasRegistry.registerAlias(alias, clazz);
          }
        } catch (ClassNotFoundException e) {
          throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
        }
      }
    }
  }
}

2.3.1 从指定的包中解析别名和类型的映射

public void registerAliases(String packageName) {
  registerAliases(packageName, Object.class);
}

public void registerAliases(String packageName, Class<?> superType) {
  ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
  // 1. 查找包下的父类为Object.class的类,将查找结果缓存到resolverUtil的内部集合中
  resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
  // 2. 获取包下所有的查找结果
  Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
  // 3. 遍历查找结果,注册别名和类型
  for (Class<?> type : typeSet) {
    // 忽略匿名类,接口,内部类
    if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
      registerAlias(type);
    }
  }
}

2.3.2  从typeAlias节点中解析并注册别名

private final Map<String, Class<?>> typeAliases = new HashMap<>();

public void registerAlias(Class<?> type) {
  // 1. 获取全路径类名的简称
  String alias = type.getSimpleName();
  // 2. 如果类上存在@Alias注解,则从注解中获取简称
  Alias aliasAnnotation = type.getAnnotation(Alias.class);
  if (aliasAnnotation != null) {
    alias = aliasAnnotation.value();
  }
  // 3. 调用registerAlias重载方法注册别名和类型
  registerAlias(alias, type);
}

public void registerAlias(String alias, Class<?> value) {
  if (alias == null) {
    throw new TypeException("The parameter alias cannot be null");
  }
  // 1. 将别名转成小写
  String key = alias.toLowerCase(Locale.ENGLISH);
  // 2. 如果typeAliases已经存在某个名称的映射,并且类型跟要注册的类型相等,抛异常
  if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) {
    throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'.");
  }
  // 3. 注册别名和类型到typeAliases
  typeAliases.put(key, value);
}

2.4 plugins解析

插件是MyBatis提供的一个拓展机制,通过插件机制我们可在SQL执行过程中的某些点上做一些自定义操作,比如分页插件,在SQL执行之前动态拼接分页语句。插件的配置如下:

<!-- 插件 -->
<plugins>
    <!-- 可以通过plugin标签,添加拦截器,比如分页拦截器 -->
   <plugin interceptor="com.github.pagehelper.PageInterceptor">
        <!-- 分页合理化参数,默认文false;pageNum <= 0,查询第一页;pageNum > 总页数,查询最后一页-->
        <property name="reasonable" value="true"/>
    </plugin>
</plugins>

解析上述<plugins>节点的方法是typeAliasesElement。

private void pluginElement(XNode parent) throws Exception {
  if (parent != null) {
    // 遍历<plugins>节点下所有的<plugin>节点
    for (XNode child : parent.getChildren()) {
      // 1. 获取<plugin>节点的interceptor属性(plugin类的全限定名)
      String interceptor = child.getStringAttribute("interceptor");
      // 2. 获取<plugin>节点下的子节点name-value对,生成properties
      Properties properties = child.getChildrenAsProperties();
      // 3. 反射实例化plugin拦截器
      Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
      // 4. 为拦截器设置属性
      interceptorInstance.setProperties(properties);
      // 5. 添加拦截器到Configuration中
      configuration.addInterceptor(interceptorInstance);
    }
  }
}

// Configuration
public void addInterceptor(Interceptor interceptor) {
  interceptorChain.addInterceptor(interceptor);
}

这里interceptorChainf为Configuration类成员变量,类型为org.apache.ibatis.plugin.InterceptorChain。

public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<>();

  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }

  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }

  public List<Interceptor> getInterceptors() {
    return Collections.unmodifiableList(interceptors);
  }

}

其实就是维护了一个拦截器集合。

2.5 environments解析

<environments>主要用于配置环境,比如我们有多个环境,开发环境,预发环境,线上环境,每个环境对应的Mysql资源是不一样的,这时候我们可以通过<environments>配置多套环境的Mysql资源。不用在发布到不同环境之前,修改Mysql连接。其实<environments>也主要是用于配置数据源和事务管理器,如下:

<!-- 环境配置 -->
<environments default="default">
    <environment id="default">
        <transactionManager type="JDBC"></transactionManager>
        <dataSource type="POOLED">
            <property name="driver" value="${dataSource.driver}"/>
            <property name="url" value="${dataSource.url}"/>
            <property name="username" value="${dataSource.username}"/>
            <property name="password" value="${dataSource.password}"/>
        </dataSource>
    </environment>
</environments>

解析上述<environments>节点的方法是environmentsElement。

private void environmentsElement(XNode context) throws Exception {
  if (context != null) {
    // 1. 获取默认的JDBC环境名称
    if (environment == null) {
      environment = context.getStringAttribute("default");
    }
    // 2. 遍历<environments>标签下的每一个<environment>标签
    for (XNode child : context.getChildren()) {
      // 2.1 获取<environment>标签id属性
      String id = child.getStringAttribute("id");
      // 2.2 判断当前的<environment>是不是默认的JDBC环境,如果是则解析该环境下的TransactionFactory和DataSource
      if (isSpecifiedEnvironment(id)) {
        // 解析当前环境下配置的TransactionFactory
        TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
        // 解析当前环境下配置的DataSource
        DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
        DataSource dataSource = dsFactory.getDataSource();
        Environment.Builder environmentBuilder = new Environment.Builder(id)
            .transactionFactory(txFactory)
            .dataSource(dataSource);
        configuration.setEnvironment(environmentBuilder.build());
      }
    }
  }
}

2.2判断当前的<environment>是不是默认的JDBC环境,也就<environments>配置的default属性值。所以不难看出:

  1. <environments>标签下的default属性是一个必填属性(否则Configuration的transactionFactory和dataSource无法被赋值)
  2. 可以对多个环境配置多个的<environment>标签,MyBatis只会加载其中的一个<environment>(default配置的环境)

transactionManagerElement方法是根据<transactionManager>标签,获取事物管理器。上述配置文件中配置的是”JDBC”,那么实例化出来的是JdbcTransactionFactory(”JDBC到JdbcTransactionFactory的对应关系在Configuration对象的typeAliasRegistry中)。除了JdbcTransactionFactory之外,还有ManagedTransactionFactory和SpringManagedTransactionFactory,其中JdbcTransactionFactory和ManagedTransactionFactory是MyBatis原生支持的,SpringManagedTransactionFactory是Spring框架支持的。

dataSourceElement方法是根据<dataSource>标签,获取数据源工厂DataSourceFactory。本系列文章配置的是”POOLED”,那么实例化出来的是PooledDataSourceFactory(”POOLED到PooledDataSourceFactory的对应关系在Configuration对象的typeAliasRegistry中),除了PooledDataSourceFactory之外,还有UnpooledDataSourceFactory和JndiDataSourceFactory,都是MyBatis原生支持的。

getDataSource方法,根据DataSourceFactory获取DataSource,在MyBatis中根据配置分三种类型:

  • PooledDataSourceFactory对应的DataSource是PooledDataSource
  • UnpooledDataSourceFactory对应的DataSource是UnpooledDataSource
  • JndiDataSourceFactory对应的DataSource要去JNDI服务上去找

2.6 mappers解析

<mappers>节点用于配置sql映射文件,一般有如下几种配置方式:

<!-- mapper文件映射配置 -->
<mappers>
    <package name="com.zhuoli.service.mybatis.explore.mapper"/>
</mappers>
<!-- mapper文件映射配置 -->
<mappers>
    <mapper resource="mapper/user_info.xml"/>
</mappers>
<!-- mapper文件映射配置 -->
<mappers>
    <mapper url="/Users/chenhao/Documents/IdeaProject/mybatis-explore/src/main/resources/mapper/user_info.xml"/>
</mappers>
<!-- mapper文件映射配置 -->
<mappers>
    <mapper class="com.zhuoli.service.mybatis.explore.mapper.UserInfoMapper"/>
</mappers>

解析上述<environments>节点的方法是mapperElement。

private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    // 遍历<mappers>节点下的子节点
    for (XNode child : parent.getChildren()) {
      // 如果子节点为<package>
      if ("package".equals(child.getName())) {
        String mapperPackage = child.getStringAttribute("name");
        configuration.addMappers(mapperPackage);
      } else {
        // 否则子节点为<mapper>,则获取子节点<mapper>的"resource"、"url"、"class"属性,三个属性只能定义其中的一个
        String resource = child.getStringAttribute("resource");
        String url = child.getStringAttribute("url");
        String mapperClass = child.getStringAttribute("class");
        if (resource != null && url == null && mapperClass == null) {
          // "resource"不为空
          ErrorContext.instance().resource(resource);
          InputStream inputStream = Resources.getResourceAsStream(resource);
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
          mapperParser.parse();
        } else if (resource == null && url != null && mapperClass == null) {
          // "url"属性不为空
          ErrorContext.instance().resource(url);
          InputStream inputStream = Resources.getUrlAsStream(url);
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
          mapperParser.parse();
        } else if (resource == null && url == null && mapperClass != null) {
          // "class"属性不为空
          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.");
        }
      }
    }
  }
}

关于sql映射文件到的解析过程,我们放在下篇文章详细介绍,这里不过多阐述了。我们只需要明确,通过mapperElement,我们将sql映射文件解析到了Configuration类的某些成员变量中。而之后我们通过解析得到的Configuration对象构建出SqlsessionFactory,通过SqlsessionFactory可以构建出Sqlsession,而构建出的Sqlsession是包含我们解析的Configuration信息的(最终mybatis配置文件可以生效,就是因为解析得到的Configuration对象被传递到Sqlsession中)。

3. 构建SqlsessionFactory

回到SqlSessionFactoryBuilder类的build方法:

public SqlSessionFactory build(InputStream inputStream) {
  return build(inputStream, null, null);
}

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
  try {
    // 1. 创建一个配置文件解析器XMLConfigBuilder
    XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
    // 2. 调用配置文件解析器的parse方法,将配置文件解析为Configuration对象
    // 调用build方法,通过Configuration对象构建DefaultSqlSessionFactory
    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.
    }
  }
}

public SqlSessionFactory build(Configuration config) {
  return new DefaultSqlSessionFactory(config);
}

通过调用XMLConfigBuilder的parse方法,我们将mybatis配置文件解析为Configuration对象,之后我们调用XMLConfigBuilder中重载的build方法,构建出一个DefaultSqlSessionFactory对象。

参考链接:

1. Mybatis源码

2. 根据配置文件创建SqlSessionFactory

3. 【MyBatis源码分析】Configuration加载

赞(1) 打赏
Zhuoli's Blog » Mybatis源码解读『配置文件解析』
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址