1. 整合
2. 整合原理
回到 spring-mybatis.xml
配置文件中,MyBatis 整合 SpringFramework 的核心就是那个 **SqlSessionFactoryBean**
,我们的切入点就是从这里出发。
2.1 SqlSessionFactoryBean的设计
先简单看一下 SqlSessionFactoryBean
的内部成员,一上来我们就可以看到两个非常核心的东西:
复制 public class SqlSessionFactoryBean
implements FactoryBean < SqlSessionFactory > , InitializingBean , ApplicationListener < ApplicationEvent > {
// ......
private Resource configLocation;
private Configuration configuration;
嚯,MyBatis 的配置文件路径,以及 MyBatis 的核心 **Configuration**
对象!有这些东西,想必构造 SqlSessionFactory
也应该不成问题吧。
再往下看,更多的熟悉的东西都会映入我们眼帘:
复制 private Resource [] mapperLocations;
private DataSource dataSource;
private TransactionFactory transactionFactory;
private Properties configurationProperties;
private Interceptor [] plugins;
private TypeHandler < ? >[] typeHandlers;
private String typeHandlersPackage;
// ......
这些属性都不需要小册一一解释吧,各位都是从前面一路学过来的,这些也都能看得懂。
另外,观察这个 SqlSessionFactoryBean
实现的接口:
FactoryBean<SqlSessionFactory>
:可以通过 getObject
方法构造 SqlSessionFactory
InitializingBean
:有初始化逻辑(猜想会不会就是这个初始化的切入点加载了 MyBatis 的相关东西呢?)
ApplicationListener<ApplicationEvent>
:监听一些事件(实际上它只监听了 ContextRefreshedEvent
事件)
所以接下来的着眼点,我们就可以从这几个接口对应的方法往下看。按照 SpringFramework 对这几个接口的处理,我们先来看 InitializingBean
接口的 afterPropertiesSet
方法。
2.2 afterPropertiesSet
这个 afterPropertiesSet
方法只有一句话,但是这句话至关重要:
复制 @ Override
public void afterPropertiesSet() throws Exception {
// 判断 ......
this . sqlSessionFactory = buildSqlSessionFactory() ;
}
构建 **SqlSessionFactory**
!!!这动作可太关键了,我们一定要好好看下去。但是这个方法非常非常长,所以小册会拆解成多段来看。
2.2.1 处理MyBatis全局配置对象
复制 protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
final Configuration targetConfiguration;
XMLConfigBuilder xmlConfigBuilder = null ;
// 事先构造过Configuration,直接处理
if ( this . configuration != null ) {
targetConfiguration = this . configuration ;
if ( targetConfiguration . getVariables () == null ) {
targetConfiguration . setVariables ( this . configurationProperties );
} else if ( this . configurationProperties != null ) {
targetConfiguration . getVariables () . putAll ( this . configurationProperties );
}
} else if ( this . configLocation != null ) {
// 传入全局配置文件路径
xmlConfigBuilder = new XMLConfigBuilder( this . configLocation . getInputStream() , null , this . configurationProperties ) ;
targetConfiguration = xmlConfigBuilder . getConfiguration ();
} else {
// 啥也没有,一切走默认
targetConfiguration = new Configuration() ;
Optional . ofNullable ( this . configurationProperties ) . ifPresent (targetConfiguration :: setVariables);
}
// ......
首先的这一段,它会先准备一个 MyBatis 的全局配置对象 Configuration
,并根据是否事先注入 configuration
对象或者传入全局配置文件路径,决定是否准备 XMLConfigBuilder
。如果确实需要 XMLConfigBuilder
的处理,在下面会有调用它的 parse
方法,后面我们会看到。
2.2.2 处理内置组件
复制 Optional . ofNullable ( this . objectFactory ) . ifPresent (targetConfiguration :: setObjectFactory);
Optional . ofNullable ( this . objectWrapperFactory ) . ifPresent (targetConfiguration :: setObjectWrapperFactory);
Optional . ofNullable ( this . vfs ) . ifPresent (targetConfiguration :: setVfsImpl);
这三个组件就不用多解释了吧,之前第 7 章也遇见了,这里的设置只是为了考虑到 MyBatis 整合 SpringFramework 时,有些配置是在 <bean>
中配置而没有在 MyBatis 全局配置文件中而进行的分别处理。
2.2.3 别名处理
复制 // ......
if ( hasLength( this . typeAliasesPackage ) ) {
scanClasses( this . typeAliasesPackage , this . typeAliasesSuperType ) . stream ()
. filter (clazz -> ! clazz . isAnonymousClass ()) . filter (clazz -> ! clazz . isInterface ())
. filter (clazz -> ! clazz . isMemberClass ()) . forEach ( targetConfiguration . getTypeAliasRegistry () :: registerAlias);
}
if ( ! isEmpty( this . typeAliases ) ) {
Stream . of ( this . typeAliases ) . forEach (typeAlias -> {
targetConfiguration . getTypeAliasRegistry () . registerAlias (typeAlias);
LOGGER . debug (() -> "Registered type alias: '" + typeAlias + "'" );
});
}
// ......
别名的包扫描,和某些特定类的别名设置,这个也都很简单了,也很容易理解。
默认情况下,包扫描注册的别名就是类名(首字母大写),如果类上有标注 @Alias
注解,则取注解属性值。
2.2.4 处理插件、类型处理器
复制 // ......
if ( ! isEmpty( this . plugins ) ) {
Stream . of ( this . plugins ) . forEach (plugin -> {
targetConfiguration . addInterceptor (plugin);
LOGGER . debug (() -> "Registered plugin: '" + plugin + "'" );
});
}
if ( hasLength( this . typeHandlersPackage ) ) {
scanClasses( this . typeHandlersPackage , TypeHandler . class ) . stream () . filter (clazz -> ! clazz . isAnonymousClass ())
. filter (clazz -> ! clazz . isInterface ()) . filter (clazz -> ! Modifier . isAbstract ( clazz . getModifiers ()))
. forEach ( targetConfiguration . getTypeHandlerRegistry () :: register);
}
if ( ! isEmpty( this . typeHandlers ) ) {
Stream . of ( this . typeHandlers ) . forEach (typeHandler -> {
targetConfiguration . getTypeHandlerRegistry () . register (typeHandler);
LOGGER . debug (() -> "Registered type handler: '" + typeHandler + "'" );
});
}
targetConfiguration . setDefaultEnumTypeHandler (defaultEnumTypeHandler);
// ......
接下来是处理 MyBatis 插件,以及 TypeHandler
,内容也非常简单,扫一眼即可。
留一个小细节,默认情况下最后一行的那个 defaultEnumTypeHandler
为 null ,除非我们在注册 SqlSessionFactoryBean
时注入过,否则 MyBatis 处理枚举类型时仍会使用默认的处理器。
2.2.5 处理边角组件
复制 // ......
if ( ! isEmpty( this . scriptingLanguageDrivers ) ) {
Stream . of ( this . scriptingLanguageDrivers ) . forEach (languageDriver -> {
targetConfiguration . getLanguageRegistry () . register (languageDriver);
LOGGER . debug (() -> "Registered scripting language driver: '" + languageDriver + "'" );
});
}
Optional . ofNullable ( this . defaultScriptingLanguageDriver )
. ifPresent (targetConfiguration :: setDefaultScriptingLanguage);
if ( this . databaseIdProvider != null ) { // fix #64 set databaseId before parse mapper xmls
try {
targetConfiguration . setDatabaseId ( this . databaseIdProvider . getDatabaseId ( this . dataSource ));
} catch ( SQLException e) {
throw new NestedIOException( "Failed getting a databaseId" , e) ;
}
}
Optional . ofNullable ( this . cache ) . ifPresent (targetConfiguration :: addCache);
// ......
接下来的一些组件都是边角性的了,我们平时也不会主动碰它,所以直接忽略就好啦。
2.2.6 解析MyBatis全局配置文件
复制 // ......
if (xmlConfigBuilder != null ) {
try {
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 ();
}
}
// ......
上面的边边角角东西都处理的差不多了,接下来就是加载全局配置文件了,由于 xmlConfigBuilder
在执行构造器时已经把全局 Configuration
注入进去了,所以这个 parse
动作完事后,配置文件的内容也就都进入 Configuration
中了。
2.2.7 处理数据源和事务工厂
复制 // ......
targetConfiguration . setEnvironment ( new Environment( this . environment ,
this . transactionFactory == null ? new SpringManagedTransactionFactory() : this . transactionFactory ,
this . dataSource ) );
// ......
这段话只有一句代码,但这里面包含了两部分:数据源与事务工厂 ,注意默认情况下 MyBatis 与 SpringFramework 整合之后底层使用的事务工厂不再是 JdbcTransactionFactory
,而是 SpringManagedTransactionFactory
,不过底层的逻辑与原生 jdbc 并无太大差别。
2.2.8 处理Mapper
复制 // ......
if ( this . mapperLocations != null ) {
if ( this . mapperLocations . length == 0 ) {
LOGGER . warn (() -> "Property 'mapperLocations' was specified but matching resources are not found." );
} else {
for ( Resource mapperLocation : this . mapperLocations ) {
if (mapperLocation == null ) {
continue ;
}
try {
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder( mapperLocation . getInputStream() ,
targetConfiguration , mapperLocation . toString() , targetConfiguration . 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." );
}
return this . sqlSessionFactoryBuilder . build (targetConfiguration);
}
因为 SqlSessionFactoryBean
只能传入 mapper.xml 的路径,所以这里的处理逻辑只有加载和解析 mapper.xml ,至于 Mapper 接口的话,那是另外的 MapperScannerConfigurer
了,我们马上就说它。
经过这么一大段 afterPropertiesSet
的逻辑,SqlSessionFactory
也就创建出来了。
2.3 事件监听
别忘了,最上面我们在看 SqlSessionFactoryBean
的接口实现中,还实现了一个 ApplicationListener
呢,那对应的 onApplicationEvent
方法我们也应该关注一下:
复制 @ Override
public void onApplicationEvent( ApplicationEvent event) {
if (failFast && event instanceof ContextRefreshedEvent) {
// fail-fast -> check all statements are completed
this . sqlSessionFactory . getConfiguration () . getMappedStatementNames ();
}
}
注意看,它只监听了 ContextRefreshedEvent
类型的事件,那就意味着等 IOC 容器刷新完毕后,这个逻辑就会随之执行,先不看逻辑具体是什么,看一下这行注释:
fail-fast -> check all statements are completed.
快速失败->检查所有语句是否已完成。
所以说这个动作相当于是 MyBatis 初始化完成后的一个收尾检查性质的处理咯。点进去看一下 getMappedStatementNames
方法都干了些啥:
复制 public Collection< String > getMappedStatementNames() {
buildAllStatements() ;
return mappedStatements . keySet ();
}
等一下,这个方法是有返回值的,但上面没用到,所以这个动作只是为了触发 buildAllStatements();
这一行代码咯,那为什么不直接调用它呢?
废话,要不是这个方法设置成 protected 或者 private ,你觉得 MyBatis 不会直接调用它吗?(狗头)
聊回来,我们看看这个 buildAllStatements
方法都干了些什么:
复制 protected void buildAllStatements() {
parsePendingResultMaps() ;
// 处理失败的cache-ref再处理一遍
if ( ! incompleteCacheRefs . isEmpty ()) {
synchronized (incompleteCacheRefs) {
incompleteCacheRefs . removeIf (x -> x . resolveCacheRef () != null );
}
}
// 处理失败的statement再处理一遍
if ( ! incompleteStatements . isEmpty ()) {
synchronized (incompleteStatements) {
incompleteStatements . removeIf (x -> {
x . parseStatementNode ();
return true ;
});
}
}
// 处理失败的Mapper方法再处理一遍
if ( ! incompleteMethods . isEmpty ()) {
synchronized (incompleteMethods) {
incompleteMethods . removeIf (x -> {
x . resolve ();
return true ;
});
}
}
}
可以发现,之前我们在第 10 章和第 13 章中看到的那些没有处理好的东西,在 IOC 容器刷新完毕后,MyBatis 还是不想放弃它们,想再给它们一次机会,于是就把它们又尝试着处理了一次。当然,这也是初始化阶段的最后一次机会了,这次要是再处理不了,那就拉倒了,真不管了。
到此为止,整个 SqlSessionFactoryBean
的任务也就全部完成了,大概抓住几个要点即可:
可以传入全局配置文件,供 MyBatis 解析和处理
SqlSessionFactoryBean
只负责 mapper.xml 的处理,那 Mapper 接口怎么办呢?哎别急,下面还有一个组件呢。
2.4 MapperScannerConfigurer
从类名上也能看得出来,它就是扫描 Mapper 接口的核心了。
2.4.1 类的设计
首先请各位注意一点,这个家伙的本质是一个 BeanDefinitionRegistryPostProcessor
:
复制 public class MapperScannerConfigurer
implements BeanDefinitionRegistryPostProcessor , InitializingBean , ApplicationContextAware , BeanNameAware {
这个细节相当重要,在 Spring 小册的第 28 章中我们有提过它,BeanDefinitionRegistryPostProcessor
的处理阶段最好不要直接使用 getBean
或者相似的方法去获取 IOC 容器中的 Bean ,如果只是为了检查或者依赖相关的 bean 的话,可以只依赖 bean 的 name 。
MyBatis 当然帮我们考虑到这个问题了,于是这个组件的内部设计是这样的:
复制 private SqlSessionFactory sqlSessionFactory;
private SqlSessionTemplate sqlSessionTemplate;
private String sqlSessionFactoryBeanName;
private String sqlSessionTemplateBeanName;
对应的,setter 方法也做了特殊处理:
复制 // 标注为Deprecated,提示我们不要用它
@ Deprecated
public void setSqlSessionFactory( SqlSessionFactory sqlSessionFactory) {
this . sqlSessionFactory = sqlSessionFactory;
}
public void setSqlSessionFactoryBeanName( String sqlSessionFactoryName) {
this . sqlSessionFactoryBeanName = sqlSessionFactoryName;
}
可以说是非常贴心了哈,我们在实际用的时候也要注意为好,否则真的会出一些预期之外的问题。
2.4.2 核心处理逻辑
说完了这个要注意的点,我们还是要看核心的处理逻辑,BeanDefinitionRegistryPostProcessor
的核心方法是 postProcessBeanDefinitionRegistry
,这个方法会向 BeanDefinitionRegistry
中注册新的 BeanDefinition
,具体的逻辑我们可以来看看源码逻辑:
复制 public void postProcessBeanDefinitionRegistry( BeanDefinitionRegistry registry) {
if ( this . processPropertyPlaceHolders ) {
processPropertyPlaceHolders() ;
}
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry) ;
// 一大堆set方法
scanner . registerFilters ();
scanner . scan (
StringUtils . tokenizeToStringArray ( this . basePackage , ConfigurableApplicationContext . CONFIG_LOCATION_DELIMITERS ));
}
很明显,这里面的核心是这个 **ClassPathMapperScanner**
,它会执行包扫描的动作,并且将扫描到的 Mapper 接口都收集起来,构造为一个一个的 MapperFactoryBean
( MapperFactoryBean
想必各位都不会觉得陌生吧,它就是适配 Mapper 接口的工厂 Bean )。那么核心的逻辑也就是 ClassPathMapperScanner
的 scan 方法了。
2.4.2.1 scan
很不巧,这个 scan
方法其实是 ClassPathBeanDefinitionScanner
的,因为 ClassPathMapperScanner
继承自 ClassPathBeanDefinitionScanner
,所以会先来到 ClassPathBeanDefinitionScanner
中:
复制 public int scan( String ... basePackages ) {
int beanCountAtScanStart = this . registry . getBeanDefinitionCount ();
doScan(basePackages) ;
// Register annotation config processors, if necessary.
if ( this . includeAnnotationConfig ) {
AnnotationConfigUtils . registerAnnotationConfigProcessors ( this . registry );
}
return ( this . registry . getBeanDefinitionCount () - beanCountAtScanStart);
}
嚯,又是 SpringFramework 的经典套路了,xxx 方法最终调用 doXxx 方法,真正负责干活的是 doXxx 方法 。其实读到这里我们应该有一种感觉:doXxx 方法应该是类似于模板方法那样,让子类重写了 吧!OK 我们继续往下看。
2.4.2.2 doScan
复制 public Set< BeanDefinitionHolder > doScan( String ... basePackages ) {
Set < BeanDefinitionHolder > beanDefinitions = super . doScan (basePackages);
if ( beanDefinitions . isEmpty ()) {
LOGGER . warn (() -> "No MyBatis mapper was found in '" + Arrays . toString (basePackages)
+ "' package. Please check your configuration." );
} else {
// 注意看这里
processBeanDefinitions(beanDefinitions) ;
}
return beanDefinitions;
}
ClassPathMapperScanner
重写了 doScan
方法,也仅仅是多了一层 BeanDefinition
的处理而已,包扫描的逻辑还是用的 ClassPathBeanDefinitionScanner
的,那我们就不关心具体如何扫描了,主要是看扫描到 Mapper 接口之后都干了什么。
2.4.2.3 processBeanDefinitions
方法体略微有点长,不过没必要拆解开了,小册给源码中关键逻辑都加了注释,所以不用担心看起来费劲:
复制 private void processBeanDefinitions( Set< BeanDefinitionHolder > beanDefinitions) {
GenericBeanDefinition definition;
for ( BeanDefinitionHolder holder : beanDefinitions) {
definition = (GenericBeanDefinition) holder . getBeanDefinition ();
// 获取Mapper接口的全限定名
String beanClassName = definition . getBeanClassName ();
// logger ......
// the mapper interface is the original class of the bean
// but, the actual class of the bean is MapperFactoryBean
// MapperFactoryBean的构造方法需要传入Mapper接口名
definition . getConstructorArgumentValues () . addGenericArgumentValue (beanClassName); // issue #59
definition . setBeanClass ( this . mapperFactoryBeanClass );
definition . getPropertyValues () . add ( "addToConfig" , this . addToConfig );
// 给MapperFactoryBean传入SqlSessionFactory
boolean explicitFactoryUsed = false ;
if ( StringUtils . hasText ( this . sqlSessionFactoryBeanName )) {
definition . getPropertyValues () . add ( "sqlSessionFactory" ,
new RuntimeBeanReference( this . sqlSessionFactoryBeanName ) );
explicitFactoryUsed = true ;
} else if ( this . sqlSessionFactory != null ) {
definition . getPropertyValues () . add ( "sqlSessionFactory" , this . sqlSessionFactory );
explicitFactoryUsed = true ;
}
// 同样的逻辑处理SqlSessionTemplate ......
if ( ! explicitFactoryUsed) {
LOGGER . debug (() -> "Enabling autowire by type for MapperFactoryBean with name '" + holder . getBeanName () + "'." );
definition . setAutowireMode ( AbstractBeanDefinition . AUTOWIRE_BY_TYPE );
}
definition . setLazyInit (lazyInitialization);
}
}
大体走下来,就这么两大步骤:拿着 Mapper 接口的全限定名创建 MapperFactoryBean
,注入 SqlSessionFactory
和 SqlSessionTemplate
。
处理好这些 BeanDefinition
之后,其实 BeanDefinitionRegistry
中就有这些 MapperFactoryBean
的定义了,后续的 bean 实例化阶段也就都能创建出对应的代理对象了,程序执行阶段也就可以拿得到了。
到这里,MyBatis 整合 SpringFramework 的两个重要组件的原理剖析也就全部完成了,内容不是很难,不过需要小伙伴们前期熟悉 SpringFramework 中 IOC 的一些高级特性,以便更好地理解。