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 的一些高级特性,以便更好地理解。