目录
一、Mybatis和Spring集成项目准备
在第一篇配置的基础上增加扩展基础的Spring集成,Mybatis基础配置篇见此(一)Mybatis持久化框架原理之项目搭建。同时,要想看懂这篇mybatis-spring的集成原理,首先需要搞懂mybatis核心的处理过程,具体原理分析见下列几篇源码解读:
1.pom文件改动
<properties>
<!-- mybatis版本号 -->
<mybatis.version>3.4.1</mybatis.version>
<!-- spring 版本 -->
<spring.version>4.3.5.RELEASE</spring.version>
<!-- mybatis spring 连接包 -->
<mybatis.spring.version>2.0.2</mybatis.spring.version>
<!-- junit 版本 -->
<junit.version>4.12</junit.version>
</properties>
<dependencies>
<!-- mybatis核心包 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>${mybatis.version}</version>
</dependency>
<!-- spring 核心包 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- mybatis spring 连接包 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>${mybatis.spring.version}</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.20</version>
</dependency>
<!-- 测试junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
</dependencies>
2.Spring配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 引入properties文件 -->
<context:property-placeholder location="classpath:database.properties"/>
<!-- 定义数据源 -->
<bean id="dataSource" class="oracle.jdbc.pool.OracleDataSource">
<property name="driverType" value="${connection.driver}"/>
<property name="URL" value="${connection.url}"/>
<property name="user" value="${connection.username}"/>
<property name="password" value="${connection.password}"/>
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 数据源 -->
<property name="dataSource" ref="dataSource"/>
<!-- mybatis 配置文件 -->
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<!-- 指定实体类映射文件,可以同时指定某一包以及子包下面的所有配置文件,
mapperLocations和configLocation有一个即可,当需要为实体类指定别名时,
可指定configLocation属性,再在mybatis总配置文件中采用mapper引入实体类映射文件 -->
<property name="mapperLocations" value="classpath*:*Mapper.xml"/>
</bean>
<!-- 指定需要引用的接口和sqlSessionFactory对象 -->
<bean id="demoMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
<property name="mapperInterface" value="com.iboxpay.mapper.DemoMapper"/>
<property name="sqlSessionFactory" ref="sqlSessionFactory"/>
</bean>
</beans>
3.Java测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class TestMybatisSpringTest {
@Autowired
private DemoMapper demoMapper;
@Test
public void testSpringMybatis() {
System.out.println(demoMapper.select("1", 1, 's'));
}
}
/*
打印结果:
[DemoEntity(sno=516429, name=test, sex=男, birth=Tue May 19 11:05:16 CST 2020, grade=2)]
*/
二、UML图和流程分析
1.UML类图
下图是此方法的UML类图结构:
上面的UML类图涉及了Spring的接口使用,有兴趣可以去翻看一系列的Spring接口。
由上面的UML类图可以看出来,mybatis-spring集成包最重要的为四个类,但基于上面的实例我们此次只分析以下三个类,MapperScannerConfigurer类不在此例子中做分析:
- SqlSessionFactoryBean:实现了Spring的FactoryBean接口类,将其变成和Spring相关联的SqlSession的工厂类。
- SqlSessionTemplate:Mybatis中SqlSession的实现类,内部实际上封装了SqlSessionFactory和SqlSession,经常看到,但是在本例子中没有使用;
- MapperFactoryBean:类似于SqlSessionFactoryBean,只是该类针对的是Mapper接口形成的FactoryBean,里面封装了mapperInterface和SqlSessionTemplate,以便于和传统Mybatis一样从SqlSession中拿取Mapper代理类,进而操作数据库。
其中,mybatis-spring包集成spring采用的是实现spring的通用接口,后续再说一下此集成过程中源码流程。
2.流程分析
流程如下:
大致流程如图中所述,都是mybatis利用Spring的Bean初始化机制来集成到Spring中的,接下来我们看看其具体的实现细节。
三、源码分析
1.SqlSessionFactoryBean配置
该类是Spring上下文创建SqlSessionFactory的FactoryBean,并在依赖注入时将SqlSession-Factory传递给Mybatis的mapper代理工厂。首先看到其实现的三个接口:
- FactoryBean:Spring中有BeanFactory也有FactoryBean,很容易把这个搞混,BeanFactory指的是Spring工厂的最高接口,而FactoryBean即是为某一类Bean创建一个工厂,这个工厂只产生这一类的Bean。SqlSessionFactoryBean实现的接口是FactoryBean<SqlSessionFactory>,指定了SqlSessionFactory,因此这个Bean只会产生SqlSessionFactory;
- InitializingBean:实现该接口的类,将在类的成员属性被spring框架初始化之后调用其中唯一的接口方法afterPropertiesSet;
- ApplicationListener:spring的发布/订阅模式(也叫观察者模式),当ApplicationEvent被发布的时候,将会调用ApplicationListener的实现类,SqlSessionFactoryBean的事件触发为ContextRefreshedEvent,当spring容器初始化之后将会调用此方法。
SqlSessionFactoryBean该类关键源码如下:
public class SqlSessionFactoryBean
implements FactoryBean<SqlSessionFactory>, InitializingBean,
ApplicationListener<ApplicationEvent> {
// config.xml文件对应的资源对象
private Resource configLocation;
// mybatis最重要的配置对象,里面有mybatis运行时所需要的所有数据配置
private Configuration configuration;
// mapper.xml文件对应的资源对象数组
private Resource[] mapperLocations;
// 数据源
private DataSource dataSource;
// 熟悉的SqlSessionFactory构造器
private SqlSessionFactoryBuilder sqlSessionFactoryBuilder =
new SqlSessionFactoryBuilder();
// SqlSessionFactory获取SqlSession的工厂
private SqlSessionFactory sqlSessionFactory;
// 其它的成员对象都忽略,都是在Configuration中可以找到的
// 暂时看到这几个重要的对象,同时中间忽略这些成员对象的setter和getter方法
@Override
public void afterPropertiesSet() throws Exception {
// 刚刚说过,当这个Bean初始化之后Spring工厂将会调用这个方法
// 忽略判断数据源dataSource和sqlSessionFactoryBuilder为空语句
// 这里也会判断configuration和configLocation这两个对象不能同时存在,否则
// 否则将会报错
// 确定必要对象的值不为空后,进入构造sqlSessionFactory方法
this.sqlSessionFactory = buildSqlSessionFactory();
}
protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
// 这个方法比较长,但总体而言和原生mybatis解析config.xml文件流程还是差不多的
final Configuration targetConfiguration;
// 熟悉的解析config.xml文件的XMLConfigBuilder构造器
XMLConfigBuilder xmlConfigBuilder = null;
// 如果configuration对象不为空
if (this.configuration != null) {
// 直接将configuration赋值给targetConfiguration变量
// configuration不为空则意味着至少config.xml文件那些流程已经解析过了
targetConfiguration = this.configuration;
// 为variables属性再赋值
if (targetConfiguration.getVariables() == null) {
targetConfiguration.setVariables(this.configurationProperties);
} else if (this.configurationProperties != null) {
targetConfiguration.getVariables()
.putAll(this.configurationProperties);
}
} else if (this.configLocation != null) {
// 如果configuration为空且configLocation配置了config.xml文件位置
// 则实例化XMLConfigBuilder构造器,注意这里只是实例化了XMLConfigBuilder
// 还没有调用parse方法,因此config.xml文件还没有被解析
xmlConfigBuilder = new XMLConfigBuilder(this.configLocation
.getInputStream(), null, this.configurationProperties);
// 将里面实例化的configuration赋值给targetConfiguration
targetConfiguration = xmlConfigBuilder.getConfiguration();
} else {
// 如果configLocation和configuration都为空,则创建一个默认的
// Configuration对象
targetConfiguration = new Configuration();
Optional.ofNullable(this.configurationProperties)
.ifPresent(targetConfiguration::setVariables);
}
// 设置setObjectFactory、setObjectWrapperFactory和setVfsImpl对象,略
...
// 别名包属性不为空
if (hasLength(this.typeAliasesPackage)) {
// 如果不为空则获取包下的class类
// 再将其注册进别名注册中心typeAliasRegistry
scanClasses(this.typeAliasesPackage, this.typeAliasesSuperType)
.stream().filter(clazz -> !clazz.isAnonymousClass())
.filter(clazz -> !clazz.isInterface())
.filter(clazz -> !clazz.isMemberClass())
.forEach(targetConfiguration
.getTypeAliasRegistry()::registerAlias);
}
// 如果显示的配置了别名类,则直接将这些类配置到别名注册中心typeAliasRegistry
if (!isEmpty(this.typeAliases)) {
Stream.of(this.typeAliases).forEach(typeAlias -> {
targetConfiguration.getTypeAliasRegistry()
.registerAlias(typeAlias);
});
}
// 如果插件不为空的话,则设置添加插件到configuration对象中
if (!isEmpty(this.plugins)) {
Stream.of(this.plugins).forEach(plugin -> {
targetConfiguration.addInterceptor(plugin);
});
}
// 类似别名一样,如果设置了TypeHandler类型处理器包,则搜索出来再注册
// 到typeHandlerRegistr注册中心
if (hasLength(this.typeHandlersPackage)) {
scanClasses(this.typeHandlersPackage, TypeHandler.class).stream()
.filter(clazz -> !clazz.isAnonymousClass())
.filter(clazz -> !clazz.isInterface())
.filter(clazz -> !Modifier.isAbstract(clazz.getModifiers()))
.filter(clazz ->
ClassUtils.getConstructorIfAvailable(clazz) != null)
.forEach(targetConfiguration.getTypeHandlerRegistry()::register);
}
// 类似别名
if (!isEmpty(this.typeHandlers)) {
Stream.of(this.typeHandlers).forEach(typeHandler -> {
targetConfiguration.getTypeHandlerRegistry()
.register(typeHandler);
});
}
// 配置语言驱动器languageDriver
if (!isEmpty(this.scriptingLanguageDrivers)) {
Stream.of(this.scriptingLanguageDrivers).forEach(languageDriver -> {
targetConfiguration.getLanguageRegistry()
.register(languageDriver);
});
}
// 如果手动配置了defaultDriverClass则将手动配置的值赋值给configuration
Optional.ofNullable(this.defaultScriptingLanguageDriver)
.ifPresent(targetConfiguration::setDefaultScriptingLanguage);
// 设置databaseIdProvider
if (this.databaseIdProvider != null) {
try {
targetConfiguration.setDatabaseId(
this.databaseIdProvider.getDatabaseId(this.dataSource));
} catch (SQLException e) {
throw new NestedIOException();
}
}
// 如果配置了cache缓存对象则添加到configuration对象中
Optional.ofNullable(this.cache)
.ifPresent(targetConfiguration::addCache);
// 如果解析config.xml文件的构造器不为空(即配置了config.xml文件)
if (xmlConfigBuilder != null) {
try {
// 调用构造器的parse,执行解析config.xml文件流程
xmlConfigBuilder.parse();
} catch (Exception ex) {
throw new NestedIOException();
} finally {
ErrorContext.instance().reset();
}
}
// 如果手动配置了mapperLocations(即配置了mapper.xml文件位置)
if (this.mapperLocations != null) {
if (this.mapperLocations.length == 0) {
// 为空不进行操作,只会打印debug日志告诉mapperLocations为空
} else {
for (Resource mapperLocation : this.mapperLocations) {
// 过滤为null的资源对象
if (mapperLocation == null) {
continue;
}
try {
// 和解析<mapper/>标签引入的mapper.xml文件一样
// 使用XMLMapperBuilder构造器解析mapper.xml文件
XMLMapperBuilder xmlMapperBuilder =
new XMLMapperBuilder(mapperLocation.getInputStream(),
targetConfiguration, mapperLocation.toString(),
targetConfiguration.getSqlFragments());
// 这里需要注意的是,如果统一个mapper在config.xml中被引入了
// 在mapperLocations对象也指定了,会导致mapper.xml文件被解析两次
// 添加ResultMap时判断存在重复值将会抛出异常
// 即要么采用在config.xml中配置mapper.xml文件,要么采用
// 只配置mapperLocations对象值,两者混合用容易出错
xmlMapperBuilder.parse();
} catch (Exception e) {
throw new NestedIOException();
} finally {
ErrorContext.instance().reset();
}
}
}
}
// 前面的流程已经解析完,可以把这个方法分为两个流程,第一个流程则是添加
// 在Spring配置中配置的属性值,第二个流程则是接入原生mybatis配置方式
// 在平常的使用中,原生的mybatis配置方式基本不适用,而是全部使用Spring
// 方式管理配置,因此建议不配置config.xml文件,直接配置mapperLocations
// 这些属性,可以更直观的看到自己的配置;
// 像原生Mybatis一样,解析完XML文件则使用configuration对象构造
// SqlSessionFactory对象,默认是DefaultSqlSessionFactory类型
return this.sqlSessionFactoryBuilder.build(targetConfiguration);
}
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (failFast && event instanceof ContextRefreshedEvent) {
// 当上下文刷新完毕后,将会再次加载mybatis前一次失败的配置
this.sqlSessionFactory.getConfiguration().getMappedStatementNames();
}
}
}
到此,我们已经达到了原生mybatis使用时获得加载完所有信息的sqlSessionFactory工厂类,接下来看一下该集成方式的下一步。
2.MapperFactoryBean配置
我们使用原生的Mybatis时都要从sqlSessionFactory调用openSession获得SqlSession对象,然后再用这个SqlSession对象调用getMapper方法从configuration对象中的mapperRegistry获得已经被MapperProxyFactory工厂反向代理过的mapper接口对象。接着调用mapper接口的方法即可。
那么Spring集成的MapperFactoryBean使用方式和原生的有何不同?接下来一起看看。
applicationContext.xml配置:
<bean id="demoMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
<property name="mapperInterface" value="com.iboxpay.mapper.DemoMapper"/>
<property name="sqlSessionFactory" ref="sqlSessionFactory"/>
</bean>
可以看到该配置将刚刚构造完成的sqlSessionFactory赋值给MapperFactoryBean,并且指定了对应的mapper。接下来看到其关键源码部分:
public class MapperFactoryBean<T> extends SqlSessionDaoSupport
implements FactoryBean<T> {
// 被封装的接口类型,FactoryBean调用getObject将会返回这个类型Bean
private Class<T> mapperInterface;
private boolean addToConfig = true;
@Override
protected void checkDaoConfig() {
// 可以看到这个方法是重写的,因此父类肯定有操作,父类的流程我们等下再去分析
super.checkDaoConfig();
// 封装的接口类型不能为空
notNull(this.mapperInterface, "...");
// 在父类中已经设置过了sqlSession,因此这里的sqlSession是必有值的
Configuration configuration = getSqlSession().getConfiguration();
// 默认会把mapperInterface添加到configuration对象中的mapperRegistry对象中
if (this.addToConfig &&
!configuration.hasMapper(this.mapperInterface)) {
try {
// 同时添加到configuration中,保持一致
configuration.addMapper(this.mapperInterface);
} catch (Exception e) {
throw new IllegalArgumentException(e);
} finally {
ErrorContext.instance().reset();
}
}
}
// 其它方法略过,只看这个方法
@Override
public T getObject() throws Exception {
// 实际还是会调用configuration的getMapper方法,从mapperRegistry中获取
// mapper接口的代理对象,类型是MapperProxy
return getSqlSession().getMapper(this.mapperInterface);
}
}
看到这个类肯定一脸懵逼,其父类中的操作将会解除我们的疑惑。
2.1 DaoSupport类
这是Spring为持久化框架设置的一个抽象类,这个类实现了InitializingBean接口,其关键源码如下:
public abstract class DaoSupport implements InitializingBean {
@Override
public final void afterPropertiesSet() throws IllegalArgumentException,
BeanInitializationException {
// 检查Dao类的配置
checkDaoConfig();
// 交给子类来实现初始化dao的操作
try {
// 方法交给子类去实现
initDao();
}
catch (Exception ex) {
throw new BeanInitializationException();
}
}
protected abstract void checkDaoConfig()
throws IllegalArgumentException;
protected void initDao() throws Exception {
}
}
2.2 SqlSessionDaoSupport
这个类集成了Spring为持久化框架实现的抽象类DaoSupport,让我们看看该类的具体实现源码:
public abstract class SqlSessionDaoSupport extends DaoSupport {
private SqlSessionTemplate sqlSessionTemplate;
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
// 这个方法是搭配配置sqlSessionFactory使用的,如果配置了sqlSessionFactory
// 那么这个方法将一定会被调用,因为Spring工厂初始化Bean的时候会递归其setter
// 方法,如果工厂里有这个类型的Bean,将会被赋值进来;
// 如果本类的sqlSessionTemplate为空或者sqlSessionTemplate中的
// sqlSessionFactory对象和传入的不一致,则调用createSqlSessionTemplate方法
// 创建sqlSessionTemplate对象
if (this.sqlSessionTemplate == null || sqlSessionFactory != this
.sqlSessionTemplate.getSqlSessionFactory()) {
this.sqlSessionTemplate =
createSqlSessionTemplate(sqlSessionFactory);
}
}
protected SqlSessionTemplate createSqlSessionTemplate(
SqlSessionFactory sqlSessionFactory) {
// sqlSessionFactory是Spring工厂传入的Bean
return new SqlSessionTemplate(sqlSessionFactory);
}
public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate){
// 如果Spring工厂中创建了sqlSessionTemplate对象,则会调用进来并赋值
this.sqlSessionTemplate = sqlSessionTemplate;
}
@Override
protected void checkDaoConfig() {
// 实例化之后,将会判断sqlSessionTemplate不为空
notNull(this.sqlSessionTemplate, "Property 'sqlSessionFactory' or"+
" 'sqlSessionTemplate' are required");
}
}
详细说明看代码中的注释。截止到这,其实Spring工厂已经获得了目标mapper接口的代理对象工厂,并且也已经交给了Spring工厂管理,等使用@Autowired等注解时便会自动调用MapperFactory-Bean工厂Bean的getObject方法,像原生Mybatis使用方法一样,调用getMapper获得mapper接口的代理对象MapperProxy。
3.SqlSessionTemplate
前面我们已经看到了,在SqlSessionDaoSupport类中,Mybatis根据Spring工厂初始化Bean的流程中,使用setter机制设置了sqlSessionFactory对象,并初始化了一个SqlSessionTemplate对象,并且如果Spring工厂中有这个对象,又再会调用settter方法设置SqlSessionTemplate。
因此这个类在mybatis集成到spring中是绝对需要分析一下的,接下来看看SqlSessionTemplate的关键源码部分是如何实现的:
public class SqlSessionTemplate implements SqlSession, DisposableBean {
// 配置的sqlSessionFactory对象
private final SqlSessionFactory sqlSessionFactory;
// 配置的执行类型,有Simple,有Batch等
private final ExecutorType executorType;
// 内部实际代理的SqlSession对象,调用selectOne和selectList这些方法
// 实际将会调用到这个对象来,会使用内部类SqlSessionInterceptor增加方法
private final SqlSession sqlSessionProxy;
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory,
ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
// 根据SqlSessionFactory和SqlSession接口使用SqlSessionInterceptor
// 代理对象增加
this.sqlSessionProxy = (SqlSession)
newProxyInstance(SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class }, new SqlSessionInterceptor());
}
// 下面是一系列SqlSession的接口方法,都是调用sqlSessionProxy的相应方法,略
...
// 加强sqlSessionProxy对象的内部代理类,使用JDK动态代理增强,因为SqlSession
// 本身就是一个接口
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
// 根据sqlSessionFactory和executorType对象获取session,方法内部实际上
// 也调用了sqlSessionFactory的openSession方法,只是前面会判断当前
// 是否含有sqlSession对象,因此这里的SqlSession对象还是DefaultSqlSession
SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this
.sqlSessionFactory,
SqlSessionTemplate.this.executorType, SqlSessionTemplate.this
.exceptionTranslator);
try {
// 调用原代理方法,即外部的mapper接口方法实际上调用的还是sqlSession
// 对象的方法
Object result = method.invoke(sqlSession, args);
// 如果事务没有被Spring事务管理器管理则调用sqlSession.commit手动提交
// 否则交给Spring事务管理去操作事务流程
if (!isSqlSessionTransactional(sqlSession,
SqlSessionTemplate.this.sqlSessionFactory)) {
// 手动提交事务
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 {
// 关闭会话
if (sqlSession != null) {
closeSqlSession(sqlSession, SqlSessionTemplate
.this.sqlSessionFactory);
}
}
}
}
}
在spring框架使用具体的mapper类时,将会通过MapperFactoryBean的getObject来获得具体的mapper对象,并注入使用的类中,getSqlSession().getMapper(this.mapperInterface)是该方法中的具体代码,到此,基本上就完成了mybatis原生过程的getMapper,接下来只需要调用mapper中的方法即可。
也可以看到SqlSessionTemplate类中实现了SqlSession接口,而具体的实现方法则是直接调用sqlSessionProxy的方法,并且为SqlSessionTemplate的每次方法调用也添加了一层代理。
到此,使用MapperFactoryBean方式和spring集成原理便结束了。