MyBatis 原理 及 源码分析( SqlSessionFactory、SqlSession、代理接口,一二级缓存 )
一、回顾Mybatis的使用
Mybatis应该是现在我们项目中使用非常频繁的框架,它几乎消除了几乎所有的JDBC代码和参数的手工设置以及对结果集的检索封装,让我们可以使用简单的XML或注解用于配置和原始映射。
还记得我们在配置Mybatis的时候都要写一个 mybatis_config.xml 最常写的应该数据库连接信息,还有Mapper.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>
<!-- 环境配置 -->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<!-- 数据库连接相关配置 ,这里动态获取config.properties文件中的内容-->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/testdb"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<!-- mapping文件路径配置 -->
<mappers>
<mapper resource="mappers/UserMapper.xml"/>
</mappers>
</configuration>
上面配置了数据库连接信息,和mapping文件的路径,如果要查询数据库的信息,是不是还要再来个接口和 Mapper.xml 文件:
public interface UserMapper {
UserEntity getUser(int id);
}
<?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="com.mybatis.demo.mapper.UserMapper">
<select id="getUser" parameterType="int"
resultType="com.mybatis.demo.entity.UserEntity">
select * from user where id=#{id}
</select>
</mapper>
相信上面这种基本上大家都这样写过吧,那再来看下下面的代码是否熟悉:
public class TestMyBatis {
public static void main(String[] args) {
try {
// 基本mybatis环境
// 1.定义mybatis_config文件地址
String resources = "mybatis_config.xml";
// 2.获取InputStreamReaderIo流
Reader reader = Resources.getResourceAsReader(resources);
// 3.获取SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
// 4.获取Session
SqlSession sqlSession = sqlSessionFactory.openSession();
// 5.操作Mapper接口
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
UserEntity user = mapper.getUser(175);
System.out.println(user.getName());
} catch (Exception e) {
e.printStackTrace();
}
}
}
上面这是直接使用Mybatis的方式调的mapper,大家在项目中应该大多都是SSM或者SpringBoot 已经封装的自动帮我们实现了上面的这种代码,在这里写上面这种形式更方便下面对源码的解读和大家的理解。
二、Mybatis 源码执行原理流程
- 当项目启动的时候,首先读取我们配置的
mybatis_config.xml文件,然后转化为InputStreamReader格式使用SqlSessionFactoryBuilder().build()方法解析XML,并且mybatis_config.xml文件只可被解析一次,如果手动再调用解析会报:Each XMLConfigBuilder can only be used once.错误。 - 解析
mybatis_config.xml文件时,会将settings、environments、mappers等信息解析装配到Configuration对象中,供其他地方使用,同时Mapper.xml中的信息比如namespace接口地址会使用反射拿到Class,存放到 名为knownMappers的HashMap中,还有Mapper.xml中的select|insert|update|delete语句,会根据id为key,value为MappedStatement的对象存放到名为mappedStatements的HashMap中。 - 解析完XML会返回一个
DefaultSqlSessionFactory的对象。下面可以通过这个对象openSession()方法拿到一个SqlSession对象。 - 在获取
SqlSession对象会根据cacheEnabled的值是否创建缓存执行器 ,注意Mybatis的二级缓存默认是开启的,也就是cacheEnabled默认是为true,所以这里默认创建的是二级缓存的CachingExecutor,如果设成false,返回的便是SimpleExecutor,现在好多网上说二级缓存默认关闭需要注意下,下面看源码的时候可以看下这个变量的值,至于在项目中为何二级缓存没有生效,可以检查下xml配置文件有没有<cache></cache>声明或者是否手动把cacheEnabled设置为了false,在使用默认的二级缓存的时候 Mybatis 是采用的 HashMap作为缓存容器,在并发情况下还是比较容易发生线程安全问题,如果要使用二级缓存,可以使用Redis来缓存,配置到Mybatis中。 - 在上面已经拿到一个
Executor执行器,通过执行器有为我们创建了一个DefaultSqlSession返回出去。 - 在拿到了
SqlSession后便可以获取Mapper接口对象了,SqlSession中有个getMapper()方法,通过传入一个Class 便可拿到该对象,是怎么实现的呢,其实就是使用的java的动态代理技术,做了个代理类,在第二点的时候我提到了一个knownMappers的HashMap容器存放namespace中的接口Class,现在在getMapper的时候就是根据传入的Class 到knownMappers容器中取,取到之后 Mybatis 通过MapperProxy类实现了InvocationHandler代理Mapper接口。 - 再通过
SqlSession的getMapper()方法上面已经拿到了Mybatis 给我们的代理对象,所以下面在执行对象中的方法时,肯定执行的时代理类中的invoke方法,在这个方法中 Mybatis 又提供了一个MapperMethodMapper执行对象,来帮助我们执行下面的操作,并且在这里中为了性能提升,还对MapperMethod做了个简单的HashMap 缓存。 - 在
MapperMethod对象中,其实还是调用了sqlSession中的方法,如果是返回一个对象,就会使用sqlSession的selectOne方法执行,其实selectOne方法还是又调用的selectList。接着如果是一个查询操作,会使用Executor执行器的query方法,会先触发CachingExecutor的query方法先查询缓存这个缓存就是二级缓存,如果没有,会再去父类的BaseExecutor的query在这边会再查询一级缓存,还是没有就会去SimpleStatementHandler这个类下的query方法使用java 原声的Connection、Statement去查询数据库,并将查询的数据整理成List类型返回,如果List中只有一个数据,就将第0个返回出去。 - 上面拿到了数据库的返回结果,会先存放到一级缓存中,如果二级没有关闭然后再放到二级缓存中,然后返还给最终数据给最初的调用者。
别看Mybatis我们使用的那么简单,其实执行原理还是挺多的,其中有很多是需要我们学习和借鉴的东西,如果对上面的流程不太明白,下面便是对源码的解读,方便我们对上面流程的理解。
三、Mybatis源码解读-SqlSessionFactory初始化配置解读
在初始化SqlSessionFactory的时候,Mybatis为我们提供了SqlSessionFactoryBuilder().build(reader)方法,里面需要传一个Reader对象:
这个Reader对象是通过Mybatis提供的Resources.getResourceAsReader()便可以拿到,里面传一个mybatis_config.xml的地址,Resources.getResourceAsReader() 就是使用java 的ClassLoader的getResourceAsStream 方法拿到一个InputStream对象进而转化为InputStreamReader对象,核心就是下面这段代码:
在有了mybatis_config.xml核心配置的Reader对象,便可以使用SqlSessionFactoryBuilder().build(reader)方法获取SqlSessionFactory,下面我们看下SqlSessionFactoryBuilder().build(reader)方法做了什么事情,顺着build点击去可以看到下面代码:
上面可以看出SqlSessionFactoryBuilder使用XMLConfigBuilder解析配置文件,先来看下XMLConfigBuilder的构造方法:
还记得上面在调用SqlSessionFactoryBuilder().build(reader)的时候我们传了一个Reader参数,所以environment、props都为空,其实就是对参数做了初始化,但在这里需要注意的是this.parsed变量,下面会提到它。再向下执行会走XMLConfigBuilder的parse()方法:
这parse()方法中先判断this.parsed变量是否为true,就是上面提到的变量,如果是true便报Each XMLConfigBuilder can only be used once.的错误,表示一个XMLConfigBuilder 只能被初始化一次,下面又执行到了this.parseConfiguration()方法:
在这个方法不难看出在做什么,就是解析我们的配置文件,包括上面我们在mybatis_config.xml文件中写的environments、mappers等,配置DataSource数据源就在environmentsElement()这个方法下面,这里我们主要看下圈出来的这个mapperElement()方法,在这里面做了Mapper接口绑定,下面看下这个方法:
在mapperElement()方法中先判断我们节点中是否有package节点,没有就会解析resource节点,再来看下我们在mybatis_config.xml文件中配置的mapper节点:
是不是我们就给mapper配置了resource属性,那肯定这里会走到else下,解析resource、url、class,从上面我们这个配置来看,肯定只有resource是不为空的,其余都为null,再下面的判断不就直接走第一个判断了,这里将resource通过mybatis的Resources获取为了输入流,又给了下面的XMLMapperBuilder类,别忘了这个resource是什么?不就是我们具体的接口映射的Mapper.xml文件地址吗,所以XMLMapperBuilder类,我们猜下也能想到,这个类肯定又去解析我们的Mapper.xml文件了,这里和上面XMLConfigBuilder同样的套路,使用构造函数传递参数,调用parse()方法执行逻辑,这里直接看XMLMapperBuilder类的parse()方法:
方法中先判断这个Resource是否被加载过了,可以看下configuration.isResourceLoaded方法:
判断loadedResources中是否有这个Resource,而loadedResources又是:
一个Set集合,很明显,我们看到现在,还没有看到哪里把我们的Resource添加进了这个集合中,所有肯定会走parse()中的判断,在判断中又执行了好几个方法,我们从上一个一个的看,先来看this.configurationElement()方法:
这里看到namespace、parameterMap、resultMap、select|insert|update|delete,是不是有些熟悉的感觉,这不就是我们经常在Mapper.xml中经常些的吗。相信大家一下子就能猜到每个方法中做了啥事情,我们还是主要看下上面圈出来的地方,对Mapper中增删改查select|insert|update|delete 这些statement是怎么操作的,这个statement名词就代表一个Mapper中的一个SQL的 select|insert|update|delete段,下面就直接说statement,走到buildStatementFromContext()方法中,该方法走执行了buildStatementFromContext()的重载方法:

这个方法,又是根据上面的套路的创建了一个XMLStatementBuilder,这里我们来看两个参数,builderAssistant
还有context对象
从这两个参数可以推断XMLStatementBuilder是绑定我们的具体的statement,再开看到parseStatementNode()这个方法:
是不是对id、parameterType、resultType比较熟悉,对,这就是我们再Mapper.xml 中写增删改查的时候指定的参数,再向下看执行了addMappedStatement() 不用多想肯定在这绑定了statement:
来到addMappedStatement() 这个方法中构建了一个MappedStatement 对象,来看下这个对象中的信息:

MappedStatement 对象存放了resource 资源地址,id 包名加方法名,SqlSource具体sql,及sql 的类型等等,将一个接口所对应的xml中操作的所有的信息都记录了下来,下面的configuration.addMappedStatement(statement)就不用多想了,肯定需要将该statement注册到容器中存下来,点到configuration.addMappedStatement(statement)方法中:


到这里是不是就有点小明白了,我们再Mapper.xml中写的Sql语句都被放在了这个mappedStatements容器中了,key 就是我们写的 对应接口名的 id。后面找到这个sql直接根据id就可以找到。
再看到这我们明白了早Mapper.xml中写的Sql被解析到哪了,再回到XMLMapperBuilder类的parse()方法,上面我们才分析了这个方法中的configurationElement()做了什么,再来看configuration.addLoadedResource()方法:

这里就直接将resource放在了loadedResources这个集合中,loadedResources集合大家还有印象吗,就是在说到XMLMapperBuilder类的parse()方法刚进来的判断就是判断loadedResources这个集合中是否已经存在resource,是不是和上面的代码相呼应起来了。
下面再继续看到XMLMapperBuilder类的parse()方法下的bindMapperForNamespace(),上面看了绑定Mapper.xml 中的每个statement逻辑,而这个方法就是将Mapper 和 Java端的接口相绑定:

namespace就是代表java中的接口,这个大家都很熟悉,先看上面我圈出来的,上面有个classForName() 从这个名字听着是不是很熟悉,是不和Java的反射有点像,顺着点进去,最后可以看到下面的代码:
这不就是java 的反射吗,再接着又两个add的方法很是显眼,第一个add addLoadedResource()方法,就是将 ‘namespace:’+namespace又放到了loadedResources这个集合中:
然后下面的addMapper()方法就很重要了,绑定了我们Mapper对应的接口,点下去到addMapper(Class<T> type)方法中:

看到这其实应该也明白了,Mapper接口被存放到了knownMappers容器中存放。
到现在SqlSessionFactoryBuilder().build(reader)的部分逻辑大家应该都明白了。
再回到最初的SqlSessionFactoryBuilder().build(reader)的方法中,其中的XMLConfigBuilder.parse()方法就已经看了上面那么多,还有很多大家可以自己根据上面的方法来走这看,下面看下SqlSessionFactoryBuilder().build(reader)中的this.build()方法:
到现在终于看到了SqlSessionFactory了,再默认的情况下,就是拿到的SqlSessionFactory其实就是DefaultSqlSessionFactory
一、通过SqlSessionFactory获取 SqlSession
上篇文章分析已经拿到了一个DefaultSqlSessionFactory,下面肯定需要从工厂中获取一个SqlSession ,会使用SessionFactory.openSession() 方法,我们先点进去看下,因为上面说道我们获取的是DefaultSqlSessionFactory,所以要进到DefaultSqlSessionFactory 的 openSession()方法中:

上面来到了openSessionFromDataSource()方法中,先看下参数:
execType传了个SIMPLE的类型,在这段代码中先从configuration配置中获取到当前的环境信息,然后通过TransactionFactory工厂创建了Transaction事物对象,给了下面创建执行器的newExecutor()方法,下面主要看下newExecutor()方法:
刚才提到execType传了个SIMPLE的类型,所以上面的逻辑会现创建一个SimpleExecutor 简单执行器,然后有根据cacheEnabled的值判断是否创建CachingExecutor缓存执行器,也就是二级缓存,我们先看下cacheEnabled的值:
cacheEnabled默认就为true,所以mybatis的二级缓存是默认打开的,所以这边获取的执行器的类型是CachingExecutor的缓存执行器。
再回到DefaultSqlSessionFactory.openSessionFromDataSource()方法,上面拿到了执行器后创建了一个DefaultSqlSession的一个默认的SqlSession对象返回出去,所以下面这个SqlSession其实就是DefaultSqlSession
二、通过动态代理 代理接口
所以下面再使用SqlSession 的getMapper()方法的时候走的是DefaultSqlSession中的getMapper()方法,点进去这个方法:


上面代码看到,其实是根据传入的Class,去knownMappers这个容器中获取MapperProxyFactory对象,knownMappers中的内容何时放入的,可以看下上一篇博客中又讲述。
再看下MapperProxyFactory对象的信息:
主要还是存放了Mapper对应Java的接口地址,再往下看到mapperProxyFactory.newInstance()这个方法:
看到Proxy.newProxyInstance() 是不是想起了java的动态代理,不用想那MapperProxy这个类,肯定实现了InvocationHandler:
这不就是Mybatis帮我们创建的代理类吗,那也不用想了,通过getMapper获取的对象也就是该代理类了,那下面调用接口的方法,不就是走的该代理类中的invoke()方法吗。来看invoke()方法的具体逻辑:
在invoke()方法中为我们创建了一个MapperMethod,但这里有个对MapperMethod的缓存,应该可以想到MapperMethod使用频率应该是很频繁,来看下传递给MapperMethod的参数:
mapperInterface Mapper接口
method接口中的那个方法
sqlSession.getConfiguration() 中的部分信息。
在知道传的参数下面我们看主要的代码,走到mapperMethod.execute()方法中:

根据上面参数的类型为SELECT查询类型,会进第四个判断中,还记得我们在Mapper.xml中写的 查询语句是返回一个对象:
所以在execute()方法中第四个判断中,又会走到else中,可以看到,这个执行了sqlSession.selectOne()方法最原生的调用方式来执行SQL的。
三、sqlSession.selectOne()源码解读
上面分析到,最终执行了sqlSession.selectOne()来执行的我们的SQL语句,点进去还是选择DefaultSqlSession,进到selectOne()方法中:
其实selectOne,还是调用的selectList执行了操作,接着点到selectList下


在这个方法中,statement就是接口名id,其中this.configuration.getMappedStatement就是根据id获取SQL信息:

mappedStatements容器是如何存放对象的可以看下上篇博客中讲解的。
在获取到SQL信息,使用executor.query()方法做了查询,还记得在上面获取执行器的时候,获取的是CachingExecutor执行器,所以这个executor.query()方法走的也是CachingExecutor下的query()方法:
不错,上面的缓存就是mybatis的二级缓存的执行逻辑的地方,如果没有指定缓存对象ms.getCache()也就为null,二级缓存也不会生效,所以要二级缓存生效,可以自己实现一个缓存类实现Cache注册到mybatis中,或者在xml中加上<cache></cache>也会生效但此时缓存容器是个HashMap ,这个缓存key可以看下:
这里如果缓存中没有数据,就会走到delegate.query()方法,注意这里将key也传递了下去,点到delegate.query()这个方法,到BaseExecutor下的query方法下面:
可以看到上面圈出来的代码,又是查询缓存,对这是一级缓存的逻辑,可以看到一级缓存在BaseExecutor下,二级还存在CachingExecutor下,上面代码,如果查询缓存为空的话,会走queryFromDatabase()方法:
在方法开始先在缓存中存入了一个占位符,然后执行doQuery()方法拿数据库中的信息,然后将原先的占位符清除掉,然后将查询的数据添加到缓存中,下面看下doQuery()方法,在上面有提到创建执行器的时候,先创建的 SimpleExecutor() 简单执行器,然后又根据cacheEnabled判断是否创建CachingExecutor缓存执行器,所以这个doQuery()方法会走SimpleExecutor()执行器下的doQuery()方法:
SimpleExecutor()类下的doQuery()方法,是不是有个熟悉的对象Statement java原声操作SQL的API,下面再看handler.query():

就是通过Statement对象执行了我们配置在Mapper.xml中的SQL,然后获取SQL返回的数据并返回给上一级。
到现在源码分析的差不多了,虽然只讲解了一部分源码,但是不是也学到了很多东西。

浙公网安备 33010602011771号