文章涉及的代码都将统一存放到此仓库,本章节使用SpringBoot,因此启动类为com.hzchendou.blog.demo.SpringBootApp
代码地址:Gitee
分支:lesson5
Mybatis是一款优秀的ORM框架,使用Mybatis可以降低开发成本,将开发人员从繁琐的的JDBC操作中解放出来,把更多的注意力聚焦于SQL编写。在Java开发中,我们通常使用SpringBoot作为基础开发框架,SpringBoot + Mybatis的组合是目前主流开发模式。
本文将介绍SpringBoot整合Mybatis的实现原理。
对于SpringBoot整合Mybatis,我一直有一个疑问,这个疑问驱动着我去了解实现原理。SpringBoot中的Bean默认都是单例,那么创建的Mapper也是单例,Mapper底层是委托SqlSession来执行SQL语句操作的,SqlSession同时管理着事务,在多线程环境下难道使用的是同一个SqlSession来执行的吗,这个问题的答案很明显,不可能由同一个SqlSession来执行。那么SpringBoot到底是如何处理的呢?
引入SpringBoot依赖
org.springframework.boot
spring-boot-starter-parent
2.5.6
org.springframework.boot
spring-boot-starter
我们不需要Web容器,因此不需要引入Spring MVC模块。
引入mybatis-springboot start模块
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.1.3
添加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 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基于约定大于配置的原则进行组件整合,从上面整合流程可以看出来,基于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配置的关键类,主要完成了如下功能:
上述是SpringBoot自动装配完成的事情,主要是为了创建SqlSessionTemplate这个对象,这个类是SqlSession子类,至于它的重要性将在后面介绍。
到目前为止,还没有涉及到Mapper接口代理实例创建工作(实际上实例化Mapper代理对象功能也可以在MybatisAutoConfiguration完成,前提是在Mapper接口类上使用@Mapper注解,并且没有使用@MapperScan注册,原理是相同的,但是通过源码可以发现使用@MapperScan注解能够更加快捷地实现创建Mapper接口代理对象)。
还记得我们在SpringBootApp这个启动类上使用的注解吗
@MapperScan(basePackages = "com.hzchendou.blog.demo.mapper")
这个注解帮助我们完成了Mapper接口代理创建工作,下面我们来看一下这个类完成的工作
上述流程比较繁琐,因为要处理很多情况,如果不理解也没关系,主要记住一点,使用@MapperScan会将Mapper接口包装成MapperFactoryBean这个FactoryBean,在MapperFactoryBean中使用sqlSession.getMapper(mapperInterface)创建Mapper接口代理
通过@MapperScan注解以及MybatisAutoConfiguration配置类将Mybatis组件加载到了Spring容器中主要是两类关键组件
在业务代码中,我们使用Mapper接口操作SQL语句进行数据库操作时,底层是委托SqlSessionTemplate来进行数据库操作。
我们首先来分析一下Mapper是如何创建的
通过上面分析可以知道,Mapper接口代理实际是通过MapperProxy对象来执行的(MapperProxy实现了InvocationHandler),现在我们来了解一下Mapper是如何执行的(也就是MapperProxy的invoke方法的处理逻辑)
下面我们回归到SqlSessionTemplate查看具体执行逻辑以selectOne方法为例,其它方法类似
public 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
从上面可以看出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 selectAll() {
return blogMapper.selectAll();
}
/// 这个方法没有开启事务
public List selectPage() {
PageVO pageVO = selectPage(1,2);
return pageVO.getRecords();
}
private PageVO selectPage(int page, int size) {
PageVO param = new PageVO();
param.setPage(page);
param.setSize(size);
List 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 blogs = blogService.selectAll();
log.info("查询博文记录, {}", blogs);
List 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绑定到线程变量中。
我们可以来解答这个疑惑了,
SpringBoot整合Mybatis时,只需要少量配置就能完成Mybatis初始化工作,同时将Mybatis事务交给Spring来管理,从而能够使用Spring方式配置事务
技术更新换代速度很快,我们无法在有限时间掌握全部知识,但我们可以在他人的基础上进行快速学习,学习也是枯燥无味的,加入我们学习牛人经验:
QQ:901856121
点击:加群讨论