我在使用mybatis-spring过程中一直有一个疑问,在Mybatis 源码(一)总揽中我提到过,SqlSession
和Mapper对象的声明周期是方法级别的,也就是每个请求的SqlSession
和Mapper对象是不一样的,是一个非单例的Bean。但是与Spring集成后,为什么我们可以直接注入Mapper对象,如果通过直接注入的话Mapper对象却成了单例的了?
我们带着疑问来看下Mybatis-Spring是如何实现的。
初始化 SqlSessionFactory
我们是通过SqlSessionFactoryBean
来完成Mybatis与Spring集成的,类图如下:
通过类图我们发现SqlSessionFactoryBean
实现了FactoryBean
接口,那么在Spring实例化Bean的时候会调用FactoryBean
的getObject()
方法。所以Mybatis与Spring的集成的入口就是org.mybatis.spring.SqlSessionFactoryBean#getObject()
方法,源码如下:
public SqlSessionFactory getObject() throws Exception {
if (this.sqlSessionFactory == null) {
afterPropertiesSet();
}
return this.sqlSessionFactory;
}
通过跟源码发现,在afterPropertiesSet();
方法中完成了sqlSessionFactory
的初始化。
和Mybatis 源码(二)Mybatis 初始化中介绍的一样,还是通过XMLConfigBuilder
、XMLMapperBuilder
和XMLStatementBuilder
三个建造者来完成了对Mybatis XML文件的解析。
装载映射器到Spring容器
Mybatis和Spring集成后,有三种方式将Mapper的实例装载到Spring容器,如下:
- 使用
元素 - 使用
@MapperScan
注解 - Spring XML 配置文件中注册一个
MapperScannerConfigurer
在这里我们介绍一下MapperScannerConfigurer
。
MapperScannerConfigurer
通过类图我们发现,MapperScannerConfigurer
实现了BeanDefinitionRegistryPostProcessor
,那么会执行BeanDefinitionRegistryPostProcessor
的postProcessBeanDefinitionRegistry
方法来完成Bean的装载。
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
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.registerFilters();
scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
我们可以看到通过包扫描,将会扫描出所有的Mapper类,然后注册Bean定义到Spring容器。
但是Mapper是一个接口类,是不能直接进行实例化的,所以在ClassPathMapperScanner
中,它将所有Mapper对象的BeanDefinition
给改了,将所有Mapper的接口对象指向MapperFactoryBean
工厂Bean,所以在Spring中Mybatis所有的Mapper接口对应的类是MapperFactoryBean
,源码如下:
@Override
public Set doScan(String... basePackages) {
Set beanDefinitions = super.doScan(basePackages);
for (BeanDefinitionHolder holder : beanDefinitions) {
GenericBeanDefinition definition = (GenericBeanDefinition) holder.getBeanDefinition();
definition.getPropertyValues().add("mapperInterface", definition.getBeanClassName());
definition.setBeanClass(MapperFactoryBean.class);
definition.getPropertyValues().add("addToConfig", this.addToConfig);
...
// 设置按类型注入属性,这里主要是注入sqlSessionFactory和sqlSessionTemplate
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
}
return beanDefinitions;
}
从源码我们看出ClassPathMapperScanner
主要如下的BeanDefinition
:
- 将Class指向
MapperFactoryBean
- 修改需要注入的属性值,如:
addToConfig
、sqlSessionFactory
、sqlSessionFactory
- 修改注入方式
AbstractBeanDefinition.AUTOWIRE_BY_TYPE
通过上述修改使得Mapper接口可以实例化成对象并放到Spring容器中。
MapperFactoryBean
从类图我们可以看出它是一个FactoryBean
,所以实例化的时候回去调用其getObject()
方法完成Bean的装载,源码如下:
@Override
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
这里值得说一下的是,getSqlSession()
获取到的是SqlSessionTemplate
对象,在Mapper是单例的情况下,如何保证每次访问数据库的Sqlsession
是不一样的,就是在SqlSessionTemplate
中实现的。
MapperFactoryBean
它还实现了InitializingBean
接口,利用InitializingBean
的特性,它会将Mapper接口放到Mybatis中的Configuration
对象中,源码如下:
@Override
public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException {
// Let abstract subclasses check their configuration.
checkDaoConfig();
...
}
protected void checkDaoConfig() {
super.checkDaoConfig();
notNull(this.mapperInterface, "Property 'mapperInterface' is required");
Configuration configuration = getSqlSession().getConfiguration();
if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
try {
// 将Mapper放到Mybatis的Configuration对象中
configuration.addMapper(this.mapperInterface);
} catch (Exception e) {
logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e);
throw new IllegalArgumentException(e);
} finally {
ErrorContext.instance().reset();
}
}
}
Mapper接口注入到Spring容器可以等价与如下配置,这个看起来更好理解:
- 通过上面的源码我们可以发现装载到Spring容器中的Mapper对象其实是,对应Mapper接口的代理对象
MapperProxy
,并且在容器中它是单例的。 - Mapper是单例其实是没问题的,因为Mapper本身是没有共享变量的,它是一个线程安全的类,只需要保证我们每次请求数据库所用到的
Sqlsession
不是单例的就行。为了实现这一点,MapperProxy
的SqlSession
不是直接使用DefaultSqlSession
,而是使用了SqlSessionTemplate
。
SqlSessionTemplate
SqlSessionTemplate
使用了动态代理模式+静态代理模式,对SqlSession进行增强,每次请求数据库使用新的SqlSession
放到了增强器SqlSessionInterceptor
里面来实现。
public class SqlSessionTemplate implements SqlSession, DisposableBean {
private final SqlSessionFactory sqlSessionFactory;
private final ExecutorType executorType;
private final SqlSession sqlSessionProxy;
private final PersistenceExceptionTranslator exceptionTranslator;
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
notNull(executorType, "Property 'executorType' is required");
this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
// 使用动态代理模式,对SqlSession进行增强
this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
new Class[]{SqlSession.class}, new SqlSessionInterceptor());
}
/**
* {@inheritDoc}
*/
@Override
public T selectOne(String statement) {
return this.sqlSessionProxy.selectOne(statement);
}
}
SqlSessionInterceptor
这个才是Mybatis-Spring实现原理的核心之一,在每次请求数据库的过程中它会新创建一个SqlSession
,源码如下:
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 每次获取新的SqlSession
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)) {
// force commit even on non-dirty sessions because some databases require
// a commit/rollback before calling close()
// 事务提交
sqlSession.commit(true);
}
return result;
} catch (Throwable t) {
// 异常处理
Throwable unwrapped = unwrapThrowable(t);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
// release the connection to avoid a deadlock if the translator is no loaded. See issue #22
// 资源释放
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator
.translateExceptionIfPossible((PersistenceException) unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw unwrapped;
} finally {
if (sqlSession != null) {
// 资源释放
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}
在这个增强器中它实现了每次请求新创建Sqlsession
,并且还做了统一的资源释放,事务处理等,这使得我们在和Spring集成后,不用关心资源释放等操作,将工作重心放到业务上。
总结
Mybatis-Spring的实现有两个核心点:
- 通过
MapperFactoryBean
巧妙的将Mapper接口对应的代理对象MapperProxy
装载到了Spring容器中。 - 通过
SqlSessionTemplate
,使用静态代理+动态代理模式,巧妙的实现了每次访问数据库都是用新的Sqlsession
对象。
Mybatis 源码中文注释
https://github.com/xiaolyuh/mybatis
心得
在这里Mybatis源码系列就写完了,与Spring源码相比,我非常建议大家去看下Mybatis源码,原因有以下几点:
- Mybatis源码非常工整,包结构、代码结构都很值得我们学习。
- Mybatis中使用很多设计模式,大部分设计模式都可以在这里找到其身影,可以看成是设计模式的最佳实践。
- Mybatis整体设计也非常巧妙,扩展性非常强。