MyBatis 源码解析

Posted 柚几哥哥

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MyBatis 源码解析相关的知识,希望对你有一定的参考价值。

文章目录

1、工作原理

1.1 初始化

1.1.1 系统启动的时候,加载解析全局配置文件和相应的映射文件

// 1.获取配置文件
InputStream in = Resources.getResourceAsStream("mybatis-config.xml");

全局配置文件:mybatis-config.xml

映射文件:mapper/*.xml

// 2.加载解析配置文件并获取 SqlSessionFactory 对象
// SqlSessionFactory 的实例我们没有通过 DefaulSqlSessionFactory 直接来获取
// 而是通过一个 Builder 对象来建造的
// SqlSessionFactory 生产 SqlSession 对象的 SqlSessionFactory 应该是单例
// 全局配置文件和映射文件 也只需要在 系统启动的时候完成加载操作
// 通过建造者模式来 构建复杂的对象: 1.完成配置文件的加载解析  2.完成 SqlSessionFactory 的创建
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);

加载解析的相关信息存储在SqlSessionFactory对象的Configuration属性里,

1.1.2 建造者模式帮助我们解决复杂对象的创建:

  1. 完成配置文件的加载解析
  2. 完成 SqlSessionFactory 的创建

1.2 处理SQL请求的流程

通过工厂得到SqlSession对象

// 3.根据 SqlSessionFactory 对象获取 SqlSession 对象
SqlSession sqlSession = factory.openSession();

1.2.1 通过sqlSession中提供的API方法来操作数据库

// 4.通过sqlSession中提供的API方法来操作数据库
List<User> list = sqlSession.selectList("com.boge.mapper.UserMapper.selectUserList");

1.2.2 获取接口的代码对象-得到的其实是 通过JDBC代理模式获取的一个代理对象

// 获取接口的代码对象-得到的其实是 通过JDBC代理模式获取的一个代理对象
UserMapper mapper = sqlSession.getMapper(UserMapper.class);

1.2.3 处理完请求之后,需要关闭会话SqlSession

//5.关闭会话
sqlSession.close();//关闭session  清空一级缓存

1.3 底层

全局配置文件的加载解析:Configuration

映射文件的加载解析:Configuration.mappedStatements

/**
     * MappedStatement 映射
     *
     * KEY:`$namespace.$id`
     */
    protected final Map<String, MappedStatement> mappedStatements = new StrictMap<>("Mapped Statements collection");

生产了DefaultSqlsession实例对象,完成了Executor对象的创建,以及二级缓存CachingExecutor的装饰,同时完成了插件逻辑的植入。

selectOne():二级缓存->一级缓存->数据库插入

    // 1. 读取配置文件,读成字节输入流,注意:现在还没解析
    InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");

    // 2. 解析配置文件,封装Configuration对象   创建DefaultSqlSessionFactory对象
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

    // 3. 生产了DefaultSqlsession实例对象   设置了事务不自动提交  完成了executor对象的创建
    SqlSession sqlSession = sqlSessionFactory.openSession();

    // 4.(1)根据statementid来从Configuration中map集合中获取到了指定的MappedStatement对象
       //(2)将查询任务委派了executor执行器
    User user =  sqlSession.selectOne("com.lagou.mapper.IUserMapper.findById",1);
    System.out.println(user);

    User user2 =  sqlSession.selectOne("com.lagou.mapper.IUserMapper.findById",1);
    System.out.println(user2);

    // 5.释放资源
    sqlSession.close();

1.3.1 原理图:

1.3.2 源码结构:

2、MyBatis中的缓存

2.1 缓存的作用

降低数据源的访问频率,从而提高数据源的处理能力,提高服务器的响应速度。

2.2 缓存的设计

2.2.1 架构设计

通过装饰者模式对Cache接口的工作做增强

装饰者作用
BlockingCache阻塞的 Cache 实现类
FifoCache基于先进先出的淘汰机制的 Cache 实现类
LoggingCache支持打印日志的 Cache 实现类
LruCache基于最少使用的淘汰机制的 Cache 实现类
ScheduledCache定时清空整个容器的 Cache 实现类
SerializedCache支持序列化值的 Cache 实现类
SoftCache软引用缓存装饰器
SynchronizedCache同步的 Cache 实现类
TransactionalCache支持事务的 Cache 实现类,主要用于二级缓存中
WeakCache弱引用缓存装饰器

2.2.2 一级缓存和二级缓存

一级缓存:session级别,默认开启
<!-- STATEMENT级别的缓存,使一级缓存,只针对当前执行的这一statement有效 -->
<setting name="localCacheScope" value="STATEMENT"/>

二级缓存:SqlSessionFactory级别(工厂/进程级别)

开启二级缓存:
  • 在mybatis配置文件中配置cacheEnabled为true

    <!-- 控制全局二级缓存,默认ture-->
    <setting name="cacheEnabled" value="true"/>
    <!-- 延迟加载的全局开关。开启时,所有关联对象都会延迟加载。默认false -->
    <setting name="lazyLoadingEnabled" value="true"/>
    
  • 在映射文件中添加cache标签,可以在cache标签中更细致的增加配置

    <!--二级缓存开启-->
    <cache />
    
  • 命名空间下的所有标签放开二级缓存

  • 可以通过在标签中添加 useCache=false 指定api不走二级缓存

mybatis-config.xml

2.2.3 缓存的处理顺序

  • 获取mapper映射文件中cache标签里的配置MappedStatement.getCache()
  • 如果cache配置不为空,从二级缓存中查找(List) TransactionalCacheManager.getObject(cache, key);
  • 如果没有值,则执行查询, Executor.query()这个查询实际也是先走一级缓存查询,
  • 一级缓存也没有的话,则进行DB查询
  • 先将查询到的结果放入缓存TransactionalCacheManager.putObject(cache, key, list),再返回结果
2.2.3.1 先在二级缓存中查找

原因:找到概率更高,性能角度。

一级缓存二级缓存
作用域SqlSession级别SqlSessionFactory级别
找到概率5%90%
    @Override
    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
            throws SQLException 

        // 从 MappedStatement 中获取 Cache,注意这里的 Cache 是从MappedStatement中获取的
        // 也就是我们上面解析Mapper中<cache/>标签中创建的,它保存在Configration中
        // 我们在初始化解析xml时分析过每一个MappedStatement都有一个Cache对象,就是这里
        Cache cache = ms.getCache();

        // 如果配置文件中没有配置 <cache>,则 cache 为空
        if (cache != null) 
            //如果需要刷新缓存的话就刷新:flushCache="true"
            flushCacheIfRequired(ms);
            if (ms.isUseCache() && resultHandler == null) 
                // 暂时忽略,存储过程相关
                ensureNoOutParams(ms, boundSql);
                @SuppressWarnings("unchecked")
                // 从二级缓存中,获取结果
                List<E> list = (List<E>) tcm.getObject(cache, key);
                if (list == null) 
                    // 如果没有值,则执行查询,这个查询实际也是先走一级缓存查询,一级缓存也没有的话,则进行DB查询
                    list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                    // 缓存查询结果
                    tcm.putObject(cache, key, list); // issue #578 and #116
                
                // 如果存在,则直接返回结果
                return list;
            
        
        // 不使用缓存,则从数据库中查询(会查一级缓存)
        return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    
