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;
}