思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。
作者:毅航😜
从BeanFactory
入手,逐步分析Mybatis-starter
工作原理,讲透SpringBoot
与Mybaits
整合的原理。
前言
在专栏Mybatis源码分析中笔者以学习Mybaits
时接触的四行代码为例详细的分析Mybatis
的内部工作原理,感兴趣的同学
可前往专栏Mybatis源码分析进行查看。
入门
Mybatis
的四行代码
java
//<1> 加载配置文件
InputStream is = Resources.getResourceAsStream("mybatis.xml");
//<2> 创建sessionFactory对象
sessionFactory = new SqlSessionFactoryBuilder().build(is);
//<3> 获取sqlSession对象信息
SqlSession session = factoy.openSqlSession()
//<4> 构建映射器的代理对象
UserMapper mapper = session.getMapper(UserMapper.class);
// .....调用相关方法信息
而本次我们主要探究Mybatis-starter
的工作原理,即探究分析清楚Spring
整合Mybatis
背后故事
。
橄榄FactoryBean
FactoryBean
------Spring
留给外部框架的扩展点
FactoryBean
是Spring
框架中的一种特殊Bean
。具体来看,FactoryBean
的特殊之处在于它可以向容器中注册两个Bean
,一个是它本身,一个是FactoryBean.getObject()
方法返回值所代表的Bean
。
更通俗一点来讲, FactoryBean
允许开发人员可以定制化地创建和配置Bean
实例,从而使得Bean
的创建过程更加灵活和可扩展。
下面我们便通过一个简单的例子来快速了解FactoryBean
的使用:
java
public class CustomerFactoryBean implements FactoryBean<UserService> {
@Override
public UserService getObject() throws Exception {
return new UserService();
}
}
上述代码最终会在Spring
的容器中生成了两个Bean
- 一个是类型为
FactoryBean
的customerFactoryBean
- 另一个则是类型为
UserService
的bean
其名称为customerFactoryBean
进一步,如果通过如下代码获取customerFactoryBean
,你会发现其最终的返回值的类型为UserService
而不是FactoyBean
类型。
java
public class MainApplication {
public static void main(String[] args) {
// 获取bean
Object customerFactoryBean = applicationContext.getBean("customerFactoryBean");
System.out.println(customerFactoryBean);
}
}
这其实是Spring
内部关于FactoryBean
的一个优化处理,如果要获取到FactoryBean
则需要在bean
名称前加上一个&
。例如,applicationContext.getBean("&customerFactoryBean");
知道了FactoryBean
后,接下来我们将分析Mybatis-starter
中是如何通过使用FactoryBean
来实现Spring
整合Mybatis
的原理。
但在开始分析之前,先来回顾下使用Spring
整合Mybatis
时的相关配置。
Spring
整合Mybaits
时的相关配置
Spring
整合Mybatis
时通常需要在applicationContext.xml
配置如下内容:
xml
<!-- 配置数据源 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mydatabase"/>
<property name="username" value="username"/>
<property name="password" value="password"/>
</bean>
<!-- 配置MyBatis的SqlSessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<!-- 配置Mapper所需的sql信息 -->
<property name="mapperLocations" value="classpath:mapper/*.xml"/>
</bean>
<!-- 配置MyBatis的Mapper扫描器 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.example.dao"/>
</bean>
不难发现,如上的配置文件主要做了如下工作:
- 指明数据源信息。
- 配置
SqlSessionFactoryBean
。并使用SqlSessionFactory
引用数据源,同时配置Mapper
的相关配置文件。 - 配置
MapperScannerConfigurer
。其主要用于自动扫描并注册Mapper
接口,属性basePackage
属性用于指定了Mapper
接口所在的包。
不难发现在上述配置文件中,用到了两个重要的类信息即SqlSessionFactory
与MapperScannerConfigurer
。这两个类在后续实现Spring
与Mybatis
整合中扮演着重要的角色。
开始分析之前,你一定要明白Spring
整合Mybatis
的核心逻辑就是:将使用MyBatis
时需要手动创建的对象全部注入到Spring
容器,以交由容器管理。
SqlSessionFactory
的构建
SqlSessionFactory
相关代码
java
public class SqlSessionFactoryBean implements
FactoryBean<SqlSessionFactory>, InitializingBean,
ApplicationListener<ApplicationEvent> {
// ...省略其他代码
public SqlSessionFactory getObject() throws Exception {
if (this.sqlSessionFactory == null) {
afterPropertiesSet();
}
return this.sqlSessionFactory;
}
}
经过之前对于FactoryBean
的介绍,不难发现SqlSessionFactoryBean
是Spring
中用于配置MyBatis
的SqlSessionFactory
的工厂类。
具体来看,其会向Spring
容器中注册一个SqlSessionFactory
的bean
对象。进一步,sqlSessionFactory
会通过方法afterPropertiesSet()
来完成初始化。 具体逻辑如下:
java
public void afterPropertiesSet() throws Exception {
// 省略其他逻辑
this.sqlSessionFactory = buildSqlSessionFactory();
}
由于buildSqlSessionFactory()
方法逻辑很长,此处就不带着大家逐行分析了。但你可以稍微思考下buildSqlSessionFactory()
内部会完后那些逻辑呢?
在之前的Mybatis流程分析(二):配置处理,构建SqlSession工厂中我们就曾对new SqlSessionFactoryBuilder().build(is);
背后的工作流程进行过详细的分析,其内部无非就会做两件事
- 解析
MyBatis
配置文件,构建全局配置类Configuration
对象; - 通过
new
关键字构建一个DefaultSqlSessionFactory
对象,并且将Configuration
进行传入。
而这其实恰好也是buildSqlSessionFactory()
的关键逻辑。事实上,只要你在读buildSqlSessionFactory()
方法时把握住如上两点,读懂一个小小的buildSqlSessionFactory()
方法根本不在话下。
(Ps:在笔者
看来,当你熟悉了Mybatis
背后的工作原理后。像buildSqlSessionFactory()
这些方法其实点进去看一眼就能明白其所作的逻辑,根本无需耗费太多精力去钻研其细节!我相信如果你有看过专栏之前文章的话,回头再看Mybatis
源码,一定会倍感轻松~~)
至此,相信你已经明白了SqlSessionFactoryBean
的作用了。其无非就是实解析配置文件并构建得到SqlSessionFactory
,并最终将SqlSessionFactory
放入Spring
容器中。
扫描路下的类信息并构建Bean
如果我们不引入Spring
,即原生使用Mybatis
开发时,通常会首先构建SqlSessionFactory
然后来获取SqlSeession
会话,进而通过SqlSeession
来构建Mapper
的代理对象,从而完成对数据的操纵。
但当引入Spring
后,其内部又将如何来完成SqlSeession
与Mapper
的构建呢?其关键就在于MapperScannerConfigurer
对象。接下来,我们便来分析MapperScannerConfigurer
的具体细节。
java
public class MapperScannerConfigurer
implements BeanDefinitionRegistryPostProcessor,
InitializingBean,
ApplicationContextAware,
BeanNameAware {
// 省略内部其他相关方法
}
其实MapperScannerConfigurer
内部有很多方法,当你看到 MapperScannerConfigurer
中诸多的方法你可能会有点蒙圈
,不知道该分析类中的那个方法。如果你有这个问题那么说明你对于Spring
框架还是不熟悉,那不妨关注笔者
的新专栏Spring打怪升级之路,笔者
正在从零分析Spring
框架的相关知识点,并立志于构建一个smart-spring
!
回到文章,不难发现MapperScannerConfigurer
会实现一个名为BeanDefinitionRegistryPostProcessor
的接口。而BeanDefinitionRegistryPostProcessor
是Spring
框架中的一个扩展点,其可以向容器中动态注册新的Bean
。
进一步,MapperScannerConfigurer
的postProcessBeanDefinitionRegistry
方法逻辑如下:
java
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
// 省略其他无关逻辑
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
// 委托给ClassPathBeanDefinitionScanner进行扫描得到BeanDefinition
scanner.scan(
StringUtils.tokenizeToStringArray(this.basePackage,
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
其中,postProcessBeanDefinitionRegistry
将对bean
的扫描交给ClassPathMapperScanner
,用以扫描指定包并得到BeanDefinition
。这部分的调用逻辑具体如下所示:
这里你要注意,这里扫描对应basePackage
所得到的并不是像容器中注入一个接口所对应的实例对象!此处向Spring
容器中注入bean
的类型为mapperFactoryBean
。
看到这里你可能会有点乱,我们来简单梳理一下。之前谈的SqlSessionFactoryBean
是一个FactoryBean
,其作用在于向容器中注入一个SqlSessionFactory
;之后,而后讨论的MapperScannerConfigurer
其主要作用就是为了扫描basepackage
下我们所定义的接口信息,进而封装成一个bean
放入容器。
而后出现的MapperFactoryBean
则是MapperScannerConfigurer
扫描路径下所有类型后得到bean
所对应类型。换言之,我们Dao
层所定义的接口,最终放入容器后bean
所对应的类型为MapperFactoryBean
。 如下这张图准确的反映了三者之间的关系。
窥探MapperFactoryBean
的秘密
经过上述的讨论,相信你已经清楚了。每个Mapper
接口在容器中的bean
实际是一个MapperFactoryBean
对象。通过名字不难发现,MapperFactoryBean
实际上还是一个FactoryBean
。既然是FactoryBean
那么其一定会重写getObject()
方法。接下来,我们便看看MapperFactoryBean
中的getObject()
会完成那些逻辑。
MapperFactoryBean
java
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
@Override
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
@Override
public Class<T> getObjectType() {
return this.mapperInterface;
}
@Override
public boolean isSingleton() {
// 返回true是为了让Mapper接口是一个单例的
return true;
}
}
不难发现,MapperFactoryBean
中的getObject()
方法的返回值为Mapper
接口在容器中的bean
。
如果你问我为什么这么笃定,我的答案就是 getSqlSession(). getMapper(this.mapperInterface);
!这行代码。
(注:有关Mybatis
根据Mapper
接口生成代理对象的逻辑可参考专栏Mybatis源码分析相关文章!)
这里还要强调一点,MapperFactoryBean
中getSqlSession()
所返回的SqlSession
的实际类型为SqlSessionTemplate
。进一步,SqlSessionTemplate
中会持有一个sqlSessionProxy
对象。而这个sqlSessionProxy
是SqlSession
的动态代理对象。
因此,每次调用到sqlSessionProxy
的方法时,都会从Spring
事务管理器中获取SqlSession
,并最终调用从Spring
事务管理器中获取到的SqlSession
对象,进而调用其内部对应方法。
至于这里为什么要这么绕,笔者
考虑可能出于如下几个原因:
具体原因如下:
-
事务管理 :
SqlSessionTemplate
在执行数据库操作时会自动参与Spring
事务管理,确保数据库操作与Spring
事务同步。这是非常重要的,因为在Spring
应用中,你通常会使用Spring
的事务管理功能来管理数据库事务。而SqlSessionTemplate
会根据Spring
事务的状态来创建和管理SqlSession
对象。 -
线程安全性 :
SqlSessionTemplate
确保每个线程都拥有自己的SqlSession
实例,从而避免多线程并发访问SqlSession
时可能出现的问题。
总之,MapperFactoryBean
内部使用SqlSessionTemplate
来获取SqlSession
,可以确保MyBatis
与Spring
的集成更加稳定和易于管理。这使得你可以更容易地将MyBatis
与Spring
集成,并充分利用Spring
框架提供的事务管理和异常处理机制。
(Ps:这里可能比较绕,但你只需知道其底层是通过SqlSession
会话获取对应Mapper
接口代理对象即可)
总结
至此,其实我们也就分析清楚了Spring
与Mybatis
整合的背后的秘密,其本质无非就是FactoryBean
的灵活应用,从而使得原先通过手动创建的逻辑交给容器来自动
扫描。
写在最后
或许,分析源码的过程相对枯燥,在当下这个快节奏的时代下,这种行为似乎并不讨喜,甚至有些人觉得源码
只是面试时会用到,平时工作熟练掌握crud
,同时汇报时把ppt
做好就可以了,何必耗费多余的精力去分析源码呢?这个问题其实很难去回答,但笔者
的建议就是去做让自己开心的事就行了。
于笔者而言看源码、分析源码、写作、分享
是让笔者
感到开心的一件事,写作之初笔者曾过分纠结于文章阅读量和点赞量,为此也专门发过沸点来询问广大掘友
的意见,得到的答案无非就是题材选择有问题。对于这个问题笔者认真思考过,也考虑过更换方向,但后来笔者
后期还是会选择坚持,因为笔者
更想通过文章的形式来回顾自己学编程一路而来的努力和付出,愿我们彼此的坚持最终能迎来明日的曙光,就这样吧!
再补充一点,
笔者
专栏的文章是成体系的,从头开始读效果更佳哦!当然觉得文章还可以的话,不妨点赞+收藏+关注,不错过之后的每一次更新。