Mybatis-与spring整合

在Mybatis原生的API中,有三个对外提供的核心对象

  • SqlSessionFactory
  • SqlSession
  • getMapper()方法返回的代理对象(包含H对象–MapperProxy)

虽然Mybatis对Jdbc封装置后,已经大大的简化了我们对于数据库的操作,但是在我们的业务代码中不断的创建释放SqlSession也是一件很麻烦的事情。

Mybatis基于Spring的扩展接口,对原生的Api操作进一步进行了简化,所以我们在Spring中使用mybatis时没有再看到这是哪个关键对象在代码中出现的原因。我们只需要吧Mapper接口注入到需要使用的service中,调用她的方法就OK了。

mybatis与spring整合,要弄清楚的几个关键问题:

  1. 如果使用@Autowired注入一个Mapper接口,调用接口方法就能找到sql语句执行,那么这个接口在IoC容器中也是一个代理对象吗?
  2. 如果是代理对象,还是不是SqlSession用getMapper方法获得的呢?SqlSession又是什么时候创建的?
  3. 每个会话都要产生一个SqlSession,单例的SqlSessionFactory是什么时候创建的?

Mybatis集成到Spring里面,是为了进一步简化Mybatis的使用,所以只是对mybatis做了一些封装,并没有替换Mybatis的核心对象。也就是说:mybatis jar包中的三个核心对象都会用到,mybatis-spring.jar里面只是做了一些包装或者桥梁的工作

只要我们弄明白这三个关键对象是怎么创建的,也就理解了spring集成mybatis的原理,所以本文将分成三部分进行分析:

  1. SqlSessionFactory是在哪里创建的
  2. SqlSession是在哪里创建的
  3. 代理Mapper对象是在哪里载入到ioc中的

1. 创建会话工厂SqlSessionFactory

首先去看看Mybatis在spring-boot中的自动配置类配置了一些扫描对象:

1
2
3
4
5
6
7
// MybatisAutoConfiguration
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
...
}

SqlSessionFactoryBean这个对象看来是spring与mybatis的一个桥梁类了,看看这个类中有哪些内容呢

1
2
public class SqlSessionFactoryBean
implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {}

实现了三个接口:

  • InitializingBean 接口,提供了一个方法afterPropertiesSet(),当这个bean的所有属性初始化完成将执行这个方法,定位过去看看:
1
2
3
4
public void afterPropertiesSet() throws Exception {
... // 一些判断检查
this.sqlSessionFactory = buildSqlSessionFactory();
}

调用了buildSqlSessionFactory()方法实例化一个SqlSessionFactory对象,跟进代码能发现方法里面使用XMLConfigBuilder对全局配置文件进行解析,调用了mybatis的代码逻辑,解析并创建SqlSessionFactory,之前源码分析都分析过,不再贴代码。

  • FactoryBean 说明这是一个工厂bean,使用getBean方法获取到的是泛型类型的实例对象,主要是通过接口方法getObject生产实例,定位到:
1
2
3
4
5
6
7
@Override
public SqlSessionFactory getObject() throws Exception {
if (this.sqlSessionFactory == null) {
afterPropertiesSet();
}
return this.sqlSessionFactory;
}

getObject方法获取创建的sqlSessionFactory对象,如果还没有创建,调用afterPropertiesSet方法创建之。

  • ApplicationListener 接口 ,说明这个类监听了某个spring-boot的启动事件,找到事件处理方法查看:
1
2
3
4
5
6
7
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (failFast && event instanceof ContextRefreshedEvent) {
// fail-fast -> check all statements are completed
this.sqlSessionFactory.getConfiguration().getMappedStatementNames();
}
}

从事件处理中可以看出,监听的是ContextRefreshedEvent事件,当spring上下文refresh结束将发布此事件。此处调用getMappedStatementNames()之后并没有存储结果,我猜想这里只是为了触发buildAllStatements(),解析缓存中所有未处理的语句节点。建议在添加了所有映射器后调用此方法,因为它提供了快速失败的语句验证。

1
2
3
4
public Collection<MappedStatement> getMappedStatements() {
buildAllStatements();
return mappedStatements.values();
}

2. 创建会话SqlSession

通过上面的配置,我们已经可以在容器中取获取一个DefaultSqlSessionFactory,按照编程式的开发过程,接下来就要使用OpenSession()方法创建一个sqlSession的实现类对象。

但是在spring中我们是不能直接使用DefaultSqlSession的,因为它是线程不安全的!

所以在spring里面,我们要保证sqlsession实例的线程安全,必须为每一次请求创建单独的sqlSession。但是每次请求用openSqlSession去创建很麻烦。

因此在mybatis-spring的包中,提供了一个线程安全的SqlSession的包装类,用来替代SqlSession,这个类就是SqlSessionTemplate。因为他是线程安全的,所以可以在所有的Dao层共享一个实例(容器默认单例)。

SqlSessionTemplate虽然跟DefaultSqlSession一样定义了操作数据的selectOne()、selectList()、insert()、update()、delete()等所有方法,但是没有自己的实现,全部通过代理模式调用。

1
2
3
4
@Override
public <T> T selectOne(String statement) {
return this.sqlSessionProxy.selectOne(statement);
}

那这个代理对象是怎么来的呢

1
2
3
4
5
6
7
8
9
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
...
this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class }, new SqlSessionInterceptor());
}

在构造函数中就创建了这样的一个代理对象,那既然是Jdk动态代理,调用代理方法肯定会走到第三个参数h对象的invoke方法,h对象是SqlSessionInterceptor类型实例,它是SqlSessionTemplate的一个内部类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
try {
Object result = method.invoke(sqlSession, args);
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
sqlSession.commit(true);
}
return result;
}
... // 异常处理
}
}

