1. 程式人生 > >Mybaits 原始碼解析 (十)----- 全網最詳細,沒有之一:Spring-Mybatis框架使用與原始碼解析

Mybaits 原始碼解析 (十)----- 全網最詳細,沒有之一:Spring-Mybatis框架使用與原始碼解析

在前面幾篇文章中我們主要分析了Mybatis的單獨使用,在實際在常規專案開發中,大部分都會使用mybatis與Spring結合起來使用,畢竟現在不用Spring開發的專案實在太少了。本篇文章便來介紹下Mybatis如何與Spring結合起來使用,並介紹下其原始碼是如何實現的。

Spring-Mybatis使用

新增maven依賴

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>4.3.8.RELEASE</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>1.3.2</version>
</dependency>

在src/main/resources下新增mybatis-config.xml檔案

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <typeAliases>
        <typeAlias alias="User" type="com.chenhao.bean.User" />
    </typeAliases>
    <plugins>
        <plugin interceptor="com.github.pagehelper.PageInterceptor">
            <property name="helperDialect" value="mysql"/>
        </plugin>
    </plugins>

</configuration>

在src/main/resources/mapper路徑下新增User.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    
<mapper namespace="com.chenhao.mapper.UserMapper">
    <select id="getUser" parameterType="int"
        resultType="com.chenhao.bean.User">
        SELECT *
        FROM USER
        WHERE id = #{id}
    </select>
</mapper>

在src/main/resources/路徑下新增beans.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
 
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
        <property name="url" value="jdbc:mysql://127.0.0.1:3306/test"></property>
        <property name="username" value="root"></property>
        <property name="password" value="root"></property>
    </bean>
 
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="configLocation" value="classpath:mybatis-config.xml"></property>
        <property name="dataSource" ref="dataSource" />
        <property name="mapperLocations" value="classpath:mapper/*.xml" />
    </bean>
    
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.chenhao.mapper" />
    </bean>
 
</beans>

註解的方式

  • 以上分析都是在spring的XML配置檔案applicationContext.xml進行配置的,mybatis-spring也提供了基於註解的方式來配置sqlSessionFactory和Mapper介面。
  • sqlSessionFactory主要是在@Configuration註解的配置類中使用@Bean註解的名為sqlSessionFactory的方法來配置;
  • Mapper介面主要是通過在@Configuration註解的配置類中結合@MapperScan註解來指定需要掃描獲取mapper介面的包。
@Configuration
@MapperScan("com.chenhao.mapper")
public class AppConfig {

  @Bean
  public DataSource dataSource() {
     return new EmbeddedDatabaseBuilder()
            .addScript("schema.sql")
            .build();
  }
 
  @Bean
  public DataSourceTransactionManager transactionManager() {
    return new DataSourceTransactionManager(dataSource());
  }
 
  @Bean
  public SqlSessionFactory sqlSessionFactory() throws Exception {
    //建立SqlSessionFactoryBean物件
    SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
    //設定資料來源
    sessionFactory.setDataSource(dataSource());
    //設定Mapper.xml路徑
    sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
    // 設定MyBatis分頁外掛
    PageInterceptor pageInterceptor = new PageInterceptor();
    Properties properties = new Properties();
    properties.setProperty("helperDialect", "mysql");
    pageInterceptor.setProperties(properties);
    sessionFactory.setPlugins(new Interceptor[]{pageInterceptor});
    return sessionFactory.getObject();
  }
}

對照Spring-Mybatis的方式,也就是對照beans.xml檔案來看

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

就對應著SqlSessionFactory的生成,類似於原生Mybatis使用時的以下程式碼

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build( Resources.getResourceAsStream("mybatis-config.xml"));

而UserMapper代理物件的獲取,是通過掃描的形式獲取,也就是MapperScannerConfigurer這個類

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="com.chenhao.mapper" />
</bean>

對應著Mapper介面的獲取,類似於原生Mybatis使用時的以下程式碼:

SqlSession session = sqlSessionFactory.openSession();
UserMapper mapper = session.getMapper(UserMapper.class);

