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

  • 2019 年 11 月 11 日
  • 筆記

在前面幾篇文章中我們主要分析了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的代理對象我們下一篇文章再講