您的当前位置:首页正文

mybatis源码解析(三)mybatis作为ORM持久层框架,如何巧妙利用spring扩展点进行整合。

2024-11-08 来源:个人技术集锦

前文:在mybatis源码解析的(一)(二)中,我们对mybatis如何解析配置文件,如何进行执行sql,以及四个对象和拦截器进行了解析,本篇文章主要是针对mybatis如何利用的spring的拓展点,把mybatis容器整合进spring容器初始化做一个简单的解析。同样我们还是先来解析及几个重要的类。尤其是其中三个。SqlSessionFactoryBean,
MapperScannerConfigurer和MapperFactoryBean。当然这里面也会用到spring框架底层的一些接口,例如:BeanDefinitionRegistryPostProcessor(bean信息注册器),InitializingBean(bean生命周期初始化器),FactoryBean(特殊的bean)等等接口,本文只做简单讲述,后期在spring框架底层学习专题中会进行介绍。

一、介绍本篇文章的类的作用。

本文解析了Mybatis与Spring是如何整合的,其中的关键对象包括:

SqlSessionFactoryBean: mybatis整合Spring时的 生成 SqlSessionFactory 的FactoryBean

MapperScannerConfigurer: mybatis整合Spring时的 实现方便加载Mapper接口,以及将 Mapper 偷梁换柱成 MapperFactoryBean

MapperFactoryBean: 生成 Mapper 代理对象的FactoryBean

SqlSessionFactoryBuilder: 用于创建 SqlSessionFactory

SqlSessionFactory: 用于创建 SqlSession

SqlSession: Mybatis工作的最顶层API会话接口,所有访问数据库的操作都是通过SqlSession来的

Configuration: 存放有所有的mybatis配置信息,包括mapper.xml、 mybatis-config.xml等

XMLConfigBuilder: 解析 mybatis-config.xml 配置并存放到Configuration中

XMLMapperBuilder: 解析 mapper.xml 配置并存放到Configuration中

SqlSessionTemplate: 内部维护有 SqlSession 的代理对象,解耦Mapper和SqlSession的关键对象。

二、解析mybatis整合spring。

本文就采取xml配置的方式吧。当然也可以使用java代码配置类的形式。

  <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="mapperLocations" value="classpath:mapper/*.xml"></property>
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
    </bean>

    <!-- DAO接口所在包名,Spring会自动查找其下的类 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="xxx.dao"/>
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
    </bean>

1、 SqlSessionFactoryBean : 加载xml及build SqlSessionFactory对象
  从配置中我们可以看的 SqlSessionFactoryBean 配置了数据源、mapper的xml路径、mybatis-config的xml路径。因此,不难想象,SqlSessionFactoryBean 内部实现了xml配置文件的加载及SqlSessionFactory对象的创建。我们来看下 SqlSessionFactoryBean继承关系图形:

在继承关系图中,我们发现了 InitializingBean、FactoryBean 的身影,可能清楚这个的同学,大概已经猜到了肯定有 afterPropertiesSet() 来创建 SqlSessionFactory 对象 和 getObject() 来获取 SqlSessionFactory 对象 。 话不多说,先看下getObject()实现:

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

    return this.sqlSessionFactory;
  }

getObject()相对简单,我们都知道FactoryBean子类都是通过getObject()来获取到实际的Bean对象,这里也就是SqlSessionFactory。从源码中我们看到当 sqlSessionFactory为null会去调用 afterPropertiesSet(),所以 SqlSessionFactory 肯定是由 afterPropertiesSet() 来实现创建的。继续看afterPropertiesSet()实现:

 public void afterPropertiesSet() throws Exception {
    notNull(dataSource, "Property 'dataSource' is required");
    notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
 // 解析构建sqlSessionFactory 
    this.sqlSessionFactory = buildSqlSessionFactory();
  }

afterPropertiesSet() 内部首先 验证了 dataSource 和 sqlSessionFactoryBuilder 部位null,最后调用 buildSqlSessionFactory()方法获取到 SqlSessionFactory 对象,并赋值到类字段属性 sqlSessionFactory 。 继续查看buildSqlSessionFactory()源码:

protected SqlSessionFactory buildSqlSessionFactory() throws IOException {

    // 省略了 SqlSessionFactoryBean 的属性(比如:ObjectFactory )赋值到 Configuration 对象中的操作
    //  1 Configuration : Mybatis的核心类之一,主要存放读取到的xml数据,包括mapper.xml 
    Configuration configuration;

    XMLConfigBuilder xmlConfigBuilder = null;
    if (this.configLocation != null) {
      //  2 创建  xmlConfigBuilder 对象 : 用于解析 mybatis-config.xml 数据
      xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
      configuration = xmlConfigBuilder.getConfiguration();
    } else {
      if (logger.isDebugEnabled()) {
        logger.debug("Property 'configLocation' not specified, using default MyBatis Configuration");
      }
      configuration = new Configuration();
      configuration.setVariables(this.configurationProperties);
    }

    if (xmlConfigBuilder != null) {
      try {
        //  3  XmlConfigBuilder 解析方法执行 
        xmlConfigBuilder.parse();
      } catch (Exception ex) {
        throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
      } finally {
        ErrorContext.instance().reset();
      }
    }
    
    if (!isEmpty(this.mapperLocations)) {
      for (Resource mapperLocation : this.mapperLocations) {
        if (mapperLocation == null) {
          continue;
        }
        try {
          //  4 创建  XMLMapperBuilder 对象 : 用于解析 mapper.xml 数据
          XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
              configuration, mapperLocation.toString(), configuration.getSqlFragments());
          xmlMapperBuilder.parse();
        } catch (Exception e) {
          throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
        } finally {
          ErrorContext.instance().reset();
        }
      }
    } 
    // 5 通过 SqlSessionFactoryBuilder bulid  SqlSessionFactory 对象
    return this.sqlSessionFactoryBuilder.build(configuration);
  }

整个 buildSqlSessionFactory() 源码主要有以下几个重要的点:

1、 XMLConfigBuilder ,通过调用其 parse() 方法来 解析 mybatis-config.xml 配置(如果 配置有 mapper.xml ,其会通过 XMLMapperBuilder 进行解析加载),并将解析的数据赋值到 Configuration(Mybatis的核心类之一,主要存放读取到的xml数据,包括mapper.xml,该类贯穿整个mybatis,足以见得其重要性)

2、 XMLMapperBuilder : 通过调用其 parse() 方法来 解析 mapper.xml 配置, 并将解析的数据赋值到 Configuration

3、 将存放有解析数据的 Configuration 作为 sqlSessionFactoryBuilder.build() 参数,创建 sqlSessionFactory 对象。

至此

2、 MapperScannerConfigurer :扫描Mapper接口路径,将 Mapper 偷梁换柱成 MapperFactoryBean
  MapperScannerConfigurer 是 mybatis-spring 项目中为了实现方便加载Mapper接口,以及将 Mapper 偷梁换柱成 MapperFactoryBean。查看 MapperScannerConfigurer 源码,先看下其继承关系图:

从中我们其继承了 BeanDefinitionRegistryPostProcessor 接口,熟悉Spring 的同学应该 已经大致想到了 其如何将 Mapper 偷梁换柱成 MapperFactoryBean 了。话不多说,我们来看看 MapperScannerConfigurer 是如何实现 BeanDefinitionRegistryPostProcessor 的 postProcessBeanDefinitionRegistry 方法:

 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);
    
// 这一点是通过xml我们配置的名字,通过bean注册信息的属性进行赋值,
//设置 SqlSessionFactory,mybatis官网也对此进行了说明,为什么要是用Value.而不是使用ref。
    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));
  }

我们可以发现整个方法内部其实就是通过 ClassPathMapperScanner 的 scan() 方法,查看 scan() 实现,发现其内部调用了关键方法 doScan(),那么我们来看下 doScan() 方法实现:

 @Override
  public Set<BeanDefinitionHolder> doScan(String... basePackages) {
    // 1、调用父类 ClassPathBeanDefinitionScanner的 doScan方法 加载路径下所有的mapper接口生成对应的 BeanDefinition 
    Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);

    if (beanDefinitions.isEmpty()) {
      logger.warn("No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");
    } else {
      for (BeanDefinitionHolder holder : beanDefinitions) {
        GenericBeanDefinition definition = (GenericBeanDefinition) holder.getBeanDefinition();

        // 2、 设置 被代理的 Bean(也就是Mapper) 的class信息
        definition.getPropertyValues().add("mapperInterface", definition.getBeanClassName());
        // 3、 偷梁换柱成 MapperFactoryBean 
        definition.setBeanClass(MapperFactoryBean.class);

        definition.getPropertyValues().add("addToConfig", this.addToConfig);

        boolean explicitFactoryUsed = false;
        // 4、 设置 sqlSessionFactory 
        if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
          definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
          explicitFactoryUsed = true;
        } else if (this.sqlSessionFactory != null) {
          definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
          explicitFactoryUsed = true;
        }
        
        // 5、 设置 sqlSessionTemplate
        if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
          definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
          explicitFactoryUsed = true;
        } else if (this.sqlSessionTemplate != null) {
          definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
          explicitFactoryUsed = true;
        }
        
        if (!explicitFactoryUsed) {
          definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
        }
      }
    }

    return beanDefinitions;
  }

