深入分析 MyBatis 应用程序初始化流程

302 阅读17分钟

本文为稀土掘金技术社区首发签约文章,30 天内禁止转载,30 天后未获授权禁止转载,侵权必究!
大家好,我是 王有志,一个分享硬核 Java 技术的金融摸鱼侠,欢迎大家加入 Java 人自己的交流群“共同富裕的 Java 人”。

前面我们用了 12 篇文章 学习如何使用 MyBatis,内容涵盖了绝大部分 MyBatis 的常规用法,当然也有一些知识点我们没有介绍到,如:延迟加载,鉴别器映射,枚举类型映射和自定义缓存等,主要是这些功能的使用场景相对较少,一股脑的全部学下来只会徒增大家的负担,后面如果有需求的话我们再来补充。

那么从今天开始,我们就正式进入 MyBatis 源码篇的学习了,关于这部分内容我的规划是分为 3 部分和大家分享:

  • MyBatis 应用程序初始化流程分析
  • MyBatis 应用程序执行流程分析
  • MyBatis 插件的使用与开发

今天我们就从一个简单的应用案例入手,从整体的视角来分析 MyBatis 应用程序在初始化的过程中都做了哪些事情,有哪些技巧和设计是我们可以借鉴到日常开发工作中的。

构建简单的 MyBatis 应用案例

我们直接使用 《MyBatis 入门》 中构建的简单的 MyBatis 应用程序,该程序中只包含以下 4 部分内容:

  • MyBatis 核心配置文件(mybatis-config.xml)
  • 映射器接口 UserMapper
  • Java 持久化对象 UserDO
  • 映射器文件(UserMapper.xml)

我们来为这个简单的程序写一个单元测,代码如下:

public void testSelectOrderItemByOrderId() {
  // MyBatis 应用程序初始化阶段
  Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
  SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
  SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(reader);

  // MyBatis 应用程序执行阶段
  SqlSession sqlSession = sqlSessionFactory.openSession();
  UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
  UserDO userDO = userMapper.selectUserByUserId(1);
}

这段单元测试中的代码可以分为两个部分:

  • 第 3 ~ 6 行代码,加载 MyBatis 核心配置文件,创建 SqlSessionFactory 实例,这也就是 MyBatis 应用程序的初始化阶段;
  • 第 8 ~ 10 行代码,获取 SqlSession 实例,通过 SqlSession 实例获取 MyBatis 映射器对应的接口 UserMapper,并执行查询动作,这也就是 MyBatis 应用程序的执行阶段。

今天我们的重点是 MyBatis 应用程序初始化阶段的源码实现,也就是第 3 ~ 6 行代码背后的实现原理。

使用 Resources 工具加载 MyBatis 核心配置文件

单元测试的第 3 行代码,使用了 MyBatis 提供的工具类 Resources 加载 MyBatis 的核心配置文件。Resources 是 MyBatis 提供的用于加载资源文件的工具类,采用了 Java 对工具类的命名风格(如,Java 中 Collection 的工具类是 Collections),工具类 Resources 提供了一系列的静态方法实现了不同加载资源文件的方式。 关于工具类 Resources,我们不需要深入探究它的实现原理(实际上实现原理也并不复杂),只需要了解它的常用方法即可,部分常用方法的源码如下:

public class Resources {

  /**
   * 通过资源文件路径加载资源文件
   * 返回 InputStream 对象
   */
  public static InputStream getResourceAsStream(String resource) throws IOException {
    return getResourceAsStream(null, resource);
  }

  /**
   * 通过资源文件路径加载资源文件
   * 返回 Reader 对象
   */
  public static Reader getResourceAsReader(String resource) throws IOException {  
    Reader reader;  
    if (charset == null) {  
      reader = new InputStreamReader(getResourceAsStream(resource));  
    } else {  
      reader = new InputStreamReader(getResourceAsStream(resource), charset);  
    }  
    return reader;  
  }

  /**
   * 通过 URL 加载资源文件
   * 返回 InputStream 对象
   */
  public static InputStream getUrlAsStream(String urlString) throws IOException {
    URL url = new URL(urlString);
    URLConnection conn = url.openConnection();
    return conn.getInputStream();
  }

