Spring整合Mybatis原理

Spring整合Mybatis原理

1、@MapperScan注解发挥作用

在Spring整合Mybatis的时候,只需要一个@MapperScan注解就可以来进行操作,所以更加好奇的是@MapperScan底层是怎么来做到的。

下面先来研究一下@MapperScan:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {
  ...
}

@Import注解导入了一个MapperScannerRegistrar。而在Spring启动的时候,会在org.Springframework.context.support.AbstractApplicationContext#refresh方法中会来执行@Import导入的类,看下如何来解析@Import注解的。直接来到org.Springframework.context.annotation.ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForConfigurationClass会来解析@Import注解导入进来的类,那么下面就需要来看一下MapperScannerRegistrar做了什么事情。

1.1、导入MapperScannerRegistrar类

下面来看一下MapperScannerRegistrar的结构体系

1.1.2、执行ImportBeanDefinitionRegistrar接口中的registerBeanDefinitions方法

MapperScannerRegistrar类实现了ImportBeanDefinitionRegistrar接口,而实现了接口中的方法,那么在注册BeanDefinition之前,就会来执行ImportBeanDefinitionRegistrar接口中的方法

default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
}

那么只需要看一下MapperScannerRegistrar中的registerBeanDefinitions方法是如何来进行实现的:

1.2、MapperScannerConfigurer

而在上面的代码中,主要是注册了一个MapperScannerConfigurer类型的BeanDefinition

那么来看一下这个BeanDefinition有什么特点:

MapperScannerRegistrar实现了BeanDefinitionRegistryPostProcessor接口。而这个接口中又会在ConfigurationClassPostProcessor之后发挥作用,而MapperScannerRegistrar对应的BeanDefinition是在此之前来进行解析的,所以将会在下面来解析MapperScannerRegistrar对应的BeanDefinition

那么就会来执行MapperScannerConfigurer中的postProcessBeanDefinitionRegistry方法

1.2、ClassPathMapperScanner

那么看一下Spring-Mybatis中自定义整合的ClassPathMapperScanner中的注册过滤器和扫描逻辑:

将扫描出来的类利用包含过滤器添加到当前逻辑中。

那么来看一下扫描逻辑:

然后可以看到只要接口的类

1.2.1、对筛选出来的BeanDefinition进行处理

然后对扫描出来的接口来进行筛选判断:

org.mybatis.Spring.mapper.ClassPathMapperScanner#processBeanDefinitions

1、首先获取得到接口的全限定类型。如:com.guang.dao.UserDao;

2、将当前的BeanClass设置成MapperFactoryBean,而这个类是FactoryBean类型的。在创建对象的时候,说明是要用getObject方法来创建对象的;

3、设置MapperFactoryBean构造函数中的值。在MapperFactoryBean构造函数中是Class类型,而这里设置的是String,在创建的时候Spring回来进行转换;

4、将MapperFactoryBean的注入模型设置为By-Type。也就是说,MapperFactoryBean中的setXxx中的属性会从容器中来进行查找

那么看一下MapperFactoryBean的构造方法:

  public MapperFactoryBean(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }

这里会将mapperInterface在进行赋值的时候将java.lang.String转换成Class类型,表示对应的接口。

第二个:

说明了在MapperFactoryBean中的setXxxx方法对应的xxx属性,Spring会自动来进行赋值。而MapperFactoryBean继承了SqlSessionDaoSupport,在SqlSessionDaoSupport类中的set方法中存在setSqlSessionFactory方法和setSqlSessionTemplate方法,所以容器中如果存在着对应类型的对象的时候,那么Spring到时候来进行赋值的时候将会从容器中来进行获取得到对应的bean。

所以:SqlSessionTemplate我们可以自己配置,但是SqlSessionFactory就得由我们自己来进行配置了。

1.3、SqlSessionFactoryBean类结构体系

SqlSessionFactoryBean类就是来帮我们创建SqlSessionFactory,看下SqlSessionFactory的结构如下所示:

SqlSessionFactoryBean也是一个FactoryBean类型的,产生的对象的类型是:SqlSessionFactory。

看一下对应的getObjectType方法:

public Class<? extends SqlSessionFactory> getObjectType() {
  return this.sqlSessionFactory == null ? SqlSessionFactory.class : this.sqlSessionFactory.getClass();
}

看一下对应的getObject方法:

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

  return this.sqlSessionFactory;
}
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");
  // 调用对应的buildSqlSessionFactory方法产生一下sqlSessionFactory
  this.sqlSessionFactory = buildSqlSessionFactory();
}

而根据buildSqlSessionFactory方法可以知道,我们可以通过对SqlSessionFactoryBean对象的属性来进行设置,从而实现对Configuration对象属性的设置。

其中有一行代码值得注意下:

targetConfiguration.setEnvironment(new Environment(this.environment,this.transactionFactory == null ? 
                                                   new SpringManagedTransactionFactory() : this.transactionFactory,
                                                   this.dataSource));

对于Environment对象来说,看看构造函数:

public Environment(String id, TransactionFactory transactionFactory, DataSource dataSource) {
  if (id == null) {
    throw new IllegalArgumentException("Parameter 'id' must not be null");
  }
  if (transactionFactory == null) {
    throw new IllegalArgumentException("Parameter 'transactionFactory' must not be null");
  }
  this.id = id;
  if (dataSource == null) {
    throw new IllegalArgumentException("Parameter 'dataSource' must not be null");
  }
  this.transactionFactory = transactionFactory;
  this.dataSource = dataSource;
}

需要的第二个参数是TransactionFactory(事务工厂),Spring-Mybatis整合包中,利用SpringManagedTransactionFactory类实现TransactionFactory,重写其中的方法,让当前事务交给Spring来进行处理。

那么后续Mybatis如果存在事务操作的时候,将会由Spring中的事务来进行处理。

1.3.1、SqlSessionFactoryBean使用

这里需要注意的是,下面的使用方式是通过配置类的方式来进行注入的。

因为@MapperScan注解导入的MapperScannerRegistrar的作用就是来注册一个MapperScannerConfigurer类型的BeanDefinition。

所以在下面可以直接利用@Bean注入一个对应类型的BeanDefinition,然后给BeanDefinition来设置一些属性而已。

其中在MapperScannerConfigurer实现InitializingBean接口中的afterPropertiesSet方法中判断了扫描包不能够为空,也就是说要求必须设置basePackage属性

第一种使用方式

第二种使用方式

结合SpringBoot项目使用

@ConfigurationProperties(
    prefix = "mybatis"
)
public class MybatisProperties {
    public static final String MYBATIS_PREFIX = "mybatis";
    private String typeAliasesPackage;
    private String[] mapperLocations;
    private Integer batchExecuteSize = 1000;

    public MybatisProperties() {
    }

    public String getTypeAliasesPackage() {
        return this.typeAliasesPackage;
    }

    public void setTypeAliasesPackage(String typeAliasesPackage) {
        this.typeAliasesPackage = typeAliasesPackage;
    }

    public String[] getMapperLocations() {
        return this.mapperLocations;
    }

    public void setMapperLocations(String[] mapperLocations) {
        this.mapperLocations = mapperLocations;
    }

    public Integer getBatchExecuteSize() {
        return this.batchExecuteSize;
    }

    public void setBatchExecuteSize(Integer batchExecuteSize) {
        this.batchExecuteSize = batchExecuteSize;
    }
}
@Configuration
@EnableConfigurationProperties({MybatisProperties.class})
@AutoConfigureAfter({DataSourceConfig.class})
@MapperScan({"com.guang.common.dao"})
public class MybatisConfig {
    private static final String CONFIG_LOCATION = "classpath:mybatis/mybatis-config.xml";
    private static final String COMMON_MAPPER_LOCATIONS = "classpath:com/guang/common/dao/mapper/*Mapper.xml";
    private final ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();

