24-Mybatis和Spring集成原理

Mybatis和Spring集成原理

  • 前面的系列文章都是單獨講解MyBatis,並未將其和Spring整合起來,看看整合的原理。在瞭解之前我們大概梳理一下流程:首先MyBatis的核心組件有:SqlSessionFactory和Java接口代理對象,前者用於創建SqlSession,後者是接口的代理對象實現接口訪問數據庫。

  • 本文會按照代碼,和基本的思路分析Spring整合MyBatis,如果直接需要源碼,可以跳到文章尾部;

一、代碼差異

  • 先看看集成前需要準備的代碼,包括:實體類、接口、Xml文件
@Mapper
public interface UserDao {

    List<User> findAll();
}

@Data
public class User {
    Integer id;
    private String name;
    private String job;
}

<?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.intellif.mozping.dao.UserDao" >

  <select id="findAll" resultType="user" >
		select * from user
	</select>
</mapper>
  • 前面的內容,不管MyBatis集成Spring與否都是一樣的,後面不同的地方分別展示;

1.1 MyBatis

  • 先看一段單獨使用Mybatis的代碼,代碼包括:
/**
 * @author by mozping
 * @Classname Test02NotCombine
 * @Description MyBatis單獨測試
 * @Date 2019/11/08 19:56
 */
public class Test02NotCombine {

    private static final String CONFIG_FILE_PATH = "mybatis-config.xml";

    @Test
    public void query() throws IOException {
        InputStream in = Resources.getResourceAsStream(CONFIG_FILE_PATH);
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
        SqlSession sqlSession = factory.openSession();
        UserDao mapper = sqlSession.getMapper(UserDao.class);
        List<User> all = mapper.findAll();
        for (User u : all) {
            System.out.println(u);
        }
    }
}
  • 下面是配置文件,配置了別名掃描,Xml文件,數據源等配置,這裏留意,後面和Spring集成後這些都不需要了
<?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>
        <package name="com.intellif.mozping.entity"/>
    </typeAliases>

    <!--配置environment環境 -->
    <environments default="development">
        <!-- 環境配置1,每個SqlSessionFactory對應一個環境 -->
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://192.168.12.168:3306/test"/>
                <property name="username" value="root"/>
                <property name="password" value="introcks1234"/>
            </dataSource>
        </environment>
    </environments>

    <!-- 映射文件,mapper的配置文件 -->
    <mappers>
        <!--直接映射到相應的mapper文件 -->
        <!--<mapper resource="mybatis/mapper/PlayerMapper.xml"/>-->
        <mapper resource="mapper/User.xml"/>
    </mappers>

</configuration>

1.2 MyBatis和Spring

  • 再看一段MyBatis和Spring集成後的使用代碼:
/**
 * @Description Spring和MyBatis集成後測試
 */
public class Test01Combine {

    @Test
    public void test() throws Exception {

        //ApplicationContext app = new ClassPathXmlApplicationContext("spring.xml"); //加載xml配置文件
        ApplicationContext app = new AnnotationConfigApplicationContext(MainConfig.class); //加載配置對象
        System.out.println("------cut-off---------");

        UserDao mapper = app.getBean(UserDao.class);
        List<User> all = mapper.findAll();
        for (User u : all) {
            System.out.println(u);
        }
    }
}
  • 集成後,除了最前面的實體類、接口、Xml文件,還需要一個配置類,也可以是XML配置文件,如果用配置文件,測試類中就使用註釋了的一行加載,使用配置類更加簡潔,使用AnnotationConfigApplicationContext加載,配置類如下:
/**
 * @Classname MainConfig
 * @Description Spring集成MyBatis後的配置類
 */
@Configuration
@ComponentScan("com.intellif.mozping")
public class MainConfig {

    private static final String URL = "jdbc:mysql://192.168.12.168:3306/test";
    private static final String USERNAME = "root";
    private static final String PASSWORD = "introcks1234";
    private static final String DRIVER = "com.mysql.jdbc.Driver";

    /**
     * MyBatis自身數據源
     */
    @Bean("unPooledDataSource")
    DataSource unPooledDataSource() {
        UnpooledDataSource dataSource = new UnpooledDataSource();
        dataSource.setUrl(URL);
        dataSource.setDriver(DRIVER);
        dataSource.setUsername(USERNAME);
        dataSource.setPassword(PASSWORD);
        return dataSource;
    }