  /**
   * 通过 URL 加载资源文件
   * 返回 Reader 对象
   */
  public static Reader getUrlAsReader(String urlString) throws IOException {
    Reader reader;
    if (charset == null) {
      reader = new InputStreamReader(getUrlAsStream(urlString));
    } else {
      reader = new InputStreamReader(getUrlAsStream(urlString), charset);
    }
    return reader;
  }
}

这里需要注意下,经过Resources#getResourceAsReader方法后,MyBatis 核心配置文件已经从 mybatis-config.xml 转变为了字符输入流 Reader 实例。

使用 SqlSessionFactoryBuilder 创建 SqlSessionFactory 实例

单元测试中的第 4 行代码和 5 行代码,创建了 SqlSessionFactoryBuilder 实例,并调用SqlSessionFactoryBuilder#build方法创建了 SqlSessionFactory 实例。这里创建 SqlSessionFactory 实例的过程是建造者模式的简单应用,只使用了建造者 SqlSessionFactoryBuilder 和产品 SqlSessionFactory,省略了导演和建造者接口这两个角色。

SqlSessionFactoryBuilder 中有多个SqlSessionFactoryBuilder#build方法的重载方法,以下 3 个是我们这次会使用到的,修改后的源码如下:

public class SqlSessionFactoryBuilder {

  public SqlSessionFactory build(Reader reader) {
    return build(reader, null, null);
  }

  public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    // 创建 XMLConfigBuilder 实例
    XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
    // 解析 MyBatis 核心配置文件
    Configuration configuration = parser.parse();
    // 创建 SqlSessionFactory 实例
    return build(configuration);
  }

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

我们重点关注第 7 行到第 14 行的SqlSessionFactoryBuilder#build方法,这个方法中的 3 行代码做了 3 个操作:

  • 创建 Configuration 的建造者 XMLConfigBuilder 实例;
  • 解析 MyBatis 核心配置文件,并初始化 Configuration 实例;
  • 创建 SqlSessionFactory 的默认实现类。

我依次对这 3 个操作中涉及到的内容进行分析。

创建 XMLConfigBuilder 实例

XMLConfigBuilder 的部分源码如下:

public class XMLConfigBuilder extends BaseBuilder {
  // 用于标记是否已经被解析
  private boolean parsed;
  // MyBatis 对 Java 中 XPath 的封装,用于处理 XPath 表达式
  private final XPathParser parser;
  // 用于记录当前使用的配置环境
  private String environment;
  // MyBatis 封装的反射工厂,提供了缓存反射元数据的能力
  private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory();

  public XMLConfigBuilder(Reader reader, String environment, Properties props) {
    this(Configuration.class, reader, environment, props);
  }

  public XMLConfigBuilder(Class<? extends Configuration> configClass, Reader reader, String environment, Properties props) {
    this(configClass, new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props);
  }

  private XMLConfigBuilder(Class<? extends Configuration> configClass, XPathParser parser, String environment, Properties props) {
    super(newConfig(configClass));
    this.configuration.setVariables(props);
    this.parsed = false;
    this.environment = environment;
    this.parser = parser;
  }
  
  private static Configuration newConfig(Class<? extends Configuration> configClass) {
    try {
      return configClass.getDeclaredConstructor().newInstance();
    } catch (Exception ex) {
      throw new BuilderException("Failed to create a new Configuration instance.", ex);
    }
  }
}

第 16 行代码构建 XPathParser 实例时使用了 Reader 实例,这时我们可以认为 MyBatis 核心配置文件已经转变 XPathParser 实例,另外活还需要关注下第 24 行代码,将 XMLConfigBuilder 的 parser 字段指向了 XPathParser 实例,也就是说SqlSessionFactoryBuilder#build方法中构建出的 XMLConfigBuilder 实例包含了 MyBatis 核心配置文件的配置。

第 20 行代码,这一行中有两个方法调用,首先调用XMLConfigBuilder#newConfig 方法创建 Configuration 实例,该方法中使用了 Java 的反射技术(果然,反射是框架的必备技术),接着调用了父类 BaseBuilder 的构造方法。

BaseBuilder 的结构

BaseBuilder 是 MyBatis 中定义的抽象类,它是 MyBatis 中大部分建造者的父类,它的部分源码如下:

public abstract class BaseBuilder {
  // MyBatis 核心配置文件在 MyBatis 应用程序中的映射
  protected final Configuration configuration;
  // MyBatis 的别名注册器
  protected final TypeAliasRegistry typeAliasRegistry;
  // MyBatis 的类型处理注册器
  protected final TypeHandlerRegistry typeHandlerRegistry;

