当前位置:   article > 正文

超详细解释MyBatis与Spring的集成原理_spring与mybatis整合原理

spring与mybatis整合原理

前言

最原始的MyBatis的使用,通常有如下几个步骤。

  1. 读取配置文件mybatis-config.xml构建SqlSessionFactory
  2. 通过SqlSessionFactory拿到SqlSession
  3. 通过SqlSession拿到Mapper接口的动态代理对象;
  4. 通过Mapper接口的动态代理对象执行SQL语句;
  5. 关闭SqlSession

那么当MyBatis集成到Spring中时,上述的一些对象应该会被Spring容器来管理。本篇文章将对MyBatis集成到Spring中时的关键原理进行学习。

正文

一. MyBatis关键对象生命周期

在单独使用MyBatis时,使用到了几个关键对象,分别是:SqlSessionFactoryBuilderSqlSessionFactorySqlSessionMapper接口实例。下面给出这几个关键对象的生命周期。

对象生命周期说明
SqlSessionFactoryBuilder方法局部用于创建SqlSessionFactory,当SqlSessionFactory创建完毕后,就不再需要SqlSessionFactoryBuilder
SqlSessionFactory应用级别用于创建SqlSession,由于每次与数据库进行交互时,需要先获取SqlSession,因此SqlSessionFactory应该是单例并且与应用生命周期保持一致
SqlSession请求或操作用于访问数据库,访问前需要通过SqlSessionFactory创建本次访问所需的SqlSession,访问后需要销毁本次访问使用的SqlSession,所以SqlSession的生命周期是一次请求的开始到结束
Mapper接口实例方法Mapper接口实例通过SqlSession获取,所以Mapper接口实例的生命周期最长可以与SqlSession相等,同时Mapper接口实例的最佳生命周期范围应该是方法范围,即在一个方法中通过SqlSession获取到Mapper接口实例并执行完逻辑后,该Mapper接口实例就应该被丢弃

二. 思考几个问题

如果要实现MyBatisSpring的集成,那么就需要解决单独使用MyBatis时的几个关键对象的生命周期管理,重点考虑SqlSessionFactorySqlSessionMapper接口实例。下面罗列出需要思考的问题。

  1. SqlSessionFactory在什么时候创建;
  2. SqlSession如何获取;
  3. Mapper接口实例如何生成。

三. Spring集成MyBatis示例

Spring中集成MyBatis,其核心思想就是将单独使用MyBatis时的关键对象交给Spring容器管理,下面看一下Spring集成MyBatis时的配置类,如下所示。

  1. @Configuration
  2. @ComponentScan(value = "扫描包路径")
  3. public class MybatisConfig {
  4. @Bean
  5. public SqlSessionFactoryBean sqlSessionFactory() throws Exception{
  6. SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
  7. sqlSessionFactoryBean.setDataSource(pooledDataSource());
  8. sqlSessionFactoryBean.setConfigLocation(new ClassPathResource("Mybatis配置文件名"));
  9. return sqlSessionFactoryBean;
  10. }
  11. @Bean
  12. public MapperScannerConfigurer mapperScannerConfigurer(){
  13. MapperScannerConfigurer msc = new MapperScannerConfigurer();
  14. msc.setBasePackage("映射接口包路径");
  15. return msc;
  16. }
  17. // 创建一个数据源
  18. private PooledDataSource pooledDataSource() {
  19. PooledDataSource dataSource = new PooledDataSource();
  20. dataSource.setUrl("数据库URL地址");
  21. dataSource.setUsername("数据库用户名");
  22. dataSource.setPassword("数据库密码");
  23. dataSource.setDriver("数据库连接驱动");
  24. return dataSource;
  25. }
  26. }
  27. 复制代码

如上所示,Spring集成MyBatis时,需要向容器注册SqlSessionFactoryBean(实际就是注册SqlSessionFactory)和MapperScannerConfigurerbean实例,那么下面就从这两个对象开始分析,Spring是如何集成MyBatis的。