2.2.3.2 Executor.query()先走一级缓存查询,一级缓存也没有的话,则进行DB查询
    /**
     * 记录嵌套查询的层级
     */
    protected int queryStack;
    /**
     * 本地缓存,即一级缓存
     */
    protected PerpetualCache localCache;

    @SuppressWarnings("unchecked")
    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException 
        ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
        // 已经关闭,则抛出 ExecutorException 异常
        if (closed) 
            throw new ExecutorException("Executor was closed.");
        
        // 清空本地缓存,如果 queryStack 为零,并且要求清空本地缓存。
        if (queryStack == 0 && ms.isFlushCacheRequired()) 
            clearLocalCache();
        
        List<E> list;
        try 
            // queryStack + 1
            queryStack++;
            // 从一级缓存中,获取查询结果
            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 - 1
            queryStack--;
        
        if (queryStack == 0) 
            // 执行延迟加载
            for (DeferredLoad deferredLoad : deferredLoads) 
                deferredLoad.load();
            
            // issue #601
            // 清空 deferredLoads
            deferredLoads.clear();
            // 如果缓存级别是 LocalCacheScope.STATEMENT ,则进行清理
            if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) 
                // issue #482
                clearLocalCache();
            
        
        return list;
    

3、如何扩展MyBatis中的缓存

3.1 架构理解

3.2 实际开发

/**
 * 永不过期的 Cache 实现类,基于 HashMap 实现类
 *
 * @author Clinton Begin
 */
public class PerpetualCache implements Cache 
    /**
     * 缓存容器
     */
    private Map<Object, Object> cache = new HashMap<>();
        @Override
    public void putObject(Object key, Object value) 
        cache.put(key, value);
    

3.2.1 自定义三级缓存

创建Cache接口的实现,重写putObject和getObject方法

在mapper映射文件中的cache标签里增加type属性,关联自定义的Cache接口的实现

<cache type="org.mybatis.caches.ehcache.EhcacheCache" />

如果未添加type,会默认读取 PERETUAL (二级缓存)

4、MyBatis中的涉及的设计模式

4.1 从整体架构设计分析

4.1.1 基础模块:

cache缓存模块:装饰器模式

Cache接口 定义了缓存的基本行为

PerpetualCache基于Cache实现,针对于缓存的功能 1.缓存数据淘汰;2.缓存数据的存放机制;3.缓存数据添加是否同步【阻塞】;4.缓存对象是否同步处理…做了增强处理–>代理模式

以及很多装饰类,灵活增强出适用于不同业务场景的Cache实现

logging日志模块:适配器模式、策略模式、代理模式

帮助我们适配不同的日志框架

Log接口针对不同日志框架,有不同的实现类,做增强处理

reflection反射模块:工程模式、装饰器模式
datasource数据源:工程模式
transaction事务模块:工厂模式
SqlSessionFactory:SqlSessionFactoryBuilder建造者模式

5、谈谈你对SqlSessionFactory的理解

  • 目的:创建SqlSession对象
  • 单例,在应用程序(服务)中只保存唯一的一份
  • SqlSessionFactory对象的创建是通过SqlSessionFactoryBuilder,
  • 同时也完成了全局配置文件Configuration和相关映射文件Mapper的加载和解析操作。
  • 涉及到了工厂模式和建造者模式
    /**
     * 构造 SqlSessionFactory 对象
     *
     * @param reader Reader 对象
     * @param environment 环境
     * @param properties Properties 变量
     * @return SqlSessionFactory 对象
     */
    @SuppressWarnings("Duplicates")
    public SqlSessionFactory build(Reader reader, String environment, Properties properties) 
        try 
            // 创建 XMLConfigBuilder 对象
            XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
            // 执行 XML 解析
            // 创建 DefaultSqlSessionFactory 对象
            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.
            
        
    

    /**
    * 1.我们最初调用的build
    */
    public SqlSessionFactory build(InputStream inputStream) 
        //调用了重载方法
        return build(inputStream, null, null);
    

    /**
     * 解析 XML
     *
     * 具体 MyBatis 有哪些 XML 标签,参见 《XML 映射配置文件》http://www.mybatis.org/mybatis-3/zh/configuration.html
     *
     * @param root 根节点
     */
    private void parseConfiguration(XNode root) 
        try 
            //issue #117 read properties first
            // 解析 <properties /> 标签
            propertiesElement(root.evalNode("properties"));
            // 解析 <settings /> 标签
            Properties settings = settingsAsProperties(root.evalNode("settings"));
            // 加载自定义的 VFS 实现类
            loadCustomVfs(settings);
            // 解析 <typeAliases /> 标签
            typeAliasesElement(root.evalNode("typeAliases"));
            // 解析 <plugins /> 标签
            pluginElement(root.evalNode("plugins"));
            // 解析 <objectFactory /> 标签
            objectFactoryElement(root.evalNode("objectFactory"));
            // 解析 <objectWrapperFactory /> 标签
            objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
            // 解析 <reflectorFactory /> 标签
            reflectorFactoryElement(root.evalNode("reflectorFactory"));
            // 赋值 <settings /> 到 Configuration 属性
            settingsElement(settings);
            // read it after objectFactory and objectWrapperFactory issue #631
            // 解析 <environments /> 标签
            environmentsElement(root.evalNode("environments"));
            // 解析 <databaseIdProvider /> 标签
            databaseIdProviderElement(root.evalNode("databaseIdProvider"));
            // 解析 <typeHandlers /> 标签
            typeHandlerElement(root.evalNode("typeHandlers"));
            // 解析 <mappers /> 标签
            mapperElement(root.evalNode("mappers"));
         catch (Exception e) 
            throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        
    

6、谈谈你读SqlSession的理解

6.1 SqlSession

  • 作用:通过相关API来实现对应的数据的操作

  • SqlSession对象的获取需要通SqlSessionFactory来实现,

  • 作用域是会话级别,当一个新的会话到来的时候,需要新建一个SqlSession对象;当一个会话结束后,需要关闭相关会话资源

  • 处理请求的方式:

    1.通过相关crud的API直接处理

    2.通过getMapper(xx.xml)来获取相关mapper接口的代理对象来处理

