MyBatis 核心对象,工作原理及源码解读

相关内容:
架构师系列内容:架构师学习笔记(持续更新)

Mybatis工作原理

InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new  SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlogById(1);

把文件读取成流的这一步我们就省略了。所以下面我们分成四步来分析。
第一步,我们通过建造者模式创建一个工厂类,配置文件的解析就是在这一步完成的,包括mybatis-config.xml 和Mapper 适配器文件。
在这里插入图片描述
第二步,通过SqlSessionFactory 创建一个SqlSession。
在这里插入图片描述
第三步,获得一个Mapper 对象。
在这里插入图片描述
第四步,调用接口方法
在这里插入图片描述

一、配置解析过程

首先我们要清楚的是配置解析的过程全部只解析了两种文件。一个是mybatis-config.xml 全局配置文件。另外就是可能有很多个的Mapper.xml 文件,也包括在Mapper 接口类上面定义的注解。

SqlSessionFactory sqlSessionFactory = new  SqlSessionFactoryBuilder().build(inputStream);

首先我们new 了一个SqlSessionFactoryBuilder,非常明显的建造者模式,它里面定义了很多个build 方法的重载,最终返回的是一个SqlSessionFactory 对象(单例模式)。我们点进去build 方法。这里面创建了一个XMLConfigBuilder 对象(Configuration 对象也是这个时候创建的)。
XMLConfigBuilder
XMLConfigBuilder 是抽象类BaseBuilder 的一个子类,专门用来解析全局配置文件,针对不同的构建目标还有其他的一些子类,比如:
XMLMapperBuilder:解析Mapper 映射器
XMLStatementBuilder:解析增删改查标签
在这里插入图片描述
根据我们解析的文件流,这里后面两个参数都是空的,创建了一个parser。这里有两步,第一步是调用parser 的parse()方法,它会返回一个Configuration类。
之前我们说过,也就是配置文件里面所有的信息都会放在Configuration 里面。Configuration 类里面有很多的属性,有很多是跟config 里面的标签直接对应的。
我们先看一下parse()方法:
首先会检查是不是已经解析过,也就是说在应用的生命周期里面,config 配置文件只需要解析一次,生成的Configuration 对象也会存在应用的整个生命周期中。接下来就是parseConfiguration 方法:

parseConfiguration(parser.evalNode("/configuration"));

这下面有十几个方法,对应着config 文件里面的所有一级标签。
propertiesElement()
第一个是解析<properties>标签,读取我们引入的外部配置文件。这里面又有两种类型,一种是放在resource 目录下的,是相对路径,一种是写的绝对路径的。解析的最终结果就是我们会把所有的配置信息放到名为defaults 的Properties 对象里面,最后把
XPathParser 和Configuration 的Properties 属性都设置成我们填充后的Properties对象。

settingsAsProperties()
第二个,我们把标签也解析成了一个Properties 对象,对于<settings>标签的子标签的处理在后面。
在早期的版本里面解析和设置都是在后面一起的,这里先解析成Properties 对象是因为下面的两个方法要用到。

loadCustomVfs(settings)
loadCustomVfs 是获取Vitual File System 的自定义实现类,比如我们要读取本地文件,或者FTP 远程文件的时候,就可以用到自定义的VFS 类。我们根据<settings>标签里面的<vfsImpl>标签,生成了一个抽象类VFS 的子类,并且赋值到Configuration中。

loadCustomLogImpl(settings)
loadCustomLogImpl 是根据<logImpl>标签获取日志的实现类,我们可以用到很多的日志的方案,包括LOG4J,LOG4J2,SLF4J 等等。这里生成了一个Log 接口的实现类,并且赋值到Configuration 中。

typeAliasesElement()
接下来,我们解析<typeAliases>标签,我们在讲配置的时候也讲过,它有两种定义方式,一种是直接定义一个类的别名,一种就是指定一个包,那么这个package 下面所有的类的名字就会成为这个类全路径的别名。类的别名和类的关系,我们放在一个TypeAliasRegistry 对象里面。

