mybatis源码分析6 - mybatis-spring容器初始化

1 引言

使用 MyBatis-Spring 模块,我们可以在Spring中使用mybatis,让Spring容器来管理sqlSessionFactory单例的创建。如以下代码

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  
  <property name="dataSource" ref="dataSource" />
  
  <property name="configuration">
    <bean class="org.apache.ibatis.session.Configuration">
      <property name="mapUnderscoreToCamelCase" value="true"/>
    bean>
  property>
  
  <property  name="mapperLocations"  value="classpath*:com/xxt/ibatis/dbcp/**/*.xml"/>
  
  <property  name="configLocation"  value="classpath:sqlMapConfig.xml"/> 
bean>

我们只需要指定两个属性即可,一是dataSource数据库源,二是configuration对象或configLocation配置文件所在位置。那么有这两个属性是如何创建sqlSessionFactory对象的呢,这一节我们详细分析。

2 sqlSessionFactory对象注入的流程

创建sqlSessionFactory bean时,指定的实现类是SqlSessionFactoryBean类,它是一个FactoryBean。我们知道,对于FactoryBean,Spring为我们创建的不是FactoryBean本身的对象,二是它的getObject()方法返回的对象。故我们从SqlSessionFactoryBean的getObject()方法来分析。

// 工厂bean,它返回的不是FactoryBean本身,而是它的getObject方法返回的bean
public SqlSessionFactory getObject() throws Exception {
  if (this.sqlSessionFactory == null) {
    afterPropertiesSet();
  }

  // getObject最终返回的还是一个SqlSessionFactory对象
  return this.sqlSessionFactory;
}

上面是典型的单例模式,我们到afterPropertiesSet()方法中去看。

public void afterPropertiesSet() throws Exception {
  // 各种报错
  notNull(dataSource, "Property 'dataSource' is required");
  notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
  state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
            "Property 'configuration' and 'configLocation' can not specified with together");

  // 创建sqlSessionFactory
  this.sqlSessionFactory = buildSqlSessionFactory();
}

afterPropertiesSet先做dataSource等属性值的校验,注入sqlSessionFactory的时候,必须传入dataSource属性的。然后调用buildSqlSessionFactory()方法来创建sqlSessionFactory,它是一个关键方法,我们详细分析。