6.2 SqlSession的安全问题

6.2.1 非线程安全:

6.2.2 Spring中是如何解决DefaultSqlSession的数据安全问题?

  • DefaultSqlSession是非线程安全的,也就意味着我们不能把DefaultSqlSession声明在成员变量中。
  • 每个线程都应该有自己的SqlSession实例。
  • 最佳作用域是请求或方法作用域
  • 决不能将SqlSession实例引用放在一个类的静态域,甚至一个类的实例变量也不行。
  • 应该将SqlSession放在一个和HTTP请求相似的作用域中,每次请求打开一个SqlSession,返回一个响应后就关闭他,关闭操作放在finally块中。

  • Spring中提供了SqlSessionTemplate来实现SqlSession的相关定义。
  • 其中每一个方法都通过SqlSessionProxy来操作,这是一个动态代理对象。
  • 在动态代理对象中通过方法级别的DefaultSqlSession来实现相关的数据库操作。

7、谈谈你对MyBatis的理解

  • 使用频率最高的ORM框架、持久层框架
  • 提供了非常方便的API实现CRUD
  • 支持灵活的缓存处理方案,一级缓存、二级缓存、三级缓存
  • 支持相关的延迟数据加载处理
  • 还提供了非常多的灵活标签,来实现复杂的业务处理,if forech where trim set bind…
  • 相比Hibernate(全自动化)会更加灵活

8、谈谈MyBatis中分页的理解

8.1 谈谈分页的理解:

  • 数据库层面SQL:

    mysql:LIMIT

    Oracle:rowid

8.2 分页的实现

8.2.1 逻辑分页:RowBounds

8.2.2 物理分页:拦截器实现,执行分页语句的组装

9、谈谈MyBatis中的插件原理

9.1 插件设计的目的:

方便开发人员实现对MyBatis功能的增强

设计中MyBatis允许映射语句执行过程中的某一点进行拦截调用,允许使用插件拦截的方法包括:

9.2 实现原理:

9.2.1 创建自定义Java类,通过@Interceptor注解来定义相关的方法签名

9.2.2 在对应的配置文件中通过plugin来注册自定义的拦截器

    <plugins>
        <plugin interceptor="com.github.pagehelper.PageHelper">
            <property name="dialect" value="mysql"/>
        </plugin>
    </plugins>

9.2.3 拦截器的作用

  • 检查执行的SQL
  • 对执行SQL的参数做处理
  • 对查询的结果做装饰处理
  • 对查询SQL做分表处理

10、不同Mapper中的id是否可以相同?

可以相同,每一个映射文件的namespace都会设置为对应的mapper接口的全类路径名称

保证了每个Mapper映射文件的namespace是唯一的。

11、谈谈对MyBatis架构设计的理解

11.1 接口层

面向开发者,提供相关API

11.2 核心层

核心功能的实现:增删改查操作

11.3 基础模块

支撑核心层来完成核心的功能

关注“Java后端技术全栈”

回复“面试”获取全套面试资料
本文:12006字,阅读时长:10分15秒

前面已经发过Mybatis源码解析的文章了,本文是对前面文章进行精简以及部分调整优化,总结出来的一篇万字Mybatis源码分析。

主要内容

我们从一个简单案例入手,接着就是一步一步的剥开Mybatis的源码,大量的图文结合。

Mybatis使用案例

添加mybatis和MySQL相关pom依赖。

<!-- Mybatis依赖 -->
<dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.5.2</version>
</dependency>
<!-- MySQL依赖 -->
<dependency>
       <groupId>mysql</groupId>
       <artifactId>mysql-connector-java</artifactId>
       <version>8.0.16</version>
       <scope>runtime</scope>
</dependency>

本地创建数据库,创建一张表。同时可以初始化几条数据,方便后面debug。

CREATE TABLE `t_user` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(255CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
  `pwd` varchar(255DEFAULT NULL,
  `gender` int DEFAULT NULL,
  `age` int DEFAULT NULL,
  PRIMARY KEY (`id`)
ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

实体类(entity):

public class User {
   private Integer id;
   private String userName;
   private Integer age;
   private Integer gender;
   //省略.....
}

创建UserMapper.java接口:

public interface UserMapper {  
  User selectById(Integer id);
}

创建UserMapper.xml配置:

<?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.tian.mybatis.mapper.UserMapper">
    <resultMap id="User" type="com.tian.mybatis.entity.User">
        <id column="id" property="id"/>
        <result column="name" property="userName"/>
    </resultMap> 
    <select id="selectById" resultMap="User">
        select * from t_user
        <where>
            <if test="id != null">
                id = #{id}
            </if>
        </where>
    </select>
</mapper>

创建mybatis-config.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"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/tian?useUnicode=true"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper  resource="mapper/UserMapper.xml"/>
    </mappers>
</configuration>

使用案例:

public class MybatisApplication {
    public static final String URL = "jdbc:mysql://localhost:3306/mblog?useUnicode=true";
    public static final String USER = "root";
    public static final String PASSWORD = "123456";

    public static void main(String[] args) {
        String resource = "mybatis-config.xml";
        InputStream inputStream = null;
        SqlSession sqlSession = null;
        try {
            //读取mybatis-config.xml
            inputStream = Resources.getResourceAsStream(resource);
            //解析mybatis-config.xml配置文件,创建sqlSessionFactory
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            //创建sqlSession
            sqlSession = sqlSessionFactory.openSession();
            //创建userMapper对象(UserMapper并没有实现类)
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
            //调用userMapper对象的方法
            User user = userMapper.selectById(1);
            System.out.println(user);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //关闭资源
            try {
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            sqlSession.close();
        }
    }
}

运行输出:

User{id=1, userName='田维常', age=23, gender=1}

就这么简单的搞定了,下面我们来详细的分析这个案例背后的设计原理。

猜想Mybatis是如何设计的

从上面的案例中,我们可以大致可以猜测到Mybatis一共做了哪些步骤。

1.定位到mybatis-config.xml并读取装载。获取输入流InputStream。

2.解析输入流InputStream,把mybatis-config.xml配置文件中相关配置项解析,校验,保存起来。

3.创建sqlSessionFactory对象,在我们的印象里,session就是一次会话,所以我们可以理解sqlSessionFactory就是个工厂类,就专门创建sqlSession对象,并且这个sqlSessionFactory工厂类是唯一不变的(单例)。

4.创建sqlSession,SqlSession中肯定保存了配置文件内容信息和执行数据库相关的操作。

5.获取userMapper对象,但是UserMapper是接口,并且没有实现类。怎么就可以调用其方法呢?这里猜想可能用到了动态代理。

6.userMapper接口中的方法是如何关联到SQL的,这个猜想可能是有个专门映射的类,另外,肯定使用到了接口全路径名+方法名称,这个才能确保方法和SQL关联(主要是使用的时候,都是方法名必须和SQL中statementId一致,由此猜想的)。

7.最后底层使用JDBC去操作数据库。

8.作为一个持久化框架,很有可能会使用到缓存,用来存储每次查询数据。

仅仅是个人假设不清楚源码的情况,仅仅从这个简单的案例出发的,案例中没有差距、缓存,但是下面源码分析中是有的。

面试中遇到,让你来设计一个Mybatis如何设计?

配置文件解析,并保存于Configuration中

因为第一步是读取mybatis-config.xml配置文件,这里我们就没必要阅读这部分源码了,这里得到是InputStream输入流。接下来就是基于这个输入流进行一系列牛逼的操作。

我们将从下面这行代码开始:

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

SqlSessionFactory没有构造方法,那么这里使用的就是默认无参构造方法,所以我们直接进去build方法。

//这个方法啥也没干  
public SqlSessionFactory build(InputStream inputStream) {
    //调用的是另外一个build方法
    return build(inputStream, nullnull);
 }
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      //创建一个XMLConfigBuilder对象  
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      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);
 }
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.
      }
    }
 }

