Mybatis 源碼(六)Mybatis-Spring框架實現原理

我在使用mybatis-spring過程當中一直有一個疑問,在Mybatis 源碼(一)總攬中我提到過,SqlSession和Mapper對象的聲明週期是方法級別的,也就是每一個請求的SqlSession和Mapper對象是不同的,是一個非單例的Bean。可是與Spring集成後,爲何咱們能夠直接注入Mapper對象,若是經過直接注入的話Mapper對象卻成了單例的了?java

咱們帶着疑問來看下Mybatis-Spring是如何實現的。git

初始化 SqlSessionFactory

咱們是經過SqlSessionFactoryBean來完成Mybatis與Spring集成的,類圖以下:github

經過類圖咱們發現SqlSessionFactoryBean實現了FactoryBean接口,那麼在Spring實例化Bean的時候會調用FactoryBeangetObject()方法。因此Mybatis與Spring的集成的入口就是org.mybatis.spring.SqlSessionFactoryBean#getObject()方法,源碼以下:spring

public SqlSessionFactory getObject() throws Exception {
    if (this.sqlSessionFactory == null) {
        afterPropertiesSet();
    }

    return this.sqlSessionFactory;
}

經過跟源碼發現,在afterPropertiesSet();方法中完成了sqlSessionFactory的初始化。sql

Mybatis 源碼(二)Mybatis 初始化中介紹的同樣,仍是經過XMLConfigBuilderXMLMapperBuilderXMLStatementBuilder三個建造者來完成了對Mybatis XML文件的解析。數據庫

裝載映射器到Spring容器

Mybatis和Spring集成後,有三種方式將Mapper的實例裝載到Spring容器,以下:設計模式

  • 使用 <mybatis:scan/> 元素
  • 使用 @MapperScan 註解
  • Spring XML 配置文件中註冊一個 MapperScannerConfigurer

在這裏咱們介紹一下MapperScannerConfigurer安全

MapperScannerConfigurer

經過類圖咱們發現,MapperScannerConfigurer實現了BeanDefinitionRegistryPostProcessor,那麼會執行BeanDefinitionRegistryPostProcessorpostProcessBeanDefinitionRegistry方法來完成Bean的裝載。session

public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
	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.registerFilters();
	scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}

咱們能夠看到經過包掃描,將會掃描出全部的Mapper類,而後註冊Bean定義到Spring容器。mybatis

可是Mapper是一個接口類,是不能直接進行實例化的,因此在ClassPathMapperScanner中,它將全部Mapper對象的BeanDefinition給改了,將全部Mapper的接口對象指向MapperFactoryBean工廠Bean,因此在Spring中Mybatis全部的Mapper接口對應的類是MapperFactoryBean,源碼以下:

@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
	Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);

	for (BeanDefinitionHolder holder : beanDefinitions) {
		GenericBeanDefinition definition = (GenericBeanDefinition) holder.getBeanDefinition();

		definition.getPropertyValues().add("mapperInterface", definition.getBeanClassName());
		definition.setBeanClass(MapperFactoryBean.class);

		definition.getPropertyValues().add("addToConfig", this.addToConfig);
		...
        // 設置按類型注入屬性,這裏主要是注入sqlSessionFactory和sqlSessionTemplate
		definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
	}

	return beanDefinitions;
}

從源碼咱們看出ClassPathMapperScanner主要以下的BeanDefinition

  1. 將Class指向MapperFactoryBean
  2. 修改須要注入的屬性值,如:addToConfigsqlSessionFactorysqlSessionFactory
  3. 修改注入方式AbstractBeanDefinition.AUTOWIRE_BY_TYPE

經過上述修改使得Mapper接口能夠實例化成對象並放到Spring容器中。

MapperFactoryBean

從類圖咱們能夠看出它是一個FactoryBean,因此實例化的時候回去調用其getObject()方法完成Bean的裝載,源碼以下:

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

這裏值得說一下的是,getSqlSession()獲取到的是SqlSessionTemplate對象,在Mapper是單例的狀況下,如何保證每次訪問數據庫的Sqlsession是不同的,就是在SqlSessionTemplate中實現的。

MapperFactoryBean它還實現了InitializingBean接口,利用InitializingBean的特性,它會將Mapper接口放到Mybatis中的Configuration對象中,源碼以下:

