仓库
文章涉及的代码都将统一存放到此仓库,本章节使用SpringBoot,因此启动类为com.hzchendou.blog.demo.SpringBootApp
代码地址:Gitee
分支:lesson5
简介
Mybatis是一款优秀的ORM框架,使用Mybatis可以降低开发成本,将开发人员从繁琐的的JDBC操作中解放出来,把更多的注意力聚焦于SQL编写。在Java开发中,我们通常使用SpringBoot作为基础开发框架,SpringBoot + Mybatis的组合是目前主流开发模式。
本文将介绍SpringBoot整合Mybatis的实现原理。
SpringBoot整合Mybatis配置
困惑
对于SpringBoot整合Mybatis,我一直有一个疑问,这个疑问驱动着我去了解实现原理。SpringBoot中的Bean默认都是单例,那么创建的Mapper也是单例,Mapper底层是委托SqlSession来执行SQL语句操作的,SqlSession同时管理着事务,在多线程环境下难道使用的是同一个SqlSession来执行的吗,这个问题的答案很明显,不可能由同一个SqlSession来执行。那么SpringBoot到底是如何处理的呢?
整合配置
引入SpringBoot依赖
<!-- 设置父工程为SpringBoot -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.6</version>
</parent>
<!-- 引入SpringBoot start模块,将引入SpringBoot装配模块 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
我们不需要Web容器,因此不需要引入Spring MVC模块。
引入mybatis-springboot start模块
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
添加springboot配置文件application.yaml(数据源使用SpringBoot方式创建)
spring:
datasource:
username: ""
password: ""
url: jdbc:sqlite:src/main/resources/database/sqlite.db
driver-class-name: org.sqlite.JDBC
mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
default-enum-type-handler: org.apache.ibatis.type.EnumOrdinalTypeHandler
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
添加SpringBoot启动类
@Slf4j
扫描mapper接口类,需要注册到Spring IOC容器中
@MapperScan(basePackages = "com.hzchendou.blog.demo.mapper")
@SpringBootApplication
public class SpringBootApp {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(SpringBootApp.class);
/// 从Spring容器中获取到BlogMapper实例(BlogMapper是接口,获取到的是MapperProxy类型代理对象)
BlogMapper blogMapper = context.getBean(BlogMapper.class);
List<BlogDO> blogs = blogMapper.selectAll();
log.info("查询博文记录, {}", blogs);
}
}
启动程序,运行结果如下:
Parsed mapper file: 'file [/Users/chendou/repo/hzchendou/learning/mybatisdemo/target/classes/mapper/AuthorMapper.xml]'
Parsed mapper file: 'file [/Users/chendou/repo/hzchendou/learning/mybatisdemo/target/classes/mapper/BlogMapper.xml]'
2022-07-10 19:48:20.117 INFO 19316 --- [ main] com.hzchendou.blog.demo.SpringBootApp : Started SpringBootApp in 0.966 seconds (JVM running for 1.451)
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@12f3afb5] was not registered for synchronization because synchronization is not active
Cache Hit Ratio [com.hzchendou.blog.demo.mapper.BlogMapper]: 0.0
2022-07-10 19:48:20.129 INFO 19316 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
1.执行SQL查询操作
2022-07-10 19:48:20.301 INFO 19316 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
JDBC Connection [HikariProxyConnection@1158124724 wrapping org.sqlite.SQLiteConnection@273c947f] will not be managed by Spring
==> Preparing: SELECT id, `title`, `author_id`, `tags`, `status` FROM blog
==> Parameters:
<== Columns: id, title, author_id, tags, status
<== Row: 1, 时间海绵博文, 1, 博文、时间海绵, 1
<== Row: 2, 时间海绵博文, 1, 博文、时间海绵, 1
<== Row: 3, 时间海绵博文, 1, 博文、时间海绵, 1
<== Row: 4, 时间海绵博文, 1, 博文、时间海绵, 1
<== Total: 4
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@12f3afb5]
/// 2.打印查询结果数据
2022-07-10 19:48:20.331 INFO 19316 --- [ main] com.hzchendou.blog.demo.SpringBootApp : 查询博文记录, [BlogDO(id=1, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID), BlogDO(id=2, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID), BlogDO(id=3, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID), BlogDO(id=4, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID)]
2022-07-10 19:48:20.333 INFO 19316 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
2022-07-10 19:48:20.334 INFO 19316 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.
程序运行正常,得到了预期结果
SpringBoot整合Mybatis原理
加载Mybatis配置
SpringBoot基于约定大于配置的原则进行组件整合,从上面整合流程可以看出来,基于SpringBoot只需要少量开发就能完成对Mybatis的配置
@EnableAutoConfiguration是SpringBoot中最重要的配置注解,@SpringBootApplication是一个组合注解,@EnableAutoConfiguration也包含在其中,开启该注解(默认开启,可以通过配置spring.boot.enableautoconfiguration属性进行开关)后会查找所有jar包中的META-INF/spring.factories文件,并查找所有配置在org.springframework.boot.autoconfigure.EnableAutoConfiguration属性下的类(具体逻辑在AutoConfigurationImportSelector类中的getCandidateConfigurations方法),例如上面的SpringBoot整合Mybatis,引入的包中包含spring.factories配置文件。
### 在org.mybatis.spring.boot.mybatis-spring-boot-autoconfigure jar中存在META-INF/spring.factories文件,内容如下
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
SpringBoot会自动加载MybatisLanguageDriverAutoConfiguration和MybatisAutoConfiguration,其中MybatisAutoConfiguration是完成Mybatis配置的关键类,主要完成了如下功能:
- 加载配置信息
- mybatis 配置信息
- mapper 文件
- typeAlias
- .....Mybatis其它配置信息
- 根据上述加载的配置信息创建Configuration和SqlSessionFactory
- 使用SqlSessionFactory创建SqlSession
- 这里创建的SqlSession类型是SqlSessionTemplate,这个类实现了SqlSession接口
上述是SpringBoot自动装配完成的事情,主要是为了创建SqlSessionTemplate这个对象,这个类是SqlSession子类,至于它的重要性将在后面介绍。
到目前为止,还没有涉及到Mapper接口代理实例创建工作(实际上实例化Mapper代理对象功能也可以在MybatisAutoConfiguration完成,前提是在Mapper接口类上使用@Mapper注解,并且没有使用@MapperScan注册,原理是相同的,但是通过源码可以发现使用@MapperScan注解能够更加快捷地实现创建Mapper接口代理对象)。
创建Mapper接口代理实例
还记得我们在SpringBootApp这个启动类上使用的注解吗
@MapperScan(basePackages = "com.hzchendou.blog.demo.mapper")
这个注解帮助我们完成了Mapper接口代理创建工作,下面我们来看一下这个类完成的工作
- 1、在@MapperScan注解中的@Import引入了MapperScannerRegistrar类
- 2、MapperScannerRegistrar实现了接口ImportBeanDefinitionRegistrar,Spring会调用该接口中的registerBeanDefinitions方法注册BeanDefinition
- 3、在MapperScannerRegistrar中的registerBeanDefinitions方法会注册一个MapperScannerConfigurer类型BeanDefinition(通过名字也可以知道这个类会扫描Mapper接口,通过@MapperScan中的注解信息进行扫描)
- 4、MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor接口,Spring会调用这个接口中的postProcessBeanDefinitionRegistry(通过名字就可以知道这个类会在BeanDefinition注册后进行调用)
- 5、在MapperScannerConfigurer的postProcessBeanDefinitionRegistry方法中会创建一个ClassPathMapperScanner对象,然后调用该对象的scan方法进行包路径扫描,加载Mapper接口类
- 6、在ClassPathMapperScanner的scan方法中,将扫描到的符合条件的接口类型作为构造参数,定义成MapperFactoryBean类型的BeanDefinition(每一个接口定义一个MapperFactoryBean,MapperFactoryBean是个工厂类,实现了FactoryBean接口,用于创建Mapper接口代理对象)
- 7、在MapperFactoryBean中的getObject方法(FactoryBean接口方法,Spring会调用FactoryBean接口中的getObject方法创建对象,然后将对象交给容器进行托管)中调用了sqlSession.getMapper(mapperInterface)(这个方法是不是很熟悉,就是之前文章中一直使用的获取Mapper代理对象的方法),用于获取Mapper接口代理对象
- 8、这里有个关键点,在7中调用的sqlSession是SqlSessionTemplate类型,这就是在加载Mybatis配置中创建的SqlSession实例
上述流程比较繁琐,因为要处理很多情况,如果不理解也没关系,主要记住一点,使用@MapperScan会将Mapper接口包装成MapperFactoryBean这个FactoryBean,在MapperFactoryBean中使用sqlSession.getMapper(mapperInterface)创建Mapper接口代理
通过@MapperScan注解以及MybatisAutoConfiguration配置类将Mybatis组件加载到了Spring容器中主要是两类关键组件
- Mapper接口代理对象
- SqlSessionTemplate对象
在业务代码中,我们使用Mapper接口操作SQL语句进行数据库操作时,底层是委托SqlSessionTemplate来进行数据库操作。
疑问解答
我们首先来分析一下Mapper是如何创建的
- 在SqlSessionTemplate中的getMapper方法调用Configuration的getMapper(type,sqlSession)(注意这里有两个参数,要创建的Mapper接口类型和SqlSession,SqlSession是SqlSessionTempalte本身,也就是SqlSessionTemplate将自身作为参数创建Mapper代理)
- 在Configuration的getMapper方法中调用了MapperRegistry的getMapper(type,sqlSession)方法(也是两个参数,与上面参数一致,)
- 在MapperRegistry的getMapper方法中使用了MapperProxyFactory的newInstance(sqlSession)方法(sqlSession是1中传递的SqlSessionTemplate)
- 在MapperProxyFactory的newInstance(sqlSession)方法中创建了new MapperProxy<>(sqlSession, mapperInterface, methodCache),然后使用Proxy.newProxyInstance创建代理对象
通过上面分析可以知道,Mapper接口代理实际是通过MapperProxy对象来执行的(MapperProxy实现了InvocationHandler),现在我们来了解一下Mapper是如何执行的(也就是MapperProxy的invoke方法的处理逻辑)
- 如果是Object中定义的方法,那么直接执行(比如说hashCode方法,toString方法等)
- 如果不是
- 创建一个MapperMethod方法new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())
- 调用MapperMethod中的execute(SqlSession sqlSession, Object[] args)方法执行SQL操作(这里的SqlSession就是上面创建时传递的SqlSessionTemplate)
- 然后按照SQL操作指令进行分类处理(SQL指令分为INSERT、UPDATE、DELETE、SELECT、FLUSH),最后委托给sqlSession执行具体SQL操作(这里的SqlSession就是上面创建时传递的SqlSessionTemplate)
下面我们回归到SqlSessionTemplate查看具体执行逻辑以selectOne方法为例,其它方法类似
public <T> T selectOne(String statement) {
/// 委托给sqlSessionProxy进行执行
return this.sqlSessionProxy.selectOne(statement);
}
查看sqlSessionProxy创建过程
///sqlSessionProxy是一个SqlSession类型,并且是一个final成员变量
private final SqlSession sqlSessionProxy;
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
///省略无关代码
/// 使用JDK代理模式创建,需要注意的是SqlSessionInterceptor是SqlSessionTemplate内部类
this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class }, new SqlSessionInterceptor());
}
从上面可以看出 sqlSessionProxy 是SqlSession接口的代理类,委托给SqlSessionInterceptor执行SqlSession方法,同时SqlSessionInterceptor是SqlSessionTemplate内部类,可以共享SqlSessionInterceptor内部成员变量。
查看SqlSessionInterceptor的invoke(代理执行方法入口)
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
获取SqlSession对象(这里的getSqlSession是SqlSessionUtils的静态方法)
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) {
省略无关代码
异常情况处理逻辑
} finally {
if (sqlSession != null) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
查看SqlSessionUtils.getSqlSession方法
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
///1、从事务管理器中获取SqlSession信息, 类似缓存
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
SqlSession session = sessionHolder(executorType, holder);
/// 2、如果获取到了SqlSession直接返回
if (session != null) {
return session;
}
LOGGER.debug(() -> "Creating a new SqlSession");
/// 3、如果没有那么创建一个SqlSession,这里的sessionFactory就是加载配置时创建的DefaultSessionFactory
session = sessionFactory.openSession(executorType);
/// 4、将session注册到事务管理器中
registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
return session;
}
我们需要查看两个方法TransactionSynchronizationManager.getResource和registerSessionHolder方法
首先查看TransactionSynchronizationManager.getResource方法
public static Object getResource(Object key) {
/// 如果key时一个包装类,那么获取实际的对象
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
/// 这个是关键方法,用于获取资源,这里我们要获取的是SqlSession相关资源
return doGetResource(actualKey);
}
private static Object doGetResource(Object actualKey) {
需要特别注意这个resources是一个ThreadLocal类型变量,因此与线程绑定,能够实现线程安全
Map<Object, Object> map = resources.get();
if (map == null) {
return null;
}
Object value = map.get(actualKey);
省略无关代码
return value;
}
从上面可以看出getResource是从线程变量中获取SqlSession信息,也就是说SqlSession与运行线程进行了绑定,接下来我们看一下registerSessionHolder方法
private static void registerSessionHolder(SqlSessionFactory sessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator, SqlSession session) {
SqlSessionHolder holder;
///如果启用了事务模式,那么会将Session与线程进行绑定,方便多线程环境下获取
if (TransactionSynchronizationManager.isSynchronizationActive()) {
Environment environment = sessionFactory.getConfiguration().getEnvironment();
启动事务模式下需要将事务委托给Spring容器进行管理,方便使用Spring事务功能,例如编程式事务和声明式事务
if (environment.getTransactionFactory() instanceof SpringManagedTransactionFactory) {
LOGGER.debug(() -> "Registering transaction synchronization for SqlSession [" + session + "]");
holder = new SqlSessionHolder(session, executorType, exceptionTranslator);
将SqlSession资源绑定到线程变量中
TransactionSynchronizationManager.bindResource(sessionFactory, holder);
TransactionSynchronizationManager
.registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory));
holder.setSynchronizedWithTransaction(true);
holder.requested();
} else {
if (TransactionSynchronizationManager.getResource(environment.getDataSource()) == null) {
LOGGER.debug(() -> "SqlSession [" + session
+ "] was not registered for synchronization because DataSource is not transactional");
} else {
throw new TransientDataAccessResourceException(
"SqlSessionFactory must be using a SpringManagedTransactionFactory in order to use Spring transaction synchronization");
}
}
} else {
LOGGER.debug(() -> "SqlSession [" + session
+ "] was not registered for synchronization because synchronization is not active");
}
}
从上面代码可以看出,Spring会将SqlSession资源与线程变量进行绑定,实现在多线程环境下使用SqlSession,但是这里有一个需要注意的地方,那就是执行上述逻辑的前提是要开启事务,那么如何开启事务呢,最简单的方法是使用Spring声明式事务@Transactional
创建一个BlogService类,用于查询博客文章记录
@Service
public class BlogService {
@Autowired
private BlogMapper blogMapper;
这个方法开启了事务
@Transactional
public List<BlogDO> selectAll() {
return blogMapper.selectAll();
}
/// 这个方法没有开启事务
public List<BlogDO> selectPage() {
PageVO<BlogDO> pageVO = selectPage(1,2);
return pageVO.getRecords();
}
private PageVO<BlogDO> selectPage(int page, int size) {
PageVO<BlogDO> param = new PageVO();
param.setPage(page);
param.setSize(size);
List<BlogDO> blogs = blogMapper.selectPage(param);
param.setRecords(blogs);
param.setPageSize(blogs == null ? 0 : blogs.size());
return param;
}
}
修改SpringBootApp启动类:
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(SpringBootApp.class);
BlogService blogService = context.getBean(BlogService.class);
List<BlogDO> blogs = blogService.selectAll();
log.info("查询博文记录, {}", blogs);
List<BlogDO> pageBlogs = blogService.selectPage();
log.info("查询博文分页记录, {}", pageBlogs);
}
运行结果如下:
/// 1、创建了一个新的SqlSession
Creating a new SqlSession
/// 2、将SqlSession绑定到当前线程变量中
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@169da7f2]
Cache Hit Ratio [com.hzchendou.blog.demo.mapper.BlogMapper]: 0.0
JDBC Connection [HikariProxyConnection@2042979183 wrapping org.sqlite.SQLiteConnection@1929425f] will be managed by Spring
==> Preparing: SELECT id, `title`, `author_id`, `tags`, `status` FROM blog
==> Parameters:
<== Columns: id, title, author_id, tags, status
<== Row: 1, 时间海绵博文, 1, 博文、时间海绵, 1
<== Row: 2, 时间海绵博文, 1, 博文、时间海绵, 1
<== Row: 3, 时间海绵博文, 1, 博文、时间海绵, 1
<== Row: 4, 时间海绵博文, 1, 博文、时间海绵, 1
<== Total: 4
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@169da7f2]
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@169da7f2]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@169da7f2]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@169da7f2]
2022-07-11 11:25:05.698 INFO 8884 --- [ main] com.hzchendou.blog.demo.SpringBootApp : 查询博文记录, [BlogDO(id=1, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID), BlogDO(id=2, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID), BlogDO(id=3, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID), BlogDO(id=4, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID)]
/// 3、创建了一个新的SqlSession
Creating a new SqlSession
/// 4、无法将SqlSession绑定到线程变量中,因为没有开启事务
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6f95cd51] was not registered for synchronization because synchronization is not active
Cache Hit Ratio [com.hzchendou.blog.demo.mapper.BlogMapper]: 0.0
JDBC Connection [HikariProxyConnection@209360767 wrapping org.sqlite.SQLiteConnection@1929425f] will not be managed by Spring
==> Preparing: SELECT id, `title`, `author_id`, `tags`, `status` FROM blog
==> Parameters:
<== Columns: id, title, author_id, tags, status
<== Row: 1, 时间海绵博文, 1, 博文、时间海绵, 1
<== Row: 2, 时间海绵博文, 1, 博文、时间海绵, 1
<== Row: 3, 时间海绵博文, 1, 博文、时间海绵, 1
<== Row: 4, 时间海绵博文, 1, 博文、时间海绵, 1
<== Total: 4
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6f95cd51]
2022-07-11 11:25:05.702 INFO 8884 --- [ main] com.hzchendou.blog.demo.SpringBootApp : 查询博文分页记录, [BlogDO(id=1, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID), BlogDO(id=2, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID), BlogDO(id=3, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID), BlogDO(id=4, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID)]
从上面运行日志可以看出,blogService.selectAll()方法使用了声明式事务,因此会将SqlSession绑定到线程变量中,而blogService.selectPage()没有使用声明式事务,因此不会讲SqlSession绑定到线程变量中。
我们可以来解答这个疑惑了,
- 如果启用的事务,会将SqlSession绑定到线程变量中实现线程安全,同时实现了缓存功能
- 如果没有声明事务,那么线程每次运行都将获取新的SqlSession,也是线程安全的
总结
SpringBoot整合Mybatis时,只需要少量配置就能完成Mybatis初始化工作,同时将Mybatis事务交给Spring来管理,从而能够使用Spring方式配置事务
- 使用MybatisAutoConfiguration配置类来加载Mybatis配置信息从而完成以下操作
- 创建Mybatis的Configuration对象
- 创建SqlSessionFactory对象(使用的是DefaultSqlSessionFactory)
- 创建SqlSessionTemplate对象(这是一个创建SqlSessionTemplate的模版类,底层委托给SqlSessionUtils工具类管理SqlSession,从而接入到Spring事务)
- 使用@MapperScan注解完成Mapper接口代理对象创建
- 扫描指定包下的Mapper接口
- 最后使用MapperProxy实现Mapper接口代理
- MapperProxy最终委托给SqlSessionTemplate执行SQL操作(本质就是交给SqlSessionUtils来管理SqlSession)
- 是否启用本地线程变量来保存SqlSession与线程运行方法是否启动了事务有关