【MyBatis】初始化流程详解

1,022 阅读4分钟

MyBatis初始化流程

在上一篇文章中我们写了一个简单的 MyBatis程序,并对 MyBatis配置文件进行了详细的说明

那么这篇文章我们来看看 MyBatis的初始化流程和它背后的一些黑盒操作


@Test
public void test(){
    try {
        // 读取 MyBatis配置文件
        InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");

        // 使用该配置文件构建 SqlSessionFactory实例
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        // 通过 SqlSessionFactory获取 SqlSession实例
        SqlSession sqlSession = sqlSessionFactory.openSession();

        // 调用 sqlSession.getMapper(); 方法通过 接口类对象获取 mapper
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);

        // 打印输出测试
        System.out.println(mapper.selectAllUser().toString());

    } catch (IOException e) {
        e.printStackTrace();
    }
}

以上代码为完整的测试类代码,运行后就可以获取全部的用户数据

从这些简单的代码中我们至少可以清楚 MyBatis的初始化流程做了以下这些事情

  • 通过 MyBatis内置的工具类 Resources(org.apache.ibatis.io.Resources)获取配置文件的输入流对象
  • 读取配置文件中的配置信息,并通过配置文件输入流对象构造 SqlSessionFactory
  • 通过 SqlSessionFactory 继而拿到操作数据库的 SqlSession对象
  • 通过 sqlSession对象操作数据库,其背后的实现,我们大致可以猜到是通过 反射代理

官方文档进行的说明

上述这些只是最浅显的一些东西,了解这些对我们帮助并不大 我们来看看官网对这些使用到的对象 的一些解释说明

SqlSessionFactoryBuilder

SqlSessionFactoryBuilder 有五个 build(); 方法,每一种都允许你从不同的资源中创建一个 SqlSessionFactory 实例。

SqlSessionFactory build(InputStream inputStream)
SqlSessionFactory build(InputStream inputStream, String environment)
SqlSessionFactory build(InputStream inputStream, Properties properties)
SqlSessionFactory build(InputStream inputStream, String env, Properties props)
SqlSessionFactory build(Configuration config)

第一种方法是最常用的,它接受一个指向 XML 文件(也就是之前讨论的 mybatis-config.xml 文件)的 InputStream 实例。

可选的参数是 environment 和 properties。environment 决定加载哪种环境,包括数据源和事务管理器。比如:

<environments default="development">
  <environment id="development">
    <transactionManager type="JDBC">
        ...
    <dataSource type="POOLED">
        ...
  </environment>
  <environment id="production">
    <transactionManager type="MANAGED">
        ...
    <dataSource type="JNDI">
        ...
  </environment>
</environments>

如果你调用了带 environment 参数的 build 方法,那么 MyBatis 将使用该环境对应的配置。当然,如果你指定了一个无效的环境,会收到错误。

如果你调用了不带 environment 参数的 build 方法,那么就会使用默认的环境配置(在上面的示例中,通过 default="development" 指定了默认环境)。

如果你调用了接受 properties 实例的方法,那么 MyBatis 就会加载这些属性,并在配置中提供使用。

绝大多数场合下,可以用 ${propName} 形式引用这些配置值。

回想一下,在 mybatis-config.xml 中,可以引用属性值,也可以直接指定属性值。

因此,理解属性的优先级是很重要的。在之前的文档中,我们已经介绍过了相关内容,但为了方便查阅,这里再重新介绍一下:


如果一个属性存在于下面的多个位置,那么 MyBatis 将按照以下顺序来加载它们:

  • 首先,读取在 properties 元素体中指定的属性;
  • 其次,读取在 properties 元素的类路径 resource 或 url 指定的属性,且会覆盖已经指定了的重复属性;
  • 最后,读取作为方法参数传递的属性,且会覆盖已经从 properties 元素体和 resource 或 url 属性中加载了的重复属性。

因此,通过方法参数传递的属性的优先级最高,resource 或 url 指定的属性优先级中等,在 properties 元素体中指定的属性优先级最低。


总结一下,前四个方法很大程度上是相同的,但提供了不同的覆盖选项,允许你可选地指定 environment 和/或 properties。