@Override
public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException {
    // Let abstract subclasses check their configuration.
    checkDaoConfig();
    ...
}

protected void checkDaoConfig() {
    super.checkDaoConfig();

    notNull(this.mapperInterface, "Property 'mapperInterface' is required");

    Configuration configuration = getSqlSession().getConfiguration();
    if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
        try {
            // 將Mapper放到Mybatis的Configuration對象中
            configuration.addMapper(this.mapperInterface);
        } catch (Exception e) {
            logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e);
            throw new IllegalArgumentException(e);
        } finally {
            ErrorContext.instance().reset();
        }
    }
}

Mapper接口注入到Spring容器能夠等價與以下配置,這個看起來更好理解:

<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
  <property name="mapperInterface" value="org.mybatis.spring.sample.mapper.UserMapper" />
  <property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>
  1. 經過上面的源碼咱們能夠發現裝載到Spring容器中的Mapper對象實際上是,對應Mapper接口的代理對象MapperProxy,而且在容器中它是單例的。
  2. Mapper是單例實際上是沒問題的,由於Mapper自己是沒有共享變量的,它是一個線程安全的類,只須要保證咱們每次請求數據庫所用到的Sqlsession不是單例的就行。爲了實現這一點,MapperProxySqlSession不是直接使用DefaultSqlSession,而是使用了SqlSessionTemplate

SqlSessionTemplate

SqlSessionTemplate使用了動態代理模式+靜態代理模式,對SqlSession進行加強,每次請求數據庫使用新的SqlSession放到了加強器SqlSessionInterceptor裏面來實現。

public class SqlSessionTemplate implements SqlSession, DisposableBean {

  private final SqlSessionFactory sqlSessionFactory;

  private final ExecutorType executorType;

  private final SqlSession sqlSessionProxy;

  private final PersistenceExceptionTranslator exceptionTranslator;

  public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
                            PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
    notNull(executorType, "Property 'executorType' is required");

    this.sqlSessionFactory = sqlSessionFactory;
    this.executorType = executorType;
    this.exceptionTranslator = exceptionTranslator;
    // 使用動態代理模式,對SqlSession進行加強
    this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
      new Class[]{SqlSession.class}, new SqlSessionInterceptor());
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public <T> T selectOne(String statement) {
    return this.sqlSessionProxy.selectOne(statement);
  }
}

SqlSessionInterceptor

這個纔是Mybatis-Spring實現原理的核心之一,在每次請求數據庫的過程當中它會新建立一個SqlSession,源碼以下:

private class SqlSessionInterceptor implements InvocationHandler {
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // 每次獲取新的SqlSession
    SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
      SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
    try {
      Object result = method.invoke(sqlSession, args);
      if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
        // force commit even on non-dirty sessions because some databases require
        // a commit/rollback before calling close()
        // 事務提交
        sqlSession.commit(true);
      }
      return result;
    } catch (Throwable t) {
      // 異常處理
      Throwable unwrapped = unwrapThrowable(t);
      if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
        // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
        // 資源釋放
        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) {
        // 資源釋放
        closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
      }
    }
  }
}

在這個加強器中它實現了每次請求新建立Sqlsession,而且還作了統一的資源釋放,事務處理等,這使得咱們在和Spring集成後,不用關心資源釋放等操做,將工做重心放到業務上。

總結

Mybatis-Spring的實現有兩個核心點:

  1. 經過MapperFactoryBean巧妙的將Mapper接口對應的代理對象MapperProxy裝載到了Spring容器中。
  2. 經過SqlSessionTemplate,使用靜態代理+動態代理模式,巧妙的實現了每次訪問數據庫都是用新的Sqlsession對象。

Mybatis 源碼中文註釋

https://github.com/xiaolyuh/mybatis

心得

在這裏Mybatis源碼系列就寫完了,與Spring源碼相比,我很是建議你們去看下Mybatis源碼,緣由有如下幾點:

  1. Mybatis源碼很是工整,包結構、代碼結構都很值得咱們學習。
  2. Mybatis中使用不少設計模式,大部分設計模式均可以在這裏找到其身影,能夠當作是設計模式的最佳實踐。
  3. Mybatis總體設計也很是巧妙,擴展性很是強。
相關文章
相關標籤/搜索