    /**
     * 配置sqlSessionFactoryBean,用於創建 sqlSessionFactory
     * 這裏通過Qualifier來注入一個數據源,如果注入的是dataSource,就注入DruidDataSource
     * 如果注入 unPooledDataSource,就不使用數據源,使用的是MyBatis內置的數據源
     */
    @Bean("sqlSessionFactoryBean")
    public SqlSessionFactoryBean sqlSessionFactoryBean(@Qualifier("unPooledDataSource") DataSource dataSource) {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        //1.數據源配置
        sqlSessionFactoryBean.setDataSource(dataSource);
        //2.別名掃描
        sqlSessionFactoryBean.setTypeAliasesPackage("com.intellif.mozping.entity");
        //3.Xml映射文件掃描,這裏傳的是數組,每一個元素只能代表一個xml,不能*.xml代表全部
        sqlSessionFactoryBean.setMapperLocations(new ClassPathResource("mapper/User.xml"));

        //注意前面的別名和Xml掃描都可以在配置文件配,這然後這裏加入配置就好了,
        //如果前面配置了,其實配置文件就可以不要了
        //sqlSessionFactoryBean.setConfigLocation(new ClassPathResource("mybatis-config.xml"));
        return sqlSessionFactoryBean;
    }

    @Bean("mapperScannerConfigurer")
    MapperScannerConfigurer mapperScannerConfigurer() {
        MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
        mapperScannerConfigurer.setBasePackage("com.intellif.mozping.dao");
        return mapperScannerConfigurer;
    }
}
  • MyBatis和Spring集成的關鍵是mybatis-spring這個包,源碼:https://github.com/mybatis/spring ,通過這個包來實現自動配置,我們主要看看增加的幾個Bean,其實就是我們配置的幾個Bean;
  • 其中 SqlSessionFactoryBean 用於創建 SqlSessionFactory,MapperScannerConfigurer用於掃描Java接口,unPooledDataSource是MyBatis內置的數據源(也可以使用Druid等第三方數據源),

二、主要類

2.1 SqlSessionFactoryBean

  • SqlSessionFactoryBean用於創建 SqlSessionFactory,這可以在Spring中創建SqlSessionFactory,回顧我們前面1.1中的代碼,在沒有和Spring集成時我們需要一個配置文件來創建 SqlSessionFactory ,集成之後顯然這些配置也是需要的,只不過這次是將配置交給SqlSessionFactoryBean,然後由SqlSessionFactoryBean來創建 SqlSessionFactory;

2.1.1 核心屬性

  • 核心屬性自然是原本我們需要在配置文件中寫的MyBatis的全部配置,下面只列舉了一小部分:
private Resource configLocation;
  //配置對象
  private Configuration configuration;

  //xml文件地址
  private Resource[] mapperLocations;
    
  //數據源
  private DataSource dataSource;

 //用於構建sqlSessionFactory的Builder,這個類是mybatis自身提供的,在參考文章:[Mybatis 核心流程01-初始化階段] 有分析過
  private SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();

  private SqlSessionFactory sqlSessionFactory;

  private Interceptor[] plugins;

  //類型處理器,可以自定義類型處理器
  private TypeHandler<?>[] typeHandlers;

  //別名配置
  private Class<?>[] typeAliases;
  • 下面是 SqlSessionFactoryBean 核心屬性提供的 setXX 方法:

在這裏插入圖片描述

2.1.2 buildSqlSessionFactory

  • buildSqlSessionFactory是最核心的方法,用於創建SqlSessionFactory;這裏面使用了 MyBatis 原本創建 SqlSessionFactory 的幾個Builder輔助類,包括XMLConfigBuilder,XMLMapperBuilder(將映射文件轉換爲配置)和