该类中的build重载的方法如下:

怒肝一夜 | Mybatis源码深度解析

SqlSessionFactory中提供了三种读取配置信息的方法后:字节流、字符流和Configuration配置类。

创建XMLConfigBuilder对象,这个类是BaseBuilder的子类,BaseBuilder类图:

怒肝一夜 | Mybatis源码深度解析

看到这些子类基本上都是以Builder结尾,所以这里使用的是建造者设计模式

这个类名可以猜出给类就是解析xml配置文件的。然后我们继续进入

public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
 this(new XPathParser(inputStream,...);
}

Mybatis对应解析包org.apache.ibatis.parsing:

怒肝一夜 | Mybatis源码深度解析

XPathParser基于 Java XPath 解析器,用于解析 MyBatis中

  • mybatis-config.xml
  • mapper.xml

等 XML 配置文件 。

XPathParser主要内容:

怒肝一夜 | Mybatis源码深度解析

继续上面的源码分析:

private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
        super(new Configuration());
        ErrorContext.instance().resource("SQL Mapper Configuration");
        this.configuration.setVariables(props);
        this.parsed = false;
        this.environment = environment;
        this.parser = parser;
}

构造一个XMLConfigBuilder对象,给属性设置相应值。

然后我们再回到SqlSessionFactoryBuilder中的build方法里:

XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
build(parser.parse());

先看parser.parse()方法:

public Configuration parse() {
  if (parsed) {
   throw new BuilderException("Each XMLConfigBuilder can only be used once.");
  }
  parsed = true;
  //mybatis-config.xml的一级标签
  parseConfiguration(parser.evalNode("/configuration"));
  return configuration;
}

继续parseConfiguration()方法:

private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      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"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

结合 mybatis-config.xml配置文件和解析方法,可以得出如何关联:

怒肝一夜 | Mybatis源码深度解析

mybatis-config.xml中如果把 标签当做一级标签,那么有多少个二级标签可与定义呢?

在org.apache.ibatis.builder.xml下的mybatis-3-config.dtd中已经定义了

    <!ELEMENT configuration (properties?, settings?, typeAliases?, typeHandlers?, objectFactory?, objectWrapperFactory?, reflectorFactory?, plugins?, environments?, databaseIdProvider?, mappers?)>

与之对应的mybatis-config.xsd中

 <xs:element name="configuration">
    <xs:complexType>
      <xs:sequence>
        <xs:element minOccurs="0" ref="properties"/>
        <xs:element minOccurs="0" ref="settings"/>
        <xs:element minOccurs="0" ref="typeAliases"/>
        <xs:element minOccurs="0" ref="typeHandlers"/>
        <xs:element minOccurs="0" ref="objectFactory"/>
        <xs:element minOccurs="0" ref="objectWrapperFactory"/>
        <xs:element minOccurs="0" ref="reflectorFactory"/>
        <xs:element minOccurs="0" ref="plugins"/>
        <xs:element minOccurs="0" ref="environments"/>
        <xs:element minOccurs="0" ref="databaseIdProvider"/>
        <xs:element minOccurs="0" ref="mappers"/>
      </xs:sequence>
    </xs:complexType>
 </xs:element>

同理,我们最关心的Mapper.xml中能定义哪些标签,也在mybatis-3-mapper.dtd中也定义了,另外也有与之对应的

mybatis-mapper.xsd文件中也能找到:

怒肝一夜 | Mybatis源码深度解析

关于Mybatis中标签相关就介绍到此。关于Mybatis配置文件中有哪些标签现在是不是觉得很轻松就能找到了?其实在IDEA中也会提示的。

下面我们来看看这些标签内容是如何存入configuration对象中?(这里例举部分,挑几个相对重要的)。

propertiesElement()方法:

怒肝一夜 | Mybatis源码深度解析

typeAliasesElement()方法:

怒肝一夜 | Mybatis源码深度解析

插件plugins解析

pluginElement()方法:

private void pluginElement(XNode parent) throws Exception {
  if (parent != null) {
    //可以定义多个插件  
    for (XNode child : parent.getChildren()) {
      String interceptor = child.getStringAttribute("interceptor");
      Properties properties = child.getChildrenAsProperties();
      Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
      interceptorInstance.setProperties(properties);
      configuration.addInterceptor(interceptorInstance);
    }
  }
}

Configuration中interceptorChain用来存储所有定义的插件。

//interceptorChain中有个List<Interceptor> interceptors 
protected final InterceptorChain interceptorChain = new InterceptorChain();
//存入interceptors中
public void addInterceptor(Interceptor interceptor) {
    interceptorChain.addInterceptor(interceptor);
 }

InterceptorChain插件链(连接链),责任链模式。

public class InterceptorChain {
  private final List<Interceptor> interceptors = new ArrayList<>();
  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }

  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }
}

我们继续看看Mapper.xml是如何解析的。

mapper.xml解析

我们的Mapper.xml在mybatis-config.xml中的配置是这样的:

怒肝一夜 | Mybatis源码深度解析

使用方式有以下四种:

<--! 1使用类路径 -->
<mappers>
    <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
      <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
   <mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
<--! 2使用绝对url路径 -->
<mappers>
   <mapper url="file:///var/mappers/AuthorMapper.xml"/>
   <mapper url="file:///var/mappers/BlogMapper.xml"/>
   <mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>
<--! 3使用java类名 -->
<mappers>
   <mapper class="org.mybatis.builder.AuthorMapper"/>
   <mapper class="org.mybatis.builder.BlogMapper"/>
   <mapper class="org.mybatis.builder.PostMapper"/>