// 创建SqlSessionFactory实例
protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
  // 包含了几乎所有mybatis配置信息,创建sqlSessionFactory最重要的变量,之前分析mybatis初始化的时候讲到过
  Configuration configuration;

  // 先读取sqlSessionFactory bean注入时,用来设置mybatis配置信息Configuration的属性
  // 有configuration属性或者configLocation属性两种。
  XMLConfigBuilder xmlConfigBuilder = null;

  if (this.configuration != null) {
    // 注入的是configuration属性时,它是一个bean
    configuration = this.configuration;
    // 合并configurationProperties变量到configuration的variables成员中。mybatis初始化的章节讲到过这个合并
    // configurationProperties包含的是一些动态化常量,比如数据库的username和password等信息
    // configurationProperties属性同样在sqlSessionFactory bean注入时设置进来
    if (configuration.getVariables() == null) {
      configuration.setVariables(this.configurationProperties);
    } else if (this.configurationProperties != null) {
      configuration.getVariables().putAll(this.configurationProperties);
    }

  } else if (this.configLocation != null) {
    // 注入的是configLocation属性时,它是一个String,描述了mybatis xml配置文件的位置
    // 此时使用mybatis的配置文件来配置其他属性,利用配置文件生成Configuration对象
    // 和原生mybatis一样,也是先创建XMLConfigBuilder对象,然后利用它来解析mybatis配置文件,然后将配置文件中的属性设置到configuration的相关成员变量中去
    // 此处只是创建XMLConfigBuilder和configuration对象,还没有做解析
    xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
    configuration = xmlConfigBuilder.getConfiguration();

  } else {
    // configuration属性和configLocation属性都没有注入时,只能直接构造mybatis默认的Configuration对象了
    LOGGER.debug(() -> "Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");
    configuration = new Configuration();
    // 同样合并configurationProperties属性到configuration变量的variables变量中
    if (this.configurationProperties != null) {
      configuration.setVariables(this.configurationProperties);
    }
  }

  // 注入了objectFactory属性时,一般不建议在sqlSessionFactory中注入,而是放到mybatis配置文件中。
  if (this.objectFactory != null) {
    configuration.setObjectFactory(this.objectFactory);
  }

  // 注入了objectWrapperFactory属性时,一般不建议在sqlSessionFactory中注入,而是放到mybatis配置文件中。
  if (this.objectWrapperFactory != null) {
    configuration.setObjectWrapperFactory(this.objectWrapperFactory);
  }

  // 注入了vfs属性时,一般不建议在sqlSessionFactory中注入,而是放到mybatis配置文件中。
  if (this.vfs != null) {
    configuration.setVfsImpl(this.vfs);
  }

  // 注入了typeAliasesPackage属性时,一般不建议在sqlSessionFactory中注入,而是放到mybatis配置文件中。
  if (hasLength(this.typeAliasesPackage)) {
    String[] typeAliasPackageArray = tokenizeToStringArray(this.typeAliasesPackage,
        ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
    for (String packageToScan : typeAliasPackageArray) {
      configuration.getTypeAliasRegistry().registerAliases(packageToScan,
              typeAliasesSuperType == null ? Object.class : typeAliasesSuperType);
      LOGGER.debug(() -> "Scanned package: '" + packageToScan + "' for aliases");
    }
  }

  // 注入了typeAliases属性时,一般不建议在sqlSessionFactory中注入,而是放到mybatis配置文件中。
  if (!isEmpty(this.typeAliases)) {
    for (Class typeAlias : this.typeAliases) {
      configuration.getTypeAliasRegistry().registerAlias(typeAlias);
      LOGGER.debug(() -> "Registered type alias: '" + typeAlias + "'");
    }
  }

  // 注入了plugins属性时,一般不建议在sqlSessionFactory中注入,而是放到mybatis配置文件中。
  if (!isEmpty(this.plugins)) {
    for (Interceptor plugin : this.plugins) {
      configuration.addInterceptor(plugin);
      LOGGER.debug(() -> "Registered plugin: '" + plugin + "'");
    }
  }

  // 注入了typeHandlersPackage属性时,一般不建议在sqlSessionFactory中注入,而是放到mybatis配置文件中。
  if (hasLength(this.typeHandlersPackage)) {
    String[] typeHandlersPackageArray = tokenizeToStringArray(this.typeHandlersPackage,
        ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
    for (String packageToScan : typeHandlersPackageArray) {
      configuration.getTypeHandlerRegistry().register(packageToScan);
      LOGGER.debug(() -> "Scanned package: '" + packageToScan + "' for type handlers");
    }
  }

  // 注入了typeHandlers属性时,一般不建议在sqlSessionFactory中注入,而是放到mybatis配置文件中。
  if (!isEmpty(this.typeHandlers)) {
    for (TypeHandler typeHandler : this.typeHandlers) {
      configuration.getTypeHandlerRegistry().register(typeHandler);
      LOGGER.debug(() -> "Registered type handler: '" + typeHandler + "'");
    }
  }

  // 注入了databaseIdProvider属性时,一般不建议在sqlSessionFactory中注入,而是放到mybatis配置文件中。
  if (this.databaseIdProvider != null) {//fix #64 set databaseId before parse mapper xmls
    try {
      configuration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource));
    } catch (SQLException e) {
      throw new NestedIOException("Failed getting a databaseId", e);
    }
  }

  // 注入了cache属性时,添加到configuration变量的cache map中
  if (this.cache != null) {
    configuration.addCache(this.cache);
  }

  // 使用configLocation属性时,解析mybatis xml配置文件,和直接使用原生mybatis的new SqlSessionFactoryBuild().build()方式几乎相同
  if (xmlConfigBuilder != null) {
    try {
      // 利用前面创建的xmlConfigBuilder来解析XML配置文件,并将解析后的键值对设置到configuration变量中
      xmlConfigBuilder.parse();
      LOGGER.debug(() -> "Parsed configuration file: '" + this.configLocation + "'");
    } catch (Exception ex) {
      throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
    } finally {
      ErrorContext.instance().reset();
    }
  }

  // 创建transactionFactory,用来创建transaction事务,Spring使用AOP来创建事务
  if (this.transactionFactory == null) {
    this.transactionFactory = new SpringManagedTransactionFactory();
  }

  // 设置configuration的environment变量,
  // 采用Spring注入方式时,直接指定了sqlSessionFactory下的dataSource数据库源,一般不需要在mybaits配置文件中设置environments了
  configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource));

  // 注入了mapperLocations属性时,一般不建议在sqlSessionFactory中注入,而是放到mybatis配置文件中。
  if (!isEmpty(this.mapperLocations)) {
    for (Resource mapperLocation : this.mapperLocations) {
      if (mapperLocation == null) {
        continue;
      }

      try {
        // 读取mapper配置文件,并解析
        XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
            configuration, mapperLocation.toString(), configuration.getSqlFragments());
        xmlMapperBuilder.parse();
      } catch (Exception e) {
        throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
      } finally {
        ErrorContext.instance().reset();
      }
      LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'");
    }
  } else {
    LOGGER.debug(() -> "Property 'mapperLocations' was not specified or no matching resources found");
  }

  // configuration变量创建并初始化好之后,就可以创建sqlSessionFactory对象了
  // sqlSessionFactoryBuilder的build创建DefaultSqlSessionFactory对象,默认的SqlSessionFactory
  // 这个过程之前讲解mybatis初始化的章节时,讲过了的
  return this.sqlSessionFactoryBuilder.build(configuration);
}