以下给出一个从 mybatis-config.xml 文件创建 SqlSessionFactory 的示例:

String resource = "org/mybatis/builder/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(inputStream);

注意,这里我们使用了 Resources 工具类,这个类在 org.apache.ibatis.io 包中。

Resources 类正如其名,会帮助你从类路径下、文件系统或一个 web URL 中加载资源文件。

在略读该类的源代码或用 IDE 查看该类信息后,你会发现一整套相当实用的方法。这里给出一个简表:

URL getResourceURL(String resource)
URL getResourceURL(ClassLoader loader, String resource)
InputStream getResourceAsStream(String resource)
InputStream getResourceAsStream(ClassLoader loader, String resource)
Properties getResourceAsProperties(String resource)
Properties getResourceAsProperties(ClassLoader loader, String resource)
Reader getResourceAsReader(String resource)
Reader getResourceAsReader(ClassLoader loader, String resource)
File getResourceAsFile(String resource)
File getResourceAsFile(ClassLoader loader, String resource)
InputStream getUrlAsStream(String urlString)
Reader getUrlAsReader(String urlString)
Properties getUrlAsProperties(String urlString)
Class classForName(String className)

最后一个 build 方法接受一个 Configuration 实例。

Configuration 类包含了对一个 SqlSessionFactory 实例你可能关心的所有内容。

在检查配置时,Configuration 类很有用,它允许你查找和操纵 SQL 映射(但当应用开始接收请求时不推荐使用)。

你之前学习过的所有配置开关都存在于 Configuration 类,只不过它们是以 Java API 形式暴露的。

以下是一个简单的示例,演示如何手动配置 Configuration 实例,然后将它传递给 build() 方法来创建 SqlSessionFactory。

DataSource dataSource = BaseDataTest.createBlogDataSource();
TransactionFactory transactionFactory = new JdbcTransactionFactory();

Environment environment = new Environment("development", transactionFactory, dataSource);

Configuration configuration = new Configuration(environment);
configuration.setLazyLoadingEnabled(true);
configuration.setEnhancementEnabled(true);
configuration.getTypeAliasRegistry().registerAlias(Blog.class);
configuration.getTypeAliasRegistry().registerAlias(Post.class);
configuration.getTypeAliasRegistry().registerAlias(Author.class);
configuration.addMapper(BoundBlogMapper.class);
configuration.addMapper(BoundAuthorMapper.class);

SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(configuration);

现在你就获得一个可以用来创建 SqlSession 实例的 SqlSessionFactory 了。


SqlSessionFactory

SqlSessionFactory 有六个方法创建 SqlSession 实例。通常来说,当你选择其中一个方法时,你需要考虑以下几点:

  • 事务处理:你希望在 session 作用域中使用事务作用域,还是使用自动提交(auto-commit)?(对很多数据库和/或 JDBC 驱动来说,等同于关闭事务支持)
  • 数据库连接:你希望 MyBatis 帮你从已配置的数据源获取连接,还是使用自己提供的连接?
  • 语句执行:你希望 MyBatis 复用 PreparedStatement 和/或批量更新语句(包括插入语句和删除语句)吗?

基于以上需求,有下列已重载的多个 openSession() 方法供使用。

SqlSession openSession()
SqlSession openSession(boolean autoCommit)
SqlSession openSession(Connection connection)
SqlSession openSession(TransactionIsolationLevel level)
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level)
SqlSession openSession(ExecutorType execType)
SqlSession openSession(ExecutorType execType, boolean autoCommit)
SqlSession openSession(ExecutorType execType, Connection connection)
Configuration getConfiguration();

默认的 openSession(); 方法没有参数,它会创建具备如下特性的 SqlSession:

  • 事务作用域将会开启(也就是不自动提交)。
  • 将由当前环境配置的 DataSource 实例中获取 Connection 对象。
  • 事务隔离级别将会使用驱动或数据源的默认设置。
  • 预处理语句不会被复用,也不会批量处理更新。

相信你已经能从方法签名中知道这些方法的区别,向 autoCommit 可选参数传递 true 值即可开启自动提交功能。

若要使用自己的 Connection 实例,传递一个 Connection 实例给 connection 参数即可。