pluginElement()
接下来就是解析<plugins>标签,比如Pagehelper 的翻页插件,或者我们自定义的插件。<plugins>标签里面只有<plugin>标签,<plugin>标签里面只有<property>标签。标签解析完以后,会生成一个Interceptor 对象,并且添加到Configuration 的InterceptorChain 属性里面,它是一个List。

objectFactoryElement()、objectWrapperFactoryElement()
接下来的两个标签是用来实例化对象用的, <objectFactory> 和<objectWrapperFactory> 这两个标签, 分别生成ObjectFactory 、ObjectWrapperFactory 对象,同样设置到Configuration 的属性里面。

reflectorFactoryElement()
解析reflectorFactory 标签,生成ReflectorFactory 对象(在官方3.5.1 的pdf 文档里面没有找到这个配置)。

settingsElement(settings)
这里就是对<settings>标签里面所有子标签的处理了,前面我们已经把子标签全部转换成了Properties 对象,所以在这里处理Properties 对象就可以了。
二级标签里面有很多的配置,比如二级缓存,延迟加载,自动生成主键这些。需要注意的是,我们之前提到的所有的默认值,都是在这里赋值的。如果说后面我们不知道这个属性的值是什么,也可以到这一步来确认一下。所有的值,都会赋值到Configuration 的属性里面去。

environmentsElement()
这一步是解析<environments>标签。我们前面讲过,一个environment 就是对应一个数据源,所以在这里我们会根据配
置的<transactionManager>创建一个事务工厂,根据<dataSource>标签创建一个数据源,最后把这两个对象设置成Environment 对象的属性,放到Configuration 里面。回答了前面的问题:数据源工厂和数据源在哪里创建。

databaseIdProviderElement()
解析databaseIdProvider 标签,生成DatabaseIdProvider 对象(用来支持不同厂商的数据库)。

typeHandlerElement()
跟TypeAlias 一样,TypeHandler 有两种配置方式,一种是单独配置一个类,一种是指定一个package。最后我们得到的是JavaType 和JdbcType,以及用来做相互映射的TypeHandler 之间的映射关系。最后存放在TypeHandlerRegistry 对象里面。

mapperElement()
http://www.mybatis.org/mybatis-3/zh/configuration.html#mappers
1)判断
最后就是<mappers>标签的解析。
首先会判断是不是接口,只有接口才解析;然后判断是不是已经注册了,单个Mapper重复注册会抛出异常。

扫描类型含义
resource相对路径
url绝对路径
package
class单个接口

2)注册
XMLMapperBuilder.parse()方法,是对Mapper 映射器的解析。里面有两个方法:
configurationElement()—— 解析所有的子标签, 其中buildStatementFromContext()最终获得MappedStatement 对象。
bindMapperForNamespace()——把namespace(接口类型)和工厂类绑定起来。
无论是按package 扫描,还是按接口扫描,最后都会调用到MapperRegistry 的addMapper()方法。
MapperRegistry 里面维护的其实是一个Map 容器,存储接口和代理工厂的映射关系。

3)处理注解
除了映射器文件,在这里也会去解析Mapper 接口方法上的注解。在addMapper()方法里面创建了一个MapperAnnotationBuilder,我们点进去看一下parse()方法。
parseCache() 和parseCacheRef() 方法其实是对@CacheNamespace 和@CacheNamespaceRef 这两个注解的处理。
parseStatement()方法里面的各种getAnnotation(),都是对注解的解析,比如@Options,@SelectKey,@ResultMap 等等。
最后同样会解析成MappedStatement 对象,也就是说在XML 中配置,和使用注解配置,最后起到一样的效果。
4)收尾
如果注册没有完成,还要从Map 里面remove 掉。