</mappers>

<--! 4自动扫描包下所有映射器 -->
<mappers>
   <package name="org.mybatis.builder"/>
</mappers>

源码分析:

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        //自动扫描包下所有映射器
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          //放到配置对象configuration中  
          configuration.addMappers(mapperPackage);
        } else {
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          //使用java类名
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
             //根据文件存放目录,读取XxxMapper.xml
            InputStream inputStream = Resources.getResourceAsStream(resource);
             //映射器比较复杂,调用XMLMapperBuilder
            //注意在for循环里每个mapper都重新new一个XMLMapperBuilder,来解析
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse();
          //使用绝对url路径
          } else if (resource == null && url != null && mapperClass == null) {
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            //映射器比较复杂,调用XMLMapperBuilder
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          //使用类路径    
          } else if (resource == null && url == null && mapperClass != null) {
            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.");
          }
        }
      }
    }
  }

  public BaseBuilder(Configuration configuration) {
    this.configuration = configuration;
    this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
    this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
  }

  private XMLMapperBuilder(.....) {
    super(configuration);
    this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
    this.parser = parser;
    this.sqlFragments = sqlFragments;
    this.resource = resource;
  }

把这些UserMapper类似接口保存到configuration对象中。

configuration.addMapper(mapperInterface);

到这里,配置文件mybatis-config.xml和我们定义映射文件XxxMapper.xml就全部解析完成。

关于其他配置项,解析方式类似,最终都保存到了一个Configuration大对象中。

Configuration对象类似于单例模式,就是整个Mybatis中只有一个Configuration对象。

回到SqlSessionFactoryBuilder类

前面讲到了XMLConfigBuilder中的parse方法,并返回了一个Configuration对象。

build(parser.parse());

这个build方法就是传入一个Configuration对象,然后构建一个DefaultSqlSession对象。

public SqlSessionFactory build(Configuration config) {
  return new DefaultSqlSessionFactory(config);
}

继续回到我们的demo代码中这一行代码里。

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

这一行代码就相当于:

SqlSessionFactory sqlSessionFactory = new new DefaultSqlSessionFactory();

到此。配置文件解析完毕。

配置文件解析流程

怒肝一夜 | Mybatis源码深度解析

既然已经获取到了SqlSessionFactory,那么我们就可以构建SqlSession了。下面我们来看看构建SqlSession的整个过程。

构建SqlSession

前面已经做了配置文件的解析,那么现在我们来构建SqlSession。

sqlSession = sqlSessionFactory.openSession();

前面已经分析了,这里的sqlSessionFactory是DefaultSqlSessionFactory。那么此时调用的openSession()方法为DefaultSqlSessionFactory中的方法。

public class DefaultSqlSessionFactory implements SqlSessionFactory {
  //配置文件所有内容
  private final Configuration configuration;
  //创建session
  @Override
  public SqlSession openSession() {
    //调用的是另外一个openSessionFromDataSource方法
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), nullfalse);
  }
  //其实是调用这个方法
  //protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      //对应xml标签<environments> ,这个在配置文件解析的时候就已经存放到configuration中了。
      final Environment environment = configuration.getEnvironment();
      //构建事务工厂
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      //构建一个失误对象对象  
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      //创建一个executor来执行SQL  
      final Executor executor = configuration.newExecutor(tx, execType);
      //创建一个DefaultSqlSession对象并返回
      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();
    }
  }
  
    private TransactionFactory getTransactionFactoryFromEnvironment(Environment environment) {
    if (environment == null || environment.getTransactionFactory() == null) {
      return new ManagedTransactionFactory();
    }
    return environment.getTransactionFactory();
  }

创建事务Transaction

Transaction类图:

怒肝一夜 | Mybatis源码深度解析

事务工厂类型可以配置为JDBC类型或者MANAGED类型。

  • JdbcTransactionFactory生产JdbcTransaction。

  • ManagedTransactionFactory生产ManagedTransaction。

如果配置的JDBC,则会使用Connection对象的commit()、rollback()、close()方法来管理事务。

如果我们配置的是MANAGED,会把事务交给容器来管理,比如JBOSS,Weblogic。因为我们是本地跑的程序,如果配置成MANAGED就会不有任何事务。

但是,如果是Spring+Mybatis,则没有必要配置,因为我们会直接在applicationContext.xml里配置数据源和事务管理器,从而覆盖Mybatis的配置。

把事务传给newExecutor()方法创建执行器Executor对象。

configuration.newExecutor(tx, execType)

创建Executor

调用configuration的newExecutor方法创建Executor。

final Executor executor = configuration.newExecutor(tx, execType);
//Configuration中
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    //第一步
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    //第二步
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    //第三步
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

此方法分三个步骤。

第一步:创建执行器

Executor的基本类型有三种:SIMPLE为默认类型。

public enum ExecutorType {
    SIMPLE, REUSE, BATCH
}

Executor类图:

怒肝一夜 | Mybatis源码深度解析

为什么要让抽象类BaseExecutor实现Executor接口,然后让具体实现类继承抽象类呢?

这就是模板方法模式的实现。

模板方法模式就是定义一个算法骨架,并允许子类为一个或者多个步骤提供实现。模板方法是得子类可以再不改变算法结构的情况下,重新定义算法的某些步骤。

抽象方法是在子类汇总实现的,每种执行器自己实现自己的逻辑,BaseExecutor最终会调用到具体的子类。

抽象方法

protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;
protected abstract List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException;
protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException;
protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql)  throws SQLException;
第二步:缓存装饰

在上面代码中的第二步

if (cacheEnabled) {
  executor = new CachingExecutor(executor);
}

如果cacheEnabled=true,会用装饰器设计模式对Executor进行装饰。

第三步:插件代理

缓存装饰完后,就会执行

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

这里会对Executor植入插件逻辑。

关于插件:

比如:分页插件中就需要把插件植入的Executor

怒肝一夜 | Mybatis源码深度解析

好了,到此,执行器创建的就搞定了。

Executor创建完毕后,就该创建DefaultSqlSession了,请看代码:

//创建一个DefaultSqlSession对象并返回
return new DefaultSqlSession(configuration, executor, autoCommit);

进入DefaultSqlSession的构造方法中:

public class DefaultSqlSession implements SqlSession {
   private final Configuration configuration;
   private final Executor executor;
   public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
     this.configuration = configuration;
     this.executor = executor;
     this.dirty = false;
     this.autoCommit = autoCommit;
   }
}

DefaultSqlSession中包含两个重要属性:

怒肝一夜 | Mybatis源码深度解析

自此,SqlSession对象构建完毕。

sqlSession = sqlSessionFactory.openSession();

这里的sqlSession就是:

sqlSession = new DefaultSqlSession();