四. SqlSessionFactory的创建源码分析

SqlSessionFactoryBean类图如下所示。

重点关心SqlSessionFactoryBean实现的InitializingBeanFactoryBean接口。首先是InitializingBean接口,在SqlSessionFactoryBean的初始化阶段会调用到SqlSessionFactoryBean实现的afterPropertiesSet() 方法,实现如下。

  1. public void afterPropertiesSet() throws Exception {
  2. // ......
  3. // 创建SqlSessionFactory
  4. this.sqlSessionFactory = buildSqlSessionFactory();
  5. }
  6. 复制代码

SqlSessionFactoryBeanafterPropertiesSet() 方法中会调用到buildSqlSessionFactory() 方法来创建SqlSessionFactorybuildSqlSessionFactory() 方法顾名思义,就是创建SqlSessionFactory,由于该方法很长,就不贴出其实现,总的步骤概括如下。

  1. 创建XMLConfigBuilder
  2. 使用XMLConfigBuilder解析MyBatis配置文件,得到MyBatis全局配置类Configuration
  3. 使用SqlSessionFactoryBuilder基于Configuration创建SqlSessionFactory

在得到SqlSessionFactory后就会将其赋值给SqlSessionFactoryBeansqlSessionFactory字段。现在又已知SqlSessionFactoryBean实现了FactoryBean接口,那么SqlSessionFactoryBean实际作用是构建复杂的bean,这个复杂的bean通过SqlSessionFactoryBeangetObject() 方法获取,实现如下。

  1. public SqlSessionFactory getObject() throws Exception {
  2. if (this.sqlSessionFactory == null) {
  3. afterPropertiesSet();
  4. }
  5. return this.sqlSessionFactory;
  6. }
  7. 复制代码

那么SqlSessionFactoryBean的作用实际就是解析配置文件并构建得到SqlSessionFactory,并最终将SqlSessionFactory放入Spring容器中。

五. SqlSession和Mapper实例创建源码分析

通过分析SqlSessionFactoryBean知道了SqlSessionFactoryBean会构建SqlSessionFactory并放到Spring容器中,那么有了SqlSessionFactory,现在要和数据库交互还需要SqlSession以及Mapper接口实例,但是如果只是单纯的将SqlSessionMapper接口实例创建出来并放入Spring容器,那么肯定会引入线程不安全的问题,所以在Spring集成MyBatis中,肯定对SqlSessionMapper接口实例有特殊处理,所以下面继续分析MapperScannerConfigurer, 来探究Spring如何管理SqlSessionMapper接口实例。

MapperScannerConfigurer的类图如下所示。

