超详细解释MyBatis与Spring的集成原理

3549 篇文章 121 订阅

前言

最原始的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时的配置类,如下所示。

@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)和MapperScannerConfigurerbean实例,那么下面就从这两个对象开始分析,Spring是如何集成MyBatis的。

四. SqlSessionFactory的创建源码分析

SqlSessionFactoryBean类图如下所示。

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

public void afterPropertiesSet() throws Exception {
    
    // ......

    // 创建SqlSessionFactory
    this.sqlSessionFactory = buildSqlSessionFactory();
}
复制代码

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

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

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

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接口实例,但是如果只是单纯的将SqlSessionMapper接口实例创建出来并放入Spring容器,那么肯定会引入线程不安全的问题,所以在Spring集成MyBatis中,肯定对SqlSessionMapper接口实例有特殊处理,所以下面继续分析MapperScannerConfigurer, 来探究Spring如何管理SqlSessionMapper接口实例。

MapperScannerConfigurer的类图如下所示。

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

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的特殊处理,特殊处理的方法在ClassPathMapperScannerprocessBeanDefinitions() 方法中,由于该方法也特别长,下面仅将关键部分罗列出来。

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的生命周期的属性注入阶段,会调用到MapperFactoryBeansetSqlSessionFactory() 方法来设置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就是MyBatisMapper接口生成的动态代理对象,这一点后面再分析),每创建一个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执行关闭逻辑,那么这里就知道了两件事情。

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

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

public T getObject() throws Exception {
    return getSqlSession().getMapper(this.mapperInterface);
}

public SqlSession getSqlSession() {
    return this.sqlSessionTemplate;
}
复制代码

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

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

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接口的动态代理对象的一次请求时序图如下。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值