Spring集成Mybatis源码分析
一、Mybatis的启动流程
Spring能够与Mybatis的完美集成,只要引入mybatis-spring.jar包,都不需要配置mybatis-Config.xml文件,就可以通过Spring的IOC获取到Mybatis的Mapper来进行持久化操作,这篇文章将会从源码级别讲述mybatis与spring的集成机制。
public static void main(String[] args) throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
// 初始化配置, 创建SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 创建会话
SqlSession sqlSession = sqlSessionFactory.openSession();
// 获取Mapper代理类
BlogMapper blogMapper = sqlSession.getMapper(BlogMapper.class);
// 执行用户请求
Blog blog = blogMapper.selectBlog(1L);
}
以上面代码为例,在Mybatis启动的过程中,首先会通过加载xml配置,生成SqlSessionFactory,然后当发起Mapper请求时,则通过Mapper代理类中的SqlSession完成一次数据库会话,这个具体过程可参考我的其他Mybatis源码文章,在这个过程中,Mybatis的四大组件都参与了会话,但是他们的生命周期却是不同,如下图:
- Configuration 管理Mybatis的全局配置,包括缓存、日志、映射器等,加载之后就不会改变,作用于整个应用
- SqlSessionFactory 会话工厂,负责创建SqlSession, 作用于整个应用
- SqlSession 会话,负责和数据库交互,真正实现持久化操作,和Mapper、Connection、事务都是一对一,作用域是一次请求,不支持跨线程使用
- Mapper 映射器,映射用户调用的接口,内部维护一个SqlSession,使用SqlSession实现持久化操作,因为是通过SqlSession的getMapper使用动态代理创建,所以作用域和会话相同,也不支持跨线程。
通过上面的描述,可以发现Mybatis的Mapper的使用并不符合Spring中使用的条件,在Spring中,需要将Mapper作为Bean注入到Service中,实现重复使用,上面和SqlSession绑定的Mapper显然不符合需求
二、集成机制原理
@Configuration
public class MybatisConfig {
@Bean
public MapperScannerConfigurer mapperScannerConfigurer(SqlSessionFactory sqlSessionFactory) {
MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
mapperScannerConfigurer.setBasePackage("com.example.dao");
return mapperScannerConfigurer;
}
@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
return sqlSessionFactoryBean;
}
@Bean
public DataSource dataSource() {
DataSource dataSource = new DriverManagerDataSource("jdbc:mysql://192.168.117.128:3306/pattern?characterEncoding=UTF-8&serverTimeZone=GMT", "root", "root");
return dataSource;
}
public static void main(String[] args) {
// 初始化Spring容器
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(MybatisConfig.class);
// 获取Mapper代理类
BlogMapper blogMapper = applicationContext.getBean("blogMapper", BlogMapper.class);
// 执行用户请求
Blog one = blogMapper.findOne(1L);
System.out.println(one);
}
}
上面是Spring集成Mybatis的代码配置,配置中初始化了两个Bean,分别是MapperScannerConfigurer和SqlSessionFactoryBean,MapperScannerConfigurer类的作用扫描Mapper包下的Mapper接口,并将他们的Bean信息保存IOC中。SqlSessionFactoryBean类用于加载和初始化Mybatis的配置,完成Configuration、SqlSessionFactory的初始化。
如上图,就是集成简单流程,下面将从包扫描、Mybatis配置初始化、请求过程三个部分分析集成的源码。
Mapper包扫描
Mapper包扫描的流程如上图,MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor,以postProcessBeanDefinitionRegistry()方法为入口,开始包扫描,源码如下:
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
if (this.processPropertyPlaceHolders) {
processPropertyPlaceHolders();
}
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
// ...
// 扫描包
scanner.scan(
StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
之后,会通过ClassPathMapperScanner的doScan方法获取BeanDefinitionHolder,BeanDefinitionHolder封装了Mapper代理类的name和描述,之后会调用processBeanDefinitions()处理BeanDefinitionHolder,将获取BeanDefinitionHolder中的BeanDefinition,并封装为MapperFactoryBean的BeanDefinition注册到IOC中, 到此,就完成了整个包扫描流程,源码如下:
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;
}
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
AbstractBeanDefinition definition;
BeanDefinitionRegistry registry = getRegistry();
// 遍历beanDefinitions
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (AbstractBeanDefinition) holder.getBeanDefinition();
// ...
// the mapper interface is the original class of the bean
// but, the actual class of the bean is MapperFactoryBean 真实的Bean类型为MapperFactoryBean
definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName);
definition.setBeanClass(this.mapperFactoryBeanClass);
// ...
if (!definition.isSingleton()) {
BeanDefinitionHolder proxyHolder = ScopedProxyUtils.createScopedProxy(holder, registry, true);
if (registry.containsBeanDefinition(proxyHolder.getBeanName())) {
registry.removeBeanDefinition(proxyHolder.getBeanName());
}
// 注册Bean到IOC
registry.registerBeanDefinition(proxyHolder.getBeanName(), proxyHolder.getBeanDefinition());
}
}
}
Mybatis配置初始化
如上图,包扫描完成之后,就是SqlSessionFactoryBean的初始化,SqlSessionFactoryBean实现了InitializingBean接口实现了afterPropertiesSet方法,在该方法调用 buildSqlSessionFactory()方法,首先初始化了Configuration,之后使用SqlSessionFactoryBuilder创建SqlSessionFactory,这个SqlSessionFactory之后会注入到SqlSessionTemplate中,并在SqlSessionTemplate中创建SqlSession,这里Configuration和SqlSessionFactory都是Mybatis的组件,所以这里首先完成了Mybatis的配置集成。
Mapper请求过程
集成之后的Mapper请求要比原生Mybatis的Mapper复杂很多,Mapper也是通过Spring IOC获取,前面已经说过IOC存储的是MapperFactoryBean,所以Mapper代理类的也是通过MapperFactoryBean创建,所以代码分析也从这里开始。
如图,MapperFactoryBean实现了FactoryBean和SqlSessionDaoSupport接口,FactoryBean接口用于Spring中创建复杂Bean,SqlSessionDaoSupport抽象类持有一个SqlSessionTemplate,这个SqlSessionTemplate之后会被注入到Mapper代理类中, MapperFactoryBean创建Mapper代理类时,并不是使用Spring代理创建,而是委托给Mybatis的Configuration组件创建,这里Mapper的获取和原生Mybatis相同,不同是注入代理类的SqlSession不再是DefaultSqlSession对象,而是SqlSessionTemplate,在SqlSessionTemplate中又持有一个SqlSession的代理类.
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
private Class<T> mapperInterface;
// ...
// 获取Mapper
@Override
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
// ...
}
public abstract class SqlSessionDaoSupport extends DaoSupport {
private SqlSessionTemplate sqlSessionTemplate;
// ...
// 注入sqlSessionFactory到SqlSessionTemplate中
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
if (this.sqlSessionTemplate == null || sqlSessionFactory != this.sqlSessionTemplate.getSqlSessionFactory()) {
this.sqlSessionTemplate = createSqlSessionTemplate(sqlSessionFactory);
}
}
// ...
protected SqlSessionTemplate createSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
// ...
// 获取SqlSessionTemplate
public SqlSession getSqlSession() {
return this.sqlSessionTemplate;
}
// ...
}
public class SqlSessionTemplate implements SqlSession, DisposableBean {
private final SqlSessionFactory sqlSessionFactory;
private final ExecutorType executorType;
private final SqlSession sqlSessionProxy;
// ...
// 从Configuration中获取Mapper
@Override
public <T> T getMapper(Class<T> type) {
return getConfiguration().getMapper(type, this);
}
// ...
@Override
public Configuration getConfiguration() {
return this.sqlSessionFactory.getConfiguration();
}
// ...
}
根据上面可以代码可知,SqlSessionTemplate是集成实现的关键,所以下面来分析SqlSessionTemplate的代码,首先SqlSessionTemplate中持有一个SqlSession的代理类,也就是sqlSessionProxy对象,SqlSessionTemplate的增删改查都是通过这个代理类实现.同时可以发现不支持commit等事务方法,由此可以推断,集成之后,Mybatis不在管理事务,而是交给Spring管理.
public class SqlSessionTemplate implements SqlSession, DisposableBean {
private final SqlSessionFactory sqlSessionFactory;
private final ExecutorType executorType;
private final SqlSession sqlSessionProxy;
// ...
// 从Configuration中获取Mapper
@Override
public <T> T getMapper(Class<T> type) {
return getConfiguration().getMapper(type, this);
}
// ...
@Override
public Configuration getConfiguration() {
return this.sqlSessionFactory.getConfiguration();
}
// ...
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;
// 创建sqlSessionProxy代理类
this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class }, new SqlSessionInterceptor());
}
// ...
// 通过sqlSessionProxy代理类实现数据库操作
@Override
public <T> T selectOne(String statement, Object parameter) {
return this.sqlSessionProxy.selectOne(statement, parameter);
}
@Override
public <K, V> Map<K, V> selectMap(String statement, String mapKey) {
return this.sqlSessionProxy.selectMap(statement, mapKey);
}
// ...
// Spring事务无法被覆盖
@Override
public void commit(boolean force) {
throw new UnsupportedOperationException("Manual commit is not allowed over a Spring managed SqlSession");
}
@Override
public void rollback() {
throw new UnsupportedOperationException("Manual rollback is not allowed over a Spring managed SqlSession");
}
// ...
}
之后最重要的一个点就是SqlSessionInterceptor类,SqlSessionInterceptor类InvocationHandler接口,实现了invoke方法,这个方法中记录了事务处理以及集成Mybatis一级缓存失效的原因.
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 创建一个DefaultSqlSession对象
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有没有被Spring事务管理
// 如果被管理,强制提交
sqlSession.commit(true);
}
return result;
} catch (Throwable t) {
Throwable unwrapped = unwrapThrowable(t);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator
.translateExceptionIfPossible((PersistenceException) unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw unwrapped;
} finally {
// 关闭DefaultSqlSession对象
if (sqlSession != null) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}
可以发现在invoke方法中,创建了DefaultSqlSession执行数据库操作,同时,Mybatis事务交由Spring管理,这也是之前commit方法不支持使用的原因,另外就是每次操作都会关闭DefaultSqlSession对象,因为Mybatis的一级缓存是SqlSession域缓存,所以这也导致一级缓存失效.