protected SqlSessionFactory buildSqlSessionFactory() throws IOException {

    Configuration 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) {
      xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
      configuration = xmlConfigBuilder.getConfiguration();
    } else {
      configuration = new Configuration();
      if (this.configurationProperties != null) {
        configuration.setVariables(this.configurationProperties);
      }
    }

    if (this.objectFactory != null) {
      configuration.setObjectFactory(this.objectFactory);
    }

    if (this.objectWrapperFactory != null) {
      configuration.setObjectWrapperFactory(this.objectWrapperFactory);
    }

    if (this.vfs != null) {
      configuration.setVfsImpl(this.vfs);
    }

    if (hasLength(this.typeAliasesPackage)) {
      String[] typeAliasPackageArray = tokenizeToStringArray(this.typeAliasesPackage,
          ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
      for (String packageToScan : typeAliasPackageArray) {
        configuration.getTypeAliasRegistry().registerAliases(packageToScan,
                typeAliasesSuperType == null ? Object.class : typeAliasesSuperType);
      }
    }

    if (!isEmpty(this.typeAliases)) {
      for (Class<?> typeAlias : this.typeAliases) {
        configuration.getTypeAliasRegistry().registerAlias(typeAlias);
      }
    }

    if (!isEmpty(this.plugins)) {
      for (Interceptor plugin : this.plugins) {
        configuration.addInterceptor(plugin);
      }
    }

    if (hasLength(this.typeHandlersPackage)) {
      String[] typeHandlersPackageArray = tokenizeToStringArray(this.typeHandlersPackage,
          ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
      for (String packageToScan : typeHandlersPackageArray) {
        configuration.getTypeHandlerRegistry().register(packageToScan);
      }
    }

    if (!isEmpty(this.typeHandlers)) {
      for (TypeHandler<?> typeHandler : this.typeHandlers) {
        configuration.getTypeHandlerRegistry().register(typeHandler);
      }
    }

    if (this.databaseIdProvider != null) {//fix #64 set databaseId before parse mapper xmls
      try {
        configuration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource));
      } catch (SQLException e) {
        throw new NestedIOException("Failed getting a databaseId", e);
      }
    }

    if (this.cache != null) {
      configuration.addCache(this.cache);
    }

    if (xmlConfigBuilder != null) {
      try {
        xmlConfigBuilder.parse();

      } catch (Exception ex) {
        throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
      } finally {
        ErrorContext.instance().reset();
      }
    }

    if (this.transactionFactory == null) {
      this.transactionFactory = new SpringManagedTransactionFactory();
    }

    configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource));

    if (!isEmpty(this.mapperLocations)) {
      for (Resource mapperLocation : this.mapperLocations) {
        if (mapperLocation == null) {
          continue;
        }
        try {
         //加載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();
        }
      }
    } 
    //根據配置對象創建 SqlSessionFactory
    return this.sqlSessionFactoryBuilder.build(configuration);
  }
  • 大概梳理一下SqlSessionFactoryBean的設計,本身提供了很多SetXX方法用於設置配置,配置的設置比較靈活,既可以單獨設置某一些配置,也可以設置配置對象或者配置文件的問題,因此我們既可以保留mybatis的配置文件,也可以去掉通過SqlSessionFactoryBean注入相關的配置。
  • SqlSessionFactoryBean繼承了FactoryBean(不瞭解FactoryBean需要補一補啦),因此它是一個工廠Bean,可以創建SqlSessionFactory;
  • 在構建SqlSessionFactory的時候,所用到的方式還是保留了MyBatis中本身構建的方式,SqlSessionFactoryBean 本身就是將配置轉換爲配置對象,這裏從最後一行代碼 this.sqlSessionFactoryBuilder.build(configuration) 可以看出來,SqlSessionFactoryBean本身就做了一下配置轉換,那麼創建 SqlSessionFactory 的時機是什麼時候呢,看2.1.3 。
這裏需要我們對整個MyBatis的原理,流程需要有一定的瞭解,可以參考前面的相關文章。

2.1.3 創建時機

  • 前面我們看到了創建SqlSessionFactory的方法細節,那麼創建的時機是在什麼時候?SqlSessionFactoryBean 實現了 InitializingBean接口,時機就在Bean初始化完成後調用該接口的回調方法afterPropertiesSet,代碼如下:
  @Override
  public void afterPropertiesSet() throws Exception {
  
    this.sqlSessionFactory = buildSqlSessionFactory();
  
  }
  • 這裏會創建 SqlSessionFactory ,且是在Bean初始化完成之後調用 afterPropertiesSet , 這和Bean的生命週期知識相關。

2.2 MapperFactoryBean

  • 在我們瞭解MapperScannerConfigurer之前,先簡單看看 MapperFactoryBean 的作用,它的作用就是創建Java接口的代理對象,對於MyBatis這個細節不清楚的建議閱讀參考文章[2]
  • MapperFactoryBean也實現了FactoryBean,我們看看getObject方法,其實就是獲取SqlSession之後再獲取Mapper對象,和我們1.1中的代碼 UserDao mapper = sqlSession.getMapper(UserDao.class) 一樣,因此其實 MapperFactoryBean 本身也不需要做太多工作,因爲獲取代理對象的工作還是委託給 SqlSession來做的,而這是MyBatis本身提供的機制。
  @Override
  public T getObject() throws Exception {
    return getSqlSession().getMapper(this.mapperInterface);
  }
  • 由MapperFactoryBean 的作用我們知道,如果我們有一個接口比如前面的 UserDao ,需要得到代理對象就可以實例化一個MapperFactoryBean類型的Bean,註釋中給出的配置方式如下:
    <bean id="baseMapper" class="org.mybatis.spring.mapper.MapperFactoryBean" abstract="true" lazy-init="true">
      <property name="sqlSessionFactory" ref="sqlSessionFactory" />
    </bean>
 
    <bean id="oneMapper" parent="baseMapper">
      <property name="mapperInterface" value="my.package.MyMapperInterface" />
    </bean>
 
    <bean id="anotherMapper" parent="baseMapper">
      <property name="mapperInterface" value="my.package.MyAnotherMapperInterface" />
    </bean>
  • 不過假設我們有很多接口,爲每一個接口配置一次顯然不方便,在直接使用MyBatis的過程中,我們通過UserDao mapper = sqlSession.getMapper(UserDao.class);來獲取代理對象,與Spring集成之後,他會將全部的代理對象創建好放在IOC容器,因此就有了MapperScannerConfigurer