注意,我们没有提供同时设置 Connection 和 autoCommit 的方法,这是因为 MyBatis 会依据传入的 Connection 来决定是否启用 autoCommit。

对于事务隔离级别,MyBatis 使用了一个 Java 枚举包装器来表示,称为 TransactionIsolationLevel,事务隔离级别支持 JDBC 的五个隔离级别(NONE、READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ 和 SERIALIZABLE),并且与预期的行为一致。

你可能对 ExecutorType 参数感到陌生。这个枚举类型定义了三个值:

  • ExecutorType.SIMPLE:该类型的执行器没有特别的行为。它为每个语句的执行创建一个新的预处理语句。
  • ExecutorType.REUSE:该类型的执行器会复用预处理语句。
  • ExecutorType.BATCH:该类型的执行器会批量执行所有更新语句,如果 SELECT 在多个更新中间执行,将在必要时将多条更新语句分隔开来,以方便理解。

提示: 在 SqlSessionFactory 中还有一个方法我们没有提及,就是 getConfiguration();,这个方法会返回一个 Configuration 实例,你可以在运行时使用它来检查 MyBatis 的配置。

提示: 如果你使用过 MyBatis 的旧版本,可能还记得 session、事务和批量操作是相互独立的。

在新版本中则不是这样,上述三者都包含在 session 作用域内;你不必分别处理事务或批量操作就能得到想要的全部效果。


SqlSession

正如之前所提到的,SqlSession 在 MyBatis 中是非常强大的一个类,它包含了所有执行语句、提交或回滚事务以及获取映射器实例的方法。

SqlSession 类的方法超过了 20 个,为了方便理解,我们将它们分成几种组别。

语句执行方法

这些方法被用来执行定义在 SQL 映射 XML 文件中的 SELECT、INSERT、UPDATE 和 DELETE 语句。你可以通过名字快速了解它们的作用,每一方法都接受语句的 ID 以及参数对象,参数可以是原始类型(支持自动装箱或包装类)、JavaBean、POJO 或 Map。

<T> T selectOne(String statement, Object parameter)
<E> List<E> selectList(String statement, Object parameter)
<T> Cursor<T> selectCursor(String statement, Object parameter)
<K,V> Map<K,V> selectMap(String statement, Object parameter, String mapKey)
int insert(String statement, Object parameter)
int update(String statement, Object parameter)
int delete(String statement, Object parameter)

selectOne 和 selectList 的不同仅仅是 selectOne 必须返回一个对象或 null 值。如果返回值多于一个,就会抛出异常;如果你不知道返回对象会有多少,请使用 selectList。

如果需要查看某个对象是否存在,最好的办法是查询一个 count 值(0 或 1)。

selectMap 稍微特殊一点,它会将返回对象的其中一个属性作为 key 值,将对象作为 value 值,从而将多个结果集转为 Map 类型值。

由于并不是所有语句都需要参数,所以这些方法都具有一个不需要参数的重载形式。

游标(Cursor)与列表(List)返回的结果相同,不同的是,游标借助迭代器实现了数据的惰性加载。

try (Cursor<MyEntity> entities = session.selectCursor(statement, param)) {
   for (MyEntity entity:entities) {
      // 处理单个实体
   }
}

insert、update 以及 delete 方法返回的值表示受该语句影响的行数。

<T> T selectOne(String statement)
<E> List<E> selectList(String statement)
<T> Cursor<T> selectCursor(String statement)
<K,V> Map<K,V> selectMap(String statement, String mapKey)
int insert(String statement)
int update(String statement)
int delete(String statement)

最后,还有 select 方法的三个高级版本,它们允许你限制返回行数的范围,或是提供自定义结果处理逻辑,通常在数据集非常庞大的情形下使用。

<E> List<E> selectList (String statement, Object parameter, RowBounds rowBounds)
<T> Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds)
<K,V> Map<K,V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowbounds)
void select (String statement, Object parameter, ResultHandler<T> handler)
void select (String statement, Object parameter, RowBounds rowBounds, ResultHandler<T> handler)

RowBounds 参数会告诉 MyBatis 略过指定数量的记录,并限制返回结果的数量。