整个构建过程:

怒肝一夜 | Mybatis源码深度解析

获取UserMapper接口的代理对象

前面我们已经Mybatis配置文件解析到获取SqlSession,下面我们来分析从SqlSession到userMapper:

UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

我们已经知道了这里的sqlSession使用的是默认实现类DefaultSqlSession。

所以,我们的故事就从这个类开始,直接进入DefaultSqlSession的getMapper方法。

//DefaultSqlSession中  
private final Configuration configuration;
//type=UserMapper.class
@Override
public <T> getMapper(Class<T> type) {
  return configuration.getMapper(type, this);
}

三个疑问:

怒肝一夜 | Mybatis源码深度解析

问题1:getMapper返回的是个什么对象?

上面可以看出,getMapper方法调用的是Configuration中的getMapper方法。然后我们进入Configuration中

//Configuration中  
protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
////type=UserMapper.class
public <T> getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
}

这里也没做什么,继续调用MapperRegistry中的getMapper:

//MapperRegistry中
public class MapperRegistry {
  //主要是存放配置信息
  private final Configuration config;
  //MapperProxyFactory 的映射
  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();

  //获得 Mapper Proxy 对象
  //type=UserMapper.class,session为当前会话
  public <T> getMapper(Class<T> type, SqlSession sqlSession) {
    //这里是get,那就有add或者put
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
   try {
      //创建实例
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }
  
  //解析配置文件的时候就会调用这个方法,
  //type=UserMapper.class
  public <T> void addMapper(Class<T> type) {
    // 判断 type 必须是接口,也就是说 Mapper 接口。
    if (type.isInterface()) {
        //已经添加过,则抛出 BindingException 异常
        if (hasMapper(type)) {
            throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
        }
        boolean loadCompleted = false;
        try {
            //添加到 knownMappers 中
            knownMappers.put(type, new MapperProxyFactory<>(type));
            //创建 MapperAnnotationBuilder 对象,解析 Mapper 的注解配置
            MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
            parser.parse();
            //标记加载完成
            loadCompleted = true;
        } finally {
            //若加载未完成,从 knownMappers 中移除
            if (!loadCompleted) {
                knownMappers.remove(type);
            }
        }
    }
}
}

MapperProxyFactory对象里保存了mapper接口的class对象,就是一个普通的类,没有什么逻辑。

在这个类里可以理解使用了单例模式methodCache(注册式单例模式),和工厂模式getMapper()。

public class MapperProxyFactory<T{
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();

  public MapperProxyFactory(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }
 public T newInstance(SqlSession sqlSession) {
  //创建MapperProxy对象
  final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
  return newInstance(mapperProxy);
}
//最终以JDK动态代理创建对象并返回
 protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
}

继续看这行重点代码:

    return mapperProxyFactory.newInstance(sqlSession);

newInstance()方法:

依然是稳稳的基于 JDK Proxy 实现,而 InvocationHandler 参数是 MapperProxy 对象。

//UserMapper 的类加载器
//接口是UserMapper
//h是mapperProxy对象
public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                       InvocationHandler h)
{
}

问题2:为什么就可以调用他的方法?

上面调用newInstance方法时候创建了MapperProxy对象,并且是当做newProxyInstance的第三个参数,所以MapperProxy类肯定实现了InvocationHandler。

进入MapperProxy类中:

//果然实现了InvocationHandler接口
public class MapperProxy<Timplements InvocationHandlerSerializable {

  private static final long serialVersionUID = -6424540398559729838L;
  private final SqlSession sqlSession;
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache;

  public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
    this.sqlSession = sqlSession;
    this.mapperInterface = mapperInterface;
    this.methodCache = methodCache;
  }
  //调用userMapper.selectById()实质上是调用这个invoke方法
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      //如果是Object的方法toString()、hashCode()等方法  
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (method.isDefault()) {
        //JDK8以后的接口默认实现方法  
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    //创建MapperMethod对象
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    //下一篇再聊
    return mapperMethod.execute(sqlSession, args);
  }
}

也就是说,getMapper方法返回的是一个JDK动态代理对象(类型是$Proxy+数字)。这个代理对象会继承Proxy类,实现被代理的接口UserMpper,里面持有了一个MapperProxy类型的触发管理类。

当我们调用UserMpper的方法时候,实质上调用的是MapperProxy的invoke方法。

怒肝一夜 | Mybatis源码深度解析

userMapper=$Proxy6@2355。

怒肝一夜 | Mybatis源码深度解析

现在我们回答前面的问题:

为什么要在MapperRegistry中保存一个工厂类,原来他是用来创建并返回代理类的。这里是代理模式的一个非常经典的应用。

MapperProxy如何实现对接口的代理?

我们知道,JDK动态代理有三个核心角色:

  • 被代理类(即就是实现类)
  • 接口
  • 实现了InvocationHanndler的触发管理类,用来生成代理对象。

被代理类必须实现接口,因为要通过接口获取方法,而且代理类也要实现这个接口。

怒肝一夜 | Mybatis源码深度解析

而Mybatis中并没有Mapper接口的实现类,怎么被代理呢?它忽略了实现类,直接对Mapper接口进行代理。

MyBatis动态代理:

怒肝一夜 | Mybatis源码深度解析

在Mybatis中,JDK动态代理为什么不需要实现类呢?

这里我们的目的其实就是根据一个可以执行的方法,直接找到Mapper.xml中statement ID ,方便调用。

最后返回的userMapper就是MapperProxyFactory的创建的代理对象,然后这个对象中包含了MapperProxy对象,

问题3:到底是怎么根据Mapper.java找到Mapper.xml的?

最后我们调用userMapper.selectUserById(),本质上调用的是MapperProxy的invoke()方法。

怒肝一夜 | Mybatis源码深度解析

如果根据(接口+方法名找到Statement ID ),这个逻辑在InvocationHandler子类(MapperProxy类)中就可以完成了,其实也就没有必要在用实现类了。

自此上面三个问题已经全部解决。

整个流程

怒肝一夜 | Mybatis源码深度解析

自此,我们已经拿到了UserMapper接口的代理对象。接下来我们就去调用这个代理对象的方法。

UserMapp中的方法和SQL关联

加油坚持,我们已经到从这一行代码开始:

User user = userMapper.selectById(1));

这一行代码搞完,也就代表着我们Mybatis源码第一遍搞完。想想就很开心,嘿嘿~,加油

继续开撸。

通过前面的分析,我们已经知道了userMapper是通过动态代理生成的代理对象,所以调用这个代理对象的任意方法都是执行触发管理类MapperProxy的invoke()方法。

分为两部分,

1- MapperProxy.invoke()到Executor.query(方法和SQL关联)。2- Executor.query到JDBC中的SQL执行