    public MybatisConfig() {
    }

    @Bean(
        name = {"sqlSessionFactory"}
    )
    @ConditionalOnClass({DataSource.class})
    @ConditionalOnBean({DataSource.class})
    public SqlSessionFactory sqlSessionFactory(MybatisProperties mybatisProperties, DataSource dataSource, ObjectProvider<Interceptor> interceptorObjectProvider) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setConfigLocation(this.getConfigLocation());
        sqlSessionFactoryBean.setMapperLocations(this.getMapperLocations(mybatisProperties));
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setTypeAliasesPackage(mybatisProperties.getTypeAliasesPackage());
        List<Interceptor> interceptorList = (List)interceptorObjectProvider.stream().collect(Collectors.toList());
        sqlSessionFactoryBean.setPlugins((Interceptor[])interceptorList.toArray(new Interceptor[0]));
        return sqlSessionFactoryBean.getObject();
    }

    @Primary
    @Bean(
        name = {"simpleSqlSessionTemplate"}
    )
    @ConditionalOnClass({SqlSessionFactory.class})
    @ConditionalOnBean({SqlSessionFactory.class})
    public SqlSessionTemplate simpleSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.SIMPLE);
    }

    @Bean(
        name = {"batchSqlSessionTemplate"}
    )
    @ConditionalOnClass({SqlSessionFactory.class})
    @ConditionalOnBean({SqlSessionFactory.class})
    public SqlSessionTemplate batchSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH);
    }

    @Bean(
        name = {"mapperBatchExecutor"}
    )
    @ConditionalOnClass({SqlSessionTemplate.class})
    @ConditionalOnBean(
        value = {SqlSessionTemplate.class},
        name = {"batchSqlSessionTemplate"}
    )
    public MapperBatchExecutor mapperBatchExecutor(@Qualifier("batchSqlSessionTemplate") SqlSessionTemplate batchSqlSessionTemplate, MybatisProperties mybatisProperties) {
        return new MapperBatchExecutor(batchSqlSessionTemplate, mybatisProperties.getBatchExecuteSize());
    }

    @Bean(
        name = {"pagingInterceptor"}
    )
    public PagingInterceptor pagingInterceptor() {
        return new PagingInterceptor();
    }

    private Resource getConfigLocation() {
        return this.resourceResolver.getResource("classpath:mybatis/mybatis-config.xml");
    }

    private Resource[] getMapperLocations(MybatisProperties mybatisProperties) throws IOException {
        Resource[] mapperLocations = this.resourceResolver.getResources("classpath:com/guang/common/dao/mapper/*Mapper.xml");
        String[] var3 = mybatisProperties.getMapperLocations();
        int var4 = var3.length;

        for(int var5 = 0; var5 < var4; ++var5) {
            String mapperLocation = var3[var5];
            Resource[] addedMapperLocations = this.resourceResolver.getResources(mapperLocation);
            Resource[] originMapperLocations = mapperLocations;
            mapperLocations = new Resource[mapperLocations.length + addedMapperLocations.length];
            System.arraycopy(originMapperLocations, 0, mapperLocations, 0, originMapperLocations.length);
            System.arraycopy(addedMapperLocations, 0, mapperLocations, originMapperLocations.length, addedMapperLocations.length);
        }

        return mapperLocations;
    }
}

1.4、SqlSessionTemplate

首先看一下体系结构图

实现了SqlSession接口,那么就有了SqlSession的功能。

说明,我们可以使用SqlsesseionTemplate对象来代替SqlSession对象来进行使用。

1.4.1、构造方法

看一下对应的构造方法,只有一个构造方法。也就是说,如果想要来设置SqlSessionTemplate成为bean,那么当前容器中必须要有一个SqlSessionFactory类型的Bean。