由类图可知,MapperScannerConfigurer其实是一个bean工厂后置处理器,其会扫描配置的包路径下的所有Mapper接口并为这些Mapper接口生成BeanDefinition并缓存起来,为接口生成BeanDefinition,那么MyBatis肯定做了特殊处理,才能够让Mapper接口对应的BeanDefinition也能够创建bean,所以下面看一下MapperScannerConfigurerpostProcessBeanDefinitionRegistry() 方法的实现,如下所示。

  1. public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
  2. if (this.processPropertyPlaceHolders) {
  3. processPropertyPlaceHolders();
  4. }
  5. ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
  6. scanner.setAddToConfig(this.addToConfig);
  7. scanner.setAnnotationClass(this.annotationClass);
  8. scanner.setMarkerInterface(this.markerInterface);
  9. scanner.setSqlSessionFactory(this.sqlSessionFactory);
  10. scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
  11. scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
  12. scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
  13. scanner.setResourceLoader(this.applicationContext);
  14. scanner.setBeanNameGenerator(this.nameGenerator);
  15. scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
  16. if (StringUtils.hasText(lazyInitialization)) {
  17. scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization));
  18. }
  19. if (StringUtils.hasText(defaultScope)) {
  20. scanner.setDefaultScope(defaultScope);
  21. }
  22. scanner.registerFilters();
  23. // 委托给ClassPathBeanDefinitionScanner进行扫描得到BeanDefinition
  24. // 然后ClassPathMapperScanner对得到的BeanDefinition进行特殊处理
  25. scanner.scan(
  26. StringUtils.tokenizeToStringArray(this.basePackage,
  27. ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
  28. }
  29. 复制代码

MapperScannerConfigurer将扫描指定包并得到BeanDefinition的逻辑委托给了ClassPathBeanDefinitionScanner,然后又在ClassPathMapperScanner中完成了对Mapper接口对应的BeanDefinition的特殊处理,特殊处理的方法在ClassPathMapperScannerprocessBeanDefinitions() 方法中,由于该方法也特别长,下面仅将关键部分罗列出来。

  1. AbstractBeanDefinition definition;
  2. // ......
  3. // 将bean的Class对象设置为MapperFactoryBean的Class对象
  4. definition.setBeanClass(this.mapperFactoryBeanClass);
  5. 复制代码

其实processBeanDefinitions()中的特殊处理最关键的就是将BeanDefinition的Class对象设置为了MapperFactoryBean.class,那么后续基于所有Mapper接口的BeanDefinition来创建bean时,创建出来的bean的类型为MapperFactoryBean,下面先看一下MapperFactoryBean的类图。

通过类图可以发现,MapperFactoryBean中有一个sqlSessionFactory属性字段,其类型就是SqlSessionFactory,那么在MapperFactoryBean的生命周期的属性注入阶段,会调用到MapperFactoryBeansetSqlSessionFactory() 方法来设置sqlSessionFactory属性字段的值,但其实MapperFactoryBean和其父类中是没有sqlSessionFactory属性字段的,但是却提供了sqlSessionFactory属性字段的get()set() 方法,那么这样做的用意是什么呢,继续看setSqlSessionFactory() 方法以寻求答案,setSqlSessionFactory() 方法在MapperFactoryBean的父类SqlSessionDaoSupport中,如下所示。

  1. // 入参的SqlSessionFactory就是容器中的SqlSessionFactory
  2. public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
  3. if (this.sqlSessionTemplate == null || sqlSessionFactory != this.sqlSessionTemplate.getSqlSessionFactory()) {
  4. this.sqlSessionTemplate = createSqlSessionTemplate(sqlSessionFactory);
  5. }
  6. }
  7. 复制代码

现在知道,setSqlSessionFactory() 方法设置sqlSessionFactory属性字段是假,设置sqlSessionTemplate属性字段才是真。sqlSessionTemplate属性字段类型是SqlSessionTemplate,在上面的setSqlSessionFactory() 方法中,会先创建一个SqlSessionTemplate对象,然后赋值给sqlSessionTemplate属性字段,创建SqlSessionTemplate对象的逻辑会一路调用到SqlSessionTemplate的如下构造方法,实现如下。

  1. public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
  2. PersistenceExceptionTranslator exceptionTranslator) {
  3. // ......
  4. // 将SqlSessionFactory赋值给SqlSessionTemplate的sqlSessionFactory字段
  5. this.sqlSessionFactory = sqlSessionFactory;
  6. this.executorType = executorType;
  7. this.exceptionTranslator = exceptionTranslator;
  8. // sqlSessionProxy字段类型是SqlSession,并且是一个动态代理对象
  9. this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
  10. new Class[] { SqlSession.class }, new SqlSessionInterceptor());
  11. }
  12. 复制代码

