Mybatis-Spring源码分析(一):Spring整合Mybatis的配置方式与内部实现

概述

  • 在一个完整的JavaWeb项目中,通常包括web层,service层,dao层这三层结构,整个项目的类对应的bean对象,通过Spring的IOC框架来管理。所以为了方便mybatis框架的使用,mybatis提供了对spring框架的接入实现,在项目的pom.xml中通常需要增加以下配置来引入Spring对mybatis框架相关组件的管理:

    <dependency>
      <groupId>org.mybatisgroupId>
      <artifactId>mybatis-springartifactId>
      <version>x.x.xversion>
    dependency>
    

    以上配置已经在内部引入了mybatis相关的包,故不需要引入以下配置了:如果不基于spring来使用mybatis,则通常使用以下配置:

    dependency>
      <groupId>org.mybatisgroupId>
      <artifactId>mybatisartifactId>
      <version>x.x.xversion>
    dependency>
    
  • 结合Spring来使用mybatis之后,则需要通过Spring的方式来配置mybatis的相关组件对应的bean对象,如SqlSessionFactory等。Spring一般支持通过在Spring的XML配置文件applicationContext.xml中配置,或者使用注解配置。具体使用方法可以参照官方文档:mybatis-spring

Spring整合Mybatis的配置方式与实现原理

Mybatis中主要包含SqlSessionFactory,SQL配置mapper.xml,Mapper接口三大组件,所以Spring整合Mybatis也是基于这三个组件来展开的。

一. SqlSessionFactory和mapper.xml的配置

SqlSessionFactory的配置
  • 在mybatis-spring中提供了一个SqlSessionFactoryBean,即实现了spring的FactoryBean接口,来生成SqlSessionFactory的对象bean并注入到Spring容器来管理。所以可以在spring的XML配置applicationContext.xml中配置该bean,如果基于Java的配置方式,通常在@Configuration注解的配置类使用@Bean注解一个名为sqlSessionFactory的方法来注入该bean对象,其中类型为org.mybatis.spring.SqlSessionFactoryBean。以下为在applicationContext.xml中的配置:

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
       	<property name="dataSource" ref="dataSource" />
       	<property name="mapperLocations" value="classpath:mybatis/mapper/**/*.xml" />
       	<property name="configLocation" value="classpath:mybatis/mybitas-config.xml" />
    bean>
    
mapper.xml在SqlSessionFactory中的配置
  • 由以上配置可知,sqlSessionFactory对应的bean标签内包含dataSource,mapperLocations,configLocation三个属性,分别为:数据源bean引用,mapper.xml配置文件位置,mybatisConfig.xml配置文件位置。

  • 其中mapperLocations属性是可选的:(1)如果Mapper接口和mapper.xml在相同的包内,则不需要指定;(2)如果在mybatis的配置文件mybatisConfig.xml中已经通过mappers节点配置了,则不需要指定,如下为mybatisConfig.xml的mappers节点:

    <mappers>
        <mapper resource="org/mybatis/builder/AuthorMapper.xml"/> 
        <mapper url="file:///var/mappers/BlogMapper.xml"/> 
        <mapper class="org.mybatis.builder.PostMapper"/> 
    mappers>
    
  • 所以mapper.xml的加载位置,既可以在Spring的XML配置applicationContext.xml中,作为sqlSessionFactory的bean节点内部的一个mapperLocations属性来指定,也可以在mybatis自身的配置文件mybatisConfig.xml的mappers节点来指定。

  • 在spring的IOC容器中管理的是sqlSessionFactory这个bean,与跟不使用spring的mybatis一样,在sqlSessionFactory内部通过配置属性Configuration来维护mapper.xml相关的SQL,以及其他配置信息。

二. Mapper接口的配置:以应用代码的调用方式来区分

在spring中配置Mapper接口时,可以基于之后的调用方式来配置。

1. 依赖SqlSession调用
  • 在应用代码中,可以指定Mapper接口,依赖SqlSession来获取该Mapper接口对应的代理对象MapperProxy,从而进行调用。其中MapperProxy实现了JDK的InvocationHandler接口,为Mapper接口在mybatis中对应的动态代理对象,故可以直接通过Mapper接口的引用来调用对应的方法。在spring中提供了一个线程安全的SqlSession接口实现SqlSessionTemplate。
    1. 配置SqlSessionTemplate作为spring的bean:SqlSessionTemplate是线程安全的,内部基于动态代理来实现线程安全,即在代理方法invoke中每次创建一个临时的SqlSession对象来调用。

      
      <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
          <constructor-arg index="0" ref="sqlSessionFactory">constructor-arg>
      bean>
      
    2. 在DAO中注入SqlSessionSession,并指定需要调用的Mapper接口,其中selectOne直接指定Mapper接口类全限定名和方法的字符串,getMapper则指定Mapper接口的类对象。

      @Repository
      public class UserDao{
      
          // 注入SqlSessionTemplate的bean对象
          @Autowired
          private SqlSessionTemplate sqlSessionTemplate;
          
          // 指定mapper接口的类名和方法
          public User getUser(int id) {
              return sqlSessionTemplate.selectOne(UserMapper.class.getName() + ".getUser", id);
          }   
          
          // 指定mapper接口,获取动态代理对象引用
          public User getUserV2(int id) {
              UserMapper dao = sqlSessionTemplate.getMapper(UserMapper.class);
              return dao.getUser(id);
          }
      }
      
  • 在这种方式的实现中,Mapper接口对应的代理对象MapperProxy并没有注册到spring的IOC容器中,而只是sqlSession注册到了spring的IOC容器。由于sqlSession对象包含了sqlSessionFactory的引用,故可以间接从sqlSessionFactory对象内部的Configuration中获取该mapper接口对应的MapperProxy对象。