RowBounds 类的 offset 和 limit 值只有在构造方法时才能传入,其它时候是不能修改的。

int offset = 100;
int limit = 25;
RowBounds rowBounds = new RowBounds(offset, limit);

数据库驱动决定了略过记录时的查询效率。为了获得最佳的性能,建议将 ResultSet 类型设置为 SCROLL_SENSITIVE 或 SCROLL_INSENSITIVE(换句话说:不要使用 FORWARD_ONLY)。

ResultHandler 参数允许自定义每行结果的处理过程。你可以将它添加到 List 中、创建 Map 和 Set,甚至丢弃每个返回值,只保留计算后的统计结果。你可以使用 ResultHandler 做很多事,这其实就是 MyBatis 构建 结果列表的内部实现办法。

从版本 3.4.6 开始,ResultHandler 会在存储过程的 REFCURSOR 输出参数中传递使用的 CALLABLE 语句。

它的接口很简单:

package org.apache.ibatis.session;
public interface ResultHandler<T> {
  void handleResult(ResultContext<? extends T> context);
}

ResultContext 参数允许你访问结果对象和当前已被创建的对象数目,另外还提供了一个返回值为 Boolean 的 stop 方法,你可以使用此 stop 方法来停止 MyBatis 加载更多的结果。

使用 ResultHandler 的时候需要注意以下两个限制:

  • 使用带 ResultHandler 参数的方法时,收到的数据不会被缓存。
  • 当使用高级的结果映射集(resultMap)时,MyBatis 很可能需要数行结果来构造一个对象。如果你使用了 ResultHandler,你可能会接收到关联(association)或者集合(collection)中尚未被完整填充的对象。

立即批量更新方法

当你将 ExecutorType 设置为 ExecutorType.BATCH 时,可以使用这个方法清除(执行)缓存在 JDBC 驱动类中的批量更新语句。

List<BatchResult> flushStatements()

事务控制方法

有四个方法用来控制事务作用域。当然,如果你已经设置了自动提交或你使用了外部事务管理器,这些方法就没什么作用了。然而,如果你正在使用由 Connection 实例控制的 JDBC 事务管理器,那么这四个方法就会派上用场:

void commit()
void commit(boolean force)
void rollback()
void rollback(boolean force)

默认情况下 MyBatis 不会自动提交事务,除非它侦测到调用了插入、更新或删除方法改变了数据库。

如果你没有使用这些方法提交修改,那么你可以在 commit 和 rollback 方法参数中传入 true 值,来保证事务被正常提交(注意,在自动提交模式或者使用了外部事务管理器的情况下,设置 force 值对 session 无效)。

大部分情况下你无需调用 rollback(),因为 MyBatis 会在你没有调用 commit 时替你完成回滚操作。不过,当你要在一个可能多次提交或回滚的 session 中详细控制事务,回滚操作就派上用场了。

提示 MyBatis-Spring 和 MyBatis-Guice 提供了声明式事务处理,所以如果你在使用 Mybatis 的同时使用了 Spring 或者 Guice,请参考它们的手册以获取更多的内容。

本地缓存

Mybatis 使用到了两种缓存:本地缓存(local cache)和二级缓存(second level cache)。

每当一个新 session 被创建,MyBatis 就会创建一个与之相关联的本地缓存。

任何在 session 执行过的查询结果都会被保存在本地缓存中,所以,当再次执行参数相同的相同查询时,就不需要实际查询数据库了。本地缓存将会在做出修改、事务提交或回滚,以及关闭 session 时清空。

默认情况下,本地缓存数据的生命周期等同于整个 session 的周期。

由于缓存会被用来解决循环引用问题和加快重复嵌套查询的速度,所以无法将其完全禁用;但是你可以通过设置 localCacheScope=STATEMENT 来只在语句执行时使用缓存。

注意,如果 localCacheScope 被设置为 SESSION,对于某个对象,MyBatis 将返回在本地缓存中唯一对象的引用。

对返回的对象(例如 list)做出的任何修改将会影响本地缓存的内容,进而将会影响到在本次 session 中从缓存返回的值。

