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总体设计也很是巧妙,扩展性很是强。
相关文章
相关标签/搜索