SqlSessionDaoSupport
  • 在DAO中除了可以直接注入sqlSessionTemplate,然后通过指定Mapper接口来获取对应的MapperProsxy代理对象之外,DAO类自身可以继承SqlSessionDaoSupport,然后在DAO的方法中通过调用getSession方法来获取sqlSessionTemplate:

    public class UserDaoImpl extends SqlSessionDaoSupport implements UserDao {  
      public User getUserById(User user) {  
         return (User) getSqlSession().selectOne(UserMapper.class.getName() + "".getUser", user);  
      }  
    } 
    
2. 不直接依赖SqlSession,只依赖Mapper接口
  • 应用代码不直接依赖SqlSession,只依赖Mapper接口,是基于mybatis-spring提供的MapperFactoryBean实现的。在这种方式中SqlSession没有注册到spring的IOC容器中,而是Mapper接口对应的代理对象MapperProxy注册到了spring的IOC容器中,刚好与第一种方式相反,MapperProxy在内部包含了SqlSession对象的引用。

  • MapperFactoryBean实现了FactoryBean接口,spring在启动创建bean对象时,调用MapperFactoryBean的getObject方法:

    1. 调用getSqlSession方法获取内部依赖SqlSession,具体为线程安全的SqlSessionTemplate,然后调用SqlSession的getMapper方法,创建指定Mapper接口对应的代理对象MapperProxy,然后将该代理对象注册到sqlSessionFactory的Configuration中;
    2. 将该方法的返回值,即该代理对象注册到spring的IOC容器中,从而可以在应用代码中直接注入使用。
  • MapperFactoryBean的getObject方法实现如下:

    // spring容器创建bean对象实例时调用,如在spring容器启动时,会创建单例的bean对象实例,
    // 该类也是单例,故也会在spring启动时创建bean对象,即会调用这个方法。
    @Override
    public T getObject() throws Exception {
        // getSqlSession返回sqlSessionTemplate对象,这个对象为线程安全的,被所有mapper共享。
    
        // getMapper方法创建mapper对象,具体为一个代理对象MapperProxy,该getObject方法返回后,
        // 将该代理对象注册成spring容器中的一个单例bean对象实例。
        getSqlSession().getMapper(this.mapperInterface);
    }
    
  • 故实际注册到spring容器的bean是Mapper接口在mybatis内部对应的代理对象MapperProxy,在应用代码中可以直接注入该代理对象bean,并通过该代理对象bean来调用Mapper接口的方法来触发对应mapper.xml中定义的SQL的执行。

(1)一个个Mapper接口配置
  • 在应用代码中,可以在spring的XML配置文件applicationContext.xml中直接配置Mapper接口对应的bean,其中类型为MapperFactoryBean,这种方式需要为每个Mapper都配置一次。

      
    <bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">   
        <property name="mapperInterface" value="com.yzxie.demo.dao.UserMapper" />   
        <property name="sqlSessionFactory" ref="sqlSessionFactory" />   
    bean>  
    
    <bean id="blogMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">   
        <property name="mapperInterface" value="com.yzxie.demo.dao.BlogMapper" />   
        <property name="sqlSessionFactory" ref="sqlSessionFactory" />   
    bean>  
    
  • 在内部通过mapperInterface属性指定mapper接口,通过sqlSessionFactory来指定sqlSessionFactory引用。

  • 然后在应用代码中,通常为DAO层中,可以注入该mapper代理对象直接调用:

    @Repository
    public class UserDao{
        // 注入userMapper
        @Autowired
        private UserMapper userMapper;
        
        public User getUser(int id) {
            return userMapper.getUser(id);
        }   
    }
    
(2)自动扫描Mapper接口配置
  • 通过一个个来配置mapper略显繁琐,所以在mybatis-spring中提供了MapperScannerConfigurer这个类用于自动扫描给定包下面的mapper接口并在内部,自动为每个mapper接口创建对应的MapperFactoryBean。在spring的XML配置applicationContext.xml的配置方式如下:扫描com.yzxie.demo.dao.mapper包下的所有Mapper接口。

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

MapperScannerConfigurer的内部实现方法

  • MapperScannerConfigurer是BeanDefinitionRegistryPostProcessor接口的一个实现类,BeanDefinitionRegistryPostProcessor实现了BeanFactoryPostProcessor。即MapperScannerConfigurer是一个BeanFactory后置处理器,在spring的IOC容器的生命周期中,spring容器启动创建好对应的beanDefinitions之后,会调用BeanFactoryPostProcess对beanDefinition进行加工,之后才是创建对应的bean对象实例注册的spring的IOC容器中。

  • BeanDefinitionRegistryPostProcessor这个BeanFactoryPostProcessor的主要作用是往spring容器中注册更多的beanDefintions,具体在postProcessBeanDefinitionRegistry方法定义,如下为MapperScannerConfigurer的postProcessBeanDefinitionRegistry实现:

    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
        if (this.processPropertyPlaceHolders) {
          // 处理basePackages,beanName上面的placeholder
          processPropertyPlaceHolders();
        }
    
        // 类路径扫描器
        ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
        scanner.setAddToConfig(this.addToConfig);
        // 根据注解annotationClass,特定接口markerInterface对该类路径进行过滤筛选,默认不过滤
        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);
    
        // 过滤需要加载哪些类文件到内存,生成Resource对象,在下一步继续进行筛选确定哪些是candidateBeanDefinition
        scanner.registerFilters();
    
        // 从basePackage指定的包加载接口类并生成BeanDefinition注册到BeanFactory
        scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
    }
    
  • postProcessBeanDefinitionRegistry实现为通过ClassPathMapperScanner来扫描该指定包类路径获取相关的Mapper接口,其中通过调用scanner.registerFilters()设置需要加载哪些类作为Mapper接口:默认为加载包下面的所有类文件为Resource。

    public void registerFilters() {
        boolean acceptAllInterfaces = true;
    
        // if specified, use the given annotation and / or marker interface
        if (this.annotationClass != null) {
          addIncludeFilter(new AnnotationTypeFilter(this.annotationClass));
          acceptAllInterfaces = false;
        }
    
        // override AssignableTypeFilter to ignore matches on the actual marker interface
        if (this.markerInterface != null) {
          addIncludeFilter(new AssignableTypeFilter(this.markerInterface) {
            @Override
            protected boolean matchClassName(String className) {
              return false;
            }
          });
          acceptAllInterfaces = false;
        }
    
        if (acceptAllInterfaces) {
          // 默认为对包下面的所有类都进行加载成Resource,
          // 然后再使用isCandidateComponent方法进行判断是否需要生成BeanDefinition并注册到Spring容器
          // default include filter that accepts all classes
          addIncludeFilter((metadataReader, metadataReaderFactory) -> true);
        }
    
        // 在上面加载所有类的基础上,排除掉package-info.java这个类
        // exclude package-info.java
        addExcludeFilter((metadataReader, metadataReaderFactory) -> {
          String className = metadataReader.getClassMetadata().getClassName();
          return className.endsWith("package-info");
        });
    }
    
  • 然后在scan方法中,使用这些类对应的Resource来判断每个类是否为接口,如果是接口且该接口为顶层接口,即没有继承、实现其他接口,则认为是Mapper接口,核心判断方法为ClassPathMapperScanner的isCandidateComponent方法:

    // 判断给定的类是否需要生成BeanDefinition注册到Spring容器
    @Override
    protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
        // 接口且该接口为顶层接口,即没有继承、实现其他接口,则为候选mapper
        return beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().isIndependent();
    }
    

注解配置方式

  • 以上分析都是在spring的XML配置文件applicationContext.xml进行配置的,mybatis-spring也提供了基于注解的方式来配置sqlSessionFactory和Mapper接口。

  • sqlSessionFactory主要是在@Configuration注解的配置类中使用@Bean注解的名为sqlSessionFactory的方法来配置;

  • Mapper接口主要是通过在@Configuration注解的配置类中结合@MapperScan注解来指定需要扫描获取mapper接口的包。

    @Configuration
    @MapperScan("org.mybatis.spring.sample.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 sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource());
        return sessionFactory.getObject();
      }
    }
    
  • 在内部实现中@MapperScan注解的解析器为MapperScannerRegistrar,定义如下:MapperScannerRegistrar实现了ImportBeanDefinitionRegistrar接口,spring在处理@Configuration注解的配置类时,会处理@MapperScan注解调用这个接口的registerBeanDefinitions方法,注册更多的beanDefinitions到spring的IOC容器中。

    @Import(MapperScannerRegistrar.class)
    @Repeatable(MapperScans.class)
    public @interface MapperScan {
    
       ...
       
    }
    
    public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
       ...
    }
    

你可能感兴趣的:(Mybatis)