因此,不要对 MyBatis 所返回的对象作出更改,以防后患。

你可以随时调用以下方法来清空本地缓存:

void clearCache()

确保 SqlSession 被关闭

void close()

对于你打开的任何 session,你都要保证它们被妥善关闭,这很重要。

保证妥善关闭的最佳代码模式是这样的:

SqlSession session = sqlSessionFactory.openSession();
try (SqlSession session = sqlSessionFactory.openSession()) {
    // 假设下面三行代码是你的业务逻辑
    session.insert(...);
    session.update(...);
    session.delete(...);
    session.commit();
}

提示 和 SqlSessionFactory 一样,你可以调用当前使用的 SqlSession 的 getConfiguration 方法来获得 Configuration 实例。

Configuration getConfiguration()

......


梳理 MyBatis初始化流程

MyBatis的官方文档对这三个主要对象进行了详细的介绍和说明,但对初始化流程的介绍并不那么详细和易懂

光靠它来理解 MyBatis的初始化流程是非常勉强的,所以我想自己梳理一遍 加深自己的理解 也给看到这篇博客的人提供一些思路

切入

我们从 SqlSessionFactoryBuild.bulid();方法切入 看一下它是如何构建 SqlSessionFactory的:

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

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

......

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      return build(parser.parse());
    ......
    }
  }

我们点进 SqlSessionFactoryBuild类的源码会发现,重载的bulid();方法在 return处都调用了参数最多的重载方法


XMLConfigBuilder

而在这个方法中我们发现它通过传入的三个参数实例化了 一个 XMLConfigBuilder实例对象;

这个对象的名字 为 parser,从名字就能够看出该对象是用来解析 XML配置文件的;它又在 return处调用了一个 parse();方法

我们点进该方法会发现它的返回值类型为 Configuration,这个 Configuration非常的关键,它里面封装了我们配置文件中所有的信息,甚至包括 注册的 mapper.xml文件及其中的 sql

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

我们发现调用该方法后标志位 parsed会为 true,如果再次调用就会抛出异常,提示一个 XMLConfigBuilder只能使用一次;当然这是一些细枝末节。

真正重要的是底下的 parseConfiguration();方法,可以看到该方法参数中 parser调用了 evalNode();解析了 <configuration>标签


parseConfiguration方法

也就是说我们写在 <configuration>标签中的配置项都会在这里被解析,并传到 parseConfiguration();方法中

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

到这里就明白了,为什么在配置文件中 这些配置项的书写必须按照一定顺序了,我们从这些解析配置项的方法着手,再深入一些


properties

xml文件,properties配置项示例:

<properties resource="db.properties">
  <property name="db.username" value="root"/>
  <property name="db.password" value="123456"/>
</properties>

解析方法:

private void propertiesElement(XNode context) throws Exception {
  if (context != null) {
    // 获取<properties>标签的所有子标签
    Properties defaults = context.getChildrenAsProperties();
    // 获取<properties>标签上的resource属性
    String resource = context.getStringAttribute("resource");
    // 获取<properties>标签上的url属性
    String url = context.getStringAttribute("url");
    // resource和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.");
    }
    if (resource != null) {
      // 获取resource属性值对应的properties文件中的键值对,并添加至defaults容器中        
      defaults.putAll(Resources.getResourceAsProperties(resource));
    } else if (url != null) {
      // 获取url属性值对应的properties文件中的键值对,并添加至defaults容器中
      defaults.putAll(Resources.getUrlAsProperties(url));
    }
    // 获取configuration中原本的属性,并添加至defaults容器中
    Properties vars = configuration.getVariables();
    if (vars != null) {
      defaults.putAll(vars);
    }
    parser.setVariables(defaults);
    // 将defaults容器添加至configuration中
    configuration.setVariables(defaults);
  }
}

首先读取<resources>标签体中的所有<resource>标签,并将每个标签的 name和 value属性存入Properties中。

然后读取<resources>标签上的resource、url属性,并获取指定配置文件中的 name和 value,也存入Properties中。(PS:由此可知,如果 resource标签上定义的属性和 properties文件中的属性重名,那么 properties文件中的属性值会覆盖 resource标签上定义的属性值。)