// MapperRegistry.java
finally {
if (!loadCompleted) {
knownMappers.remove(type);
}

最后,MapperRegistry 也会放到Configuration 里面去。
第二步是调用另一个build()方法,返回DefaultSqlSessionFactory。

总结
在这一步,我们主要完成了config 配置文件、Mapper 文件、Mapper 接口上的注解的解析。我们得到了一个最重要的对象Configuration,这里面存放了全部的配置信息,它在属性里面还有各种各样的容器。最后,返回了一个DefaultSqlSessionFactory,里面持有了Configuration 的实例。

二、会话创建过程

这是第二步, 我们跟数据库的每一次连接, 都需要创建一个会话, 我们用openSession()方法来创建。
DefaultSqlSessionFactory —— openSessionFromDataSource() 这个会话里面,需要包含一个Executor 用来执行SQL。Executor 又要指定事务类型和执行器的类型。所以我们会先从Configuration 里面拿到Enviroment,Enviroment 里面就有事务工厂。
1、创建Transaction

属性产生工厂类产生事务
JDBCJdbcTransactionFactoryJdbcTransaction
MANAGEDManagedTransactionFactoryManagedTransaction
classJdbcTransactionFactoryJdbcTransaction

如果配置的是JDBC,则会使用Connection 对象的commit()、rollback()、close()管理事务。如果配置成MANAGED,会把事务交给容器来管理,比如JBOSS,Weblogic。因为我们跑的是本地程序,如果配置成MANAGE 不会有任何事务。如果是Spring + MyBatis , 则没有必要配置, 因为我们会直接在applicationContext.xml 里面配置数据源和事务管理器,覆盖MyBatis 的配置。

2、创建Executor
我们知道,Executor 的基本类型有三种:SIMPLE、BATCH、REUSE,默认是SIMPLE(settingsElement()读取默认值),他们都继承了抽象类BaseExecutor。
在这里插入图片描述
问题:三种类型的区别(通过update()方法对比)?
SimpleExecutor:每执行一次update 或select,就开启一个Statement 对象,用完立刻关闭Statement 对象。
ReuseExecutor:执行update 或select,以sql 作为key 查找Statement 对象,存在就使用,不存在就创建,用完后,不关闭Statement 对象,而是放置于Map 内,供下一次使用。简言之,就是重复使用Statement 对象。
BatchExecutor:执行update(没有select,JDBC 批处理不支持select),将所有sql 都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement 对象,每个Statement 对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC 批处理相同。
如果配置了cacheEnabled=ture,会用装饰器模式对executor 进行包装:new CachingExecutor(executor)。
包装完毕后,会执行:

executor = (Executor) interceptorChain.pluginAll(executor);

此处会对executor 进行包装。最终返回DefaultSqlSession,属性包括Configuration、Executor 对象。

总结:创建会话的过程,我们获得了一个DefaultSqlSession,里面包含了一个Executor,它是SQL 的执行者。

三、获得Mapper 对象

现在我们已经有一个DefaultSqlSession 了,必须找到Mapper.xml 里面定义的Statement ID,才能执行对应的SQL 语句。
找到Statement ID 有两种方式:
一种是直接调用session 的方法,在参数里面传入Statement ID,这种方式属于硬编码,我们没办法知道有多少处调用,修改起来也很麻烦。另一个问题是如果参数传入错误,在编译阶段也是不会报错的,不利于预先发现问题。

Blog blog = (Blog)  session.selectOne("com.demo.mapper.BlogMapper.selectBlogById", 1);

所以在MyBatis 后期的版本提供了第二种方式,就是定义一个接口,然后再调用Mapper 接口的方法。
由于我们的接口名称跟Mapper.xml 的namespace 是对应的,接口的方法跟statement ID 也都是对应的,所以根据方法就能找到对应的要执行的SQL。

BlogMapper mapper = session.getMapper(BlogMapper.class);

在这里我们主要研究一下Mapper 对象是怎么获得的,它的本质是什么。
DefaultSqlSession 的getMapper()方法,调用了Configuration 的getMapper()方法。

configuration.<T>getMapper()

Configuration 的getMapper()方法,又调用了MapperRegistry 的getMapper()方法。

mapperRegistry.getMapper()

我们知道,在解析mapper 标签和Mapper.xml 的时候已经把接口类型和类型对应的MapperProxyFactory 放到了一个Map 中。获取Mapper 代理对象,实际上是从Map 中获取对应的工厂类后,调用以下方法创建对象:

MapperProxyFactory.newInstance()

最终通过代理模式返回代理对象:

return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(),  new Class[]{ mapperInterface }, mapperProxy);

