MyBatis 之源码浅读
环境简介与入口
记录一下尝试阅读Mybatis源码的过程,这篇笔记是我一边读,一遍记录下来的,虽然内容也不多,对Mybatis整体的架构体系也没有摸的很清楚,起码也能把这个过程整理下来,这也是我比较喜欢的一种学习方式吧
单独Mybatis框架搭建的环境,没有和其他框架整合
入口点的源码如下:
@Test
public void test01() {
try {
this.resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
// 2. 创建SqlSessionFactory工厂
this.factory = new SqlSessionFactoryBuilder().build(resourceAsStream);
// 3. 创建sqlSession
// todo 怎么理解这个sqlSession? 首先它是线程级别的,线程不安全, 其次它里面封装了大量的CRUD的方法
this.sqlSession = factory.openSession();
IUserDao mapper = this.sqlSession.getMapper(IUserDao.class);
List<User> all = mapper.findAll();
for (User user : all) {
System.out.println(user);
}
// 事务性的操作,自动提交
this.sqlSession.commit();
// 6, 释放资源
this.sqlSession.close();
this.resourceAsStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
构建SqlSessionFactory
首先跟进这个,看看如何构建SqlSessionFactory
对象
this.factory = new SqlSessionFactoryBuilder().build(resourceAsStream);
这个SqlSessionFactoryBuilder
类的存在很简单,取名也叫他构建器,Mybatis的官网是这样解释它的,这个类可以被实例化(因为它有且仅有一个默认的无参构造),使用它的目的就是用来创建多个SqlSessionFactory实例,最好不要让他一直存在,进而保证所有用来解析xml的资源可以被释放
所以跳过对这个构建器的关注,转而看的build()
方法
首先会来到这个方法,直接可以看到存在一个XML配置解析器,这其实并不意外,毕竟现在是MyBatis是孤军一人,就算我们使用的是注解开发模式,不也得存在主配置文件不是?
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
接着看看parser.parse()
,它里面会解析主配置文件中的信息,解析哪些信息呢? 源码如下: 很清楚的看到,涵盖mybatis配置文件中的所有的标签
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
...
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
第一个问题: 解析的配置信息存在哪里呢? 其实存放在一个叫Configuration
的封装类中, 这个上面的解析器是XMLConfigBuilder
这个封装类的保存者是它的父类BaseBuilder
继续跟进build()
方法,源码如下:
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
小结: 至此也就完成了DefaultSqlSessionFactory()的构建,回想下,这个构建器真的太无私了,牺牲了自己,不仅仅创建了默认的SqlSessionFactory,还将配置文件的信息给了他
打开SqlSession
创建完成SqlSession工厂的创建, 我们继续跟进this.sqlSession = factory.openSession();
, 从工厂中获取一个SqlSession
跟进几个空壳方法,我们很快就能来到下面的方法:
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
什么是SqlSession呢? 注释是这么说的,他是MyBatis在java中主要干活的接口,通过这个接口,你可以执行命令(它里面定义了大量的 诸如selectList
类似的方法),获取mapper,合并事务
The primary Java interface for working with MyBatis.
Through this interface you can execute commands, get mappers and manage transactions.
通过上面的我贴出来的函数,大家可以看到,通过事务工厂实例化了一个事物Transaction
,那么问题来了,这个Transaction
又是什么呢? 注释是这么解释的: Transaction 包装了一个数据库的连接,处理这个连接的生命周期,包含: 它的创建,准备 提交/回滚 和 关闭
紧接着创建执行器:configuration.newExecutor(tx, execType)
,默认创建的是CachingExecutor
它维护了一个SimpleExecutor
, 这个执行器的特点是 在每次执行完成后都会关闭 statement 对象
关于mybatis的执行器,其实挺多事的,打算专门写一篇笔记
继续看 new DefaultSqlSession()
我们得知,这个sqlSession的默认实现类是DefaultSqlSession
第三个参数autocommit为false, 这也是为什么我们如果不手动提交事务时,虽然测试会通过,但是事务不会被持久化的原因
小结: 当前函数是 openSession()
, 如果说它是打开一个session,那跟没说是一样的,通过源码我们也看到了,这一步其实是Mybatis将 数据库连接,事务,执行器进行了一下封装然后返回给程序员
获取Mapper -- maperProxy
我们交给mybatis的mapper是一个接口,看看Mybatis是如何实例化我们的结果,返回给我们一个代理对象的
跟进源码: IUserDao mapper = this.sqlSession.getMapper(IUserDao.class);
,经过一个空方法,我们进入Configuration
类中的函数如下:
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
从mapperRegistry
中获取mapper,他是Configuration
属性如下: 可以看到这个mapperRegistry
甚至包含了Configuration
,甚至还多了个 knownMappers
那么问题来了,这个knownMappers
是干啥呢? 我直接说,这个map就是在上面解析xml配置文件时,存放程序员在<mappers>
标签下配置的<maper>
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
// 详情:
public class MapperRegistry {
private final Configuration config;
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
继续跟进这个getMapper()
如下: 并且我们在配置文件中是这样配置的
<mappers>
<mapper class="com.changwu.dao.IUserDao"/>
</mappers>
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
所以不难想象,我们肯定能获取到结果,通过上面的代码我们能看到,获取到的对象被强转成了MapperProxyFactory
类型,它的主要成员如下: 说白了,这个 map代理工厂是个辅助对象,它是对程序员提供的mapper结果的描述,同时内置使用jdk动态代理的逻辑为mapper创建代理对象
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();
...
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
说到了为mapper创建动态代理,就不得不去看看是哪个类充当了动态代理的需要的InvoketionHandler -- 这个类是mybatis中的MapperProxy, 没错它实现了InvocationHandler接口,重写了invoke()
逻辑,源码如下:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
小结: 至此当前模块的获取mapper对象已经完结了,我们明明白白的看到了MyBatis为我们的mapper使用jdk的动态代理创建出来代理对象, 这也是为什么我们免去了自己写实现类的粗活
执行Map -- maperProxy
上一个模块我们知道了Mybatis为我们创建出来了mapper接口的代理对象,那当我们获取到这个代理对象之后执行它的mapper.findAll();
实际上触发的是代理对象的invoke()
方法
所以说,接着看上面的MapperProxy
的invoke()
方法:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
上面的方法我们关注两个地方,第一个地方就是final MapperMethod mapperMethod = cachedMapperMethod(method);
,见名知意: 缓存MapperMethod
第一个问题: 这个MapperMethod
是什么? 它其实是Mybatis为sql命令+方法全限定名设计的封装类
*/
public class MapperMethod {
private final SqlCommand command;
private final MethodSignature method;
说白了,就是想为MapperProxy保存一份map格式的信息,key=方法全名 value=MapperMethod(command,method),存放在MapperProxy的属性methodCache中
comand -> "name=com.changwu.dao.IUserDao,fingAll"
method -> "result= public abstract java.util.List.com.changwu.dao.IUserDao.findAll()"
为什么要给这个MapperProxy保存这里呢? 很简单,它是mapper的代理对象啊,程序员使用的就是他,在这里留一份method的副本,再用的话多方便?
完成缓存后,继续看它如何执行方法:,跟进 mapperMethod.execute(sqlSession, args)
因为我使用的是@Select("select * from user")
,所以一定进入下面的 result = executeForMany(sqlSession, args);
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
..
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
...
}
所以我们继续关注这个 executeForMany(sqlSession, args);
方法,看他的第一个参数是sqlSession
,也就是我们的DefaultSqlSession
,他里面存在两大重要对象: 1是configuration 配置对象, 2是Executor 执行器对象
继续跟进:
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
List<E> result;
Object param = method.convertArgsToSqlCommandParam(args);
if (method.hasRowBounds()) {
RowBounds rowBounds = method.extractRowBounds(args);
result = sqlSession.selectList(command.getName(), param, rowBounds);
} else {
result = sqlSession.selectList(command.getName(), param);
}
我们在继续跟进这个selectList
方法之前,先看看这个command.getName()是啥? 其实我们上面的代码追踪中有记录: 就是name=com.changwu.dao.IUserDao,fingAll
继续跟进去到下面的方法:
这个方法就比较有趣了,首先来说,下面的入参都是什么
statement = "com.changwu.dao.IUserDao.findAll"
parameter=null
第二个问题:MappedStatement
是什么? 它是一个对象,维护了很多很多的配置信息,但是我们关心它里面的两条信息,这其实可以理解成一种方法与sql之间的映射,如下图
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
前面阅读时,我们知道Mybatis为我们创建的默认的执行器 Executor是CachingExecutor
,如下图
继续跟进,主要做了下面三件事, 获取到绑定的sql,然后调用SimpleExecutor
缓存key,然后继续执行query()
方法
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
接着调用SimpleExecutor
的query()
方法,然后我们关注SimpleExecutor
的doQuery()
方法,源码如下
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
我们注意到.在SimpleExcutor中创建的了一个 XXXStatementHandler这样一个处理器, 所以我们的只管感觉就是,sql真正执行者其实并不是Executor,而是Executor会为每一条sql的执行重新new 一个 StatementHandler ,由这个handler去具体的执行sql
关于这个StatementHandler到底是是何方神圣? 暂时了解它是Mybatis定义的一个规范接口,定义了如下功能即可
public interface StatementHandler {
// sql预编译, 构建statement对象
Statement prepare(Connection connection, Integer transactionTimeout)
throws SQLException;
// 对prepare方法构建的预编译的sql进行参数的设置
void parameterize(Statement statement)
throws SQLException;
// 批量处理器
void batch(Statement statement)
throws SQLException;
// create update delete
int update(Statement statement)
throws SQLException;
// select
<E> List<E> query(Statement statement, ResultHandler resultHandler)
throws SQLException;
<E> Cursor<E> queryCursor(Statement statement)
throws SQLException;
// 获取sql的封装对象
BoundSql getBoundSql();
// 获取参数处理对象
ParameterHandler getParameterHandler();
}
了解了这个StatementHandler是什么,下一个问题就是当前我们创建的默认的statement是谁呢? routingStatementHandler
如下图
创建preparedStatement
马上马就发生了一件悄无声息的大事!!!根据现有的sql等信息,构建 PreparedStatement,我们关注这个 prepareStatement(handler, ms.getStatementLog());
方法,通过调试我们得知,prepareStatement()
是RoutingStatementHandler
的抽象方法,被PreparedStatementHandler
重写了,所以我们去看它如何重写的,如下:
@Override
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
ErrorContext.instance().sql(boundSql.getSql());
Statement statement = null;
try {
statement = instantiateStatement(connection);
我们关注这个instantiateStatement()
方法, 并且进入它的connection.prepareStatement(sql);
方法,如下图:
纯洁的微笑... 见到了原生JDK, jdbc的亲人...
创建完事这个 preparedStatement,下一步总该执行了吧...绕这么多圈...
我们回到上面代码中的return handler.query(stmt, resultHandler);
准备执行,此时第一个参数就是我们的刚创建出来的PreparedStatement, 回想一下,上面创建的这个默认的statement中的代表是PreparedStatementHandler
,所以,我们进入到这个StatementHandler的实现类RountingStatementHandler
中,看他的query()
方法
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
return delegate.query(statement, resultHandler);
}
调用RountingStatementHandler
中维护的代表的StatementHandler也就是PreparedStatementHandler
的query()
方法,顺势跟进去,最终会通过反射执行jdbc操作,如图, 我圈出来的对象就是我们上面创建出来的preparedStatement
提交事务
跟进conmit()
方法,分成两步
- 清空缓存
- 提交事务
清空缓存是在CachingExecutor
中调用了SimpleExecutor
简单执行器的方法commit(required)
@Override
public void commit(boolean required) throws SQLException {
delegate.commit(required);
tcm.commit();
}
接在SimpleExecutor
的父类BaseExecutor
中完成
@Override
public void commit(boolean required) throws SQLException {
if (closed) {
throw new ExecutorException("Cannot commit, transaction is already closed");
}
clearLocalCache();
flushStatements();
if (required) {
transaction.commit();
}
}
提交事务的操作在 tcm.commit();
中完成
本文到这里也就行将结束了,如果您觉得挺好玩的,欢迎点赞支持,有错误的话,也欢迎批评指出
MyBatis 之源码浅读的更多相关文章
- Spark 源码浅读-SparkSubmit
Spark 源码浅读-任务提交SparkSubmit main方法 main方法主要用于初始化日志,然后接着调用doSubmit方法. override def main(args: Array[St ...
- Handlebars模板引擎中的each嵌套及源码浅读
若显示效果不佳,可移步到愚安的小窝 Handlebars模板引擎作为时下最流行的模板引擎之一,已然在开发中为我们提供了无数便利.作为一款无语义的模板引擎,Handlebars只提供极少的helper函 ...
- Java集合&Spring源码浅读
记录自己现在知道的,以后了解了更多的话,再继续补上来 Java集合类 Collection 接口 说明:是List,set 的父类.定义了集合初始模样.集合只存储对象. Jdk8文档,内部方法定义有: ...
- Layui 源码浅读(模块加载原理)
经典开场 // Layui ;! function (win) { var Lay = function () { this.v = '2.5.5'; }; win.layui = new Lay() ...
- 读源码【读mybatis的源码的思路】
✿ 需要掌握的编译器知识 ★ 编译器为eclipse为例子 调试准备工作(步骤:Window -> Show View ->...): □ 打开调试断点Breakpoint: □ 打开变量 ...
- mybatis缓存源码分析之浅谈缓存设计
本文是关于mybatis缓存模块设计的读后感,关于缓存的思考,关于mybatis的缓存源码详细分析在另一篇文章:https://www.cnblogs.com/gmt-hao/p/12448896.h ...
- mybatis generator 源码学习
mybatis/generator 源码地址mybatis/parent 源码地址1. 分别点击Download ZIP下载到本地. 2. 解压generator-master.zip中的core到g ...
- 【狂神说】JAVA Mybatis 笔记+源码
简介 自学的[狂神JAVA]MyBatis GitHub源码: https://github.com/Donkequan/Mybatis-Study 分享自写源码和笔记 配置用的 jdk13.0.2 ...
- spring-cloud-square源码速读(spring-cloud-square-okhttp篇)
欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...
随机推荐
- 使用 Envoy 和 AdGuard Home 阻挡烦人的广告
原文链接:使用 Envoy 和 AdGuard Home 阻挡烦人的广告 通常我们使用网络时,宽带运营商会为我们分配一个 DNS 服务器.这个 DNS 通常是最快的,距离最近的服务器,但会有很多问题, ...
- Spring Boot核心(基本配置)
上一篇Spring Boot我们简单讲了如何快速创建一个SpringBoot项目. 大家都知道SpringBoot非常强大,可以轻松与各种工具集成,但是我们知其然,也必须知其所以然. 今天开始就和大家 ...
- pt-table-sync 配合pt-table-checksum 修复主从不一致
pt-table-sync 配合pt-table-checksum 修复主从不一致. 先执行下面这条语句,打印出要执行的命令,确认无误后再将 --print 改为 --execute 执行.注意 ...
- 跟我学SpringCloud | 第二十章:Spring Cloud 之 okhttp
1. 什么是 okhttp ? okhttp 是由 square 公司开源的一个 http 客户端.在 Java 平台上,Java 标准库提供了 HttpURLConnection 类来支持 HTTP ...
- python unittest+parameterized,单元测试框架+参数化
总要写新的自动化测试模块,在这里把demo记录下来,后面方便自己直接复制粘贴 from nose_parameterized import parameterized import unittest ...
- 大文件分割、命令脚本 - Python
日志文件分割.命名 工作中经常会收到测试同学.客户同学提供的日志文件,其中不乏几百M一G的也都有,毕竟压测一晚上产生的日志量还是很可观的,xDxD,因此不可避免的需要对日志进行分割,通常定位问题需要针 ...
- selenium-05-问题2
现在的项目组用开源的Selenium做测试,但不得不说,这个东东bug奇多,下面是我遇到的一些问题,有些提供了解决方法,有些则需要继续研究,希望对各位看官有所帮助. 一.不能从命令行运行Seleniu ...
- 由"跨域"引出的一个终极思想(jsonp)
1.什么是跨域? 当协议.子域名.主域名.端口号中任意一个不相同时,都算作不同域. 跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,但是因为浏览器存在一个"同源策略&q ...
- Python邮件发送功能
import smtplibfrom email.mime.text import MIMEText_user = "1147016115@qq.com"#发件人_pwd = &q ...
- shell检测网站地址是否存活
#!/bin/bash . /etc/init.d/functions url_list=(www.baidu.com) ChkCurl(){ i=0 while [ $i -lt 2 ] do cu ...