最终,携带所有属性的 Properties对象会被存储在Configuration对象中。


settings

xml文件 setting配置项示例:

<settings>
  <setting name="cacheEnabled" value="true"/>
  <setting name="lazyLoadingEnabled" value="true"/>
  <setting name="multipleResultSetsEnabled" value="true"/>
</settings>

<settings>属性的解析过程和 <properties>属性的解析过程极为类似,这里不再赘述。最终,所有的 setting属性都被存储在Configuration对象中。


typeAliases

xml文件 typeAliases配置项示例(两种方式):

<typeAliases>
  <typeAlias alias="user" type="com.molu.pojo.User"/>
</typeAliases>
<typeAliases>
  <package name="com.molu.pojo"/>
</typeAliases>

解析方法:

  private void typeAliasesElement(XNode parent) {
  if (parent != null) {
    // 遍历<typeAliases>下的所有子标签
    for (XNode child : parent.getChildren()) {
      // 若当前结点为<package>
      if ("package".equals(child.getName())) {
        // 获取<package>上的name属性(包名)
        String typeAliasPackage = child.getStringAttribute("name");
        // 为该包下的所有类起个别名,并注册进configuration的typeAliasRegistry中          
        configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
      } 
      // 如果当前结点为< typeAlias >
      else {
        // 获取alias和type属性
        String alias = child.getStringAttribute("alias");
        String type = child.getStringAttribute("type");
        // 注册进configuration的typeAliasRegistry中
        try {
          Class<?> clazz = Resources.classForName(type);
          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);
        }
      }
    }
  }
}

如果<typeAliases>标签下定义了<package>标签,那么 MyBatis会给该包下的所有类起一个别名(以类名首字母小写作为别名)

如果<typeAliases>标签下定义了<typeAlias>标签,那么 MyBatis就会给指定的类起指定的别名。

这些别名都会被存入configuration的 typeAliasRegistry容器中。


mappers

xml文件 mappers配置项示例(四种方式):

<mappers>
  <package name="com.molu.mapper"/>
</mappers>
<mappers>
  <mapper resource="com/molu/mapper/UserMapper.xml"/>
</mappers>
<mappers>
  <mapper url="file:///var/mapper/UserMapper.xml"/>
</mappers>
<mappers>
  <mapper class="com.molu.mapper.UserMapper"/>
</mappers>