这个方法比较长,详细内容读者可以逐行看上面代码和注释,注释应该已经十分详尽了。我们总结下这个方法的流程。

  1. 先读取mybatis配置信息,它通过sqlSessionFactory注入时,传入的configuration对象或者configLocation String来分析配置信息。

    1)传入的是configuration属性时,合并configurationProperties属性到configuration对象中去即可。

    2)传入的是configLocation属性时,它是一个String,描述了mybatis xml配置文件的位置。先创建XMLConfigBuilder对象和configuration对象,后面几步会解析mybatis配置文件,然后将配置文件中的属性设置到configuration的相关成员变量中去(这个过程和原生mybatis相同)

    3)configuration属性和configLocation属性都没有注入时,只能直接构造mybatis默认的Configuration对象了

  2. 再读取创建sqlSessionFactory bean时,传入的其他属性,如objectFactory objectWrapperFactory vfs typeAliasesPackage typeAliases plugins typeHandlersPackage typeHandlers databaseIdProvider等。如果我们使用配置文件位置信息configLocation来解析mybatis配置信息的话,这些属性均不需要传入。如果采用configuration对象的方式,或者configLocation和configuration都没有传入的话,则需要这些属性了。一般建议采用configLocation的方式,将mybatis的配置信息和Spring配置信息相分离。

  3. 使用configLocation属性时,解析mybatis xml配置文件,和直接使用原生mybatis的new SqlSessionFactoryBuild().build()方式几乎相同。

  4. 创建transactionFactory,用来创建transaction事务,Spring使用AOP来创建事务

  5. 设置configuration的environment变量,利用传入的dataSource属性

  6. 读取创建sqlSessionFactory bean时,传入的mapperLocations属性。如果采用configLocation指定mybatis配置文件位置的方式,则一般不需要在Spring中配置mapperLocations

  7. sqlSessionFactoryBuilder的build创建DefaultSqlSessionFactory对象

这个方法很关键,且流程很长。大家最重要的是要知道,创建sqlSessionFactory时指定mybatis配置信息,有三种方式。一是直接configuration对象,包含了配置信息各项参数。二是configLocation字符串,指定了配置文件的位置。三是configuration和configLocation均没有配置,完全依靠Spring配置文件中指定objectFactory typeHandlers 等属性。明白了这一点,上面的代码就会比较清晰了。

为了将Spring配置信息和mybatis配置信息相分离,从而让各个XML各司其职,也避免Spring配置文件过于膨胀,我们一般采用configLocation的方式。这种方式和原生mybatis创建sqlSessionFactory的过程极其类似,都是通过XMLConfigBuilder解析XML配置文件,并将解析到的键值对设置到Configuration对象的相关变量中去。这一过程我们在前面讲解mybatis初始化的章节中已经详细介绍了,故此处不详细讲解了。最后我们看sqlSessionFactoryBuilder.build()方法。

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

这个方法十分简单,构造sqlSessionFactory的默认实现类DefaultSqlSessionFactory,并传入前面创建并解析好的configuration对象即可。configuration包含了几乎所有的mybatis配置信息,十分重要。

3 总结

Spring容器中sqlSessionFactory的创建其实是十分简单的,特别是采用了configLocation方式的时候。创建过程基本是依赖原生mybatis的执行流程的。从这儿也可以看出代码分层有利于代码适配。这也是我们平时自己设计框架时要要注意的地方,尽量让层次分明,模块解耦,这样才能简易的适配不同的环境,从而提高可移植性。

下一节我们分析mybatis-spring中,sqlSession是如何操作数据库的

相关文章

mybatis源码分析1 - 框架

mybatis源码分析2 - SqlSessionFactory的创建

mybatis源码分析3 - sqlSession的创建

mybatis源码分析4 - sqlSession读写数据库完全解析

mybatis源码分析5 - mapper读写数据库完全解析

mybatis源码分析6 - mybatis-spring容器初始化

mybatis源码分析7 - mybatis-spring读写数据库全过程

你可能感兴趣的:(移动开发)