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

时间:2022-06-04 17:10:40

1 引言

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

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!--指定数据源,不用再在mybatis的XML配置文件中指定environment了-->
<property name="dataSource" ref="dataSource" />
<!--指定configuration对象,它是创建sqlSessionFactory的核心,包含mybatis几乎全部的配置信息-->
<property name="configuration">
<bean class="org.apache.ibatis.session.Configuration">
<property name="mapUnderscoreToCamelCase" value="true"/>
</bean>
</property>
<!--数据库映射mapper文件的位置-->
<property name="mapperLocations" value="classpath*:com/xxt/ibatis/dbcp/**/*.xml"/>
<!--或指定指定sqlMapConfig总配置文件位置configLocation,建议采用这种mybatis配置单独放在另一个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读写数据库全过程