接著我們就可以在Service中直接從Spring的BeanFactory中獲取了,如下

所以我們現在就主要分析下在Spring中是如何生成SqlSessionFactory和Mapper介面的

SqlSessionFactoryBean的設計與實現

大體思路

  • mybatis-spring為了實現spring對mybatis的整合,即將mybatis的相關元件作為spring的IOC容器的bean來管理,使用了spring的FactoryBean介面來對mybatis的相關元件進行包裝。spring的IOC容器在啟動載入時,如果發現某個bean實現了FactoryBean介面,則會呼叫該bean的getObject方法,獲取實際的bean物件註冊到IOC容器,其中FactoryBean介面提供了getObject方法的宣告,從而統一spring的IOC容器的行為。
  • SqlSessionFactory作為mybatis的啟動元件,在mybatis-spring中提供了SqlSessionFactoryBean來進行包裝,所以在spring專案中整合mybatis,首先需要在spring的配置,如XML配置檔案applicationContext.xml中,配置SqlSessionFactoryBean來引入SqlSessionFactory,即在spring專案啟動時能載入並建立SqlSessionFactory物件,然後註冊到spring的IOC容器中,從而可以直接在應用程式碼中注入使用或者作為屬性,注入到mybatis的其他元件對應的bean物件。在applicationContext.xml的配置如下:
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
       // 資料來源
       <property name="dataSource" ref="dataSource" />
       // mapper.xml的資原始檔,也就是SQL檔案
       <property name="mapperLocations" value="classpath:mybatis/mapper/**/*.xml" />
       //mybatis配置mybatisConfig.xml的資原始檔
       <property name="configLocation" value="classpath:mybatis/mybitas-config.xml" />
</bean>

介面設計與實現

SqlSessionFactory的介面設計如下:實現了spring提供的FactoryBean,InitializingBean和ApplicationListener這三個介面,在內部封裝了mybatis的相關元件作為內部屬性,如mybatisConfig.xml配置資原始檔引用,mapper.xml配置資原始檔引用,以及SqlSessionFactoryBuilder構造器和SqlSessionFactory引用。

// 解析mybatisConfig.xml檔案和mapper.xml,設定資料來源和所使用的事務管理機制,將這些封裝到Configuration物件
// 使用Configuration物件作為構造引數,建立SqlSessionFactory物件,其中SqlSessionFactory為單例bean,最後將SqlSessionFactory單例物件註冊到spring容器。
public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {

  private static final Logger LOGGER = LoggerFactory.getLogger(SqlSessionFactoryBean.class);

  // mybatis配置mybatisConfig.xml的資原始檔
  private Resource configLocation;

  //解析完mybatisConfig.xml後生成Configuration物件
  private Configuration configuration;

  // mapper.xml的資原始檔
  private Resource[] mapperLocations;

  // 資料來源
  private DataSource dataSource;

  // 事務管理,mybatis接入spring的一個重要原因也是可以直接使用spring提供的事務管理
  private TransactionFactory transactionFactory;

  private Properties configurationProperties;

  // mybatis的SqlSessionFactoryBuidler和SqlSessionFactory
  private SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();

  private SqlSessionFactory sqlSessionFactory;
  
  
  // 實現FactoryBean的getObject方法
  @Override
  public SqlSessionFactory getObject() throws Exception {
  
    //...

  }
  
  // 實現InitializingBean的
  @Override
  public void afterPropertiesSet() throws Exception {
  
    //...
    
  }
  // 為單例
  public boolean isSingleton() {
    return true;
  }
}

我們重點關注FactoryBean,InitializingBean這兩個介面,spring的IOC容器在載入建立SqlSessionFactoryBean的bean物件例項時,會呼叫InitializingBean的afterPropertiesSet方法進行對該bean物件進行相關初始化處理。

InitializingBean的afterPropertiesSet方法