  public BaseBuilder(Configuration configuration) {
    this.configuration = configuration;
    this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
    this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
  }
}

BaseBuilder 是在构造方法中完成了成员变量的赋值,其中 Configuration 是通过 XMLConfigBuilder#newConfig 方法获取的,而 TypeAliasRegistry 和 TypeHandlerRegistry 都是通过 Configuration 获取的,这也就是说在 Configuration 实例的创建过程中,就已经完成了 TypeAliasRegistry 实例和 TypeHandlerRegistry 实例的创建

Configuration 的结构

Configuration 是 MyBatis 核心配置文件在 MyBatis 应用程序中的映射,它的部分源码如下:

public class Configuration {
  // 省略其它成员变量
  protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry(this);
  protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();

  public Configuration() {
    typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
    // 省略其它的别名注册
    typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);
  }
}

Configuration 中声明了大量的成员变量,其中大部分都为 MyBatis 核心配置文件中的元素的映射项,上面的源码中我们忽略了这部分内容。另外,Configuration 中还声明了两个注册器:

  • TypeHandlerRegistry,类型处理器注册器
  • TypeAliasRegistry,别名注册器

Configuration 的构造方法中,针对于 TypeAliasRegistry 进行了大量的别名注册,同时在 TypeAliasRegistry 的构造方法中也进行了大量的别名注册,部分源码如下:

public TypeAliasRegistry() {
  registerAlias("string", String.class);
  registerAlias("byte", Byte.class);
  registerAlias("char", Character.class);
  registerAlias("character", Character.class);
  registerAlias("long", Long.class);
  registerAlias("short", Short.class);
  registerAlias("int", Integer.class);
  registerAlias("integer", Integer.class);
  registerAlias("double", Double.class);
  registerAlias("float", Float.class);
  registerAlias("boolean", Boolean.class);
  // 省略其它别名注册
  registerAlias("ResultSet", ResultSet.class);
}

两处别名注册有什么差别呢? Configuration 的构造方法中,只注册了于 MyBatis 核心配置文件有关的别名,而在 TypeAliasRegistry 的构造方法中进行了大量通用别名的注册,这样做的好处是通过构造方法创建的 TypeAliasRegistry 始终保持着它作为工具的“纯粹性”,不包含与 Configuration 相关的逻辑。

另外,TypeHandlerRegistry 的初始化与 TypeAliasRegistry 类似,虽然 Configuration 的构造方法中没有进行任何类型处理器的注册,但是在 TypeHandlerRegistry 的构造方法中注册了大量的类型处理器。

解析 MyBatis 核心配置文件

上面我们已经分析了 XMLConfigBuilder 实例的构建过程,下面我们来看XMLConfigBuilder#parse方法的处理过程,该方法部分源码如下:

public class XMLConfigBuilder extends BaseBuilder {
  
  public Configuration parse() {
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }
  
  private void parseConfiguration(XNode root) {
    // 解析 properties 元素
    propertiesElement(root.evalNode("properties"));
    // 解析 settings 元素
    Properties settings = settingsAsProperties(root.evalNode("settings"));
    loadCustomVfsImpl(settings);
    loadCustomLogImpl(settings);
    settingsElement(settings);
    // 解析 typeAliases 元素
    typeAliasesElement(root.evalNode("typeAliases"));
    // 解析 typeHandlers 元素
    typeHandlersElement(root.evalNode("typeHandlers"));
    // 解析 plugins 元素
    pluginsElement(root.evalNode("plugins"));
    // 解析 objectFactory 元素
    objectFactoryElement(root.evalNode("objectFactory"));
    // 解析 objectWrapperFactory 元素
    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
    // 解析 reflectorFactory 元素
    reflectorFactoryElement(root.evalNode("reflectorFactory"));
    // 解析 environments 元素
    environmentsElement(root.evalNode("environments"));
    // 解析 databaseIdProvider 元素
    databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    // 解析 mappers 元素
    mappersElement(root.evalNode("mappers"));
  }
}

通过源码可以看到XMLConfigBuilder#parse方法是通过调用XMLConfigBuilder#parseConfiguration方法完成的 MyBatis 核心配置文件的解析。

