目标
1、 掌握 MyBatis 的工作流程
2、 掌握 MyBatis 的架构分层与模块划分
3、 掌握 MyBatis 缓存机制
4、 通过阅读 MyBatis 源码掌握 MyBatis 底层工作原理与设计思想
首先在 MyBatis 启动的时候我们要去解析配置文件,包括全局配置文件和映射器配置文件,这里面包含了我们怎么控制 MyBatis 的行为,和我们要对数据库下达的指令,也就是我们的 SQL 信息。我们会把它们解析成一个 Configuration 对象。
接下来就是我们操作数据库的接口,它在应用程序和数据库中间,代表我们跟数据库之间的一次连接:这个就是 SqlSession 对象。
我 们 要 获 得 一 个 会 话 , 必须有一 个会话工厂SqlSessionFactory 。SqlSessionFactory 里面又必须包含我们的所有的配置信息,所以我们会通过一个Builder 来创建工厂类。
我们知道,MyBatis 是对 JDBC 的封装,也就是意味着底层一定会出现 JDBC 的一些核心对象,比如执行 SQL 的 Statement,结果集 ResultSet。在 Mybatis 里面,SqlSession 只是提供给应用的一个接口,还不是 SQL 的真正的执行对象。SqlSession 持有了一个 Executor 对象,用来封装对数据库的操作。
在执行器 Executor 执行 query 或者 update 操作的时候我们创建一系列的对象,来处理参数、执行 SQL、处理结果集,这里我们把它简化成一个对象:StatementHandler,在阅读源码的时候我们再去了解还有什么其他的对象。
这个就是 MyBatis 主要的工作流程,如图:
MyBatis 架构分层与模块划分
在 MyBatis 的主要工作流程里面,不同的功能是由很多不同的类协作完成的,它们分布在 MyBatis jar 包的不同的 package 里面。
我们来看一下 MyBatis 的 jar 包(基于 3.5.4)
大概有 300 多个类,这样看起来不够清楚,不知道什么类在什么环节工作,属于什么层次。
跟 Spring 一样,MyBatis 按照功能职责的不同,所有的 package 可以分成不同的工作层次。
我们可以把 MyBatis 的工作流程类比成餐厅的服务流程。
第一个是跟客户打交道的服务员,它是用来接收程序的工作指令的,我们把它叫做接口层。
第二个是后台的厨师,他们根据客户的点菜单,把原材料加工成成品,然后传到窗口。这一层是真正去操作数据的,我们把它叫做核心层。
最后就是餐厅也需要有人做后勤(比如清洁、采购、财务),来支持厨师的工作和整个餐厅的运营。我们把它叫做基础层。
来看一下这张图,我们根据刚才的分层,和大体的执行流程,做了这么一个总结。当然,从不同的角度来描述,架构图的划分有所区别,这张图画起来也有很多形式。我们先从总体上建立一个印象。每一层的主要对象和主要的功能我们也给大家分析一下。
接口层
首先接口层是我们打交道最多的。核心对象是 SqlSession,它是上层应用和 MyBatis打交道的桥梁,SqlSession 上定义了非常多的对数据库的操作方法。接口层在接收到调用请求的时候,会调用核心处理层的相应模块来完成具体的数据库操作。
核心处理层
接下来是核心处理层。既然叫核心处理层,也就是跟数据库操作相关的动作都是在这一层完成的。核心处理层主要做了这几件事:
- 把接口中传入的参数解析并且映射成 JDBC 类型;
- 解析 xml 文件中的 SQL 语句,包括插入参数,和动态 SQL 的生成;
- 执行 SQL 语句;
- 处理结果集,并映射成 Java 对象。
插件也属于核心层,这是由它的工作方式和拦截的对象决定的。
基础支持层
最后一个就是基础支持层。基础支持层主要是一些抽取出来的通用的功能(实现复用),用来支持核心处理层的功能。比如数据源、缓存、日志、xml 解析、反射、IO、事务等等这些功能。
好了,大体架构讲解清楚了,让我们正式开始阅读源码吧
MyBatis 源码解读
分析源码,我们还是从编程式的 demo 入手,先贴上我的mybatis总体配置文件:
<?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="db.properties"></properties>
<settings>
<!-- 打印查询语句 -->
<setting name="logImpl" value="STDOUT_LOGGING" />
<!-- 控制全局缓存(二级缓存)-->
<setting name="cacheEnabled" value="true"/>
<!-- 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。默认 false -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 当开启时,任何方法的调用都会加载该对象的所有属性。默认 false,可通过select标签的 fetchType来覆盖-->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- Mybatis 创建具有延迟加载能力的对象所用到的代理工具,默认JAVASSIST -->
<!--<setting name="proxyFactory" value="CGLIB" />-->
<!-- STATEMENT级别的缓存,使一级缓存,只针对当前执行的这一statement有效 -->
<!--
<setting name="localCacheScope" value="STATEMENT"/>
-->
<setting name="localCacheScope" value="SESSION"/>
</settings>
<typeAliases>
<typeAlias alias="blog" type="com.caecc.domain.Blog" />
</typeAliases>
<!-- <typeHandlers>
<typeHandler handler="com.caecc.type.MyTypeHandler"></typeHandler>
</typeHandlers>-->
<!-- 对象工厂 -->
<!-- <objectFactory type="com.caecc.objectfactory.GPObjectFactory">
<property name="gupao" value="666"/>
</objectFactory>-->
<!-- <plugins>
<plugin interceptor="com.caecc.interceptor.SQLInterceptor">
<property name="gupao" value="betterme" />
</plugin>
<plugin interceptor="com.caecc.interceptor.MyPageInterceptor">
</plugin>
</plugins>-->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/><!-- 单独使用时配置成MANAGED没有事务 -->
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="BlogMapper.xml"/>
<mapper resource="BlogMapperExt.xml"/>
</mappers>
</configuration>
编写测试类:
@Test
public void testSelect() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession(); // ExecutorType.BATCH
try {
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlogById(1);
System.out.println(blog);
} finally {
session.close();
}
}
把文件读取成流的这一步我们就省略了。所以下面我们分成四步来分析
第一步,我们通过建造者模式创建一个工厂类,配置文件的解析就是在这一步完成的,包括 mybatis-config.xml 和 Mapper 适配器文件。
问题:解析的时候怎么解析的,做了什么,产生了什么对象,结果存放到了哪里。解析的结果决定着我们后面有什么对象可以使用,和到哪里去取。
第二步,通过 SqlSessionFactory 创建一个 SqlSession。
问题:SqlSession 是用来操作数据库的,返回了什么实现类,除了 SqlSession,还创建了什么对象,创建了什么环境?
第三步,获得一个 Mapper 对象。
问题:Mapper 是一个接口,没有实现类,是不能被实例化的,那获取到的这个Mapper 对象是什么对象?为什么要从 SqlSession 里面去获取?为什么传进去一个接口,然后还要用接口类型来接收?
第四步,调用接口方法。
问题:我们的接口没有创建实现类,为什么可以调用它的方法?那它调用的是什么方法?它又是根据什么找到我们要执行的 SQL 的?也就是接口方法怎么和 XML 映射器里面的 StatementID 关联起来的?
此外,我们的方法参数是怎么转换成 SQL 参数的?获取到的结果集是怎么转换成对象的?接下来我们就会详细分析每一步的流程,包括里面有哪些核心的对象和关键的方法
一、 配置解析过程
首先我们要清楚的是配置解析的过程全部只解析了两种文件。一个是mybatis-config.xml 全局配置文件。另外就是可能有很多个的 Mapper.xml 文件,也包括在 Mapper 接口类上面定义的注解。
我们从 mybatis-config.xml 开始。
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSessionFactoryBuilder这个类特别简单,处理构造函数就是build函数,看着挺多,实际都是重载的函数。
public class SqlSessionFactoryBuilder {
public SqlSessionFactory build(Reader reader) {
return build(reader, null, null);
}
public SqlSessionFactory build(Reader reader, String environment) {
return build(reader, environment, null);
}
public SqlSessionFactory build(Reader reader, Properties properties) {
return build(reader, null, properties);
}
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
public SqlSessionFactory build(InputStream inputStream, String environment) {
return build(inputStream, environment, null);
}
public SqlSessionFactory build(InputStream inputStream, Properties properties) {
return build(inputStream, null, properties);
}
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// 用于解析 mybatis-config.xml,同时创建了 Configuration 对象 >>
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 解析XML,最终返回一个 DefaultSqlSessionFactory >>
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.
}
}
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
}
非常明显的建造者模式,它里面定义了很多个 build 方法的重载,最终返回的是一个SqlSessionFactory 对象(单例模式)。我们点进去 build 方法。build核心的重载函数:
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// 用于解析 mybatis-config.xml,同时创建了 Configuration 对象 >>
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 解析XML,最终返回一个 DefaultSqlSessionFactory >>
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.
}
}
}
这里面创建了一个 XMLConfigBuilder 对象(Configuration 对象也是这个时候创建的)。
XMLConfigBuilder
XMLConfigBuilder 是抽象类 BaseBuilder 的一个子类,专门用来解析全局配置文件,针对不同的构建目标还有其他的一些子类,比如:
XMLMapperBuilder:解析 Mapper 映射器
XMLStatementBuilder:解析增删改查标签
根据我们解析的文件流,这里后面两个参数都是空的,创建了一个 parser。这里有两步,第一步是调用 parser 的 parse()方法,它会返回一个 Configuration类。其实BaseBuilder作为抽象类,它这个名字起的不好,如果是spring代码,肯定起AbstractBaseBuilder这个名字(懂的都懂)。插一嘴,程序员比的是代码量吗?不,是设计思想。
配置文件里面所有的信息都会放在Configuration 里面。其实Concifuration就放在基类BaseBuilder中:
番外:源码看多了的读者此时就能猜到,里面不是8个基本变量,就是各种集合。我工作写框架就这么干,看名字觉着牛逼,到最底层就是各种变量和集合的维护而已。
Configuration 类里面有很多的属性(自己去看,我不可能都贴不出来,想得美),有很多是跟 config 里面的标签直接对应的,一目了然。其实mybatis的源码特别直观易读,比Spring好读多了,没那么绕,Spring读完一遍,人疯了。那么Configuration啥时候创建的呢,就在XMLConfigBuilder的构造器里:
简单看一下Configuration的构造方法:
什么? 原来就是别名注册,啥叫注册?大部分源码框架里的注册就是往HashMap里面堆数据!不信你看:
这时候看一下的我配置文件的这个配置:
我们先看一下 parse()方法:
首先会检查是不是已经解析过,也就是说在应用的生命周期里面,config 配置文件只需要解析一次,生成的 Configuration 对象也会存在应用的整个生命周期中。
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// XPathParser,dom 和 SAX 都有用到 >>
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
接下来就是 parseConfiguration 方法:
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
// 对于全局配置文件各种标签的解析
propertiesElement(root.evalNode("properties"));
// 解析 settings 标签
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
// 类型别名
typeAliasesElement(root.evalNode("typeAliases"));
// 插件
pluginElement(root.evalNode("plugins"));
// 用于创建对象
objectFactoryElement(root.evalNode("objectFactory"));
// 用于对对象进行加工
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 反射工具箱
reflectorFactoryElement(root.evalNode("reflectorFactory"));
// settings 子标签赋值,默认值就是在这里提供的 >>
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
// 创建了数据源 >>
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
// 解析引用的Mapper映射器
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
全是解析的工作。这下面有十几个方法,对应着 config 文件里面的所有一级标签。接下来我们就挨个分析分析,我就喜欢一行一行扣源码,真有意思。
propertiesElement(root.evalNode("properties"));
第一个是解析<properties>标签,读取我们引入的外部配置文件。这里面又有两种类型,一种是放在 resource 目录下的,是相对路径,一种是写的绝对路径的。
看我的mybatis.config中有这么一个配置
看源码,我很用心写了注释,相信你看的懂:
解析的最终结果就是我们会把所有的配置信息放到名为 defaults 的 Properties 对象里面,最后把XPathParser 和 Configuration 的 Properties 属性都设置成我们填充后的 Properties对象。其实指向的都是同一个对象。
settingsAsProperties()
第二个,我们把<settings>标签也解析成了一个 Properties 对象,对于<settings>标签的子标签的处理在后面。
对应mybatis-config.xml配置文件中:
loadCustomVfs(settings)
对这个方法不感兴趣,我用不到!不读了。
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 对象(在官方 的 pdf 文档里面没有找到这个配置)。反射工具箱。
settingsElement(settings)
这里就是对<settings>标签里面所有子标签的处理了,前面我们已经把子标签全部转换成了 Properties 对象,所以在这里处理 Properties 对象就可以了。
二级标签里面有很多的配置,比如二级缓存,延迟加载,自动生成主键这些。需要注意的是,我们之前提到的所有的默认值,都是在这里赋值的。如果说后面我们不知道这个属性的值是什么,也可以到这一步来确认一下。
所有的值,都会赋值到 Configuration 的属性里面去。
这些属性都设置在Configuration对象里,所以,当你不知道setting下有哪些属性可以设置时,不妨看看这里,我这里从别的文章中截图简要说明一下上面属性的意思,到目前为止只是解析配置,至于怎么用这些配置,别着急,后面会说。
大体有个印象,回查。
environmentsElement()
这一步是解析<environments>标签。一个 environment 就是对应一个数据源,所以在这里我们会根据配置的<transactionManager>创建一个事务工厂,根据<dataSource>标签创建一个数据源,最后把这两个对象设置成 Environment 对象的属性,放到 Configuration 里面。关于数据库连接池,读者(我自己)可以参考这篇文章https://blog.csdn.net/crave_shy/article/details/46611205。
databaseIdProviderElement()
解析 databaseIdProvider 标签,生成 DatabaseIdProvider 对象(用来支持不同厂商的数据库)。
typeHandlerElement()
跟 TypeAlias 一样,TypeHandler 有两种配置方式,一种是单独配置一个类,一种是指定一个 package。最后我们得到的是JavaType和JdbcType,以及用来做相互映射的TypeHandler 之间的映射关系。最后存放在 TypeHandlerRegistry 对象里面。
mapperElement(root.evalNode("mappers"));
我们现在就要来定义 SQL 映射语句了。 但首先,我们需要告诉 MyBatis 到哪里去找到这些语句。 在自动查找资源方面,Java 并没有提供一个很好的解决方案,所以最好的办法是直接告诉 MyBatis 到哪里去找映射文件。 你可以使用相对于类路径的资源引用,或完全限定资源定位符(包括 file:/// 形式的 URL),或类名和包名等。例如:
<!-- 使用相对于类路径的资源引用 -->
<mappers>
<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
<mapper resource="org/mybatis/builder/BlogMapper.xml"/>
<mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
<!-- 使用完全限定资源定位符(URL) -->
<mappers>
<mapper url="file:///var/mappers/AuthorMapper.xml"/>
<mapper url="file:///var/mappers/BlogMapper.xml"/>
<mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>
<!-- 使用映射器接口实现类的完全限定类名 -->
<mappers>
<mapper class="org.mybatis.builder.AuthorMapper"/>
<mapper class="org.mybatis.builder.BlogMapper"/>
<mapper class="org.mybatis.builder.PostMapper"/>
</mappers>
<!-- 将包内的映射器接口实现全部注册为映射器 -->
<mappers>
<package name="org.mybatis.builder"/>
</mappers>
这些配置会告诉 MyBatis 去哪里找映射文件,剩下的细节就应该是每个 SQL 映射文件了,这个没啥还纠结的,定死的,代码无非就是按这几种方式解析,if..else..罢了。无论是按 package 扫描,还是按接口扫描,最后都会调用到 MapperRegistry 的addMapper()方法。MapperRegistry 里面维护的其实是一个 Map 容器,存储接口和代理工厂的映射关系。详细看一下如何解析的Mapper吧:
// 通过解析 mappers 节点,找到Mapper文件
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 查看mappers节点中是否有 package 节点,有就解析,否则就解析 mapper子节点
/*
<mappers>
<package name="com.test"/>
<!-- <mapper resource="mapper/DemoMapper.xml"/> -->
</mappers>
*/
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
// 解析 mapper 节点,mapper 节点中有三个属性(resource、url、class),但是只能存在一个
/**
* resource:表示文件夹下xml文件
* <mapper resource="mapper/DemoMapper.xml"/>
*
* class:DemoMapper 动态代理接口,DemoMapper.xml文件
* <mapper class="com.test.DemoMapper" />
*
* url:表示盘符下的绝对路径,绝对不推荐使用
* <mapper resource="F:\javaEE\workspace\MyBatisDemo_My\mybatis-3-master\src\main\resources\mapper\DemoMapper.xml"/>
*
*/
/*
<mappers>
<!-- <package name="com.test"/> -->
<mapper resource="mapper/DemoMapper.xml"/>
</mappers>
*/
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
// 三个属性只能是其中的一个有value,否则就抛异常
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
// 如果resource有值,就解析mapper.xml文件
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
// 如果url有值,则通过绝对路径获取输入流,获取mapper.xml并解析
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
// 如果class有值,就获取Class对象
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
首先是包扫描,啥意思?比如我的mybaits-config.xml文件是这么配置的(实际在springboot项目中就是则会么配置的):
意思就是扫描该包下所有的mapper文件。
if ("package".equals(child.getName())) {
//获取包的名称
String mapperPackage = child.getStringAttribute("name");
//解析包下的mapper文件
configuration.addMappers(mapperPackage);
}
进入到addMappers方法里面详细看一下“
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
public void addMappers(String packageName) {
//
mapperRegistry.addMappers(packageName);
}
还是configuration中的mapper注册工厂mapperRegistry,里面就是封装了HashMap
进入到addMappers方法
就是遍历这个包下的所有class,然后调用addMapper方法,进入看看
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {//判断是不是接口,Mapper文件是一个接口,后面会动态代理生成一个实体
if (hasMapper(type)) {//判断改接口是否已经注册过了
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// !Map<Class<?>, MapperProxyFactory<?>> 存放的是接口类型,和对应的工厂类的关系,工厂就是存放了接口的类型及相关方法,将来反射用
knownMappers.put(type, new MapperProxyFactory<>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
// 注册了接口之后,根据接口,开始解析所有方法上的注解,例如 @Select >>
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
拿到mapper,后就看是解析这个mapper,最主要的就是这个MapperAnnotationBuilder的parse方法,根据名字一看就是注解解析(其实我们也很少在Mapper里以注解的方式写sql),看看吧:
public void parse() {
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
// 先判断 Mapper.xml 有没有解析,没有的话先解析 Mapper.xml(例如定义 package 方式)
loadXmlResource();
configuration.addLoadedResource(resource);
assistant.setCurrentNamespace(type.getName());
// 处理 @CacheNamespace
parseCache();
// 处理 @CacheNamespaceRef
parseCacheRef();
// 获取所有方法
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
// issue #237
if (!method.isBridge()) {
// 解析方法上的注解,添加到 MappedStatement 集合中 >>
parseStatement(method);
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
parsePendingMethods();
}
不知道你曾经好奇过没有,Mapper接口是怎么跟xml文件对应起来的,就是在这里,loadXmlResource这个方法:
private void loadXmlResource() {
// Spring may not know the real resource name so we check a flag
// to prevent loading again a resource twice
// this flag is set at XMLMapperBuilder#bindMapperForNamespace
if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
//拿到类名,然后根据类名在后面加上.xml
String xmlResource = type.getName().replace('.', '/') + ".xml";
// #1347
InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
if (inputStream == null) {
// Search XML mapper that is not in the module but in the classpath.
try {
inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
} catch (IOException e2) {
// ignore, resource is not required
}
}
if (inputStream != null) {
XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
// 解析 Mapper.xml >>
xmlParser.parse();
}
}
}
他会根据你的类全路径名,在后面改成.xml,然后在相同的目录下找到这个xml文件,所以你的xml文件资源全路径必须跟类全路径一致。
xmlParser.parse();才是真正的解析xml文件:
public void parse() {
// 总体上做了两件事情,对于语句的注册和接口的注册
if (!configuration.isResourceLoaded(resource)) {
// 1、具体增删改查标签的解析。
// 一个标签一个MappedStatement。 >>
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
// 2、把namespace(接口类型)和工厂类绑定起来,放到一个map。
// 一个namespace 一个 MapperProxyFactory >>
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
parser.evalNode("/mapper")相当于拿到了整个mapper.xml的文件内容,然后依次解析各个标签,后面备用。configurationElement方法进入:
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
// 添加缓存对象
cacheRefElement(context.evalNode("cache-ref"));
// 解析 cache 属性,添加缓存对象
cacheElement(context.evalNode("cache"));
// 创建 ParameterMapping 对象
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// 创建 List<ResultMapping>
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 解析可以复用的SQL
sqlElement(context.evalNodes("/mapper/sql"));
// 解析增删改查标签,得到 MappedStatement >>
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
方法很多,我就不一一解析了,反正就是解析,然后存内存里,后面用的话会慢慢从内存中拿到配置就行匹配。
如果你不是根据package方法配置的,就走下面的resource和url,其实都是一样的,无非就是少了一层Mapper接口到xml的映射。不想写了,自己看。
小结
在这一步,我们主要完成了 config 配置文件、Mapper 文件、Mapper 接口上的注解的解析。
我们得到了一个最重要的对象 Configuration,这里面存放了全部的配置信息,它在属性里面还有各种各样的容器后,返回了一个 DefaultSqlSessionFactory,里面持有了 Configuration 的实例。
二、会话创建过程
解析工作结束了,所有东西都在Configuration,都是静态的,悄悄的躺在内存里,现在我们让他们动起来。
下一步访问数据库,访问之前我们需要先跟数据库建立一条链接,也就是创建一条会话
SqlSession session = sqlSessionFactory.openSession();
进入DefaultSqlSessionFactory的openSession方法,一直跟下去:
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();
}
}
首先向我们走来的是enviroment代表队,就是他啦
然后我们根据这个配置呢,就开始创建事务工厂,这里从Environment对象中取出一个TransactionFactory,它是解析evironments标签的时候创建的。autoCommit默认为false。我们的事务管理器是JDBC类型,所以我们返回来的事务也是JDBC类型:
public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) {
return new JdbcTransaction(ds, level, autoCommit);
}
他会使用connection对象的commit()、rollbach()、close()管理事务,这个connection就是驱动提供的链接
源码也很简单
如果配置成MANAGED,会把事务交给容器来管理,比如JBOSS,Weblogic。因为我们跑的是本地程序配置成MANAGED不会有任何事务。如果是spring+mybati,则没有必要配置,因为我们会在applicationContxt.xml里面配置数据源和事务管理器,覆盖mybatis的配置。
下一步就是创建执行器final Executor executor = configuration.newExecutor(tx, execType);
,可以细分成3个步骤:
- 创建执行器
Executor的基本类型有3种:
public enum ExecutorType {
SIMPLE, REUSE, BATCH
}
默认就是SIMPLE
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
// 默认 SimpleExecutor
executor = new SimpleExecutor(this, transaction);
}
一看到这个Base开头的抽象类,就知道他肯定存储着公共变量和公共方法,提高了代码的复用性,在策略模式和模板方法模式中很常用。在这里就用到了模板方法模式
模板方法模式,定义了一个算法骨架,并允许子类为一个或多个步骤提供实现。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法的某些步骤。
- 缓存装饰
如果cacheEnable=true,会用装饰器模式对executor进行装饰。
// 二级缓存开关,settings 中的 cacheEnabled 默认是 true
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
- 插件代理
// 植入插件的逻辑,至此,四大对象已经全部拦截完毕
executor = (Executor) interceptorChain.pluginAll(executor);
- 返回SqlSession的实现
最终返回DefaultSession,它的属性包括Configuration、Eexecutor对象。
return new DefaultSqlSession(configuration, executor, autoCommit);
小结
创建会话的过程,我们获得了一个DefaultSqlSession,里面包含了一个Executor,Executor是SQL的实际执行对象。
三、获得Mapper对象
现在我们已经有一个 DefaultSqlSession 了,必须找到 Mapper.xml 里面定义的Statement ID,才能执行对应的 SQL 语句。找到 Statement ID 有两种方式:一种是直接调用 session 的方法,在参数里面传入Statement ID,这种方式属于硬编码,我们没办法知道有多少处调用,修改起来也很麻烦。
在 MyBatis 后期的版本提供了第二种方式,就是定义一个接口,然后再调用Mapper 接口的方法。由于我们的接口名称跟 Mapper.xml 的 namespace 是对应的,接口的方法跟statement ID 也都是对应的,所以根据方法就能找到对应的要执行的 SQL。
BlogMapper mapper = session.getMapper(BlogMapper.class);
在这里我们主要研究一下 Mapper 对象是怎么获得的,它的本质是什么。
DefaultSqlSession 的 getMapper()方法,调用了 Configuration 的 getMapper()方法。
@Override
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}
Configuration 的 getMapper()方法,又调用了 MapperRegistry 的 getMapper()方法。
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
我们知道,在解析 mapper 标签和 Mapper.xml 的时候已经把接口类型和类型对应的MapperProxyFactory 放到了一个 Map 中。获取 Mapper 代理对象,实际上是从Map 中获取对应的工厂类后,调用以下方法创建对象:
mapperProxyFactory.newInstance(sqlSession);
最终通过代理模式返回代理对象:
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
// 1:类加载器:2:被代理类实现的接口、3:实现了 InvocationHandler 的触发管理类
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
获得 Mapper 对象的过程,实质上是获取了一个 MapperProxy 的代理对象。MapperProxy 中有 sqlSession、mapperInterface、methodCache。
四、执行SQL
Blog blog = mapper.selectBlog(1);
由于所有的 Mapper 都是 MapperProxy 代理对象,所以任意的方法都是执行MapperProxy 的 invoke()方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// toString hashCode equals getClass等方法,无需走到执行SQL的流程
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
// 提升获取 mapperMethod 的效率,到 MapperMethodInvoker(内部接口) 的 invoke
// 普通方法会走到 PlainMethodInvoker(内部类) 的 invoke
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
首先判断是否需要去执行 SQL,还是直接执行方法。Object 本身的方法和 Java 8 中接口的默认方法不需要去执行 SQL。
cachedInvoker会对方法做一个缓存处理
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
// Java8 中 Map 的方法,根据 key 获取值,如果值是 null,则把后面Object 的值赋给 key
// 如果获取不到,就创建
// 获取的是 MapperMethodInvoker(接口) 对象,只有一个invoke方法
return methodCache.computeIfAbsent(method, m -> {
if (m.isDefault()) {
// 接口的默认方法(Java8),只要实现接口都会继承接口的默认方法,例如 List.sort()
try {
if (privateLookupInMethod == null) {
return new DefaultMethodInvoker(getMethodHandleJava8(method));
} else {
return new DefaultMethodInvoker(getMethodHandleJava9(method));
}
} catch (IllegalAccessException | InstantiationException | InvocationTargetException
| NoSuchMethodException e) {
throw new RuntimeException(e);
}
} else {
// 创建了一个 MapperMethod
return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
});
} catch (RuntimeException re) {
Throwable cause = re.getCause();
throw cause == null ? re : cause;
}
}
这里加入缓存是为了提升 MapperMethod 的获取速度,Map 的 computeIfAbsent()方法:只有 key 不存在或者 value 为 null 的时候才调用 mappingFunction()。最后调用了下面方法:
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
// SQL执行的真正起点
return mapperMethod.execute(sqlSession, args);
}
MapperMethod里面主要有两个属性,一个是SqlCommand,一个是MethodSignature,这两个都是 MapperMethod 的内部类。
另外,还定义了多个execute方法
在这一步,根据不同的 type 和返回类型:
调用 convertArgsToSqlCommandParam()将参数转换为 SQL 的参数。
Object param = method.convertArgsToSqlCommandParam(args);
调用 sqlSession 的 insert()、update()、delete()、selectOne ()方法,我们以查询
为例,会走到 selectOne()方法。
result = sqlSession.selectOne(command.getName(), param);
DefaultSqlSession.selectOne()
public <T> T selectOne(String statement, Object parameter) {
// 来到了 DefaultSqlSession
// Popular vote was to return null on 0 results and throw exception on too many.
List<T> list = this.selectList(statement, parameter);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}
selectOne()最终也是调用了 selectList()。
public <E> List<E> selectList(String statement, Object parameter) {
// 为了提供多种重载(简化方法使用),和默认值
// 让参数少的调用参数多的方法,只实现一次
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
在 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)。
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 获取SQL
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 创建CacheKey:什么样的SQL是同一条SQL? >>
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
首先找到绑定的sql,然后从 Configuration 中获取 MappedStatement, 然后从 BoundSql 中获取 SQL 信息,创建 CacheKey。这个 CacheKey 就是缓存的 Key。
CachingExecutor 走完了会走BaseExecutor 的query
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// 异常体系之 ErrorContext
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
// flushCache="true"时,即使是查询,也清空一级缓存
clearLocalCache();
}
List<E> list;
try {
// 防止递归查询重复处理缓存
queryStack++;
// 查询一级缓存
// ResultHandler 和 ResultSetHandler的区别
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 真正的查询流程
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
queryStack 用于记录查询栈,防止递归查询重复处理缓存。
flushCache=true 的时候,会先清理本地缓存(一级缓存):clearLocalCache();
如果没有缓存,会从数据库查询:queryFromDatabase()
如果 LocalCacheScope == STATEMENT,会清理本地缓存。
queryFromDatabase()执行方法如下:
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
// 先占位
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 三种 Executor 的区别,看doUpdate
// 默认Simple
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
// 移除占位符
localCache.removeObject(key);
}
// 写入一级缓存
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
先在缓存用占位符占位。执行查询后,移除占位符,放入数据。执行 Executor 的 doQuery(),默认是 SimpleExecutor。
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();
// 注意,已经来到SQL处理的关键对象 StatementHandler >>
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 获取一个 Statement对象
stmt = prepareStatement(handler, ms.getStatementLog());
// 执行查询
return handler.query(stmt, resultHandler);
} finally {
// 用完就关闭
closeStatement(stmt);
}
}
创建 StatementHandler
在 configuration.newStatementHandler()中,new 一个 StatementHandler,先得到 RoutingStatementHandler。
RoutingStatementHandler里面没有任何的实现 ,是用来创建基本的StatementHandler 的。这里会根据 MappedStatement 里面的 statementType 决定StatementHandler 的 类 型 。 默 认 是 PREPARED ( STATEMENT 、 PREPARED 、CALLABLE)。
StatementHandler 里面包含了处理参数的 ParameterHandler 和处理结果集的ResultSetHandler。创建 Statement
用 new 出来的 StatementHandler 创建 Statement 对象——prepareStatement()方法对语句进行预编译,处理参数。
handler.parameterize(stmt) ;
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
// 获取 Statement 对象
stmt = handler.prepare(connection, transaction.getTimeout());
// 为 Statement 设置参数
handler.parameterize(stmt);
return stmt;
}
执行的 StatementHandler 的 query()方法
RoutingStatementHandler 的 query()方法。
delegate 委派,最终执行 PreparedStatementHandler 的 query()方法。执行 PreparedStatement 的 execute()方法
后面就是 JDBC 包中的 PreparedStatement 的执行了。ResultSetHandler 处理结果集
return resultSetHandler.handleResultSets(ps);
ResultSetHandler 只有一个实现类:DefaultResultSetHandler。也就是执DefaultResultSetHandler 的 handleResultSets ()方法。
首先我们会先拿到第一个结果集,如果没有配置一个查询返回多个结果集的情况,一般只有一个结果集。如果下面的这个 while 循环我们也不用,就是执行一次。然后会调用 handleResultSet()方法。