注意这里的参数信息:

  • 1、SqlSessionFactory是创建SqlSessionTemplate对象时候传入进来的,然后赋值给当前SqlSessionTemplate类中的属性sqlSessionFactory;
  • 2、ExecutorType执行器类型是从当前SqlSessionFactory获取得到的,然后赋值给当前SqlSessionTemplate类中的属性ExecutorType;
  • 3、sqlSessionProxy是sqlSession的代理对象,是利用JDK动态代理产生的对象;

1.4.2、增删改查方法

随便看一下都是类似如下所示,使用sqlSessionProxy来进行执行的

public <T> T selectOne(String statement, Object parameter) {
  return this.sqlSessionProxy.selectOne(statement, parameter);
}

那么执行方法的时候,肯定会指定到InvocationHandler中的invoke方法中来。

那么来看一下SqlSessionInterceptor中的invoke方法。

1.4.3、SqlSessionInterceptor

从上面来看,可以知道是一个InvocationHandler,那么直接来看一下对应的invoke方法实现:

Spring整合Mybatis后为什么一级缓存失效

Spring-Mybatis提供的用来整合Spring和Mybatis的,所以又利用到了Spring中的事务中的内容,那么重点来研究一下这里的代码:

SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
                                      SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);

首先要获取得到对应的SqlSession,SqlSession是mybatis中的门面。获取得到了SqlSession之后,才能够来执行对应的方法。

那么看一下如何获取得到SqlSession的。

首先看看是如何放入到当前线程上下文的。

这里有几个需要注意的点:

  • 1、mybatis中的数据源和@Transactional注解或者是TransactionTemplate使用的要是同一个数据源;
  • 2、一定要在开启事务的情况,获取的才是同一个SqlSession;没有开启事务,每次获取得到的SqlSession都是新创建的;

然后看下执行阶段,如下所示:

@Transactional
public void insert(){
  userMapper.insert(new User());
  orderMapper.insert(new Order());
}

看一下对应的执行

首先执行的时候判断

public static boolean isSqlSessionTransactional(SqlSession session, SqlSessionFactory sessionFactory) {
  notNull(session, NO_SQL_SESSION_SPECIFIED);
  notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
  // 判断是否是同一个SqlSession
  SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
  return (holder != null) && (holder.getSqlSession() == session);
}

如果是同一个SqlSession,那么就先不提交!如果不是同一个SqlSession,那么一个方法执行完成之后,就执行对应的SqlSession.commit()方法,各自占用一个数据库连接。最终如果正常的执行的话,始终占用的是同一个数据库连接,在同一个数据库连接中来执行对应的方法。

Mybatis中的一级缓存是基于SqlSession来实现的,所以在执行同一个sql时,如果使用的是同一个SqlSession对象,那么就能利用到一级缓存,提高sql的执行效率。 但是在Spring整合Mybatis后,如果没有执行某个方法时,该方法上没有加@Transactional注解,也就是没有开启Spring事务,那么后面在执行具体sql时,没执行一个sql时都会新生成一个SqlSession对象来执行该sql,这就是我们说的一级缓存失效(也就是没有使用同一个SqlSession对象),而如果开启了Spring事务,那么该Spring事务中的多个sql,在执行时会使用同一个SqlSession对象,从而一级缓存生效,具体的底层执行流程在上图。

个人理解:实际上Spring整合Mybatis后一级缓存失效并不是问题,是正常的实现,因为,一个方法如果没有开启Spring事务,那么在执行sql时候,那就是每个sql单独一个事务来执行,也就是单独一个SqlSession对象来执行该sql,如果开启了Spring事务,那就是多个sql属于同一个事务,那自然就应该用一个SqlSession来执行这多个sql。所以,在没有开启Spring事务的时候,SqlSession的一级缓存并不是失效了,而是存在的生命周期太短了(执行完一个sql后就被销毁了,下一个sql执行时又是一个新的SqlSession了)。

sqlsession的提交和回滚在哪里

