目录
前言
Mybatis的简单使用
搭建项目
原理分析
Mybaits中的事务管理
Spring整合Mybatis的原理
SqlSessionFactoryBean的初始化流程
@MapperScan工作原理
MapperScannerConfigurer分析
MapperFactoryBean分析
整合原理总结
总结
专题要点如下:
本文要解决的是第二点,Mybatis的使用、原理及跟Spring整合原理分析
。
pom
文件添加如下依赖
-
-
org.mybatis
-
mybatis
-
3.4
.6
-
-
-
-
mysql
-
mysql-connector-java
-
8.0
.15
-
创建mybaits
配置文件,mybatis-config.xml
- <?xml version="1.0" encoding="UTF-8" ?>
- <!DOCTYPE configuration
- PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
- "http://mybatis.org/dtd/mybatis-3-config.dtd">
- <configuration>
- <environments default="development">
- <environment id="development">
- <transactionManager type="JDBC"/>
- <dataSource type="POOLED">
- <property name="password" value="123"/>
- <property name="username" value="root"/>
- <property name="driver" value="com.mysql.jdbc.Driver"/>
- <property name="url"
- value="jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8"/>
- </dataSource>
- </environment>
- </environments>
- <mappers>
- <mapper resource="mapper/userMapper.xml"/>
- </mappers>
- </configuration>
创建mapper.xml
文件如下
-
"1.0" encoding=
"UTF-8" ?>
-
-
PUBLIC
"-//ibatis.apache.org//DTD Mapper 3.0//EN"
-
"http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd">
-
"org.apache.ibatis.dmz.mapper.UserMapper">
-
<
select id=
"selectOne" resultType=
"org.apache.ibatis.dmz.entity.User">
-
select * from user where id = #{id}
-
select>
-
-
-
实体类如下
-
public class User {
-
-
private
int id;
-
-
private String name;
-
-
private
int age;
-
-
// 省略getter/setter方法
-
-
@Override
-
public String toString() {
-
return
"User{" +
-
"id=" + id +
-
", name='" + name +
'\'' +
-
", age=" + age +
-
'}';
-
}
-
}
-
测试代码如下
-
public class Main {
-
public static void main(String[] args) throws Exception {
-
String resource =
"mybatis-config.xml";
-
InputStream resourceAsStream = Resources.getResourceAsStream(resource);
-
// 1.解析XML配置
-
SqlSessionFactoryBuilder builder =
new SqlSessionFactoryBuilder();
-
// 2.基于解析好的XML配置创建一个SqlSessionFactory
-
SqlSessionFactory sqlSessionFactory = builder.build(resourceAsStream);
-
// 3.通过SqlSessionFactory,创建一个SqlSession
-
SqlSession sqlSession = sqlSessionFactory.openSession();
-
// 4.测试直接调用mapper.xml中的方法
-
Object o = sqlSession.selectOne(
"org.apache.ibatis.dmz.mapper.UserMapper.selectOne",
2);
-
if(o instanceof User){
-
System.out.
println(
"直接执行mapper文件中的sql查询结果:"+o);
-
}
-
// 5.获取一个代理对象
-
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
-
// 6.调用代理对象的方法
-
System.out.
println(
"代理对象查询结果:"+mapper.selectOne(
1));
-
}
-
}
-
-
// 程序输出如下,分别对应了我本地数据库中的两条记录
-
// 直接执行mapper文件中的sql查询结果:User{id=2, name='dmz', age=18}
-
// 代理对象查询结果:User{id=1, name='dmz', age=18}
原理分析
因为本专栏不是对mybatis
的源码分析专题(笔者对于三大框架都会做一个源码分析专题),所以对这块的原理分析不会牵涉到过多源码级别的内容。
从上面的例子中我们可以看到,对于Mybatis
的使用主要有两种形式
-
直接通过sqlsession
调用相关的增删改查的API
,例如在我们上面的例子中就直接调用了sqlsession
的selectOne
方法完成了查询。使用这种方法我们需要传入namespace+statamentId
以便于Mybatis
定位到要执行的SQL
,另外还需要传入查询的参数
-
第二种形式,则是先通过sqlsession
创建一个代理对象
,然后调用代理对象的方法完成查询
本文要探究的原理主要是第二种形式的使用,换而言之,就是Mybatis
是如何生成这个代理对象的。在思考Mybatis是如何做的之前,我们不妨想一想,如果是我们自己要实现这个功能,那么你会怎么去做呢?
如果是我的话,我会这么做:
当然我这种做法省略了很多细节,比如如何将方法参数绑定到SQL
,如何封装结果集,是否对同样的Sql
进行缓存等等。正常Mybatis在执行Sql时起码需要经过下面几个流程
9
其中,Executor负责维护缓存以及事务的管理,它会将对数据库的相关操作委托给StatementHandler
完成,StatementHandler
会先通过ParameterHandler
完成对Sql语句的参数的绑定,然后调用JDBC相关的API去执行Sql得到结果集,最后通过ResultHandler
完成对结果集的封装。
本文只是对这个流程有个大致的了解即可,详细的流程介绍我们在Mybatis的源码分析专栏中再聊~
Mybaits中的事务管理
Mybatis中的事务管理主要有两种方式
-
使用JDBC
的事务管理机制:即利用JDBC
中的java.sql.Connection
对象完成对事务的提交(commit())、回滚(rollback())、关闭(close())等
-
使用MANAGED的事务管理机制:这种机制MyBatis
自身不会去实现事务管理,而是让程序的容器如(tomcat,jboss)来实现对事务的管理
在文章开头的例子中,我在mybatis-config.xml
配置了
type="JDBC"/>
这意味着我们选用了JDBC
的事务管理机制,那么我们在哪里可以开启事务呢?实际上Mybatis默认是关闭自动提交的,也就是说事务默认就是开启的。而是否开启事务我们可以在创建SqlSession
时进行控制。SqlSessionFactory
提供了以下几个用于创建SqlSession
的方法
-
SqlSession openSession()
-
SqlSession openSession(boolean autoCommit)
-
SqlSession openSession(Connection connection)
-
SqlSession openSession(TransactionIsolationLevel level)
-
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level)
-
SqlSession openSession(ExecutorType execType)
-
SqlSession openSession(ExecutorType execType, boolean autoCommit)
-
SqlSession openSession(ExecutorType execType, Connection connection)
我们在觉得使用哪个方法来创建SqlSession
主要是根据以下几点
-
是否要关闭自动提交,意味着开启事务
-
使用外部传入的连接对象还是从配置信息中获取到的连接对象
-
使用哪种执行方式,一共有三种执行方式
-
ExecutorType.SIMPLE
:每次执行SQL
时都创建一个新的PreparedStatement
-
ExecutorType.REUSE
:复用PreparedStatement
对象
-
ExecutorType.BATCH
:进行批处理
在前面的例子中,我们使用的是空参的方法来创建SqlSession
对象的,这种情况下Mybatis会创建一个开启了事务的、从配置的连接池中获取连接的、事务隔离级别跟数据库保持一致的、执行方式为ExecutorType.SIMPLE
的SqlSession对象。
我们基于上面的例子来体会一下Mybatis中的事务管理,代码如下:
-
public class Main {
-
public static void main(String[] args) throws Exception {
-
String resource =
"mybatis-config.xml";
-
InputStream resourceAsStream = Resources.getResourceAsStream(resource);
-
// 1.解析XML配置
-
SqlSessionFactoryBuilder builder =
new SqlSessionFactoryBuilder();
-
// 2.基于解析好的XML配置创建一个SqlSessionFactory
-
SqlSessionFactory sqlSessionFactory = builder.build(resourceAsStream);
-
// 3.开启一个SqlSession
-
SqlSession sqlSession = sqlSessionFactory.openSession();
-
// 4.获取一个代理对象
-
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
-
User user =
new User();
-
user.setId(
3);
-
user.setName(
"dmz111");
-
user.setAge(
27);
-
// 插入一条数据
-
mapper.insert(user);
-
// 抛出一个异常
-
throw
new RuntimeException(
"发生异常!");
-
}
-
}
运行上面的代码,我们会发现数据库中并不会新增一条数据,但是如果我们在创建SqlSession
时使用下面这种方式
SqlSession sqlSession = sqlSessionFactory.openSession(true);
即使发生了异常,数据仍然会插入到数据库中
Spring整合Mybatis的原理
首先明白一点,虽然我在之前介绍了Mybatis
的事务管理,但是当Mybatis
跟Spring进行整合时,事务的管理完全由Spring进行控制!所以对于整合原理的分析不会涉及到事务的管理
我们先来看一个Spring整合Mybatis
的案例,我这里以JavaConfig的形式进行整合,核心配置如下:
-
@Configuration
-
@ComponentScan(
"com.dmz.mybatis.spring")
-
// 扫描所有的mapper接口
-
@MapperScan(
"com.dmz.mybatis.spring.mapper")
-
public class MybatisConfig {
-
-
@Bean
-
public DataSource dataSource() {
-
DriverManagerDataSource driverManagerDataSource =
new DriverManagerDataSource();
-
driverManagerDataSource.setPassword(
"123");
-
driverManagerDataSource.setUsername(
"root");
-
driverManagerDataSource.setDriverClassName(
"com.mysql.jdbc.Driver");
-
driverManagerDataSource.setUrl(
"jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8");
-
return driverManagerDataSource;
-
}
-
-
// 需要配置这个SqlSessionFactoryBean来得到一个SqlSessionFactory
-
@Bean
-
public SqlSessionFactoryBean sqlSessionFactoryBean() throws Exception {
-
SqlSessionFactoryBean sqlSessionFactoryBean =
new SqlSessionFactoryBean();
-
sqlSessionFactoryBean.setDataSource(dataSource());
-
PathMatchingResourcePatternResolver patternResolver =
new PathMatchingResourcePatternResolver();
-
sqlSessionFactoryBean.setMapperLocations(patternResolver.getResources(
"classpath:mapper/*.xml"));
-
return sqlSessionFactoryBean;
-
}
-
-
// 使用Spring中的DataSourceTransactionManager管理事务
-
@Bean
-
public TransactionManager transactionManager() {
-
DataSourceTransactionManager dataSourceTransactionManager =
new DataSourceTransactionManager();
-
dataSourceTransactionManager.setDataSource(dataSource());
-
return dataSourceTransactionManager;
-
}
-
}
从这段配置中我们可以提炼出一个关键信息,如果我们要弄清楚Spring是如何整合Mybatis的,我们应该要弄明白两点
-
@MapperScan
这个注解干了什么?
-
SqlSessionFactoryBean
这个Bean的创建过程中干了什么?
接下来我们就分为两点来进行讨论
SqlSessionFactoryBean的初始化流程
首先我们看看这个类的继承关系
继承关系
源码分析
看到它实现了InitializingBean
接口,那我们第一反应肯定是查看下它的afterPropertiesSet
方法,其源码如下:
-
public void afterPropertiesSet() throws Exception {
-
// 调用buildSqlSessionFactory方法完成对成员属性sqlSessionFactory的赋值
-
this.sqlSessionFactory = buildSqlSessionFactory();
-
}
-
-
// 通过我们在配置中指定的信息构建一个SqlSessionFactory
-
// 如果你对mybatis的源码有一定了解的话
-
// 这个方法做的事情实际就是先构造一个Configuration对象
-
// 这个Configuration对象代表了所有的配置信息
-
// 等价于我们通过myabtis-config.xml指定的配置信息
-
// 然后调用sqlSessionFactoryBuilder的build方法创建一个SqlSessionFactory
-
protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
-
-
final Configuration targetConfiguration;
-
-
// 接下来是通过配置信息构建Configuration对象的过程
-
// 我这里只保留几个重要的节点信息
-
XMLConfigBuilder xmlConfigBuilder = null;
-
-
-
// 我们可以通过configLocation直接指定mybatis-config.xml的位置
-
if (this.configuration != null) {
-
targetConfiguration = this.configuration;
-
if (targetConfiguration.getVariables() == null) {
-
targetConfiguration.setVariables(this.configurationProperties);
-
}
else
if (this.configurationProperties != null) {
-
targetConfiguration.getVariables().putAll(this.configurationProperties);
-
}
-
}
else
if (this.configLocation != null) {
-
xmlConfigBuilder =
new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
-
targetConfiguration = xmlConfigBuilder.getConfiguration();
-
}
else {
-
LOGGER.debug(
-
() ->
"Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");
-
targetConfiguration =
new Configuration();
-
Optional.ofNullable(this.configurationProperties).ifPresent(targetConfiguration::setVariables);
-
}
-
-
// 可以指定别名
-
if (hasLength(this.typeAliasesPackage)) {
-
scanClasses(this.typeAliasesPackage, this.typeAliasesSuperType).stream()
-
.filter(clazz -> !clazz.isAnonymousClass()).filter(clazz -> !clazz.isInterface())
-
.filter(clazz -> !clazz.isMemberClass()).forEach(targetConfiguration.getTypeAliasRegistry()::registerAlias);
-
}
-
-
if (!isEmpty(this.typeAliases)) {
-
Stream.of(this.typeAliases).forEach(typeAlias -> {
-
targetConfiguration.getTypeAliasRegistry().registerAlias(typeAlias);
-
LOGGER.debug(() ->
"Registered type alias: '" + typeAlias +
"'");
-
});
-
}
-
-
// 这里比较重要,注意在这里将事务交由了Spring进行管理
-
targetConfiguration.setEnvironment(
new Environment(this.environment,
-
this.transactionFactory == null ?
new SpringManagedTransactionFactory() : this.transactionFactory,
-
this.dataSource));
-
-
// 可以直接指定mapper.xml
-
if (this.mapperLocations != null) {
-
if (this.mapperLocations.length ==
0) {
-
LOGGER.warn(() ->
"Property 'mapperLocations' was specified but matching resources are not found.");
-
}
else {
-
for (Resource mapperLocation : this.mapperLocations) {
-
if (mapperLocation == null) {
-
continue;
-
}
-
try {
-
XMLMapperBuilder xmlMapperBuilder =
new XMLMapperBuilder(mapperLocation.getInputStream(),
-
targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
-
xmlMapperBuilder.parse();
-
} catch (Exception e) {
-
throw
new NestedIOException(
"Failed to parse mapping resource: '" + mapperLocation +
"'", e);
-
} finally {
-
ErrorContext.instance().reset();
-
}
-
LOGGER.debug(() ->
"Parsed mapper file: '" + mapperLocation +
"'");
-
}
-
}
-
}
else {
-
LOGGER.debug(() ->
"Property 'mapperLocations' was not specified.");
-
}
-
-
return this.sqlSessionFactoryBuilder.build(targetConfiguration);
-
}
可以看到在初始化阶段做的最重要的是就是给成员变量sqlSessionFactory
赋值,同时我们知道这是一个FactoryBean
,那么不出意外,它的getObject可以是返回了这个被赋值的成员变量,其源码如下:
-
public SqlSessionFactory getObject() throws Exception {
-
// 初始化阶段已经赋值了
-
if (this.sqlSessionFactory == null) {
-
afterPropertiesSet();
-
}
-
// 果不其然,直接返回
-
return this.sqlSessionFactory;
-
}
@MapperScan工作原理
查看@MapperScan
这个注解的源码我们会发现
-
@Retention(RetentionPolicy.RUNTIME)
-
@Target(ElementType.TYPE)
-
@Documented
-
@Import(MapperScannerRegistrar.class)
-
@Repeatable(MapperScans.class)
-
public @
interface MapperScan {
-
-
// basePackages属性的别名,等价于basePackages
-
String[] value()
default {};
-
-
// 扫描的包名
-
String[] basePackages()
default {};
-
-
// 可以提供一个类,以类的包名作为扫描的包
-
Class>[] basePackageClasses()
default {};
-
-
// BeanName的生成器,一般用默认的就好啦
-
Class extends BeanNameGenerator> nameGenerator()
default BeanNameGenerator.class;
-
-
// 指定要扫描的注解
-
Class extends Annotation> annotationClass()
default Annotation.class;
-
-
// 指定标记接口,只有继承了这个接口才会被扫描
-
Class> markerInterface()
default Class.class;
-
-
// 指定SqlSessionTemplate的名称,
-
// SqlSessionTemplate是Spring对Mybatis中SqlSession的封装
-
String sqlSessionTemplateRef()
default
"";
-
-
// 指定SqlSessionFactory的名称
-
String sqlSessionFactoryRef()
default
"";
-
-
// 这个属性是什么意思呢?Spring跟Mybatis整合
-
// 最重要的事情就是将Mybatis生成的代理对象交由Spring来管理
-
// 实现这个功能的就是这个MapperFactoryBean
-
Class extends MapperFactoryBean> factoryBean()
default MapperFactoryBean.class;
-
-
// 是否对mapper进行懒加载,默认为false
-
String lazyInitialization()
default
"";
-
-
}
接着我们就来看看MapperScannerRegistrar
做了什么,其源码如下:
-
// 这里我们只关注它的两个核心方法
-
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
-
// 获取到@MapperScan这个注解中的属性
-
AnnotchaationAttributes mapperScanAttrs = AnnotationAttributes
-
.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
-
if (mapperScanAttrs != null) {
-
// 紧接着开始向Spring容器中注册bd
-
registerBeanDefinitions(importingClassMetadata, mapperScanAttrs, registry,
-
generateBaseBeanName(importingClassMetadata,
0));
-
}
-
}
-
-
void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs,
-
BeanDefinitionRegistry registry, String beanName) {
-
-
// 打算注册到容器中的bd的beanClass属性为MapperScannerConfigurer.class
-
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
-
builder.addPropertyValue(
"processPropertyPlaceHolders",
true);
-
-
// 省略部分代码
-
// ....
-
// 这部分代码就是将注解中的属性获取出来
-
// 放到MapperScannerConfigurer这个beanDefinition中
-
-
// 最后将这个beanDefinition注册到容器中
-
registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
-
-
}
到这里我们可以确定了,@MapperScan
这个注解最大的作用就是向容器中注册一个MapperScannerConfigurer
,我们顺藤摸瓜,再来分析下MapperScannerConfigurer
是用来干嘛的
MapperScannerConfigurer分析
继承关系
image-20200722092411193
从上面这张图中我们能得出的一个最重要的信息就是,MapperScannerConfigurer
是一个Bean工厂的后置处理器,并且它实现的是BeanDefinitionRegistryPostProcessor
,而BeanDefinitionRegistryPostProcessor
通常都是用来完成扫描的,我们直接定位到它的postProcessBeanDefinitionRegistry
方法,源码如下:
方法分析
-
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
-
if (this.processPropertyPlaceHolders) {
-
// 处理@MaperScan注解属性中的占位符
-
processPropertyPlaceHolders();
-
}
-
// 在这里创建了一个ClassPathMapperScanner
-
// 这个类继承了ClassPathBeanDefinitionScanner,并复写了它的doScan、registerFilters等方法
-
// 其整体行为跟ClassPathBeanDefinitionScanner差不多,
-
// 关于ClassPathBeanDefinitionScanner的分析可以参考之前的《你知道Spring是怎么解析配置类的吗?》
-
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
来进行扫描,这个类本身继承自ClassPathBeanDefinitionScanner
,关于ClassPathBeanDefinitionScanner
在之前的文章中已经做过详细分析了,见《你知道Spring是怎么解析配置类的吗?》如果你没有看过之前的文章,问题也不大,你只需要知道是这个类完成了扫描并将扫描得到的BeanDefinition
注册到容器中即可。ClassPathMapperScanner
复写了这个类的doScan
方法已经registerFilters
,而在doScan
方法中这个类只是简单调用了父类的doScan
方法完成扫描在对扫描后得到的BeanDefinition
做一些后置处理,也就是说ClassPathMapperScanner
只是在父类的基础上定义了自己的扫描规则,通过对扫描后的BeanDefinition
会做进一步的处理。
基于此,我们先来看看,它的扫描规则是怎么样的?查看其registerFilters
及isCandidateComponent
方法,代码如下:
-
// 这个方法的代码还是很简单的
-
public void registerFilters() {
-
boolean acceptAllInterfaces =
true;
-
-
// 第一步,判断是否要扫描指定的注解
-
// 也就是判断在@MapperScan注解中是否指定了要扫描的注解
-
if (this.annotationClass != null) {
-
addIncludeFilter(
new AnnotationTypeFilter(this.annotationClass));
-
acceptAllInterfaces =
false;
-
}
-
-
// 第二步,判断是否要扫描指定的接口
-
// 同样也是根据@MapperScan注解中的属性做判断
-
if (this.markerInterface != null) {
-
addIncludeFilter(
new AssignableTypeFilter(this.markerInterface) {
-
@Override
-
protected boolean matchClassName(String className) {
-
return
false;
-
}
-
});
-
acceptAllInterfaces =
false;
-
}
-
-
// 如果既没有指定注解也没有指定标记接口
-
// 那么所有.class文件都会被扫描
-
if (acceptAllInterfaces) {
-
addIncludeFilter((metadataReader, metadataReaderFactory) ->
true);
-
}
-
-
// 排除package-info文件
-
addExcludeFilter((metadataReader, metadataReaderFactory) -> {
-
String className = metadataReader.getClassMetadata().getClassName();
-
return className.endsWith(
"package-info");
-
});
-
}
-
-
// 这个方法会对扫描出来的BeanDefinition进行检查,必须符合要求才会注册到容器中
-
// 从这里我们可以看出,BeanDefinition必须要是接口才行
-
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
-
return beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().isIndependent();
-
}
从上面两个方法中我们可以得出结论,默认情况下@MapperScan
注解会扫描指定包下的所有接口。
在前文我们也提到了,ClassPathBeanDefinitionScanner
不仅自定义了扫描的规则,而且复写了doScan
方法,在完成扫描后会针对扫描出来的BeanDefinition
做一下后置处理,那么它做了什么呢?我们查看它的processBeanDefinitions
方法,其源码如下:
-
// 下面这个方法看起来代码很长,实际做的事情确很简单
-
// 主要做了这么几件事
-
// 1.将扫描出来的BeanDefinition的beanClass属性设置为MapperFactoryBeanClass.class
-
// 2.在BeanDefinition的ConstructorArgumentValues添加一个参数
-
// 限定实例化时使用MapperFactoryBeanClass的带参构造函数
-
// 3.检查是否显示的配置了sqlSessionFactory或者sqlSessionTemplate
-
// 4.如果没有进行显示配置,那么将这个BeanDefinition的注入模型设置为自动注入
-
private void processBeanDefinitions(Set
beanDefinitions) {
-
GenericBeanDefinition definition;
-
for (BeanDefinitionHolder holder : beanDefinitions) {
-
definition = (GenericBeanDefinition) holder.getBeanDefinition();
-
String beanClassName = definition.getBeanClassName();
-
-
// 往构造函数的参数集合中添加了一个值,那么在实例化时就会使用带参的构造函数
-
// 等价于在XML中配置了
-
//
-
definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName);
-
-
// 将真实的BeanClass属性设置为mapperFactoryBeanClass
-
definition.setBeanClass(this.mapperFactoryBeanClass);
-
-
definition.getPropertyValues().add(
"addToConfig", this.addToConfig);
-
-
// 开始检查是否显示的指定了sqlSessionFactory或者sqlSessionTemplate
-
boolean explicitFactoryUsed =
false;
-
-
// 首先检查是否在@MapperScan注解上配置了sqlSessionFactoryRef属性
-
if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
-
-
// 如果配置了的话,那么在这个bd的属性集合中添加一个RuntimeBeanReference
-
// 等价于在xml中配置了
-
//
-
definition.getPropertyValues().add(
"sqlSessionFactory",
-
new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
-
explicitFactoryUsed =
true;
-
// 如果@MapperScan上没有进行配置
-
// 那么检查是否为这个bean配置了sqlSessionFactory属性
-
// 正常来说我们都不会进行配置,会进入自动装配的逻辑
-
}
else
if (this.sqlSessionFactory != null) {
-
definition.getPropertyValues().add(
"sqlSessionFactory", this.sqlSessionFactory);
-
explicitFactoryUsed =
true;
-
}
-
-
// 省略sqlSessionTemplate部分代码
-
// 逻辑跟sqlSessionFactory属性的处理逻辑一致
-
// 需要注意的是,如果同时显示指定了sqlSessionFactory跟sqlSessionTemplate
-
// 那么sqlSessionFactory的配置将失效
-
// .....
-
-
if (!explicitFactoryUsed) {
-
// 如果没有显示的配置,那么设置为自动注入
-
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
-
}
-
// 默认不是懒加载
-
definition.setLazyInit(lazyInitialization);
-
}
-
}
从上面的代码中我们不难看到一个最特殊的操作,扫描出来的BeanDefinition
并没有直接用去创建Bean,而是先将这些BeanDefinition
的beanClass
属性全部都设置成了MapperFactoryBean
,从名字上我们就能知道他是一个FactoryBean
,那么不难猜测肯定是通过这个FactoryBean
的getObject
方法来创建了一个代理对象,我们查看下这个类的源码:
MapperFactoryBean分析
继承关系
我们重点看下它的两个父类即可
-
DaoSupport
:这个类是所有的数据访问对象(DAO)的基类,它定义的所有DAO的初始化模板,它实现了InitializingBean
接口,核心方法就是afterPropertiesSet
,其源码如下:
- public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException {
- // 子类可以实现这个方法去检查相关的配置信息
- checkDaoConfig();
-
- // 子类可以实现这个方法去进行一些初始化操作
- try {
- initDao();
- }
- catch (Exception ex) {
- throw new BeanInitializationException("Initialization of DAO failed", ex);
- }
- }
-
SqlSessionDaoSupport
:这个类是专门为Mybatis
设计的,通过它能获取到一个SqlSession
,起源吗如下:
- public abstract class SqlSessionDaoSupport extends DaoSupport {
-
- private SqlSessionTemplate sqlSessionTemplate;
-
- // 这个是核心方法
- public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
- if (this.sqlSessionTemplate == null || sqlSessionFactory != this.sqlSessionTemplate.getSqlSessionFactory()) {
- this.sqlSessionTemplate = createSqlSessionTemplate(sqlSessionFactory);
- }
- }
-
- // 省略一些getter/setter方法
-
- // 在初始化时要检查sqlSessionTemplate,确保其不为空
- @Override
- protected void checkDaoConfig() {
- notNull(this.sqlSessionTemplate, "Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required");
- }
- }
我们在整合Spring跟Mybatis时,就是调用setSqlSessionFactory
完成了对这个类中SqlSessionTemplate
的初始化。前面我们也提到了MapperFactoryBean
默认使用的是自动注入,所以在创建每一个MapperFactoryBean
的属性注入阶段,Spring容器会自动查询是否有跟MapperFactoryBean
中setter方法的参数类型匹配的Bean,因为我们在前面进行了如下配置:
通过我们配置的这个sqlSessionFactoryBean
能得到一个sqlSessionFactory
,因此在对MapperFactoryBean
进行属性注入时会调用setSqlSessionFactory
方法。我们可以看到setSqlSessionFactory
方法内部就是通过sqlSessionFactory
创建了一个sqlSessionTemplate
。它最终会调用到sqlSessionTemplate
的一个构造函数,其代码如下:
- 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;
- this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
- new Class[] { SqlSession.class }, new SqlSessionInterceptor());
- }
SqlSessionTemplate
本身实现了org.apache.ibatis.session.SqlSession
接口,它的所有操作最终都是依赖其成员变量sqlSessionProxy
,sqlSessionProxy
是通过jdk动态代理生成的,对于动态代理生成的对象其实际执行时都会调用到InvocationHandler
的invoke方法,对应到我们上边的代码就是SqlSessionInterceptor
的invoke方法,对应代码如下:
- 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 {
- // 第二步,调用sqlSession对应的方法
- Object result = method.invoke(sqlSession, args);
-
- // 检查是否开启了事务,如果没有开启事务那么强制提交
- 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 {
- // 关闭sqlSession
- if (sqlSession != null) {
- closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
- }
- }
- }
- }
我们再来看看,他在获取SqlSession
是如何获取的,不出意外的话肯定也是调用了Mybaits
的sqlSessionFactory.openssion
方法创建的一个sqlSession
,代码如下:
- public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
- PersistenceExceptionTranslator exceptionTranslator) {
-
- notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
- notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
-
- SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
-
- SqlSession session = sessionHolder(executorType, holder);
- if (session != null) {
- return session;
- }
- // 看到了吧,在这里调用了SqlSessionFactory创建了一个sqlSession
- LOGGER.debug(() -> "Creating a new SqlSession");
- session = sessionFactory.openSession(executorType);
- // 如果开启了事务的话并且事务是由Spring管理的话,会将sqlSession绑定到当前线程上
- registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
-
- return session;
- }
方法分析
对于MapperFactoryBean
我们关注下面两个方法就行了
-
// 之前分析过了,这个方法会在MapperFactoryBean进行初始化的时候调用
-
protected void checkDaoConfig() {
-
super.checkDaoConfig();
-
Configuration configuration = getSqlSession().getConfiguration();
-
//addToConfig默认为true的,将mapper接口添加到mybatis的配置信息中
-
if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
-
try {
-
configuration.addMapper(this.mapperInterface);
-
} catch (Exception e)
-
throw
new IllegalArgumentException(e);
-
} finally {
-
ErrorContext.instance().reset();
-
}
-
}
-
}
-
-
// 简单吧,直接调用了mybatis中现成的方法获取一个代理对象然后放入到容器中
-
@Override
-
public T getObject() throws Exception {
-
return getSqlSession().getMapper(this.mapperInterface);
-
}
整合原理总结
首先我们知道,Mybatis
可以通过下面这种方式直接生成一个代理对象
-
String resource =
"mybatis-config.xml";
-
InputStream resourceAsStream = Resources.getResourceAsStream(resource);
-
SqlSessionFactoryBuilder builder =
new SqlSessionFactoryBuilder();
-
SqlSessionFactory sqlSessionFactory = builder.build(resourceAsStream);
-
SqlSession sqlSession = sqlSessionFactory.openSession();
-
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
基于这个代理对象,我们可以执行任意的Sql语句,那么如果Spring想要整合Mybatis,只需要将所有的代理对象管理起来即可,如何做到这一步呢?
这里就用到了Spring提供的一些列扩展点,首先,利用了BeanDefinitionRegistryPostProcessor
这个扩展点,利用它的postProcessBeanDefinitionRegistry
方法完成了对mapper接口的扫描,并将其注册到容器中,但是这里需要注意的是,它并不是简单的进行了扫描,在完成扫描的基础上它将所有的扫描出来的BeanDefinition的beanClass属性都替换成了MapperFactoryBean
,这样做的原因是因为我们无法根据一个接口来生成Bean,并且实际生成代理对象的逻辑是由Mybatis控制的而不是Spring控制,Spring只是调用了mybatis的API来完成代理对象的创建并放入到容器中,基于这种需求,使用FactoryBean
是再合适不过了。
还有通过上面的分析我们会发现,并不是一开始就创建了一个SqlSession
对象的,而是在实际方法执行时才会去获取SqlSession
的。
总结
本文我们主要学习了Mybatis的基本使用,并对Mybatis的事务管理以及Spring整合Mybatis的原理进行了分析,其中最重要的便是整合原理的分析
,之前有小伙伴问我能不能介绍一些实际使用了Spring提供的扩展点的例子,我相信这就是最好的一个例子。
本文为事务专栏的第二篇,之所以特地写一篇mybaits的文章是因为后续我们不仅要分析单独的Spring中的事务管理,还得分析Spring整合Mybatis的事务管理,虽然Spring整合Mybatis后完全由Spring来进行管理事务,但是我们要知道Mybatis自身是有自己的事务管理机制的,那么Spring是如何接手的呢?对于这个问题,在后续的文章中我会做详细分析
本文就到这里啦,如果本位对你有帮助的话,记得帮忙三连哈,感谢~!
我叫DMZ,一个在学习路上匍匐前行的小菜鸟!
往期精选
Spring官网阅读笔记
Spring杂谈
JVM系列文章
Spring源码专题