mybatis源码分析2 - SqlSessionFactory的创建

1 主要类

初始化mybatis的过程,其实就是创建SqlSessionFactory单例的过程。下面是一个简单的初始化例子。

String resource = "main/resources/SqlMapConfig.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
  • 1
  • 2
  • 3

初始化流程大致分为如下几步

  1. mybatis读取全局xml配置文件,解析XML中各个节点元素
  2. 将节点元素键值对,设置到Configuration实例的相关变量中
  3. 由Configuration实例创建SqlSessionFactory单例对象

我们先来分析初始化过程中涉及的主要类

1.SqlSessionFactoryBuilder:用来创建SqlSessionFactory实例,典型的builder链式创建模式。 
2.XMLConfigBuilder:主要有三个作用:

- 解析XML文件,生成XNode对象。XNodeXML文件中元素节点的描述。
- 创建并初始化Configuration实例
- 利用root根XNode对象,解析其各个子节点xNode,获取其中的属性或子元素,生成键值对。然后设置到Configuration实例的相关变量中
  • 1
  • 2
  • 3

3.Configuration:它是一个数据类,包含了几乎所有的mybatis配置信息,他们会影响mybatis的执行流程。初始化阶段最重要的就是创建并设置Configuration的相关变量。 
4. SqlSessionFactory:创建SqlSession实例的工厂类,一般我们都把它作为单例使用。它的默认实现为DefaultSqlSessionFactory

2 流程

我们一般通过new SqlSessionFactoryBuilder().build()来创建SqlSessionFactory对象。下面是SqlSessionFactoryBuilder的build()方法

// 创建SqlSessionFactory单例
// @Param inputStream: 读取的全局XML配置文件的输入流,配置信息都在这个文件中
// @param environment:指定的此SqlSessionFactory的数据库环境,默认为default
// @param properties: 设置一些动态化常量,会和XML中的properties 中常量合并在一起
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
  try {
    // 先构建XML文件解析器
    XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
    // 解析得到Configuration对象,这个对象包含了几乎所有的mybatis配置文件中的信息,十分关键。
    // 然后利用Configuration对象构建SqlSessionFactory单例
    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.
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

build方法比较简单,从中可以分析出mybatis初始化的三大流程,即读取XML文件,设置Configuration中相关变量,创建SqlSessionFactory.下面我们一步步分析。

2.1 读取XML文件,解析节点元素

mybatis初始化第一步,就是读取全局XML配置文件,解析文件节点,生成xNode对象。下面来分析这个过程,先来看XMLConfigBuilder的实例化过程。

// new XMLConfigBuilder()的过程。
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
    // 利用XML配置文件输入流,创建XPathParser解析对象
    this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}

private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
  // 先创建Configuration实例对象,也会做一些基本的初始化。主要是注册一些别名供后面解析XML用,如JDBC
  super(new Configuration());
  ErrorContext.instance().resource("SQL Mapper Configuration");
  // 将new SqlSessionFactoryBuilder().build()中传入的Properties键值对,设置到configuration对象的variables变量中,它会和后面解析properties 子节点得到的键值对做合并。他们主要作用是配置常量动态化。
  this.configuration.setVariables(props);
  this.parsed = false;
  this.environment = environment;
  this.parser = parser;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

实例化XMLConfigBuilder后,configuration对象就创建好了,它的成员变量暂时还没有设置好。接下来就是读取XML文件,生成xNode节点了。xNode链式节点是一个XML文件的Java描述,每个xNode都对应XML中的一个节点元素。下面来分析parser.parse(),代码如下