这里会先用getSqlSession创建一个sqlSession对象,把sqlSessionFactory、执行器类型、异常解析器传进去

总结一下:因为DefaultSqlSession自己做不到每次请求产生一个新的实例,所以 创建一个代理类也实现SqlSession,提供跟默认实现一样的方法,在任何一个方法被调用的时候都先创建一个DefaultSqlSession在调用被代理对象的响应方法。

Mybatis还再自带了一个线程安全的sqlSession实现:SqlSessionManager,实现方式与上面一样,如果不集成到spring中的情况下还要保证线程安全,那么就用SqlSessionManager

与JdbcTemplate、RedisTemplate一样,SqlSessionTe可以简化Mybatis在spring中的使用,也是spring与mybaits整合中最关键的一个类。

2.2 如何加载一个SqlSessionTemplate到ioc容器

因为SqlSessionTemplate是线程安全的,那么在Dao层如何拿到一个SqlSessionTemplate呢?

在spring中,Mybatis中提供了一个抽象的支持类SqlSessionDaoSupport,该类中持有一个SqlSessionTemplate对象,并且提供了一个getSqlSession()方法,让我们获取SqlSessionTemplate。

那么我们让Dao层的实现类继承抽象类SqlSessionDaoSupport,就能获取到SqlSessionTemplate对象了。

在spring-boot中,自动配置类也为我们自动装配了一个SqlSessionTemplate:

1
2
3
4
5
6
7
8
9
10
@Bean  // MybatisAutoConfiguration
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
ExecutorType executorType = this.properties.getExecutorType();
if (executorType != null) {
return new SqlSessionTemplate(sqlSessionFactory, executorType);
} else {
return new SqlSessionTemplate(sqlSessionFactory);
}
}

3. 接口的扫描注册

自动配置代码中有这样一个配置:

1
@Import(AutoConfiguredMapperScannerRegistrar.class)

导入了一个动态装载类AutoConfiguredMapperScannerRegistrar,该类实现了ImportBeanDefinitionRegistrar接口,在应用启动时会调用到接口方法registerBeanDefinitions

1
2
3
4
5
6
7
@Override  // AutoConfiguredMapperScannerRegistrar
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
...
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
...
registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());
}

该方法向Ioc容器中注册了一个配置类MapperScannerConfigurer,这个配置类就是用来扫描Mapper接口的。

MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor接口,该接口是BeanFactoryPostProcessor接口的子接口,定义了方法postProcessBeanDefinitionRegistry()

实现这个接口,就可以Spring创建Bean之前,修改某些BeanDefinition的属性。

那么MapperScannerConfigurer重写该方法要做什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
if (this.processPropertyPlaceHolders) {
processPropertyPlaceHolders();
}

ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
scanner.setAddToConfig(this.addToConfig);
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);
scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
if (StringUtils.hasText(lazyInitialization)) {
scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization));
}
scanner.registerFilters();
scanner.scan(
StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}

创建了一个ClassPathMapperScanner扫描器,经过一番配置之后,执行扫描器的scan操作

1
2
3
4
5
6
7
8
9
10
// `ClassPathMapperScanner`
public int scan(String... basePackages) {
int beanCountAtScanStart = this.registry.getBeanDefinitionCount();
doScan(basePackages); // 调用重载的doscan
// Register annotation config processors, if necessary.
if (this.includeAnnotationConfig) {
AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
}
return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
}

首先调用的doscan是ClassPathMapperScanner重载父类的

1
2
3
4
5
6
7
8
9
10
11
@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);

if (beanDefinitions.isEmpty()) {
logger.warn("No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");
} else {
processBeanDefinitions(beanDefinitions);
}
return beanDefinitions;
}

接着会调用它的父类的doscan方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
for (String basePackage : basePackages) {
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
for (BeanDefinition candidate : candidates) {
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
candidate.setScope(scopeMetadata.getScopeName());
String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
if (candidate instanceof AbstractBeanDefinition) {
postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
}
if (candidate instanceof AnnotatedBeanDefinition) {
AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
}
if (checkCandidate(beanName, candidate)) {
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
definitionHolder =
AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
beanDefinitions.add(definitionHolder);
registerBeanDefinition(definitionHolder, this.registry);
}
}
}
return beanDefinitions;
}

回到子类ClassPathMapperScanner的doscan方法,扫描完之后执行processBeanDefinitions方法,在该方法中,Mapper的beanDefinition中的BeanClass被改为了MapperFactoryean

1
2
3
4
5
6
7
8
9
10
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
GenericBeanDefinition definition;
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (GenericBeanDefinition) holder.getBeanDefinition();
...
definition.getConstructorArgumentValues()
.addGenericArgumentValue(definition.getBeanClassName());
definition.setBeanClass(this.mapperFactoryBean.getClass());
...
}

也就是说所有的Mapper接口,在容器里面逗比注册成了一个支持泛型的MapperFactoryBean了。为什么要这样做了,进去看看这个类:

1
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {...}

这个类继承了抽象类SqlSessionDaoSupport,这不就解决了之前的问题,现在每一个注入Mapper的地方,都可以拿到SqlSessionTemplate。

分析到这里,只剩下最后一个问题,有没有用到MapperProxy?如果注册时MapperFactoryBean,难道注入使用的也是MapperFactoryBean吗,这个类并不是代理类啊

4. 接口的注入使用

所以注入的到底是一个什么对象,注意看MaperactoryBean也实现了FactoryBean,我们已经见过一次,它可以在getObject方法中修改获取Bean实例的行为。

1
2
3
4
@Override // MapperFactoryBean
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}

所以最终还是回到了mybatis的api中,getMapper方法获取到就是一个代理对象。MapperProxy就是该代理对象的h参数