到这里就知道了,每个Mapper接口在容器中的bean实际是一个MapperFactoryBean对象(暂且这么认为,因为MapperFactoryBean实际上还是一个FactoryBean,它实际会将其getObject() 方法的返回值作为Mapper接口在容器中的bean,这个bean就是MyBatisMapper接口生成的动态代理对象,这一点后面再分析),每创建一个MapperFactoryBean对象都会new一个SqlSessionTemplate对象,每new一个SqlSessionTemplate对象,就会通过JDK动态代理生成一个SqlSession的动态代理对象,那么现在再看一下SqlSession的动态代理对象中的InvocationHandler(实际为SqlSessionInterceptor)的invoke() 方法的逻辑,如下所示。

  1. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  2. // 从Spring事务管理器获取SqlSession
  3. // 如果当前存在事务,则从当前事务中获取SqlSession
  4. // 如果获取到的SqlSession为空,则通过SqlSessionFactory新建一个SqlSeesion,并将这个SqlSeesion与事务同步
  5. SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
  6. SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
  7. try {
  8. // 这里没有执行被代理的SqlSession的方法
  9. // 而是执行从Spring事务管理器获取到的SqlSession的方法
  10. Object result = method.invoke(sqlSession, args);
  11. if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
  12. // 如果SqlSession不由Spring事务管理器管理,则这里在关闭SqlSession前主动提交一次事务
  13. sqlSession.commit(true);
  14. }
  15. return result;
  16. } catch (Throwable t) {
  17. Throwable unwrapped = unwrapThrowable(t);
  18. if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
  19. closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
  20. sqlSession = null;
  21. Throwable translated = SqlSessionTemplate.this.exceptionTranslator
  22. .translateExceptionIfPossible((PersistenceException) unwrapped);
  23. if (translated != null) {
  24. unwrapped = translated;
  25. }
  26. }
  27. throw unwrapped;
  28. } finally {
  29. if (sqlSession != null) {
  30. // 如果SqlSession不由Spring事务管理器管理,则在这里关闭SqlSession
  31. closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
  32. }
  33. }
  34. }
  35. 复制代码

上述invoke() 方法中的逻辑是,每次调用到invoke() 方法,都会从Spring事务管理器获取SqlSession,然后执行获取到的SqlSession的方法,并且invoke() 方法的最后还会对获取到的SqlSession执行关闭逻辑,那么这里就知道了两件事情。

  1. 被代理的SqlSession并没有被调用,每次invoke() 方法中调用的都是从Spring事务管理器中获取的SqlSession
  2. SqlSession的关闭不需要用户操心,每次使用完都会在invoke() 方法的最后被关闭。

那么现在只差最后一步,就可以看清楚Spring集成MyBatis的整体逻辑了,那就是上面提到MapperFactoryBean对象,现在已经知道Mapper接口在容器中的bean是一个MapperFactoryBean对象,但是其实MapperFactoryBean还实现了FactoryBean接口,那么Mapper接口在容器中的bean实际是MapperFactoryBeangetObject() 方法的返回值,下面看一下MapperFactoryBeangetObject() 方法的实现。

  1. public T getObject() throws Exception {
  2. return getSqlSession().getMapper(this.mapperInterface);
  3. }
  4. public SqlSession getSqlSession() {
  5. return this.sqlSessionTemplate;
  6. }
  7. 复制代码

继续看SqlSessionTemplategetMapper() 方法,如下所示。

  1. public <T> T getMapper(Class<T> type) {
  2. return getConfiguration().getMapper(type, this);
  3. }
  4. 复制代码

SqlSessionTemplategetMapper() 方法其实和DefaultSqlSessiongetMapper() 方法一样,就是通过ConfigurationgetMapper() 方法为Mapper接口生成动态代理对象,那么现在知道了,Spring集成MyBatis后,MyBatis还是会为每个Mapper接口生成一个动态代理对象并注册到Spring容器中让Spring管理,那么这里Mapper接口的动态代理对象与单独使用MyBatis时的动态代理对象有什么不同,可以概括如下。

  1. 原始使用MyBatis时Mapper接口的动态代理对象中的SqlSession是通过SqlSessionFactory创建出来的DefaultSqlSession
  2. Spring集成MyBatis后Mapper接口的动态代理对象中的SqlSessionSqlSessionTemplate,而SqlSessionTemplate会将请求转发给其持有的sqlSessionProxysqlSessionProxySqlSession的动态代理对象,每次调用到sqlSessionProxy的方法时,都会在sqlSessionProxyInvocationHandlerinvoke() 方法中从Spring事务管理器中获取SqlSession,并最终调用从Spring事务管理器中获取到的SqlSession的方法。