第一部分流程图:

怒肝一夜 | Mybatis源码深度解析

MapperProxy.invoke()

先来看看这个invoke()方法里有些什么逻辑。

//MapperProxy类
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
      //首先判断是否为Object本身的方法,是则不需要去执行SQL,
      //比如:toString()、hashCode()等方法。
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (method.isDefault()) {
        //判断是否JDK8以后的接口默认实现方法。
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    //<3>  
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    //<4>
    return mapperMethod.execute(sqlSession, args);
}

从缓存获取MapperMethod,这里加入了缓存主要是为了提升MapperMethod的获取速度。这个设计非常的有意思,缓存的使用在Mybatis中也是非常之多。

private final Map<Method, MapperMethod> methodCache;
private MapperMethod cachedMapperMethod(Method method) {
    return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
  }

Map的computeIfAbsent方法:根据key获取值,如果值为null,则把后面的Object的值付给key。

继续看MapperMethod这个类,定义了两个属性command和method,以及两个静态内部类。

public class MapperMethod {
 private final SqlCommand command;
 private final MethodSignature method;
 public static class SqlCommand {
    private final String name;
    private final SqlCommandType type;
    public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
      final String methodName = method.getName();
      final Class<?> declaringClass = method.getDeclaringClass();
      //获得 MappedStatement 对象
      MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
            configuration);
      // <2> 找不到 MappedStatement
      if (ms == null) {
        // 如果有 @Flush 注解,则标记为 FLUSH 类型
        if (method.getAnnotation(Flush.class) !null) {
            name = null;
            type = SqlCommandType.FLUSH;
        } else { 
            // 抛出 BindingException 异常,如果找不到 MappedStatement
            //(开发中容易见到的错误)说明该方法上,没有对应的 SQL 声明。
            throw new BindingException("Invalid bound statement (not found): "
                    + mapperInterface.getName() + "." + methodName);
        }
       //找到 MappedStatement
       } else {
        // 获得 name
        //id=com.tian.mybatis.mapper.UserMapper.selectById
        name = ms.getId();
        // 获得 type=SELECT
        type = ms.getSqlCommandType();
        //如果type=UNKNOWN
        if (type == SqlCommandType.UNKNOWN) { // 抛出 BindingException 异常,如果是 UNKNOWN 类型
            throw new BindingException("Unknown execution method for: " + name);
        }
       }
    }   
    private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
                                               Class<?> declaringClass, Configuration configuration)
 
{
      // 获得编号
      //com.tian.mybatis.mapper.UserMapper.selectById
      String statementId = mapperInterface.getName() + "." + methodName;
      //如果有,获得 MappedStatement 对象,并返回
      if (configuration.hasStatement(statementId)) {
        //mappedStatements.get(statementId);  
        //解析配置文件时候创建并保存Map<String, MappedStatement> mappedStatements中
        return configuration.getMappedStatement(statementId);
      // 如果没有,并且当前方法就是 declaringClass 声明的,则说明真的找不到
      } else if (mapperInterface.equals(declaringClass)) {
        return null;
      }
      // 遍历父接口,继续获得 MappedStatement 对象
      for (Class<?> superInterface : mapperInterface.getInterfaces()) {
        if (declaringClass.isAssignableFrom(superInterface)) {
            MappedStatement ms = resolveMappedStatement(superInterface, methodName,
                    declaringClass, configuration);
            if (ms != null) {
                return ms;
            }
        }
      }
      // 真的找不到,返回 null
      return null;
   } 
    //....
 }
public static class MethodSignature {
    private final boolean returnsMap;
    private final Class<?> returnType;
    private final Integer rowBoundsIndex;
    //....
}

怒肝一夜 | Mybatis源码深度解析

SqlCommand封装了statement ID,比如说:

com.tian.mybatis.mapper.UserMapper.selectById

和SQL类型。

public enum SqlCommandType {
UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
}

另外还有个属性MethodSignature,主要是封装的是返回值的类型和方法入参。这里我们debug看看这个MapperMethod对象返回的内容和我们案例中代码的关联。

怒肝一夜 | Mybatis源码深度解析

妥妥的,故事继续,我们接着看MapperMethod中execute方法。

MapperMethod.execute

先来看看这个方法的整体逻辑:

 public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case SELECT:
         //部分代码省略
         Object param = method.convertArgsToSqlCommandParam(args);
          //本次是QUERY类型,所以这里是重点  
          result = sqlSession.selectOne(command.getName(), param);
          if (method.returnsOptional()
              && (result == null || !method.getReturnType().equals(result.getClass()))) {
            result = Optional.ofNullable(result);
          }
        break;
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    } 
    return result;
  }

这个方法中,根据我们上面获得的不同的type(INSERT、UPDATE、DELETE、SELECT)和返回类型:

1.调用convertArgsToSqlCommandParam()将方法参数转换为SQL的参数。2.调用sqlSession的insert()、update()、dalete()、selectOne()方法。我们这个案例是查询,这里回到了DefaultSqlSession中selectOne方法中。

SqlSession.selectOne方法

继续DefaultSqlSession中的selectOne()方法:

//DefaultSqlSession中  
@Override
public <T> selectOne(String statement, Object parameter) {
    //这是一种好的设计方法
    //不管是执行多条查询还是单条查询,都走selectList方法(重点)
    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 {
      //数据库中没有数据就返回null
      return null;
    }
 }

这里调用的是selectList方法。

@Override
public <E> List<E> selectList(String statement, Object parameter) {
    return this.selectList(statement, parameter, RowBounds.DEFAULT);
 }
 @Override
 public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      //从configuration获取MappedStatement
      //此时的statement=com.tian.mybatis.mapper.UserMapper.selectById
      MappedStatement ms = configuration.getMappedStatement(statement);
      //调用执行器中的query方法
      return executor.query(...);
    } catch (Exception e) {
     //.....
    } finally {
      ErrorContext.instance().reset();
    }
 }

在这个方法里是根据statement从configuration对象中获取MappedStatement。

MappedStatement ms = configuration.getMappedStatement(statement);

在configuration中getMappedStatement方法:

//存放在一个map中的
//key是statement=com.tian.mybatis.mapper.UserMapper.selectById,value是MappedStatement
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>();  
  public MappedStatement getMappedStatement(String id) {
    return this.getMappedStatement(id, true);
  }
public MappedStatement getMappedStatement(String id, boolean validateIncompleteStatements) 
    return mappedStatements.get(id);
}

而MappedStatement里面有xml中增删改查标签配置的所有属性,包括id、statementType、sqlSource、入参、返回值等。

怒肝一夜 | Mybatis源码深度解析

到此,我们UserMapper类中的方法已经和UserMapper.xml中的sql给彻底关联起来了。继续

executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);

这里执行的是执行器Executor中的query()方法。

Executor.query()方法

这里的Executor对象是在调用openSession方法的时候创建的。

下面来看看调用执行器的query()放的整个流程(第二部分流程):

怒肝一夜 | Mybatis源码深度解析

下面我们看看具体源码是如何实现的。

CachingExecutor.query()

在CachingExecutor中

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

BoundSql中主要是SQL和参数:

怒肝一夜 | Mybatis源码深度解析

既然是缓存,我们肯定想到key-value数据结构。

下面来看看这个key生成规则:

这个二级缓存是怎么构成的呢?并且还要保证在查询的时候必须是唯一。

怒肝一夜 | Mybatis源码深度解析

也就说,构成key主要有:

方法相同、翻页偏移量相同、SQL相同、参数相同、数据源环境相同才会被认为是同一个查询。

怒肝一夜 | Mybatis源码深度解析

这里大家知道这个层面就已经阔以了。如果向更深入的搞,就得把hashCode这些扯进来了,请看上面这个张图里前面的几个属性。

处理二级缓存

首先是从ms中取出cache对象,判断cache对象是否为null,如果为null,则没有查询二级缓存和写入二级缓存的流程。

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException 
{
    Cache cache = ms.getCache();
    //判断是否有二级缓存
    if (cache != null) {
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

那么这个Cache对象是什么创建的呢?

二级缓存如何开启?

配置项:

<configuration>
 <settings>
  <setting name="cacheEnabled" value="true|false" />
 </settings>
</configuration>

cacheEnabled=true表示二级缓存可用,但是要开启话,需要在Mapper.xml内配置。

<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
<!--或者 简单方式-->
<cache/>

对配置项属性说明:

  • flushInterval="60000",间隔60秒清空缓存,这个间隔60秒,是被动触发的,而不是定时器轮询的。
  • size=512,表示队列最大512个长度,大于则移除队列最前面的元素,这里的长度指的是CacheKey的个数,默认为1024。
  • readOnly="true",表示任何获取对象的操作,都将返回同一实例对象。如果readOnly="false",则每次返回该对象的拷贝对象,简单说就是序列化复制一份返回。
  • eviction:缓存会使用默认的Least Recently Used(LRU,最近最少使用的)算法来收回。FIFO:First In First Out先进先出队列。

在解析Mapper.xml的XMLMapperBuilder类中的cacheElement()方法里。

怒肝一夜 | Mybatis源码深度解析

解析二级缓存中的标签:

怒肝一夜 | Mybatis源码深度解析

创建Cache对象:

怒肝一夜 | Mybatis源码深度解析

二级缓存处理完了,就来到BaseExecutor的query方法中。

 BaseExecutor.query()

第一步,清空缓存

if (queryStack == 0 && ms.isFlushCacheRequired()) {
    clearLocalCache();
}

queryStack用于记录查询栈,防止地柜查询重复处理缓存。

flushCache=true的时候,会先清理本地缓存(一级缓存)。

如果没有缓存会从数据库中查询

 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);

在看看这个方法的逻辑:

private <E> List<E> queryFromDatabase(...) throws SQLException {
    List<E> list;
    //使用占位符的方式,先抢占一级缓存。
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      //删除上面抢占的占位符  
      localCache.removeObject(key);
    }
    //放入一级缓存中
    localCache.putObject(key, list);
    return list;
}

先在缓存使用占位符占位,然后查询,移除占位符,将数据放入一级缓存中。

执行Executor的doQuery()方法,默认使用SimpleExecutor。

list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);

下面就来到了SimpleExecutor中的doQuery方法。

SimpleExecutor.doQuery

@Override
public <E> List<E> doQuery(....) throws SQLException {
    Statement stmt = null;
    try {
      //获取配置文件信息  
      Configuration configuration = ms.getConfiguration();
      //获取handler
      StatementHandler handler = configuration.newStatementHandler(....);
      //获取Statement
      stmt = prepareStatement(handler, ms.getStatementLog());
      //执行RoutingStatementHandler的query方法  
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
}
创建StatementHandler

在configuration中newStatementHandler()里,创建了一个StatementHandler,先得到RoutingStatementHandler(路由)。

public StatementHandler newStatementHandler() {
    StatementHandler statementHandler = new RoutingStatementHandler
    //执行StatementHandler类型的插件    
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
}

RoutingStatementHandler创建的时候是用来创建基本的StatementHandler的。这里会根据MapperStament里面的statementType决定StatementHandler类型。

怒肝一夜 | Mybatis源码深度解析

默认是PREPARED。

StatementHandler里面包含了处理参数的ParameterHandler和处理结果集的ResultHandler。

怒肝一夜 | Mybatis源码深度解析

上面说的这几个对象正式被插件拦截的四大对象,所以在创建的时都要用拦截器进行包装的方法。

怒肝一夜 | Mybatis源码深度解析

对于插件相关的,请看前面发的插件的文章。

创建Statement

怒肝一夜 | Mybatis源码深度解析

创建对象后就会执行RoutingStatementHandler的query方法。

//RoutingStatementHandler中 
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    //委派delegate=PreparedStatementHandler
    return delegate.query(statement, resultHandler);
}

这里设计很有意思,所有的处理都要使用RoutingStatementHandler来路由,全部通过委托的方式进行调用。

然后执行到PreparedStatementHandler中的query方法。

@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    //JDBC的流程了  
    ps.execute();
    //处理结果集,如果有插件代理ResultHandler,会先走到被拦截的业务逻辑中
    return resultSetHandler.handleResultSets(ps);
}

看到了ps.execute();表示已经到JDBC层面了,这时候SQL就已经执行了。后面就是调用DefaultResultSetHandler类进行处理。

到这里,SQL语句就执行完毕,并将结果集赋值并返回了。

整个流程


从调用userMapper的selectById()方法开始,到JDBC中SQL执行的整个流程图。

感兴趣的小伙伴,可以对照着这张流程图就行一步一步的debug。

总结

本文从一个案例代码出发,到解析Mybatis的配置文件,到创建SqlSession对象,到获取UserMapper接口的代理对象,到调用代理对象方法,再到让方法和SQL关联起来,最后执行SQL,返回结果集。

涉及到的设计模式:单例模式、建造者设计模式、模板方法模式、代理模式、装饰器模式等

技术难点:动态代理



以上是关于MyBatis 源码解析的主要内容,如果未能解决你的问题,请参考以下文章

MyBatis 3源码解析

Mybatis源码解析:linux查看java版本号

Mybatis源码解析:java语言是编译解释型语言

Mybatis 源码:Mybatis配置解析

Mybatis源码解析Mybatis源码体系结构

原来resultMap解析完是这样(mybatis源码)