对于SqlSession来说,最终的commit和rollback方法,都会调用到connection.commit()和connection.rollback()方法上去。

而在Spring接管了事务之后,在Spring控制的事务中,也会在方法提交或者回滚之后释放数据库连接。

流程

Spring整合Mybatis之后SQL执行流程: Spring整合Mybatis之后SQL执行流程 | ProcessOn免费在线作图,在线流程图,在线思维导图 |

三、总结

由很多框架都需要和Spring进行整合,而整合的核心思想就是把其他框架所产生的对象放到Spring容器中,让其成为Bean。

比如Mybatis,Mybatis框架可以单独使用,而单独使用Mybatis框架就需要用到Mybatis所提供的一些类构造出对应的对象,然后使用该对象,就能使用到Mybatis框架给我们提供的功能,和Mybatis整合Spring就是为了将这些对象放入Spring容器中成为Bean,只要成为了Bean,在我们的Spring项目中就能很方便的使用这些对象了,也就能很方便的使用Mybatis框架所提供的功能了。

Mybatis-Spring 新版本底层源码执行流程

1、通过@MapperScan导入了MapperScannerRegistrar类;
2、MapperScannerRegistrar类实现了ImportBeanDefinitionRegistrar接口,所以Spring在启动时会调用MapperScannerRegistrar类中的registerBeanDefinitions方法;

3、在registerBeanDefinitions方法中定义了一个ClassPathMapperScanner对象,用来扫描mapper,设置ClassPathMapperScanner对象可以扫描到接口,因为在Spring中是不会扫描接口的
4、同时因为ClassPathMapperScanner中重写了isCandidateComponent方法,导致isCandidateComponent只会认为接口是备选者Component
通过利用Spring的扫描后,会把接口扫描出来并且得到对应的BeanDefinition
5、接下来把扫描得到的BeanDefinition进行修改,把BeanClass修改为MapperFactoryBean,把AutowireMode修改为byType
6、扫描完成后,Spring就会基于BeanDefinition去创建Bean了,相当于每个Mapper对应一个FactoryBean
7、在MapperFactoryBean中的getObject方法中,调用了getSqlSession()去得到一个sqlSession对象,然后根据对应的Mapper接口生成一个Mapper接口代理对象,这个代理对象就成为Spring容器中的Bean
8、sqlSession对象是Mybatis中的,一个sqlSession对象需要SqlSessionFactory来产生
9、MapperFactoryBean的AutowireMode为byType,所以Spring会自动调用set方法,有两个set方法,一个setSqlSessionFactory,一个setSqlSessionTemplate,而这两个方法执行的前提是根据方法参数类型能找到对应的bean,所以Spring容器中要存在SqlSessionFactory类型的bean和SqlSessionTemplate类型的bean。(注意要注册两个的话,记得要在SIMPLESQLSESSIONTEMPLATE上加上@Primary注解)
10、如果你定义的是一个SqlSessionFactory类型的bean,那么最终也会被包装为一个SqlSessionTemplate对象,并且赋值给sqlSession属性
11、而在SqlSessionTemplate类中就存在一个getMapper方法,这个方法中就产生一个Mapper接口代理对象
到时候,当执行该代理对象的某个方法时,就会进入到Mybatis框架的底层执行流程,详细的请看下图

Spring整合Mybatis之后SQL执行流程: Spring整合Mybatis之后SQL执行流程 | ProcessOn免费在线作图,在线流程图,在线思维导图 |

有点遗漏的步骤,我在这里补充一下:

带来的好处是,可以不使用@MapperScan注解,而可以直接定义一个Bean,比如:

@Bean
public static MapperScannerConfigurer mapperScannerConfigurer() {
	MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
	mapperScannerConfigurer.setBasePackage("com.luban");
	return mapperScannerConfigurer;
}
posted @ 2023-02-15 16:10  雩娄的木子  阅读(370)  评论(0编辑  收藏  举报