赞
踩
最原始的MyBatis的使用,通常有如下几个步骤。
那么当MyBatis集成到Spring中时,上述的一些对象应该会被Spring容器来管理。本篇文章将对MyBatis集成到Spring中时的关键原理进行学习。
在单独使用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接口实例。下面罗列出需要思考的问题。
在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的。
SqlSessionFactoryBean类图如下所示。
重点关心SqlSessionFactoryBean实现的InitializingBean和FactoryBean接口。首先是InitializingBean接口,在SqlSessionFactoryBean的初始化阶段会调用到SqlSessionFactoryBean实现的afterPropertiesSet() 方法,实现如下。
- public void afterPropertiesSet() throws Exception {
-
- // ......
-
- // 创建SqlSessionFactory
- this.sqlSessionFactory = buildSqlSessionFactory();
- }
- 复制代码
在SqlSessionFactoryBean的afterPropertiesSet() 方法中会调用到buildSqlSessionFactory() 方法来创建SqlSessionFactory,buildSqlSessionFactory() 方法顾名思义,就是创建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容器中。
通过分析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执行关闭逻辑,那么这里就知道了两件事情。
那么现在只差最后一步,就可以看清楚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时,有几个关键对象,弄清楚这几个关键对象,也就清楚是如何集成的了。
该对象用于向Spring容器注册SqlSessionFactory的bean,所以Spring集成MyBatis时,SqlSessionFactory存在于Spring容器中,生命周期与Spring应用一致
。
该对象用于为Mapper接口生成动态代理对象,首先是调用到MapperFactoryBean的getObject() 方法,然后调用到SqlSessionTemplate的getMapper() 方法,然后调用到Configuration的getMapper() 方法,后续就是MyBatis为Mapper接口生成动态代理对象的逻辑了。
该对象由MapperFactoryBean持有,每个Mapper接口对应一个MapperFactoryBean对象,每个MapperFactoryBean对象对应一个SqlSessionTemplate对象,每个SqlSessionTemplate对象持有全局唯一的SqlSessionFactory对象和一个SqlSession的动态代理对象sqlSessionProxy,SqlSessionTemplate的所有CURD操作都是转发给sqlSessionProxy。
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接口的动态代理对象的一次请求时序图如下。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。