JDK 动态代理和MyBatis 用到的JDK 动态代理有什么区别?
JDK 动态代理:
在这里插入图片描述
JDK 动态代理代理,在实现了 InvocationHandler 的代理类里面,需要传入一个被代理对象的实现类。
MyBatis 的动态代理:

在这里插入图片描述
不需要实现类的原因:我们只需要根据接口类型+方法的名称,就可以找到Statement ID 了,而唯一要做的一件事情也是这件,所以不需要实现类。在MapperProxy里面直接执行逻辑(也就是执行SQL)就可以。

总结:
获得Mapper 对象的过程,实质上是获取了一个MapperProxy 的代理对象。
MapperProxy 中有sqlSession、mapperInterface、methodCache。
在这里插入图片描述

四、执行SQL

Blog blog = mapper.selectBlog(1);

由于所有的Mapper 都是MapperProxy 代理对象,所以任意的方法都是执行MapperProxy 的invoke()方法。
问题1:我们引入MapperProxy 为了解决什么问题?
硬编码和编译时检查问题。它需要做的事情是:根据方法查找Statement ID 的问题。
问题2:这里没有实现类,进入到invoke 方法的时候做了什么事情?它是怎么找到我们要执行的SQL 的?
我们看一下invoke()方法:

1、MapperProxy.invoke()
1)首先判断是否需要去执行SQL,还是直接执行方法。Object 本身的方法和Java 8 中接口的默认方法不需要去执行SQL。
2)获取缓存
这里加入缓存是为了提升MapperMethod 的获取速度:

// 获取缓存,保存了方法签名和接口方法的关系
final MapperMethod mapperMethod = cachedMapperMethod(method);

Map 的computeIfAbsent()方法:只有key 不存在或者value 为null 的时候才调用mappingFunction()。
2、MapperMethod.execute()
接下来又调用了mapperMethod 的execute 方法:

mapperMethod.execute(sqlSession, args);

在这里插入图片描述
MapperMethod 里面主要有两个属性, 一个是SqlCommand , 一个是MethodSignature,这两个都是MapperMethod 的内部类。另外定义了多个execute()方法。
在这一步,根据不同的type 和返回类型:
调用convertArgsToSqlCommandParam()将参数转换为SQL 的参数。
调用sqlSession 的insert()、update()、delete()、selectOne ()方法,我们以查询为例,会走到selectOne()方法。
3、DefaultSqlSession.selectOne()
selectOne()最终也是调用了selectList()。
在SelectList()中,我们先根据command name(Statement ID)从Configuration中拿到MappedStatement,这个ms 上面有我们在xml 中配置的所有属性,包括id、statementType、sqlSource、useCache、入参、出参等等。

在这里插入图片描述
然后执行了Executor 的query()方法。
前面我们说到了Executor 有三种基本类型,同学们还记得是哪几种么?SIMPLE/REUSE/BATCH,还有一种包装类型,CachingExecutor。
那么在这里到底会选择哪一种执行器呢?
我们要回过头去看看DefaultSqlSession 在初始化的时候是怎么赋值的,这个就是我们的会话创建过程。
如果启用了二级缓存,就会先调用CachingExecutor 的query()方法,里面有缓存相关的操作,然后才是再调用基本类型的执行器,比如默认的SimpleExecutor。
在没有开启二级缓存的情况下,先会走到BaseExecutor 的query()方法(否则会先走到CachingExecutor)。
在这里插入图片描述
4、BaseExecutor.query()
1)创建CacheKey
从Configuration 中获取MappedStatement, 然后从BoundSql 中获取SQL 信息,创建CacheKey。这个CacheKey 就是缓存的Key。然后再调用另一个query()方法。

2)清空本地缓存
queryStack 用于记录查询栈,防止递归查询重复处理缓存。
flushCache=true 的时候,会先清理本地缓存(一级缓存):clearLocalCache();
如果没有缓存,会从数据库查询:queryFromDatabase()
如果LocalCacheScope == STATEMENT,会清理本地缓存。

3)从数据库查询
a)缓存
先在缓存用占位符占位。执行查询后,移除占位符,放入数据。
b)查询
执行Executor 的doQuery();默认是SimpleExecutor。