public Configuration parse() {
  if (parsed) {
    throw new BuilderException("Each XMLConfigBuilder can only be used once.");
  }
  parsed = true;
  // 利用parser,创建xNode对象
  // parseConfiguration将解析后的键值对设置到Configuration实例的相关变量中,下一节详细讲解
  parseConfiguration(parser.evalNode("/configuration"));
  return configuration;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

解析XML文件,生成xNode链式节点的过程就在parser.evalNode(“/configuration”)中。它通过Apache的XPath解析器,读取并解析各个节点。这一部分我们不用仔细分析,只需要知道XNode对象中包含了XML的配置信息即可。

2.2 设置Configuration实例的相关变量

读取并解析了XML文件后,接下来就是将解析的XML配置信息,设置到configuration实例的相关变量中了。接着上一节的parseConfiguration()方法讲。

// 解析由XML文件生成的XNode对象,生成Configuration对象。这个是关键方法
private void parseConfiguration(XNode root) {
  try {
    // 解析properties  节点,存放到Configuration对象的variables变量中,用来将配置变量动态化。
    // 如配置dataSource的username password
    propertiesElement(root.evalNode("properties"));

    // 解析settings  节点,会改写Configuration中的相关值。
    // 这些值决定了mybatis的运行方式,如CacheEnabled lazyLoadingEnabled等属性
    Properties settings = settingsAsProperties(root.evalNode("settings"));
    loadCustomVfs(settings);

    // 解析typeAliases  节点,定义别名,一般用来为Java全路径类型取一个比较短的名字
    typeAliasesElement(root.evalNode("typeAliases"));

    // 解析plugins  节点, 定义插件,用来拦截某些类,从而改变这些类的执行
    // 如四大基本插件,Executor ParameterHandler StatementHandler ResultSetHandler
    pluginElement(root.evalNode("plugins"));

    // 解析objectFactory  节点,定义对象工厂,不常用。对象工厂用来创建mybatis返回的结果对象
    objectFactoryElement(root.evalNode("objectFactory"));

    // 解析objectWrapperFactory  节点, 包装Object实例,不常用
    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));

    // 解析reflectorFactory  节点,创建Reflector类反射,不常用
    reflectorFactoryElement(root.evalNode("reflectorFactory"));

    // 将解析并读取settings 节点后得到的键值对,设置到Configuration实例的相关变量中
    // 这些键值对决定了mybatis的运行方式,如果没设置,则采用默认值
    settingsElement(settings);

    // 解析environments  节点, 定义数据库环境。
    // 可以配置多个environment,每个对应一个dataSource和transactionManager
    environmentsElement(root.evalNode("environments"));

    // 解析databaseIdProvider  节点,定义数据库厂商标识。
    // mybatis可以根据不同的厂商执行不同的语句
    databaseIdProviderElement(root.evalNode("databaseIdProvider"));

    // 解析typeHandlers  节点, 定义类型处理器,用来将数据库中获取的值转换为Java类型
    typeHandlerElement(root.evalNode("typeHandlers"));

    // 解析mappers  节点, 定义映射器,也就是SQL映射语句。mappers中定义好映射文件的位置即可。
    mapperElement(root.evalNode("mappers"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

parseConfiguration逐一读取XML配置信息的子节点,然后将它们设置到configuration变量中,比如properties,typeAliases,mappers等。下面我们逐一分析。先看properties节点

// 读取并解析properties 节点, 合并到configuration的variables变量中
private void propertiesElement(XNode context) throws Exception {
  if (context != null) {
    // 获取properties 中的resource或者url属性,二者必须定义一个,否则定义有误
    Properties defaults = context.getChildrenAsProperties();
    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.");
    }

    // 从resource或者url中读取资源,生成Properties对象。Properties是一个 Hashtable,保存了<key,value>的键值对
    if (resource != null) {
      defaults.putAll(Resources.getResourceAsProperties(resource));
    } else if (url != null) {
      defaults.putAll(Resources.getUrlAsProperties(url));
    }

    // 将configuration本来就存在的Hashtable,variables变量,和从resource或者url中加载的Hashtable合并,更新到variables变量中。
    Properties vars = configuration.getVariables();
    if (vars != null) {
      defaults.putAll(vars);
    }
    parser.setVariables(defaults);
    configuration.setVariables(defaults);
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

propertiesElement用来解析properties节点,合并到configuration的variables变量中。variables变量是由new SqlSessionFactoryBuilder().build(Reader reader, String environment, Properties properties)时传入的初始properties初始化的,详细可参见前面的分析。下面来分析settings 节点。

// 读取并解析settings元素,生成Properties键值对
private Properties settingsAsProperties(XNode context) {
  if (context == null) {
    return new Properties();
  }
  Properties props = context.getChildrenAsProperties();
  // Check that all settings are known to the configuration class
  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;
}

// 获取用户自定义的vfs的实现,配置在settings元素中。
// settings中放置自定义vfs实现类的全限定名,以逗号分隔
private void loadCustomVfs(Properties props) throws ClassNotFoundException {
  String value = props.getProperty("vfsImpl");
  if (value != null) {
    // vfs实现类可以有多个,其实现类全限定名以逗号分隔
    String[] clazzes = value.split(",");
    for (String clazz : clazzes) {
      if (!clazz.isEmpty()) {
        // 反射加载自定义vfs实现类,并设置到Configuration实例中
        Class<? extends VFS> vfsImpl = (Class<? extends VFS>)Resources.classForName(clazz);
        configuration.setVfsImpl(vfsImpl);
      }
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

这一步还没有将settings配置信息设置到configuration中,会通过后面的settingsElement(settings)方法进行设置。之所以放在后面,是因为解析settings时,会利用到其他一些节点元素的解析结果,比如typeAliases的别名,plugin的拦截器等。settingsElement()源码如下

// 将解析并读取settings节点后得到的键值对,设置到Configuration实例的相关变量中
private void settingsElement(Properties props) throws Exception {
  // 设置configuration的autoMappingBehavior变量, 指定mybatis应如何自动映射数据库列到POJO对象属性。
  // 为NONE表示取消自动映射,PARTIAL表示只映射非嵌套结果集,FULL表示映射所有结果集,即使有嵌套。
  configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
  // 设置autoMappingUnknownColumnBehavior,mybatis自动映射时对未定义列应如何处理。
  // 为NONE则不做任何处理,WARNING输出提醒日志,FAILING映射失败,抛出SqlSessionException
  configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
  // 设置cacheEnabled, 是否开启二级缓存,也就是SqlSessionFactory级别的缓存。一级缓存,也即sqlSession内的HashMap,是默认开启的。
  configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
  // 设置proxyFactory,指定mybatis创建延迟加载对象所用到的动态代理工具。可为CGLIB 或 JAVASSIST
  configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
  // 设置lazyLoadingEnabled,延迟加载,开启时所有关联对象都会延迟加载,除非关联对象中设置了fetchType来覆盖它。
  configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
  // 设置aggressiveLazyLoading,开启时,调用对象内任何方法都会加载对象内所有属性,默认为false,即按需加载
  configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), false));
  // 设置 multipleResultSetsEnabled,开启时,允许单一语句返回多结果
  configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true));
  // 设置useColumnLabel,使用列标签代替列名
  configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true));
  // 设置useGeneratedKeys,允许JDBC自动生成主键
  configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false));
  // 设置defaultExecutorType,默认执行器
  // SIMPLE 普通执行器;REUSE 重用预处理语句;BATCH 批量更新
  configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
  // 设置defaultStatementTimeout,超时时间,也就是数据库驱动等待数据库响应的时间,单位为秒
  configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null));
  // 设置defaultFetchSize,每次返回的数据库结果集的行数。使用它可避免内存溢出
  configuration.setDefaultFetchSize(integerValueOf(props.getProperty("defaultFetchSize"), null));
  // 设置mapUnderscoreToCamelCase,开启自动驼峰命名规则映射,即将数据库列名a_column映射为Java属性aColumn
  configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
  // 设置safeRowBoundsEnabled,允许在嵌套语句中使用分页RowBounds
  configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false));
  // 设置localCacheScope,本地缓存的作用域。本地缓存用来加速嵌套查询和防止循环引用
  // 为SESSION,则缓存本sqlSession中的所有查询语句。为STATEMENT,则相同sqlSession的同一个调用语句才做缓存。
  configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
  // 设置jdbcTypeForNull,没有为参数提供特定的JDBC类型时,Java NULL对应的JDBC类型
  // 可为 NULL、VARCHAR 或 OTHER
  configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
  // 设置lazyLoadTriggerMethods,指定哪些方法会触发延迟加载关联对象。方法名之间用逗号隔开,如equals,clone,hashCode,toString
  configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
  // 设置safeResultHandlerEnabled,允许在嵌套语句中使用分页(ResultHandler)
  configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
  // 设置defaultScriptingLanguage,指定用来生成动态SQL语句的默认语言
  configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
  // 设置defaultEnumTypeHandler,指定Enum对应的默认的TypeHandler. 它是一个Java全限定名。如org.apache.ibatis.type.EnumTypeHandler
  @SuppressWarnings("unchecked")
  Class<? extends TypeHandler> typeHandler = (Class<? extends TypeHandler>)resolveClass(props.getProperty("defaultEnumTypeHandler"));
  configuration.setDefaultEnumTypeHandler(typeHandler);
  // 设置callSettersOnNulls,指定结果集中null值是否调用Java映射对象的setter
  configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
  // 设置useActualParamName,允许使用方法签名中的形参名作为SQL语句的参数名称。使用这个属性时,工程必须支持Java8
  configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"), true));
  // 设置returnInstanceForEmptyRow,当某一行的所有列都为空时,mybatis返回一个null实例对象。
  configuration.setReturnInstanceForEmptyRow(booleanValueOf(props.getProperty("returnInstanceForEmptyRow"), false));
  // 设置logPrefix,指定mybatis添加到日志中的前缀。设置后每一条日志都会添加这个前缀
  configuration.setLogPrefix(props.getProperty("logPrefix"));
  // 设置logImpl,指定mybatis使用的日志系统,如LOG4J
  @SuppressWarnings("unchecked")
  Class<? extends Log> logImpl = (Class<? extends Log>)resolveClass(props.getProperty("logImpl"));
  configuration.setLogImpl(logImpl);
  // 设置configurationFactory,指定生成Configuration对象的工厂类。工厂类必须包含getConfiguration()方法
  configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64