解析方法:

  private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    // 遍历<mappers>下所有子标签
    for (XNode child : parent.getChildren()) {
      // 如果当前标签为<package>
      if ("package".equals(child.getName())) {
        // 获取<package>的name属性(该属性值为mapper class所在的包名)
        String mapperPackage = child.getStringAttribute("name");
        // 将该包下的所有Mapper Class注册到configuration的mapperRegistry容器中
        configuration.addMappers(mapperPackage);
      } 
      // 如果当前标签为<mapper>
      else {
        // 依次获取resource、url、class属性
        String resource = child.getStringAttribute("resource");
        String url = child.getStringAttribute("url");
        String mapperClass = child.getStringAttribute("class");
        // 解析resource属性(Mapper.xml文件的路径)
        if (resource != null && url == null && mapperClass == null) {
          ErrorContext.instance().resource(resource);
          // 将Mapper.xml文件解析成输入流
          InputStream inputStream = Resources.getResourceAsStream(resource);
          // 使用XMLMapperBuilder解析Mapper.xml,并将Mapper Class注册进configuration对象的mapperRegistry容器中
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
          mapperParser.parse();
        } 
        // 解析url属性(Mapper.xml文件的路径)
        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();
        } 
        // 解析class属性(Mapper Class的全限定名)
        else if (resource == null && url == null && mapperClass != null) {
          // 将Mapper Class的权限定名转化成Class对象
          Class<?> mapperInterface = Resources.classForName(mapperClass);
          // 注册进configuration对象的mapperRegistry容器中
          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>,则MyBatis会将该包下的所有 Mapper Class注册到configuration的 mapperRegistry容器中。

如果当前标签为<mapper>,则会依次获取 resource、url、class属性,解析映射文件,并将映射文件对应的 Mapper Class注册到configuration的 mapperRegistry容器中。


其中,<mapper>标签的解析过程如下:

XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();

在解析前,首先需要创建 XMLMapperBuilder,创建过程如下:

private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
  // 将configuration赋给BaseBuilder
  super(configuration);
  // 创建MapperBuilderAssistant对象(该对象为MapperBuilder的协助者)
  this.builderAssistant = new  MapperBuilderAssistant(configuration, resource);
  this.parser = parser;
  this.sqlFragments = sqlFragments;
  this.resource = resource;
}

首先会初始化父类 BaseBuilder,并将 configuration赋给 BaseBuilder;

然后创建 MapperBuilderAssistant对象,该对象为 XMLMapperBuilder的协助者,用来协助 XMLMapperBuilder完成一些解析映射文件的动作。

当有了 XMLMapperBuilder后,便可进入解析<mapper>的过程:

public void parse() {
  // 若当前的Mapper.xml尚未被解析,则开始解析
  // PS:若<mappers>标签下有相同的<mapper>标签,那么就无需再次解析了
  if (!configuration.isResourceLoaded(resource)) {
    // 解析<mapper>标签
    configurationElement(parser.evalNode("/mapper"));
    // 将该Mapper.xml添加至configuration的LoadedResource容器中,下回无需再解析
    configuration.addLoadedResource(resource);
    // 将该Mapper.xml对应的Mapper Class注册进configuration的mapperRegistry容器中
    bindMapperForNamespace();
  }

  parsePendingResultMaps();
  parsePendingCacheRefs();
  parsePendingStatements();
}

configurationElement();方法

private void configurationElement(XNode context) {
try {
  // 获取<mapper>标签上的namespace属性,该属性必须存在,表示当前映射文件对应的Mapper Class是谁
  String namespace = context.getStringAttribute("namespace");
  if (namespace == null || namespace.equals("")) {
    throw new BuilderException("Mapper's namespace cannot be empty");
  }
  // 将namespace属性值赋给builderAssistant
  builderAssistant.setCurrentNamespace(namespace);
  // 解析<cache-ref>标签
  cacheRefElement(context.evalNode("cache-ref"));
  // 解析<cache>标签
  cacheElement(context.evalNode("cache"));
  // 解析<parameterMap>标签
  parameterMapElement(context.evalNodes("/mapper/parameterMap"));
  // 解析<resultMap>标签
  resultMapElements(context.evalNodes("/mapper/resultMap"));
  // 解析<sql>标签
  sqlElement(context.evalNodes("/mapper/sql"));
  // 解析sql语句      
  buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
  throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
}
}

resultMapElements();方法 该方法用于解析映射文件中所有的<resultMap>标签,这些标签会被解析成 ResultMap对象,存储在Configuration对象的 resultMaps容器中。

<resultMap>标签定义如下:

 <resultMap id="userResultMap" type="User">
  <constructor>
     <idArg column="id" javaType="int"/>
     <arg column="username" javaType="String"/>
  </constructor>
  <result property="username" column="user_name"/>
  <result property="password" column="hashed_password"/>
</resultMap>

解析方法:

private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {
  ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
  // 获取<ResultMap>上的id属性
  String id = resultMapNode.getStringAttribute("id",
    resultMapNode.getValueBasedIdentifier());
  // 获取<ResultMap>上的type属性(即resultMap的返回值类型)
  String type = resultMapNode.getStringAttribute("type",
    resultMapNode.getStringAttribute("ofType",
        resultMapNode.getStringAttribute("resultType",
            resultMapNode.getStringAttribute("javaType"))));
  // 获取extends属性
  String extend = resultMapNode.getStringAttribute("extends");
  // 获取autoMapping属性
  Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
  // 将resultMap的返回值类型转换成Class对象
  Class<?> typeClass = resolveClass(type);
  Discriminator discriminator = null;
  // resultMappings用于存储<resultMap>下所有的子标签
  List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
  resultMappings.addAll(additionalResultMappings);
  // 获取并遍历<resultMap>下所有的子标签
  List<XNode> resultChildren = resultMapNode.getChildren();
  for (XNode resultChild : resultChildren) {
    // 若当前标签为<constructor>,则将它的子标签们添加到resultMappings中去
    if ("constructor".equals(resultChild.getName())) {
      processConstructorElement(resultChild, typeClass, resultMappings);
    }
    // 若当前标签为<discriminator>,则进行条件判断,并将命中的子标签添加到resultMappings中去
    else if ("discriminator".equals(resultChild.getName())) {
      discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
    }
    // 若当前标签为<result>、<association>、<collection>,则将其添加到resultMappings中去
    else {
      // PS:flags仅用于区分当前标签是否是<id>或<idArg>,因为这两个标签的属性名为name,而其他标签的属性名为property
      List<ResultFlag> flags = new ArrayList<ResultFlag>();
      if ("id".equals(resultChild.getName())) {
        flags.add(ResultFlag.ID);
      }
      resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
    }
  }
  // ResultMapResolver的作用是生成ResultMap对象,并将其加入到Configuration对象的resultMaps容器中(具体过程见下)
  ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
  try {
    return resultMapResolver.resolve();
  } catch (IncompleteElementException  e) {
    configuration.addIncompleteResultMap(resultMapResolver);
    throw e;
  }
}

ResultMapResolver这个类很纯粹,有且仅有一个方法resolve();,用于构造ResultMap对象,并将其存入 Configuration对象的 resultMaps容器中;

而这个过程是借助于MapperBuilderAssistant.addResultMap完成的。

public ResultMap resolve() {
  return assistant.addResultMap(this.id, this.type, this.extend,  this.discriminator, this.resultMappings, this.autoMapping);
}

sqlElement();方法 该方法用于解析映射文件中所有的<sql>标签,并将这些标签存储在当前映射文件所对应的 XMLMapperBuilder对象的 sqlFragments容器中,供解析 sql语句时使用。

<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>

buildStatementFromContext方法 该方法会将映射文件中的 sql语句解析成MappedStatement对象,并存在configurationmappedStatements


创建SqlSessionFactory

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

回过头来再看一下SqlSessionFactorybuild函数,刚才说了半天,介绍了XMLConfigBuilder解析映射文件的过程,解析完成之后parser.parse()函数会返回一个包含了映射文件解析结果的configuration对象,紧接着,这个对象将作为参数传递给另一个build函数,如下:

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

这个函数将configuration作为参数,创建了DefaultSqlSessionFactory对象。 DefaultSqlSessionFactory是接口SqlSessionFactory的一个实现类,SqlSessionFactory的体系结构如下图所示:

此时,SqlSessionFactory创建完毕。


总结

理一理,首先我们要通过 SqlSessionFactoryBuild对象的 build();方法来获取 SqlSessionFactory;

我们将配置文件的输入流对象作为参数传入,其内部会调用必定会调用的重载方法(除非你直接传入 Configuration作为参数)

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

在该重载方法中,return处我们发现又调用了最底部的重载方法,该方法所需的参数类型为 Configuration

这个 Configuration十分关键,只要有了它我们就可以完成 SqlSessionFactory的构建;

但在拿到该参数之前 MyBatis又做了非常多的处理,这也是 MyBatis初始化流程最核心的地方

我们再看,会发现在这个必定会调用的重载方法中,它通过我们传入的参数 创建了一个 XMLConfigBuilder对象 parser

这个parser又调用了它自己的 parse();方法作为 最底部重载方法的参数传入(也就是说它的返回值类型为 Configuration)

我们点进这个parse();方法一探究竟,看到它又调用了一个 parseConfiguration();方法,该方法参数类型为 XNode

所需的 XNode对象则通过 parser.evalNode("/configuration");解析我们写在配置文件 <configuration>标签下的配置项获得

经过上面小两千字的说明,我们明白了 这些解析好的配置项,最终都会被封装到 configuration中被返回;作为构建 SqlSessionFactory实例的必需参数。

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

至此我们就拿到了SqlSessionFactory,至于 SqlSesionFactory到 SqlSession以及 SqlSession如何通过反射和代理最终操作数据,我们等到下次有机会再写 不知不觉篇幅已经小一万字了;就不再往后写了


放松一下眼睛

原图P站地址

画师主页