大家最好看一下我前面關於Spring原始碼的文章,有Bean的生命週期詳細原始碼分析,我們現在簡單回顧一下,在getBean()時initializeBean方法中呼叫InitializingBean的afterPropertiesSet,而在前一步操作populateBean中,以及將該bean物件例項的屬性設值好了,InitializingBean的afterPropertiesSet進行一些後置處理。此時我們要注意,populateBean方法已經將SqlSessionFactoryBean物件的屬性進行賦值了,也就是xml中property配置的dataSource,mapperLocations,configLocation這三個屬性已經在SqlSessionFactoryBean物件的屬性進行賦值了,後面呼叫afterPropertiesSet時直接可以使用這三個配置的值了。

// bean物件例項建立的核心實現方法
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
        throws BeanCreationException {

    // 使用建構函式或者工廠方法來建立bean物件例項
    // Instantiate the bean.
    BeanWrapper instanceWrapper = null;
    if (mbd.isSingleton()) {
        instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
    }
    if (instanceWrapper == null) {
        instanceWrapper = createBeanInstance(beanName, mbd, args);
    }
    
    ...

    // 初始化bean物件例項,包括屬性賦值,初始化方法,BeanPostProcessor的執行
    // Initialize the bean instance.
    Object exposedObject = bean;
    try {

        // 1. InstantiationAwareBeanPostProcessor執行:
        //     (1). 呼叫InstantiationAwareBeanPostProcessor的postProcessAfterInstantiation,
        //  (2). 呼叫InstantiationAwareBeanPostProcessor的postProcessProperties和postProcessPropertyValues
        // 2. bean物件的屬性賦值
        populateBean(beanName, mbd, instanceWrapper);

        // 1. Aware介面的方法呼叫
        // 2. BeanPostProcess執行:呼叫BeanPostProcessor的postProcessBeforeInitialization
        // 3. 呼叫init-method:首先InitializingBean的afterPropertiesSet,然後應用配置的init-method
        // 4. BeanPostProcess執行:呼叫BeanPostProcessor的postProcessAfterInitialization
        exposedObject = initializeBean(beanName, exposedObject, mbd);
    }
    

    // Register bean as disposable.
    try {
        registerDisposableBeanIfNecessary(beanName, bean, mbd);
    }
    catch (BeanDefinitionValidationException ex) {
        throw new BeanCreationException(
                mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex);
    }

    return exposedObject;
}

如上,在populateBean階段,dataSource,mapperLocations,configLocation這三個屬性已經在SqlSessionFactoryBean物件的屬性進行賦值了,呼叫afterPropertiesSet時直接可以使用這三個配置的值了。那我們來接著看看afterPropertiesSet方法

@Override
public void afterPropertiesSet() throws Exception {
    notNull(dataSource, "Property 'dataSource' is required");
    notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
    state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
              "Property 'configuration' and 'configLocation' can not specified with together");

    // 建立sqlSessionFactory
    this.sqlSessionFactory = buildSqlSessionFactory();
}

SqlSessionFactoryBean的afterPropertiesSet方法實現如下:呼叫buildSqlSessionFactory方法建立用於註冊到spring的IOC容器的sqlSessionFactory物件。我們接著來看看buildSqlSessionFactory