Tips:下面涉及到解析 MyBatis 核心配置文件中配置项的源码,如果你忘记了这些配置项的功能,可以回顾下 《MyBatis核心配置讲解(上)》《MyBatis核心配置讲解(下)》

解析 properties 元素

解析 properties 元素调用的是XMLConfigBuilder#propertiesElement方法,部分源码如下:

private void propertiesElement(XNode context) throws Exception {
  // 创建用于存储 properties 元素配置的容器
  Properties defaults = context.getChildrenAsProperties();
  // 解析 properties 元素的 resource 属性
  String resource = context.getStringAttribute("resource");
  // 解析 properties 元素的 url 属性
  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.");
  }
  // 分别解析通过 resource 属性或 url 属性的配置,并存储到容器中
  if (resource != null) {
    defaults.putAll(Resources.getResourceAsProperties(resource));
  } else if (url != null) {
    defaults.putAll(Resources.getUrlAsProperties(url));
  }
  // 解析通过 Java 方法添加的配置,并存储到容器中
  Properties vars = configuration.getVariables();
  if (vars != null) {
    defaults.putAll(vars);
  }
  parser.setVariables(defaults);
  // 将解析后的 properties 元素存储到 Configuration 实例中
  configuration.setVariables(defaults);
}

我们重点关注第 8 行代码的条件语句,这里对 properties 元素的 resource 属性和 url 属性的互斥性进行了判断,如果两者同时存在,则抛出异常。

这里正是我们在《MyBatis核心配置讲解(上)》中提到的:

properties 元素的 resource 属性与 url 属性是互斥的。

现在你能够通过源码很清晰的看到它们为什么是互斥的了。

解析 settings 元素

解析 settings 元素相对比较复杂,需要调用 4 个方法完成:

Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfsImpl(settings);
loadCustomLogImpl(settings);
settingsElement(settings);

首先是调用XMLConfigBuilder#settingsAsProperties 方法解析 settings 元素下的所有配置项,这里使用了MetaClass 检测 Configuration 中是否包含该配置项的 set 方法,该方法的源码如下:

private Properties settingsAsProperties(XNode context) {
  Properties props = context.getChildrenAsProperties();
  MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
  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).");
    }
  }
  return props;
}

接着调用了XMLConfigBuilder#loadCustomVfsImpl方法和XMLConfigBuilder#loadCustomLogImpl用于设置虚拟文件系统和日志。 最后调用了XMLConfigBuilder#settingsElement方法设置将配置项存储到 Configuration 实例中,该方法部分源码如下:

private void settingsElement(Properties props) {
  configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
  configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
  // 其它配置项
}

另外,我们也可以通过 XMLConfigBuilder#settingsElement 方法的源码来查看 MyBatis 都支持那些插件。

解析 typeAliases 元素和 typeHandlers 元素

解析 typeAliases 元素和 typeHandlers 元素分别调用了 XMLConfigBuilder#typeAliasesElement 方法和 XMLConfigBuilder#typeHandlersElement 方法,这两个方法非常相似,因此我们放在一起说明:

typeAliasesElement和typeHandlersElement方法.png

可以看到两者分别通过 Configuration 实例获取 typeAliasRegistry 和 typeHandlerRegistry,并将 MyBatis 核心配置文件中的相应配置进行注册。

解析 plugins 元素,objectFactory 元素,objectWrapperFactory 元素和 reflectorFactory 元素

解析 plugins 元素,objectFactory 元素,objectWrapperFactory 元素和 reflectorFactory 元素分别调用了 XMLConfigBuilder#pluginsElement 方法,XMLConfigBuilder#objectFactoryElement 方法,XMLConfigBuilder#objectWrapperFactoryElement 方法和 XMLConfigBuilder#reflectorFactoryElement 方法,它们的源码非常相似,我们还是用一张图来展示:

pluginsElement,objectFactoryElement,objectWrapperFactoryElement和reflectorFactoryElement方法.png

从源码的角度上看,这 4 个方法非常简单,解析 MyBatis 核心配置文件中相应的配置项,并存储到 Configuration 中,这里我们就不过多赘述了。

解析 environments 元素

解析 environments 元素调用的是 XMLConfigBuilder#environmentsElement 方法,该方法的部分源码如下:

private void environmentsElement(XNode context) throws Exception {
  if (environment == null) {
    environment = context.getStringAttribute("default");
  }
  // 遍历 environments 元素的子元素,也即是每个环境的配置
  for (XNode child : context.getChildren()) {
    // 获取环境的 Id
    String id = child.getStringAttribute("id");
    if (isSpecifiedEnvironment(id)) {
      TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
      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());
      break;
    }
  }
}