整个方法分为3个部分:

1、 调用父类 ClassPathBeanDefinitionScanner的 doScan()方法 加载路径下所有的mapper接口生成对应的 BeanDefinition

2、 通过definition.setBeanClass(MapperFactoryBean.class) 偷梁换柱成 MapperFactoryBean

3、 通过 definition.getPropertyValues().add() 添加 MapperFactoryBean 所需的 字段或者方法参数信息 : sqlSessionFactory 、 mapperInterface等

至此 MapperScannerConfigurer 的使命已经完成, 至于 MapperFactoryBean 的创建就完全交给Spring来完成了。

3、 MapperFactoryBean 、SqlSessionTemplate:Mapper与SqlSession解耦的利器
   我们知道在mybatis中,Mapper是通过 SqlSession创建的,而SqlSession的生命周期仅仅在一次会话中,那么按照这种设计,每一次会话都要去创建SqlSession,然后再通过SqlSession去创建Mapper。我们知道Mapper其实没有必要每次都去创建,它更加适合作为一个单列对象。那么怎么将SqlSession和Mapper解耦呢? 在mybatis-spring项目中通过 MapperFactoryBean 、SqlSessionTemplate 来实现的。接下来我们就来解析它们。

MapperFactoryBean

正如前面我们所看到的一样,MapperFactoryBean 其实可以理解为 Mapper的代理工厂Bean,我们可以通过 MapperFactoryBean 的方法获取到 Mapper的代理对象。先来看下 MapperFactoryBean继承关系 :
MapperFactoryBean继承关系图

我们可以看到 MapperFactoryBean 实现了 FactoryBean, 那么 肯定通过 实现 getObject() 获取到 Mapper的代理对象,查看源码如下:

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

其内部就是我们熟悉的 getSqlSession().getMapper() 创建Mapper代理对象的方法。熟悉Spring 的同学都知道 在Bean加载的过程中如果发现当前Bean对象是 FactoryBean 会去 调用getObject() 获取真正的Bean对象。不熟悉的同学可以去看下 AbstractBeanFactory 的 getBean() 方法。

但是似乎还是没有吧SqlSession和Mapper解耦的迹象呢?不着急,我们继续看下 getSqlSession(), 发现其是 父类 SqlSessionDaoSupport 实现,我们看下SqlSessionDaoSupport源码:

public abstract class SqlSessionDaoSupport extends DaoSupport {

  private SqlSession sqlSession;

  private boolean externalSqlSession;

  //  创建 SqlSession子类 SqlSessionTemplate 
  public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
    if (!this.externalSqlSession) {
      this.sqlSession = new SqlSessionTemplate(sqlSessionFactory);
    }
  }

  public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
    this.sqlSession = sqlSessionTemplate;
    this.externalSqlSession = true;
  }

  
  public SqlSession getSqlSession() {
    return this.sqlSession;
  }

  ....

}

我们发现我们获取到的SqlSession其实是其子类SqlSessionTemplate, 我们查看其构造方法源码:

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());
  }

我们可以清楚的发现,其内部维护了一个 SqlSession的字段 sqlSessionProxy ,其赋值的是代理对象 SqlSessionInterceptor。 我们再来看下 SqlSessionInterceptor 的源码:

 private class SqlSessionInterceptor implements InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      // 通过getSqlSession() 获取一个 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);
        }
      }
    }
  }

我们发现其代理实现时,通过getSqlSession() 获取一个 全新的SqlSession。也就是说创建Mapper的SqlSession和会话请求的SqlSession不是同一个。这里就完美的解耦了Mapper和SqlSession,并且保障了每次会话SqlSession的生命周期范围。

这里超前提下: getSqlSession().getMapper() 其实 是通过 configuration.getMapper() 来获取的,那么就意味着 configuration内部必须添加了Mapper信息,那么configuration是何时添加的呢? 可以看下 MapperFactoryBean的checkDaoConfig()方法,源码如下:

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

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

    Configuration configuration = getSqlSession().getConfiguration();
    if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
      try {
        configuration.addMapper(this.mapperInterface);
      } catch (Throwable t) {
        logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", t);
        throw new IllegalArgumentException(t);
      } finally {
        ErrorContext.instance().reset();
      }
    }
  }

由于父类实现了 InitializingBean 接口,并且其afterPropertiesSet() 调用了 checkDaoConfig() 方法 ,所以,至少在初始化创建MapperFactoryBean 时,就已经向 configuration内部必须添加了Mapper信息。
三、总结。
这是mybatis利用spring的拓展点,将自己对象交于spring进行管理。
解析配置信息,和生成动态代理类。在myabtis源码解析(四)中,我们将解析下看看mybatis是如何整合springBoot的。

显示全文