protected SqlSessionFactory buildSqlSessionFactory() throws IOException {

    // 配置類
   Configuration configuration;
    // 解析mybatis-Config.xml檔案,
    // 將相關配置資訊儲存到configuration
   XMLConfigBuilder xmlConfigBuilder = null;
   if (this.configuration != null) {
     configuration = this.configuration;
     if (configuration.getVariables() == null) {
       configuration.setVariables(this.configurationProperties);
     } else if (this.configurationProperties != null) {
       configuration.getVariables().putAll(this.configurationProperties);
     }
    //資原始檔不為空
   } else if (this.configLocation != null) {
     //根據configLocation建立xmlConfigBuilder,XMLConfigBuilder構造器中會建立Configuration物件
     xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
     //將XMLConfigBuilder構造器中建立的Configuration物件直接賦值給configuration屬性
     configuration = xmlConfigBuilder.getConfiguration();
   } 
   
    //略....

   if (xmlConfigBuilder != null) {
     try {
       //解析mybatis-Config.xml檔案,並將相關配置資訊儲存到configuration
       xmlConfigBuilder.parse();
       if (LOGGER.isDebugEnabled()) {
         LOGGER.debug("Parsed configuration file: '" + this.configLocation + "'");
       }
     } catch (Exception ex) {
       throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
     }
   }
    
   if (this.transactionFactory == null) {
     //事務預設採用SpringManagedTransaction,這一塊非常重要,我將在後買你單獨寫一篇文章講解Mybatis和Spring事務的關係
     this.transactionFactory = new SpringManagedTransactionFactory();
   }
    // 為sqlSessionFactory繫結事務管理器和資料來源
    // 這樣sqlSessionFactory在建立sqlSession的時候可以通過該事務管理器獲取jdbc連線,從而執行SQL
   configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource));
    // 解析mapper.xml
   if (!isEmpty(this.mapperLocations)) {
     for (Resource mapperLocation : this.mapperLocations) {
       if (mapperLocation == null) {
         continue;
       }
       try {
         // 解析mapper.xml檔案,並註冊到configuration物件的mapperRegistry
         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();
       }

       if (LOGGER.isDebugEnabled()) {
         LOGGER.debug("Parsed mapper file: '" + mapperLocation + "'");
       }
     }
   } else {
     if (LOGGER.isDebugEnabled()) {
       LOGGER.debug("Property 'mapperLocations' was not specified or no matching resources found");
     }
   }

    // 將Configuration物件例項作為引數,
    // 呼叫sqlSessionFactoryBuilder建立sqlSessionFactory物件例項
   return this.sqlSessionFactoryBuilder.build(configuration);
}

buildSqlSessionFactory的核心邏輯:解析mybatis配置檔案mybatisConfig.xml和mapper配置檔案mapper.xml並封裝到Configuration物件中,最後呼叫mybatis的sqlSessionFactoryBuilder來建立SqlSessionFactory物件。這一點相當於前面介紹的原生的mybatis的初始化過程。另外,當配置中未指定事務時,mybatis-spring預設採用SpringManagedTransaction,這一點非常重要,請大家先在心裡做好準備。此時SqlSessionFactory已經建立好了,並且賦值到了SqlSessionFactoryBean的sqlSessionFactory屬性中。

FactoryBean的getObject方法定義

FactoryBean:建立某個類的物件例項的工廠。

spring的IOC容器在啟動,建立好bean物件例項後,會檢查這個bean物件是否實現了FactoryBean介面,如果是,則呼叫該bean物件的getObject方法,在getObject方法中實現建立並返回實際需要的bean物件例項,然後將該實際需要的bean物件例項註冊到spring容器;如果不是則直接將該bean物件例項註冊到spring容器。

SqlSessionFactoryBean的getObject方法實現如下:由於spring在建立SqlSessionFactoryBean自身的bean物件時,已經呼叫了InitializingBean的afterPropertiesSet方法建立了sqlSessionFactory物件,故可以直接返回sqlSessionFactory物件給spring的IOC容器,從而完成sqlSessionFactory的bean物件的註冊,之後可以直接在應用程式碼注入或者spring在建立其他bean物件時,依賴注入sqlSessionFactory物件。

@Override
public SqlSessionFactory getObject() throws Exception {
    if (this.sqlSessionFactory == null) {
      afterPropertiesSet();
    }
    // 直接返回sqlSessionFactory物件
    // 單例物件,由所有mapper共享
    return this.sqlSessionFactory;
}

總結

由以上分析可知,spring在載入建立SqlSessionFactoryBean的bean物件例項時,呼叫SqlSessionFactoryBean的afterPropertiesSet方法完成了sqlSessionFactory物件例項的建立;在將SqlSessionFactoryBean物件例項註冊到spring的IOC容器時,發現SqlSessionFactoryBean實現了FactoryBean介面,故不是SqlSessionFactoryBean物件例項自身需要註冊到spring的IOC容器,而是SqlSessionFactoryBean的getObject方法的返回值對應的物件需要註冊到spring的IOC容器,而這個返回值就是SqlSessionFactory物件,故完成了將sqlSessionFactory物件例項註冊到spring的IOC容器。建立Mapper的代理物件我們下一篇文章再講

 

 

&n