分析完settings节点后,我们来分析typeAliases节点,源码如下

// 读取并解析typeAliases元素,并设置到Configuration的typeAliasRegistry中
private void typeAliasesElement(XNode parent) {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      if ("package".equals(child.getName())) {
        // 元素为package时,mybatis会在搜索包名下需要的Java bean。使用bean的首字母的小写的非限定名来作为它的别名
        String typeAliasPackage = child.getStringAttribute("name");
        configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
      } else {
        // 元素为typeAlias时,读取单个typeAlias 的alias和type元素,它们分别是别名和原名
        String alias = child.getStringAttribute("alias");   // 别名
        String type = child.getStringAttribute("type");     // 原名
        try {
          // 反射加载type对应的原类型,然后以alias作为key,class对象作为value放入TYPE_ALIASES这个Map中。
          // 这样使用到别名的时候就可以使用真实的class类来替换
          // 这个Map定义了很多默认的别名映射,如string byte int
          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);
        }
      }
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

typeAliasesElement会读取并解析typeAliases元素,并设置到Configuration的typeAliasRegistry中。它的子元素可以为package 或者typeAlias。下面分析plugins 节点

// 读取并解析plugin元素,并添加到Configuration的interceptorChain中
private void pluginElement(XNode parent) throws Exception {
  if (parent != null) {
    // 遍历plugins中的每个plugin
    for (XNode child : parent.getChildren()) {
      // 读取plugin中的interceptor属性,它声明了插件的实现类的全限定名
      String interceptor = child.getStringAttribute("interceptor");
      // 读取plugin中的property元素,它声明了插件类的构造参数。
      Properties properties = child.getChildrenAsProperties();
      // 有了实现类的全限定名和构造参数后,就可以反射创建插件对象实例了,并初始化它
      Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
      interceptorInstance.setProperties(properties);
      // 将创建并初始化好的插件对象实例添加到Configuration对象中
      configuration.addInterceptor(interceptorInstance);
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

pluginElement会读取并解析plugin元素,并添加到Configuration的interceptorChain中。读取它的plugin子节点,解析其中的interceptor属性和property子元素,来反射创建并初始化插件实例。下面来分析environments 节点。

// 读取并解析environments元素,并设置到Configuration实例中
private void environmentsElement(XNode context) throws Exception {
  if (context != null) {
    // environment为SqlSessionFactoryBuilder().build(inputStream,environment)时传入的String,
    // 它指定了SqlSessionFactory所使用的数据库环境。不声明的话则采用XML中default元素对应的数据库环境
    if (environment == null) {
      environment = context.getStringAttribute("default");
    }

    // 遍历各个子environment
    for (XNode child : context.getChildren()) {
      // 先获取environment的id元素
      String id = child.getStringAttribute("id");

      // 判断id是否等于上面指定的environment String。因为我们只需要加载我们指定的environment即可。
      // 这儿就可以明白为啥XML中可以配置多个数据库环境,运行时可以由我们动态选择了。
      if (isSpecifiedEnvironment(id)) {

        // 获取transactionManager元素,创建TransactionFactory实例并初始化它。
        TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));

        // 获取dataSource元素, 创建DataSourceFactory实例并初始化它
        DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
        DataSource dataSource = dsFactory.getDataSource();

        // builder设计模式创建Environment对象,它包含id,transactionFactory,dataSource成员变量
        Environment.Builder environmentBuilder = new Environment.Builder(id)
            .transactionFactory(txFactory)
            .dataSource(dataSource);

        // 将创建好的Environment对象设置到configuration实例中
        configuration.setEnvironment(environmentBuilder.build());
      }
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

environmentsElement()读取并解析environments元素,并设置到Configuration实例中。他会根据new SqlSessionFactoryBuilder().build(Reader reader, String environment, Properties properties)时指定的数据库环境environment,来找到对应的environments 子节点,然后读取它的transactionManager 和dataSource 子节点,创建并初始化它们,然后设置到Configuration实例的environment变量中。下面来看typeHandlers 子节点。

// 读取并解析typeHandler元素,并添加到typeHandlerRegistry变量中
private void typeHandlerElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      if ("package".equals(child.getName())) {
        // 使用package子元素时,mybatis自动检索包名下添加了相关注解的typeHandler,实例化它
        String typeHandlerPackage = child.getStringAttribute("name");
        typeHandlerRegistry.register(typeHandlerPackage);
      } else {
        // 使用typeHandler子元素时,分别读取javaType, jdbcType, handler三个元素。
        // javaType对应Java类型 jdbcType对应数据库中的类型,handler则为类处理器
        // 比如我们将varchar转变为String,就需要一个typeHandler
        String javaTypeName = child.getStringAttribute("javaType");
        String jdbcTypeName = child.getStringAttribute("jdbcType");
        String handlerTypeName = child.getStringAttribute("handler");

        // 利用javaTypeName, jdbcTypeName, handlerName创建实例化对象,可以为别名或者全限定名。
        // typeAlias这个时候就派上用场了。
        Class<?> javaTypeClass = resolveClass(javaTypeName);
        JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
        Class<?> typeHandlerClass = resolveClass(handlerTypeName);

        // 将实例化好的对象添加到typeHandlerRegistry中,mybatis运行时就需要用到它了。此时是注册阶段。
        if (javaTypeClass != null) {
          if (jdbcType == null) {
            typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
          } else {
            typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
          }
        } else {
          typeHandlerRegistry.register(typeHandlerClass);
        }
      }
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

typeHandlerElement() 读取并解析typeHandler元素,并添加到typeHandlerRegistry变量中。它的子元素可以为package 或typeHandler 。根据javaType jdbcType handler 三个属性指定的全限定类名或别名,创建并初始化javaTypeClass JdbcType typeHandlerClass,然后设置到typeHandlerRegistry变量中。下面来分析mappers节点。mappers定义了SQL语句映射关系,要复杂很多。

// 读取并解析mappers元素,并添加到configuration实例的mapperRegistry变量中
private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      if ("package".equals(child.getName())) {
        // 子元素为package时,mybatis将包名下所有的接口认为是mapper类。创建其类对象并添加到mapperRegistry中。
        // 此时一般是注解方式,不需要使用XML mapper文件。
        String mapperPackage = child.getStringAttribute("name");
        configuration.addMappers(mapperPackage);

      } else {
        // 子元素为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属性不为空时,读取resource对应的XML资源,并解析它
          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属性不为空时,读取url对应的xml资源,并解析它
          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对应的类对象,并添加到configuration中。
          // 仅使用mapperClass,而不使用XML mapper文件,一般是注解方式。我们一般不建议采用注解方式。
          // 后面会详细分析注解方式
          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.");
        }
      }
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

mapperElement会读取并解析mappers元素,并添加到configuration实例的mapperRegistry变量中。子元素可以为package 或mapper 。我们知道,mapper配置可以采用XML方式或者注解方式。注解方式对应package子元素或mapper子元素下的class方式。XML方式对应mapper子元素下的resource或url方式。先来看注解方式,从上面的configuration.addMapper(mapperInterface);看起

// configuration.addMapper()使用代理方式,调用到mapperRegistry中。
// configuration包含了几乎所有的配置信息,是配置的门面,十分复杂。
// 故采用外观模式和代理模式,将真正实现下沉到各个子系统中。这样通过分层可以解耦和降低复杂度。
public <T> void addMapper(Class<T> type) {
    mapperRegistry.addMapper(type);
 }

// 添加mapperClass到map中保存起来,并解析它的注解
public <T> void addMapper(Class<T> type) {
  // 我们定义的mybatis的mapper类必须是一个接口
  if (type.isInterface()) {
    // 已经添加过了的mapper就不再添加了
    if (hasMapper(type)) {
      throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
    }

    // 防止并发重入
    boolean loadCompleted = false;
    try {
      // 将新解析的mapper接口类添加到map中
      knownMappers.put(type, new MapperProxyFactory<T>(type));
      // 解析Mapper接口的各项注解,比如@Select,这是最关键的地方
      MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
      parser.parse();
      loadCompleted = true;
    } finally {
      if (!loadCompleted) {
        knownMappers.remove(type);
      }
    }
  }
}

// 遍历Mapper接口类的每个方法,解析其注解,生成MappedStatement,SqlSource和BoundSql三大主要对象
public void parse() {
    // type为mapper接口类
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {
      // 根据mapper接口类名,获取XML资源文件,并加载。Spring-mybatis会使用到,仅使用mybatis时貌似不会用到
      loadXmlResource();
      configuration.addLoadedResource(resource);
      assistant.setCurrentNamespace(type.getName());
      // 解析CacheNamespace注解,Mapper接口类注解
      parseCache();
      // 解析CacheNamespaceRef注解,Mapper接口类注解
      parseCacheRef();

      // 遍历mapper接口的各个方法,然后解析方法上的注解
      Method[] methods = type.getMethods();
      for (Method method : methods) {
        try {
          // 解析mapper,此处是关键
          if (!method.isBridge()) {
            parseStatement(method);
          }
        } catch (IncompleteElementException e) {
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
    }
    parsePendingMethods();
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62

注解方式下,解析Mapper接口类时,我们先将Mapper接口添加到Map中保存起来,然后解析类注解@CacheNamespace @CacheNamespaceRef。之后就开始最关键的步骤,遍历Mapper的各个方法,解析方法上的注解。下面我们接着分析。

void parseStatement(Method method) {
  // 反射获取入参类型和@Lang注解,为构建SqlSource做准备
  Class<?> parameterTypeClass = getParameterType(method);
  LanguageDriver languageDriver = getLanguageDriver(method);
  // 这一步十分关键,是创建SqlSource和其内的BoundSql对象的关键所在。
  // SqlSource是MappedStatement的一个属性,并用来提供BoundSql对象
  // BoundSql用来建立sql语句,它包含sql String,入参parameterObject和入参映射parameterMappings。它利用sql语句和入参,组装成最终的访问数据库的SQL语句,包括动态SQL。这是mybatis Mapper映射的最核心的地方。
  // 获取@Select @Insert @Update @Delete等注解,或者@SelectProvider @InsertProvider @UpdateProvider @DeleteProvider等注解
  // 然后解析这些注解,利用注解的value,也就是sql String解析生成SqlSource对象。后面详细分析
  SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);

  // 后面代码比较多,大多是解析方法上其他注解,如@Options @SelectKey @ResultMap。这里省略
  if (sqlSource != null) {
    ...
  }
  ...

  // 利用上面解析得到的sqlSource等变量构建mappedStatement
  assistant.addMappedStatement(...);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

下面来看sqlSource和BoundSql是如何创建的。

// 根据mapper的方法上的注解,解析得到sqlSource对象。
private SqlSource getSqlSourceFromAnnotations(Method method, Class<?> parameterType, LanguageDriver languageDriver) {
  try {
    // 解析获取 @Select @Insert @Delete @Update四者中的一个
    Class<? extends Annotation> sqlAnnotationType = getSqlAnnotationType(method);
    // 解析获取 @SelectProvider @InsertProvider @DeleteProvider @UpdateProvider四者中的一个
    Class<? extends Annotation> sqlProviderAnnotationType = getSqlProviderAnnotationType(method);
    if (sqlAnnotationType != null) {
      if (sqlProviderAnnotationType != null) {
        // 静态SQL注解和sqlProvider注解不可能同时存在
        throw new BindingException("You cannot supply both a static SQL and SqlProvider to method named " + method.getName());
      }
      // 反射获取静态sql注解
      Annotation sqlAnnotation = method.getAnnotation(sqlAnnotationType);
      // 获取注解中的value,也就是sql String
      final String[] strings = (String[]) sqlAnnotation.getClass().getMethod("value").invoke(sqlAnnotation);
      // 利用sql String和入参来组装SqlSource对象,由这两者就可以生成最终访问数据库的SQL语句了
      // 后面详细分析
      return buildSqlSourceFromStrings(strings, parameterType, languageDriver);

    } else if (sqlProviderAnnotationType != null) {
      // 反射获取SQLProvider注解,构建ProviderSqlSource类型的SqlSource。这一部分我们就不详细展开了,读者可自行分析
      Annotation sqlProviderAnnotation = method.getAnnotation(sqlProviderAnnotationType);
      return new ProviderSqlSource(assistant.getConfiguration(), sqlProviderAnnotation, type, method);
    }
    return null;
  } catch (Exception e) {
    throw new BuilderException("Could not find value method on SQL annotation.  Cause: " + e, e);
  }
}

// 利用sql String和入参来组装SqlSource对象,由这两者就可以生成最终访问数据库的SQL语句了
  private SqlSource buildSqlSourceFromStrings(String[] strings, Class<?> parameterTypeClass, LanguageDriver languageDriver) {
    // 将sql String 拼装起来,构成一整个sql。此时sql中还没有注入入参
    final StringBuilder sql = new StringBuilder();
    for (String fragment : strings) {
      sql.append(fragment);
      sql.append(" ");
    }
    // 构建sqlSource,由具体LanguageDriver实现类完成,如XMLLanguageDriver。后面不详细分析了
    return languageDriver.createSqlSource(configuration, sql.toString().trim(), parameterTypeClass);
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

2.3 由Configuration实例创建SqlSessionFactory单例对象

读取XNode中的XML配置信息,并设置到Configuration实例的相关变量中后,就可以由Configuration实例创建SqlSessionFactory单例对象了。这个过程比较简单,源码如下。

// 利用Configuration实例创建DefaultSqlSessionFactory对象
public SqlSessionFactory build(Configuration config) {
  return new DefaultSqlSessionFactory(config);
}

// 默认的SqlSessionFactory实现类。
public class DefaultSqlSessionFactory implements SqlSessionFactory {

  private final Configuration configuration;

  public DefaultSqlSessionFactory(Configuration configuration) {
    this.configuration = configuration;
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

创建SqlSessionFactory对象的过程十分简单,利用Configuration实例构造DefaultSqlSessionFactory对象即可。DefaultSqlSessionFactory是SqlSessionFactory接口的默认实现类。

3 总结

SqlSessionFactory的创建,其实也就是mybatis的初始化过程。其中的重中之重就是Configuration实例的相关变量的设置。mybatis运行时会读取这些变量,来决定执行流程。这也是mybatis设计的精巧之处,代码配置化将设置与运行相分离,配置信息XML化也大大降低了Java代码复杂度,提高了可读性和可维护性。这些精巧的构思都是我们设计框架时可以吸取的宝贵经验。

阅读更多
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页