这可能是简书最白话的MyBatis源码解读
本文是MyBatis使用以及源码浅析markdown重构版本,对原文的排版进行了重构,同时对原文内容进行了完善,欢迎大家一起来阅读。码字不易,欢迎大家转载,烦请注明出处;谢谢配合
。
简介
MyBatis是一个普遍应用并且十分优秀的持久层框架;本文将从MyBatis的使用和源码阅读两个方面展开;本文的演示环境如下:
- JDK1.8
- MySQL 8.0.15
- MyBatis 3.4.6
MyBatis使用指南
step.1 构建SqlSessionFactory
每个基于MyBatis的应用都是以SqlSessionFactory
为核心的,SqlSessionFactory
可以通过SqlSessionFactoryBuilder
来构建;常用的有两种方式,一种是通过XML配置构建,而另一种则是通过Configuration
实例对象来构建。
- 通过XML构建
@Before
public void getSqlSessionFactory() throws IOException {
String configPath = "mybatis.xml";
InputStream resourceAsStream = Resources.getResourceAsStream(configPath);
sessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
}
- mybatis.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>
<properties resource="mysql.properties"></properties>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="xin/sunce/mybatis/dao/CustomerDao.xml"/>
<mapper resource="xin/sunce/mybatis/dao/StudentDao.xml"/>
<mapper resource="xin/sunce/mybatis/dao/ClassDao.xml"/>
</mappers>
</configuration>
-
通过Configuration实例对象来构建
step.2 获取SqlSession
获取到SqlSessionFactory
以后,我们便可以获取到SqlSession
。
SqlSession sqlSession = sessionFactory.openSession();
step.3 执行SQL语句
执行SQL语句的方法也并非只用一种,既可以通过Mapper
来执行,亦可以直接通过SqlSession
来执行
- 通过Mapper执行
@Test
public void testQuery() {
SqlSession sqlSession = sessionFactory.openSession();
try {
StudentDao mapper = sqlSession.getMapper(StudentDao.class);
Student student = mapper.getStudentById(1);
LOGGER.info("query table student result: " + student.toString());
} finally {
sqlSession.close();
}
}
-
通过SqlSession执行
step.4 映射的sql语句
当然,映射的SQL
语句也是不一定必须通过XML
文件来完成的,也是可以通过注解来实现的
- 通过XML文件映射
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="xin.sunce.mybatis.dao.StudentDao">
<cache/>
<select id="getStudentById" parameterType="int" resultType="xin.sunce.mybatis.entity.Student">
SELECT id,name,age FROM student WHERE id = #{id}
</select>
</mapper>
- 通过注解映射
public interface StudentDao {
@Select("SELECT id,name,age FROM student WHERE id = #{id}")
Student getStudentById(int id);
}
作用域以及生命周期
依赖注入框架会创建线程安全的SqlSession
和Mapper
并将它们注入到你的bean
中,因此你可以直接忽略它们的生命周期;如何通过依赖注入框架来使用MyBatis
,后面我们将会介绍;你也可以参看MyBatis-Spring
。
SqlSessionFactoryBuilder
这个类可以被实例化,创建以及销毁,一旦在其创建SqlSessionFactory
,其实就不需要它了;所以它最好的作用域就是方法作用域(局部方法),当然你可以通过它创建多个SqlSessionFactory
实例,当然最好不要让SqlSessionFactoryBuilder
一直存在,以保证XML资源被用于更重要的事情。
SqlSessionFactory
SqlSessionFactory
一旦被创建就应该在应用运行期间一直存在;它不应该被频繁的销毁创建,所以它的作用域应该是应用作用域;最好通过单例模式来使用。
SqlSession
每个线程都应该有自己的SqlSession
,SqlSession
不是线程安全的,所以它不能被线程共享,所以SqlSession
不能被置于静态类,或者一个类的实例变量;所以它的作用域最好是请求或者方法。例如在HTTP请求中,应该每次收到一个请求,便打开一个SqlSession
,响应之后,立即关闭SqlSession
。
Mapper
通过使用的Demo,我们也知道Mapper
实例是通过SqlSession
获得的,所以它的作用域也是方法作用域。
以上MyBatis使用指南,主要参考MyBatis官方文档
源码解读
step.1 SqlSessionFactoryBuilder
了解以上知识,让我们对MyBatis有了进一步的了解;便于我们捕捉源码的阅读方向;我们知道SqlSessionFactory
是通过SqlSessionFactoryBuilder
来构建的,接下来我们首先来看看它。
我们看到各种SqlSessionFactoryBuilder#build
方法,通过XMLConfigBuilder
的build
方法:
public SqlSessionFactory build(InputStream inputStream,
String environment, Properties properties) {
SqlSessionFactory var5;
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream,
environment, properties);
//XMLConfigBuilder的实例调用parse(),最终返回Configuration实例
var5 = this.build(parser.parse());
} catch (Exception var14) {
throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException var13) {
}
}
return var5;
}
最终方法:通过 Configuration
实例来构建
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
以上代码也印证了我们主要介绍的两种方法来构建SqlSessionFactory
,最终会返回一个DefaultSqlSessionFactory
实例。
step.2 SqlSessionFactory
SqlSessionFactory我们可以看到SqlSessionFactory
主要提供了开启SqlSession
以及获取Configuration
的方法;知道了接口作用,我们再来看看默认的实现类DefaultSqlSessionFactory
。
step.3 SqlSession
获取SqlSession最终都交给了两个私有的方法:openSessionFromDataSource
,openSessionFromConnection
;顾名思义分别是通过数据源来获取,通过连接来获取;两个方法大同小异,我们来详细看看其中一个。
以上便是创建SqlSeesion
的过程,利用public DefaultSqlSession(Configuration configuration, Executor executor)
此构造方法来获取SqlSession
实例,而Executor
也是在这个时期由configuration#newExecutor()
方法创建的,这里需要记一下,下文介绍Executor
会涉及。
我们发现SqlSession
提供了所有对数据库的操作,各式各样的增删改查,以及获取映射Mapper
的方法;接下来我们仔细研读一下默认的实现类DefaultSqlSession
的selectList
方法。
我们发现在DefaultSqlSession
实现类中SqlSeesion
封装的对数据库的操作最终都是有Executor
来执行的;相当于SqlSeesion
提供对数据库相应的操作,而具体的职责是有Executor
来完成的,而Executor
执行的MappedStatement
是在configuration
获取的。
step.4 Executor
Executor下图是Executor
的继承实现关系
找到Executor
以后,我们离真相又近了一步;Executor从设计上就考虑到了缓存,我们可以从createCacheKey
,clearLocalCache
等方法看到其设计的巧妙之处;这里我们先忽略缓存设计(后面会做详细说明),我们先来看看它的抽象类BaseExecutor
;
我们看到BaseExecutor
对Executor
接口进行了实现,最终调用抽象方法doXX
;需要交由子类去实现。以doUpdate
,doQuery
为例,我们来看看子类实现。
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
List var9;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(
this.wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = this.prepareStatement(handler, ms.getStatementLog());
var9 = handler.query(stmt, resultHandler);
} finally {
this.closeStatement(stmt);
}
return var9;
}
最终MappedStatement
都是由StatementHandler
去执行,至此一个映射语句的完整执行流程就此结束。
看到这里有的小伙伴很能会心急了,怎么执行流程都梳理完了,还没找到Mapper
的映射过程,别急,我们忽略了一个十分重要的类Configuration
,我们再次必须强调一下,Configuration
基本存在于整个流程,从通过SqlSessionFactoryBuilder
构建SqlSessionFactory
,到SqlSessionFactory
中Executor
的创建,开启SqlSession
,再到映射语句的执行。
step.5 Configuration
还记得我们开篇就提到过Configuration
实例有两种构建方式,一种是通过XMLConfigBuilder#parse
方法,另一种是利用public Configuration(Environment environment)
构造方法;所以Configuration
从实质上来说就是XML
的java对象表示。
这些元素在Configuration
的成员变量中都可以找到;参考上图。
step.5.1 注册,重头戏,Mapper映射过程
我们先来看看Configuration
的成员变量MapperRegistry
,mappedStatements
这里先剧透一下,MapperRegistry
是Mapper
注册器,而mappedStatements
则是最终映射成的一个个sql(注解和Mapper.xml中的sql语句)集。
在构建Configuration
时会调用Configuration#addMapper()
。完成Maper类对象(class对象)
到MapperRegistry的注册,knownMappers
是MapperRegistry的成员变量用于记录注册的Mapper。
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap();
添加Maper类对象(class对象)
,获取时(getMapper
)再通MapperProxyFactory
构建MapperProxy
返回
添加完成之后则会将Mapper对应的XML进行加载,并添加到Configuration的mappedStatements中。
addMapper
调用链路如下:
MapperAnnotationBuilder#parse()
-->MapperAnnotationBuilder#loadXmlResource
-->XMLMapperBuilder#parse()
-->XMLMapperBuilder#configurationElement()
-->XMLMapperBuilder#buildStatementFromContext()
-->XMLStatementBuilder.parseStatementNode()
-->MapperBuilderAssistant#addMappedStatement()
最终由MapperBuilderAssistant
辅助完成MappedStatement
添加
step.5.2 使用,动态代理
getMapper
时,MapperProxyFactory#newInstance
构建MapperProxy
MapperProxy#invoke()
方法调用
MapperMethod#execute()
方法调用
最终回到sqlSession
,交由它去执行。
总结
通过对源码的分析认识,我们用一张流程图来总结大致流程。
流程图