第 3 行的代码,如果没有在执行 SqlSessionFactoryBuilder#build 方法时指定默认环境的名称,那么就会使用 MyBatis 核心配置文件中配置的默认环境名称,这也从侧面印证了在 MyBatis 中,Java 配置的优先级大于 XML 配置的优先级

第 9 行代码,调用了 XMLConfigBuilder#isSpecifiedEnvironment 方法,用于判断该环境是否为指定环境,该方法的源码如下:

private boolean isSpecifiedEnvironment(String id) {
  if (environment == null) {
    throw new BuilderException("No environment specified.");
  }
  if (id == null) {
    throw new BuilderException("Environment requires an id attribute.");
  }
  return environment.equals(id);
}

通过源码我们可以看到,在配置环境时,MyBatis 要求必须指定默认环境,并且每个环境配置都必须指定 Id,最后会判断当前环境是否为配置的默认环境。

我们回到 XMLConfigBuilder#environmentsElement 方法的第 9 行,结合 XMLConfigBuilder#isSpecifiedEnvironment 方法,我们可以看到,如果当前环境为默认,则会进入条件语句中,加载环境配置,否则跳过当前环境,不进行加载

另外,我们注意到条件语句的最后使用了关键字 break,也就是说,如果你在 MyBatis 核心配置文件中,配置了多个 Id 重复的环境,那么只有第一个会被加载

重名配置环境.png

解析 databaseIdProvider 元素

解析 databaseIdProvider 元素调用的是 XMLConfigBuilder#databaseIdProviderElement 方法,该方法的部分源码如下:

private void databaseIdProviderElement(XNode context) throws Exception {
  String type = context.getStringAttribute("type");
  if ("VENDOR".equals(type)) {
    type = "DB_VENDOR";
  }
  Properties properties = context.getChildrenAsProperties();
  DatabaseIdProvider databaseIdProvider = (DatabaseIdProvider) resolveClass(type).getDeclaredConstructor() .newInstance();
  databaseIdProvider.setProperties(properties);
  Environment environment = configuration.getEnvironment();
  if (environment != null) {
    String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());
    configuration.setDatabaseId(databaseId);
  }
}

关于 databaseIdProvider 元素的使用,我们已经在 《MyBatis核心配置讲解(下)》 中和大家聊过了,如果你已经忘了它的功能,可以回过头来看看之前的文章。

第 7 行代码,根据我们在 MyBatis 核心配置文件中配置的 databaseIdProvider 元素,获取 DatabaseIdProvider 的实现类,目前的版本下,MyBatis 官方已经将实现类 DefaultDatabaseIdProvider 标记为废弃,所以实际上 DatabaseIdProvider 的可用实现类只有 VendorDatabaseIdProvider 了。

第 8 行代码,将我们在 MyBatis 核心配置文件中配置的数据库 Id 存储到 DatabaseIdProvider 实例中。

第 11 行代码,调用 DatabaseIdProvider#getDatabaseId 方法,获取数据库的 Id,这里会与我们在 environments 元素配置的数据源做一个匹配,如果能够匹配成功,则返回我们在 databaseIdProvider 元素中配置的 Id,否则返回数据源实际的名称作为 Id,修改后的部分源码如下:

public String getDatabaseId(DataSource dataSource) {
  return getDatabaseName(dataSource);
}

private String getDatabaseName(DataSource dataSource) throws SQLException {
  // 获取数据源使用的数据库名称
  String productName = getDatabaseProductName(dataSource);
  if (this.properties != null) {
    // 匹配数据源使用的数据库名称与 MyBatis 核心配置文件中配置的名称
    return properties.entrySet().stream().filter(entry -> productName.contains((String) entry.getKey())).map(entry -> (String) entry.getValue()).findFirst().orElse(null);
  }
  return productName;
}

// 通过 Connection 获取数据源使用的数据库的产品名称
private String getDatabaseProductName(DataSource dataSource) throws SQLException {
  try (Connection con = dataSource.getConnection()) {
    return con.getMetaData().getDatabaseProductName();
  }
}

解析 mappers 元素

解析 mappers 元素调用的是 XMLConfigBuilder#mappersElement 方法,修改后的部分源码如下:

