mybatis-spring容器初始化
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);
}
這個方法比較長,詳細內容讀者可以逐行看上面程式碼和註釋,註釋應該已經十分詳盡了。我們總結下這個方法的流程。
先讀取mybatis配置資訊,它通過sqlSessionFactory注入時,傳入的configuration物件或者configLocation String來分析配置資訊。
1)傳入的是configuration屬性時,合併configurationProperties屬性到configuration物件中去即可。
2)傳入的是configLocation屬性時,它是一個String,描述了mybatis xml配置檔案的位置。先建立XMLConfigBuilder物件和configuration物件,後面幾步會解析mybatis配置檔案,然後將配置檔案中的屬性設定到configuration的相關成員變數中去(這個過程和原生mybatis相同)
3)configuration屬性和configLocation屬性都沒有注入時,只能直接構造mybatis預設的Configuration物件了
再讀取建立sqlSessionFactory bean時,傳入的其他屬性,如objectFactory objectWrapperFactory vfs typeAliasesPackage typeAliases plugins typeHandlersPackage typeHandlers databaseIdProvider等。如果我們使用配置檔案位置資訊configLocation來解析mybatis配置資訊的話,這些屬性均不需要傳入。如果採用configuration物件的方式,或者configLocation和configuration都沒有傳入的話,則需要這些屬性了。一般建議採用configLocation的方式,將mybatis的配置資訊和Spring配置資訊相分離。
使用configLocation屬性時,解析mybatis xml配置檔案,和直接使用原生mybatis的new SqlSessionFactoryBuild().build()方式幾乎相同。
建立transactionFactory,用來建立transaction事務,Spring使用AOP來建立事務
設定configuration的environment變數,利用傳入的dataSource屬性
讀取建立sqlSessionFactory bean時,傳入的mapperLocations屬性。如果採用configLocation指定mybatis配置檔案位置的方式,則一般不需要在Spring中配置mapperLocations
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是如何操作資料庫的