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 又提供了一个MapperMethod
Mapper执行对象,来帮助我们执行下面的操作,并且在这里中为了性能提升,还对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返回的数据并返回给上一级。
到现在源码分析的差不多了,虽然只讲解了一部分源码,但是不是也学到了很多东西。