private void mappersElement(XNode context) throws Exception {
  for (XNode child : context.getChildren()) {
    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");
      // 使用 resource 属性配置的映射器
      if (resource != null && url == null && mapperClass == null) {
        ErrorContext.instance().resource(resource);
        try (InputStream inputStream = Resources.getResourceAsStream(resource)) {
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
          mapperParser.parse();
        }
      } 
        /// 使用 url 属性配置的映射器
      else if (resource == null && url != null && mapperClass == null) {
        ErrorContext.instance().resource(url);
        try (InputStream inputStream = Resources.getUrlAsStream(url)) {
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
          mapperParser.parse();
        }
      }
        // 使用 mapperClass 属性配置的映射器
      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.");
      }
    }
  }
}

如果单纯的看 XMLConfigBuilder#mappersElement 方法的话,整体的逻辑非常简单,根据 mappers 元素的子元素 mapper 使用的配置属性,分成 3 种情况进行解析。

但是一旦结合 XMLMapperBuilder#parse 方法来看整个解析映射器文件的过程,内容就会变得非常多,而且远比 XMLConfigBuilder#parse 方法更加复杂,涉及到映射器 Mapper.xml 文件的解析,缓存的解析,结果集映射的解析和 SQL 语句的解析,所以这部分内容我们就留到下一篇文章中再来和大家详细的分析。

回顾与思考

到这里,我们已经从整体上了解了 MyBatis 应用程序初始化的流程,现在我们用一张图来总结下源码的执行流程,如下:

01.MyBatis 应用程序初始化流程分析.png 前前后后涉及到的 MyBatis 源码比较多,但最关键的也就是 SqlSessionFactoryBuilder 和 XMLConfigBuilder 这两个类了。

最后,我们来看一下通过上述的源码,我们能够借鉴哪些技巧并运用到我们日常的开发工作中。

职责分离

我们可以看到,TypeAliasRegistry 初始化数据是分别在多处进行的:

  • TypeAliasRegistry 的构造方法中,只注册了通用别名;
  • Configuration 的构造方法中,注册了与 MyBatis 核心配置文件相关的别名。

这种模块化职责分离的设计,保证了每个模块的“纯粹性”,每个模块负责处理各自的逻辑,保证了各个模块的功能不会耦合到一起。

另外,这种设计方式,也为自定义别名注册和实现别名配置的优先级提供了便利,用户可以在 MyBatis 核心配置文件中自定义别名,职责分离后 MyBatis 只需要在 Configuration 中实现加载自定义别名配置的逻辑即可,而无需在 TypeAliasRegistry 中实现解析 MyBatis 核心配置文件并加载别名配置;其次,TypeAliasRegistry 中初始化数据的顺序会早于 Configuration 中注册别名,这样也可以保证当我们需要配置与 MyBatis 中预置别名重名的自定义配置时,自定义配置可以覆盖掉 MyBatis 的预置配置。

建造者模式的应用

建造者模式,也叫做生成器模式,它将一个复杂对象的表示与构建过程分离,降低了对象构建过程与构建结果之间的耦合程度,当我们使用对象时,只需要关注对象所表示的内容,而无需关注构建细节。

通常运用得当的建造者模式会具有良好的扩展性,各个建造者之间相互独立,添加新的建造者不会对现有内容产生影响,这点也就是我们常说的对扩展是开放的,符合 SOLID 原则中的“开闭原则”,另外建造者模式也符合“单一职责原则”,建造者是独立于被建造对象的,只负责建造工作,而被建造的对象将更专注于对象的表示与逻辑。 建造者模式是 MyBatis 中运用的最广泛的设计模式之一,无论是 SqlSessionFactoryBuilder,还是 XMLConfigBuilder,以及未来还会见到的 XMLScriptBuilder 和 XMLStatementBuilder,都是建造者模式的应用。

不过 MyBatis 中只是对建造者模式的简单应用,只使用了建造者和产品(被建造的对象)这两个角色,而抛弃了建造者接口和导演(负责调度具体的建造者)这两个角色,这是因为在 MyBatis 中无论是 SqlSessionFactoryBuilder 还是 XMLConfigBuilder 都有明确的调用场景,不需要使用导演这一角色去调度具体的建造者,只需要在具体的场景中使用相应的建造者即可,这样可以避免过度设计,减少不必要的复杂性。


尾图(无二维码).png