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 源码执行原理流程

  1. 当项目启动的时候,首先读取我们配置的mybatis_config.xml文件,然后转化为InputStreamReader格式使用SqlSessionFactoryBuilder().build()方法解析XML,并且mybatis_config.xml文件只可被解析一次,如果手动再调用解析会报:Each XMLConfigBuilder can only be used once.错误。
  2. 解析mybatis_config.xml文件时,会将settings、environments、mappers等信息解析装配到Configuration对象中,供其他地方使用,同时Mapper.xml 中的信息比如namespace 接口地址会使用反射拿到Class,存放到 名为knownMappersHashMap中,还有Mapper.xml中的select|insert|update|delete 语句,会根据id为key,value为MappedStatement的对象存放到名为mappedStatements的HashMap中。
  3. 解析完XML会返回一个DefaultSqlSessionFactory 的对象。下面可以通过这个对象openSession()方法拿到一个SqlSession对象。
  4. 在获取SqlSession对象会根据 cacheEnabled的值是否创建缓存执行器 ,注意Mybatis的二级缓存默认是开启的,也就是cacheEnabled默认是为true,所以这里默认创建的是二级缓存的CachingExecutor,如果设成false,返回的便是SimpleExecutor,现在好多网上说二级缓存默认关闭需要注意下,下面看源码的时候可以看下这个变量的值,至于在项目中为何二级缓存没有生效,可以检查下xml配置文件有没有<cache></cache>声明或者是否手动把cacheEnabled设置为了false,在使用默认的二级缓存的时候 Mybatis 是采用的 HashMap作为缓存容器,在并发情况下还是比较容易发生线程安全问题,如果要使用二级缓存,可以使用Redis来缓存,配置到Mybatis中。
  5. 在上面已经拿到一个Executor执行器,通过执行器有为我们创建了一个DefaultSqlSession返回出去。
  6. 在拿到了SqlSession后便可以获取Mapper接口对象了,SqlSession中有个getMapper()方法,通过传入一个Class 便可拿到该对象,是怎么实现的呢,其实就是使用的java的动态代理技术,做了个代理类,在第二点的时候我提到了一个knownMappers的HashMap容器存放namespace中的接口Class,现在在getMapper的时候就是根据传入的Class 到knownMappers容器中取,取到之后 Mybatis 通过 MapperProxy 类实现了InvocationHandler 代理Mapper接口。
  7. 再通过SqlSessiongetMapper()方法上面已经拿到了Mybatis 给我们的代理对象,所以下面在执行对象中的方法时,肯定执行的时代理类中的invoke方法,在这个方法中 Mybatis 又提供了一个MapperMethod Mapper执行对象,来帮助我们执行下面的操作,并且在这里中为了性能提升,还对MapperMethod 做了个简单的HashMap 缓存。
  8. MapperMethod对象中,其实还是调用了sqlSession中的方法,如果是返回一个对象,就会使用sqlSessionselectOne方法执行,其实selectOne方法还是又调用的selectList。接着如果是一个查询操作,会使用Executor执行器的query方法,会先触发CachingExecutor 的query方法先查询缓存这个缓存就是二级缓存,如果没有,会再去父类的BaseExecutorquery在这边会再查询一级缓存,还是没有就会去SimpleStatementHandler这个类下的query方法使用java 原声的Connection、Statement 去查询数据库,并将查询的数据整理成List类型返回,如果List中只有一个数据,就将第0个返回出去。
  9. 上面拿到了数据库的返回结果,会先存放到一级缓存中,如果二级没有关闭然后再放到二级缓存中,然后返还给最终数据给最初的调用者。

别看Mybatis我们使用的那么简单,其实执行原理还是挺多的,其中有很多是需要我们学习和借鉴的东西,如果对上面的流程不太明白,下面便是对源码的解读,方便我们对上面流程的理解。

三、Mybatis源码解读-SqlSessionFactory初始化配置解读

在初始化SqlSessionFactory的时候,Mybatis为我们提供了SqlSessionFactoryBuilder().build(reader)方法,里面需要传一个Reader对象:
在这里插入图片描述

这个Reader对象是通过Mybatis提供的Resources.getResourceAsReader()便可以拿到,里面传一个mybatis_config.xml的地址,Resources.getResourceAsReader() 就是使用java 的ClassLoadergetResourceAsStream 方法拿到一个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变量,下面会提到它。再向下执行会走XMLConfigBuilderparse()方法:
在这里插入图片描述
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返回的数据并返回给上一级。

到现在源码分析的差不多了,虽然只讲解了一部分源码,但是不是也学到了很多东西。

posted @ 2024-12-12 10:06  CharyGao  阅读(23)  评论(0编辑  收藏  举报