2.3 MapperScannerConfigurer

  • MapperScannerConfigurer 用於掃描我們的Java接口,給他配置一個路徑即可
    @Bean("mapperScannerConfigurer")
    MapperScannerConfigurer mapperScannerConfigurer() {
        MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
        mapperScannerConfigurer.setBasePackage("com.intellif.mozping.dao");
        return mapperScannerConfigurer;
    }
  • 下面是測得代碼中獲取到的代理對象,和直接使用MyBatis是一樣的

在這裏插入圖片描述

  • MapperScannerConfigurer 掃描Java接口的機制是什麼?這裏不具體分析源碼,稍微帶一下,MapperScannerConfigurer實現了BeanDefinitionRegistryPostProcessor接口,通過接口方法postProcessBeanDefinitionRegistry來修改Bean定義信息,將包下全部的Java接口掃描並註冊Bean定義信息,設置Bean類型是MapperFactoryBean類型,因此後面容器就會初始化對應的MapperFactoryBean,有興趣可以閱讀參考文章[3]

  • 另外也可以去掉MapperScannerConfigurer這個Bean的定義,使用註解:@MapperScan(value=“com.intellif.mozping.dao”),它和Bean的效果是一樣的,具體看2.4。

  • MapperScannerConfigurer掃描多個Java接口並創建對應的代理對象,所以一般不會直接使用MapperFactoryBean,而是使用 MapperScannerConfigurer;

2.4 @MapperScan

  • @MapperScan註解可以起到和 MapperScannerConfigurer 這個Bean的一樣的功能,比如下面兩段代碼是等效的:
    //註解掃描接口
    @MapperScan(value="com.intellif.mozping.dao")

    //MapperScannerConfigurer掃描接口
    @Bean("mapperScannerConfigurer")
    MapperScannerConfigurer mapperScannerConfigurer() {
        MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
        mapperScannerConfigurer.setBasePackage("com.intellif.mozping.dao");
        return mapperScannerConfigurer;
    }
  • 如果瞭解ImportBeanDefinitionRegistrar接口(不瞭解請閱讀參考文章[4]:),那麼 @MapperScan的原理就很好理解,看下面的代碼:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {

    //省略代碼...
}
  • MapperScannerRegistrar類是關鍵,從下面的方法registerBeanDefinitions可以看到,其作用主要是將註解了MapperScan的類Bean定義信息註冊,而註解了MapperScan的正是我們MyBatis的Java接口;
public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {

  @Override
  public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    //掃描註解了MapperScan的類
    AnnotationAttributes mapperScanAttrs = AnnotationAttributes
        .fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
    if (mapperScanAttrs != null) {
      registerBeanDefinitions(mapperScanAttrs, registry, generateBaseBeanName(importingClassMetadata, 0));
    }
  }
  //省略其他代碼...
}

三、小結

類名 功能
SqlSessionFactoryBean … 創建SqlSessionFactory,是一個FactoryBean,會在InitializingBean的afterPropertiesSet回調創建 SqlSessionFactory
MapperFactoryBean 創建代理對象MethodProxy,是一個FactoryBean,MethodProxy實現目標接口且內部持有SqlSession,是接口訪問數據庫的關鍵
MapperScannerConfigurer 掃描多個Java接口並創建對應的代理對象,會在 BeanDefinitionRegistryPostProcessor 回調的時候註冊 MapperFactoryBean 相關的Bean定義信息用於後面的 MapperFactoryBean 實例化。
@MapperScan 等價替換MapperScannerConfigurer ,原理是@Import(MapperScannerRegistrar.class),MapperScannerRegistrar是一個ImportBeanDefinitionRegistrar實現類,一種註冊Bean的方式。

四、參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章