5、SimpleExecutor.doQuery()
1)创建StatementHandler
在configuration.newStatementHandler()中,new 一个StatementHandler,先得到RoutingStatementHandler。
RoutingStatementHandler 里面没有任何的实现, 是用来创建基本的StatementHandler 的。这里会根据MappedStatement 里面的statementType 决定StatementHandler 的类型。默认是PREPARED ( STATEMENT 、PREPARED 、CALLABLE)。

switch (ms.getStatementType()) {
    case STATEMENT:
        delegate = new SimpleStatementHandler(executor, ms, parameter,  rowBounds, resultHandler,boundSql);
        break;
    case PREPARED:
        delegate = new PreparedStatementHandler(executor, ms, parameter,  rowBounds,resultHandler, boundSql);
        break;
    case CALLABLE:
        delegate = new CallableStatementHandler(executor, ms, parameter,  rowBounds,resultHandler, boundSql);
        break;
    default:
        throw new ExecutorException("Unknown statement type: " +  ms.getStatementType());
}

StatementHandler 里面包含了处理参数的ParameterHandler 和处理结果集的ResultSetHandler。这两个对象都是在上面new 的时候创建的。

this.parameterHandler =  configuration.newParameterHandler(mappedStatement,parameterObject, boundSql);
this.resultSetHandler = configuration.newResultSetHandler(executor,  mappedStatement,rowBounds, parameterHandler, resultHandler, boundSql);

这三个对象都是可以被插件拦截的四大对象之一,所以在创建之后都要用拦截器进行包装的方法。

statementHandler = (StatementHandler)  interceptorChain.pluginAll(statementHandler);
parameterHandler = (ParameterHandler)  interceptorChain.pluginAll(parameterHandler);
resultSetHandler = (ResultSetHandler)  interceptorChain.pluginAll(resultSetHandler);

PS:四大对象还有一个是谁?在什么时候创建的?(Executor)

2)创建Statement
用new 出来的StatementHandler 创建Statement 对象——prepareStatement()方法对语句进行预编译,处理参数。handler.parameterize(stmt) ;

3)执行的StatementHandler 的query()方法
RoutingStatementHandler 的query()方法。
delegate 委派,最终执行PreparedStatementHandler 的query()方法。
在这里插入图片描述
4)执行PreparedStatement 的execute()方法
后面就是JDBC 包中的PreparedStatement 的执行了。

5)ResultSetHandler 处理结果集

return resultSetHandler.handleResultSets(ps);

问题:怎么把ResultSet 转换成List<Object>?
ResultSetHandler 只有一个实现类: DefaultResultSetHandler 。也就是执行DefaultResultSetHandler 的handleResultSets ()方法。
首先我们会先拿到第一个结果集,如果没有配置一个查询返回多个结果集的情况,一般只有一个结果集。如果下面的这个while 循环我们也不用,就是执行一次。然后会调用handleResultSet()方法。

MyBatis 核心对象

对象相关对象作用
ConfigurationMapperRegistry/TypeAliasRegistry/TypeHandlerRegistry包含了MyBatis 的所有的配置信息
SqlSessionSqlSessionFactory/DefaultSqlSession对操作数据库的增删改查的API 进行了封装,提供给应用层使用
ExecutorBaseExecutor/SimpleExecutor/BatchExecutor/ReuseExecutorMyBatis 执行器,是MyBatis 调度的核心,负责SQL 语句的生成和查询缓存的维护
StatementHandlerBaseStatementHandler/SimpleStatementHandler/PreparedStatementHandler/CallableStatementHandler封装了JDBC Statement 操作,负责对JDBC statement 的操作,如设置参数、将Statement 结果集转换成List 集合
ParameterHandlerDefaultParameterHandler把用户传递的参数转换成JDBC Statement 所需要的参数
ResultSetHandlerDefaultResultSetHandler把JDBC 返回的ResultSet 结果集对象转换成List 类型的集合
MapperProxyMapperProxyFactory代理对象,用于代理Mapper 接口方法
MappedStatementSqlSource/BoundSqlMappedStatement 维护了一条<select
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值