那么到这里,Spring集成MyBatis的整体逻辑就分析完毕。

六. 问题回答

1. SqlSessionFactory在什么时候创建

通过SqlSessionFactoryBeanSpring容器初始化阶段生成SqlSessionFactorybean并注册到Spring容器中。

2. SqlSession如何获取

见问题3。

3. Mapper接口实例如何生成

Mapper接口的实例由MapperFactoryBeangetObject() 方法生成,本质还是MyBatis通过ConfigurationgetMapper() 方法为Mapper接口生成动态代理对象,每个Mapper接口的动态代理对象由Spring容器管理,并且Mapper接口的动态代理对象持有的SqlSession实际为SqlSessionTemplate

当调用Mapper接口的动态代理对象的方法时,会调用到SqlSessionTemplate的方法,然后SqlSessionTemplate将调用转发到SqlSessionTemplate持有的SqlSession的动态代理对象,然后调用到SqlSession的动态代理对象InvocationHandlerinvoke() 方法,在invoke() 方法中会生成SqlSession,所以SqlSession的生成是在Mapper接口的实例的方法被调用时,且每次调用都会生成一个SqlSession

总结

Spring集成Mybatis时,有几个关键对象,弄清楚这几个关键对象,也就清楚是如何集成的了。

  1. SqlSessionFactoryBean

该对象用于向Spring容器注册SqlSessionFactorybean,所以Spring集成MyBatis时,SqlSessionFactory存在于Spring容器中,生命周期与Spring应用一致

  1. MapperFactoryBean

该对象用于为Mapper接口生成动态代理对象,首先是调用到MapperFactoryBeangetObject() 方法,然后调用到SqlSessionTemplategetMapper() 方法,然后调用到ConfigurationgetMapper() 方法,后续就是MyBatisMapper接口生成动态代理对象的逻辑了。

  1. SqlSessionTemplate

该对象由MapperFactoryBean持有,每个Mapper接口对应一个MapperFactoryBean对象,每个MapperFactoryBean对象对应一个SqlSessionTemplate对象,每个SqlSessionTemplate对象持有全局唯一SqlSessionFactory对象和一个SqlSession的动态代理对象sqlSessionProxySqlSessionTemplate的所有CURD操作都是转发给sqlSessionProxy

  1. SqlSessionInterceptor

SqlSessionTemplate持有的SqlSession的动态代理对象的InvocationHandler就是一个SqlSessionInterceptor对象,该对象的invoke() 方法会在每次被调用时创建一个SqlSession,然后执行创建出来的SqlSession的方法,并且在invoke() 方法的最后会关闭创建出来的SqlSession

所以Spring集成MyBatis后,应用程序可以通过注解注入Mapper接口的实例,注入的Mapper接口的实例实际为MyBatisMapper接口生成的动态代理对象,调用Mapper接口的实例的方法时,调用请求会发送到SqlSessionTemplate,然后SqlSessionTemplate会将调用请求转发到sqlSessionProxy,然后在sqlSessionProxyInvocationHandlerinvoke() 方法中创建SqlSession,然后调用创建出来的SqlSession的方法,调用完毕后,会在InvocationHandlerinvoke() 方法最后关闭创建出来的SqlSession所以SqlSession的生命周期也是一次请求从开始到结束

Spring集成MyBatis后,调用Mapper接口的动态代理对象的一次请求时序图如下。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/在线问答5/article/detail/892602
推荐阅读
相关标签
  

闽ICP备14008679号