前言
最原始的MyBatis的使用,通常有如下几个步骤。
- 读取配置文件mybatis-config.xml构建SqlSessionFactory;
- 通过SqlSessionFactory拿到SqlSession;
- 通过SqlSession拿到Mapper接口的动态代理对象;
- 通过Mapper接口的动态代理对象执行SQL语句;
- 关闭SqlSession。
那么当MyBatis集成到Spring中时,上述的一些对象应该会被Spring容器来管理。本篇文章将对MyBatis集成到Spring中时的关键原理进行学习。
正文
一. MyBatis关键对象生命周期
在单独使用MyBatis时,使用到了几个关键对象,分别是:SqlSessionFactoryBuilder,SqlSessionFactory,SqlSession和Mapper接口实例。下面给出这几个关键对象的生命周期。
对象 | 生命周期 | 说明 |
---|---|---|
SqlSessionFactoryBuilder | 方法局部 | 用于创建SqlSessionFactory,当SqlSessionFactory创建完毕后,就不再需要SqlSessionFactoryBuilder了 |
SqlSessionFactory | 应用级别 | 用于创建SqlSession,由于每次与数据库进行交互时,需要先获取SqlSession,因此SqlSessionFactory应该是单例并且与应用生命周期保持一致 |
SqlSession | 请求或操作 | 用于访问数据库,访问前需要通过SqlSessionFactory创建本次访问所需的SqlSession,访问后需要销毁本次访问使用的SqlSession,所以SqlSession的生命周期是一次请求的开始到结束 |
Mapper接口实例 | 方法 | Mapper接口实例通过SqlSession获取,所以Mapper接口实例的生命周期最长可以与SqlSession相等,同时Mapper接口实例的最佳生命周期范围应该是方法范围,即在一个方法中通过SqlSession获取到Mapper接口实例并执行完逻辑后,该Mapper接口实例就应该被丢弃 |
二. 思考几个问题
如果要实现MyBatis与Spring的集成,那么就需要解决单独使用MyBatis时的几个关键对象的生命周期管理,重点考虑SqlSessionFactory,SqlSession和Mapper接口实例。下面罗列出需要思考的问题。
- SqlSessionFactory在什么时候创建;
- SqlSession如何获取;
- Mapper接口实例如何生成。
三. Spring集成MyBatis示例
在Spring中集成MyBatis,其核心思想就是将单独使用MyBatis时的关键对象交给Spring容器管理,下面看一下Spring集成MyBatis时的配置类,如下所示。
@Configuration
@ComponentScan(value = "扫描包路径")
public class MybatisConfig {
@Bean
public SqlSessionFactoryBean sqlSessionFactory() throws Exception{
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(pooledDataSource());
sqlSessionFactoryBean.setConfigLocation(new ClassPathResource("Mybatis配置文件名"));
return sqlSessionFactoryBean;
}
@Bean
public MapperScannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer msc = new MapperScannerConfigurer();
msc.setBasePackage("映射接口包路径");
return msc;
}
// 创建一个数据源
private PooledDataSource pooledDataSource() {
PooledDataSource dataSource = new PooledDataSource();
dataSource.setUrl("数据库URL地址");
dataSource.setUsername("数据库用户名");
dataSource.setPassword("数据库密码");
dataSource.setDriver("数据库连接驱动");
return dataSource;
}
}
复制代码
如上所示,Spring集成MyBatis时,需要向容器注册SqlSessionFactoryBean(实际就是注册SqlSessionFactory)和MapperScannerConfigurer的bean实例,那么下面就从这两个对象开始分析,Spring是如何集成MyBatis的。
四. SqlSessionFactory的创建源码分析
SqlSessionFactoryBean类图如下所示。
重点关心SqlSessionFactoryBean实现的InitializingBean和FactoryBean接口。首先是InitializingBean接口,在SqlSessionFactoryBean的初始化阶段会调用到SqlSessionFactoryBean实现的afterPropertiesSet() 方法,实现如下。
public void afterPropertiesSet() throws Exception {
// ......
// 创建SqlSessionFactory
this.sqlSessionFactory = buildSqlSessionFactory();
}
复制代码
在SqlSessionFactoryBean的afterPropertiesSet() 方法中会调用到buildSqlSessionFactory() 方法来创建SqlSessionFactory,buildSqlSessionFactory() 方法顾名思义,就是创建SqlSessionFactory,由于该方法很长,就不贴出其实现,总的步骤概括如下。
- 创建XMLConfigBuilder;
- 使用XMLConfigBuilder解析MyBatis配置文件,得到MyBatis全局配置类Configuration;
- 使用SqlSessionFactoryBuilder基于Configuration创建SqlSessionFactory。
在得到SqlSessionFactory后就会将其赋值给SqlSessionFactoryBean的sqlSessionFactory字段。现在又已知SqlSessionFactoryBean实现了FactoryBean接口,那么SqlSessionFactoryBean实际作用是构建复杂的bean,这个复杂的bean通过SqlSessionFactoryBean的getObject() 方法获取,实现如下。
public SqlSessionFactory getObject() throws Exception {
if (this.sqlSessionFactory == null) {
afterPropertiesSet();
}
return this.sqlSessionFactory;
}
复制代码
那么SqlSessionFactoryBean的作用实际就是解析配置文件并构建得到SqlSessionFactory,并最终将SqlSessionFactory放入Spring容器中。
五. SqlSession和Mapper实例创建源码分析
通过分析SqlSessionFactoryBean知道了SqlSessionFactoryBean会构建SqlSessionFactory并放到Spring容器中,那么有了SqlSessionFactory,现在要和数据库交互还需要SqlSession以及Mapper接口实例,但是如果只是单纯的将SqlSession和Mapper接口实例创建出来并放入Spring容器,那么肯定会引入线程不安全的问题,所以在Spring集成MyBatis中,肯定对SqlSession和Mapper接口实例有特殊处理,所以下面继续分析MapperScannerConfigurer, 来探究Spring如何管理SqlSession和Mapper接口实例。
MapperScannerConfigurer的类图如下所示。
由类图可知,MapperScannerConfigurer其实是一个bean工厂后置处理器,其会扫描配置的包路径下的所有Mapper接口并为这些Mapper接口生成BeanDefinition并缓存起来,为接口生成BeanDefinition,那么MyBatis肯定做了特殊处理,才能够让Mapper接口对应的BeanDefinition也能够创建bean,所以下面看一下MapperScannerConfigurer的postProcessBeanDefinitionRegistry() 方法的实现,如下所示。
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
if (this.processPropertyPlaceHolders) {
processPropertyPlaceHolders();
}
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
scanner.setAddToConfig(this.addToConfig);
scanner.setAnnotationClass(this.annotationClass);
scanner.setMarkerInterface(this.markerInterface);
scanner.setSqlSessionFactory(this.sqlSessionFactory);
scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
scanner.setResourceLoader(this.applicationContext);
scanner.setBeanNameGenerator(this.nameGenerator);
scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
if (StringUtils.hasText(lazyInitialization)) {
scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization));
}
if (StringUtils.hasText(defaultScope)) {
scanner.setDefaultScope(defaultScope);
}
scanner.registerFilters();
// 委托给ClassPathBeanDefinitionScanner进行扫描得到BeanDefinition
// 然后ClassPathMapperScanner对得到的BeanDefinition进行特殊处理
scanner.scan(
StringUtils.tokenizeToStringArray(this.basePackage,
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
复制代码
MapperScannerConfigurer将扫描指定包并得到BeanDefinition的逻辑委托给了ClassPathBeanDefinitionScanner,然后又在ClassPathMapperScanner中完成了对Mapper接口对应的BeanDefinition的特殊处理,特殊处理的方法在ClassPathMapperScanner的processBeanDefinitions() 方法中,由于该方法也特别长,下面仅将关键部分罗列出来。
AbstractBeanDefinition definition;
// ......
// 将bean的Class对象设置为MapperFactoryBean的Class对象
definition.setBeanClass(this.mapperFactoryBeanClass);
复制代码
其实processBeanDefinitions()中的特殊处理最关键的就是将BeanDefinition的Class对象设置为了MapperFactoryBean.class
,那么后续基于所有Mapper接口的BeanDefinition来创建bean时,创建出来的bean的类型为MapperFactoryBean,下面先看一下MapperFactoryBean的类图。
通过类图可以发现,MapperFactoryBean中有一个sqlSessionFactory属性字段,其类型就是SqlSessionFactory,那么在MapperFactoryBean的生命周期的属性注入阶段,会调用到MapperFactoryBean的setSqlSessionFactory() 方法来设置sqlSessionFactory属性字段的值,但其实MapperFactoryBean和其父类中是没有sqlSessionFactory属性字段的,但是却提供了sqlSessionFactory属性字段的get() 和set() 方法,那么这样做的用意是什么呢,继续看setSqlSessionFactory() 方法以寻求答案,setSqlSessionFactory() 方法在MapperFactoryBean的父类SqlSessionDaoSupport中,如下所示。
// 入参的SqlSessionFactory就是容器中的SqlSessionFactory
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
if (this.sqlSessionTemplate == null || sqlSessionFactory != this.sqlSessionTemplate.getSqlSessionFactory()) {
this.sqlSessionTemplate = createSqlSessionTemplate(sqlSessionFactory);
}
}
复制代码
现在知道,setSqlSessionFactory() 方法设置sqlSessionFactory属性字段是假,设置sqlSessionTemplate属性字段才是真。sqlSessionTemplate属性字段类型是SqlSessionTemplate,在上面的setSqlSessionFactory() 方法中,会先创建一个SqlSessionTemplate对象,然后赋值给sqlSessionTemplate属性字段,创建SqlSessionTemplate对象的逻辑会一路调用到SqlSessionTemplate的如下构造方法,实现如下。
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
// ......
// 将SqlSessionFactory赋值给SqlSessionTemplate的sqlSessionFactory字段
this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
// sqlSessionProxy字段类型是SqlSession,并且是一个动态代理对象
this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class }, new SqlSessionInterceptor());
}
复制代码
到这里就知道了,每个Mapper接口在容器中的bean实际是一个MapperFactoryBean对象(暂且这么认为,因为MapperFactoryBean实际上还是一个FactoryBean,它实际会将其getObject() 方法的返回值作为Mapper接口在容器中的bean,这个bean就是MyBatis为Mapper接口生成的动态代理对象,这一点后面再分析),每创建一个MapperFactoryBean对象都会new一个SqlSessionTemplate对象,每new一个SqlSessionTemplate对象,就会通过JDK动态代理生成一个SqlSession的动态代理对象,那么现在再看一下SqlSession的动态代理对象中的InvocationHandler(实际为SqlSessionInterceptor)的invoke() 方法的逻辑,如下所示。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 从Spring事务管理器获取SqlSession
// 如果当前存在事务,则从当前事务中获取SqlSession
// 如果获取到的SqlSession为空,则通过SqlSessionFactory新建一个SqlSeesion,并将这个SqlSeesion与事务同步
SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
try {
// 这里没有执行被代理的SqlSession的方法
// 而是执行从Spring事务管理器获取到的SqlSession的方法
Object result = method.invoke(sqlSession, args);
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
// 如果SqlSession不由Spring事务管理器管理,则这里在关闭SqlSession前主动提交一次事务
sqlSession.commit(true);
}
return result;
} catch (Throwable t) {
Throwable unwrapped = unwrapThrowable(t);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator
.translateExceptionIfPossible((PersistenceException) unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw unwrapped;
} finally {
if (sqlSession != null) {
// 如果SqlSession不由Spring事务管理器管理,则在这里关闭SqlSession
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
复制代码
上述invoke() 方法中的逻辑是,每次调用到invoke() 方法,都会从Spring事务管理器获取SqlSession,然后执行获取到的SqlSession的方法,并且invoke() 方法的最后还会对获取到的SqlSession执行关闭逻辑,那么这里就知道了两件事情。
- 被代理的SqlSession并没有被调用,每次invoke() 方法中调用的都是从Spring事务管理器中获取的SqlSession;
- SqlSession的关闭不需要用户操心,每次使用完都会在invoke() 方法的最后被关闭。
那么现在只差最后一步,就可以看清楚Spring集成MyBatis的整体逻辑了,那就是上面提到MapperFactoryBean对象,现在已经知道Mapper接口在容器中的bean是一个MapperFactoryBean对象,但是其实MapperFactoryBean还实现了FactoryBean接口,那么Mapper接口在容器中的bean实际是MapperFactoryBean的getObject() 方法的返回值,下面看一下MapperFactoryBean的getObject() 方法的实现。
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
public SqlSession getSqlSession() {
return this.sqlSessionTemplate;
}
复制代码
继续看SqlSessionTemplate的getMapper() 方法,如下所示。
public <T> T getMapper(Class<T> type) {
return getConfiguration().getMapper(type, this);
}
复制代码
SqlSessionTemplate的getMapper() 方法其实和DefaultSqlSession的getMapper() 方法一样,就是通过Configuration的getMapper() 方法为Mapper接口生成动态代理对象,那么现在知道了,Spring集成MyBatis后,MyBatis还是会为每个Mapper接口生成一个动态代理对象并注册到Spring容器中让Spring管理,那么这里Mapper接口的动态代理对象与单独使用MyBatis时的动态代理对象有什么不同,可以概括如下。
原始使用MyBatis时
,Mapper接口的动态代理对象中的SqlSession是通过SqlSessionFactory创建出来的DefaultSqlSession;Spring集成MyBatis后
,Mapper接口的动态代理对象中的SqlSession是SqlSessionTemplate,而SqlSessionTemplate会将请求转发给其持有的sqlSessionProxy,sqlSessionProxy是SqlSession的动态代理对象,每次调用到sqlSessionProxy的方法时,都会在sqlSessionProxy的InvocationHandler的invoke() 方法中从Spring事务管理器中获取SqlSession,并最终调用从Spring事务管理器中获取到的SqlSession的方法。
那么到这里,Spring集成MyBatis的整体逻辑就分析完毕。
六. 问题回答
1. SqlSessionFactory在什么时候创建
通过SqlSessionFactoryBean在Spring容器初始化阶段生成SqlSessionFactory的bean并注册到Spring容器中。
2. SqlSession如何获取
见问题3。
3. Mapper接口实例如何生成
Mapper接口的实例由MapperFactoryBean的getObject() 方法生成,本质还是MyBatis通过Configuration的getMapper() 方法为Mapper接口生成动态代理对象,每个Mapper接口的动态代理对象由Spring容器管理,并且Mapper接口的动态代理对象持有的SqlSession实际为SqlSessionTemplate。
当调用Mapper接口的动态代理对象的方法时,会调用到SqlSessionTemplate的方法,然后SqlSessionTemplate将调用转发到SqlSessionTemplate持有的SqlSession的动态代理对象,然后调用到SqlSession的动态代理对象InvocationHandler的invoke() 方法,在invoke() 方法中会生成SqlSession,所以SqlSession的生成是在Mapper接口的实例的方法被调用时,且每次调用都会生成一个SqlSession。
总结
Spring集成Mybatis时,有几个关键对象,弄清楚这几个关键对象,也就清楚是如何集成的了。
- SqlSessionFactoryBean
该对象用于向Spring容器注册SqlSessionFactory的bean,所以Spring集成MyBatis时,SqlSessionFactory存在于Spring容器中,生命周期与Spring应用一致
。
- MapperFactoryBean
该对象用于为Mapper接口生成动态代理对象,首先是调用到MapperFactoryBean的getObject() 方法,然后调用到SqlSessionTemplate的getMapper() 方法,然后调用到Configuration的getMapper() 方法,后续就是MyBatis为Mapper接口生成动态代理对象的逻辑了。
- SqlSessionTemplate
该对象由MapperFactoryBean持有,每个Mapper接口对应一个MapperFactoryBean对象,每个MapperFactoryBean对象对应一个SqlSessionTemplate对象,每个SqlSessionTemplate对象持有全局唯一的SqlSessionFactory对象和一个SqlSession的动态代理对象sqlSessionProxy,SqlSessionTemplate的所有CURD操作都是转发给sqlSessionProxy。
- SqlSessionInterceptor
SqlSessionTemplate持有的SqlSession的动态代理对象的InvocationHandler就是一个SqlSessionInterceptor对象,该对象的invoke() 方法会在每次被调用时创建一个SqlSession,然后执行创建出来的SqlSession的方法,并且在invoke() 方法的最后会关闭创建出来的SqlSession。
所以Spring集成MyBatis后,应用程序可以通过注解注入Mapper接口的实例,注入的Mapper接口的实例实际为MyBatis为Mapper接口生成的动态代理对象,调用Mapper接口的实例的方法时,调用请求会发送到SqlSessionTemplate,然后SqlSessionTemplate会将调用请求转发到sqlSessionProxy,然后在sqlSessionProxy的InvocationHandler的invoke() 方法中创建SqlSession,然后调用创建出来的SqlSession的方法,调用完毕后,会在InvocationHandler的invoke() 方法最后关闭创建出来的SqlSession,所以SqlSession的生命周期也是一次请求从开始到结束
。
Spring集成MyBatis后,调用Mapper接口的动态代理对象的一次请求时序图如下。