【源码系列】MyBatis原理源码

2年前 (2022) 程序员胖胖胖虎阿
341 0 0

系列文章目录

【源码系列】MyBatis与Spring整合原理源码

文章目录

  • 系列文章目录
  • 前言
  • 一、简介
    • 原生Jdbc查询代码
    • Mybatis查询代码
  • 二、MyBatis中重要组件
  • 三、Mybatis原理源码
    • 1.准备工作
      • 1.创建Configuration对象
        • 1.1 Configuration对象源码总结
        • 1.2 Configuration对象源码分析
      • 2.获取SqlSessionFactory对象
      • 3.获取SqlSession对象
      • 4.获取Mapper接口代理对象
      • 4.准备阶段总结
    • 2. 执行查询过程中源码
      • 1.插件调用相关代码
      • 2.缓存相关代码
      • 3.原生Jdbc查询代码
      • 4. 结果集的处理
      • 5.执行查询过程中总结
  • 四、总结

前言

Mybatis作为使用最为广泛的ORM框架,懂得如何使用是必备技能,为了我们使用的可以更加得心应手,我们有必要知道它是如何工作的,为什么我们只需要指定一个接口,它就能将数据查询回来?以及它是怎么一步步将数据查询回来的。


一、简介

我们先看下原生的Jdbc代码,相信每个同学之前都写过这段代码吧,相信大家用了MyBatis框架以后,都没再写过这些代码了吧。我们只需要通过调用Mapper接口的方法并且在mapper.xml中编写对应的sql语句来完成Jdbl的工作, 框架只是让我们使用起来更加简单,万变不离其中,核心还是这几句代码,只不过MyBatis在Jdbc的基础上进行了一些封装,让我们无需关心Connect对象的创建、Statement对象的创建、参数的设置、结果集处理等等。Mybatis将这些步骤都封装成了一个个组件去完成对应的工作。

原生Jdbc查询代码

public static void main(String[] args) throws Exception {
    Class.forName("com.mysql.jdbc.Driver");
    // 获取数据库连接对象
    Connection conn=  DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test","root","123456");

    String sql="select * from emp where empno = ?";
    // 创建Statement对象
    PreparedStatement stm = conn.prepareStatement(sql);
    // 参数设置
    stm.setInt(1, 7369);
    // 执行查询,结果返回到ResultSet
    ResultSet rs = stm.executeQuery();

    // 处理结果集
    while(rs.next()){
      System.out.println(rs.getInt("empno"));
      System.out.println(rs.getString("ename"));
    }
    conn.close();
  }

Mybatis查询代码

  public static void main(String[] args) throws Exception {
  	// 配置文件
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    // 生成SqlSessionFactory
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    // 打开回话
    SqlSession sqlSession = sqlSessionFactory.openSession();
    // 获取代理类
    EmpDao mapper = sqlSession.getMapper(EmpDao.class);
    // 通过代理类调用查询方法
    Emp empByEmpno = mapper.findEmpByEmpno(7369);
    // 关闭会话
    sqlSession.close();
  }

二、MyBatis中重要组件

这是我之前画的mybatis源码图中关于组件及其作用的一张图,先让这些组件和大家见个面,避免后面源码分析的时候,大家一脸懵逼。
【源码系列】MyBatis原理源码

三、Mybatis原理源码

1.准备工作

1.创建Configuration对象

Configuration对象作为全局的配置对象,还是挺重要的,通过上面的组件图我们可以知道SqlSessionFactory、SqlSession都包含了该对象,可以说该整个查询过程中都可能会用到该对象。
我列举一下该类中比较重要的一些属性。

1.1 Configuration对象源码总结

这一块代码量也比较多,代码其实挺简单,就是看起来比较繁琐,我先总结一下,不想深入的同学可以跳过这里的源码。

  1. 解析给定的配置文件,加载配置文件中的配置属性设置到Configuration
  2. 解析环境对象标签,设置到environment属性中,Environment对象包含了包含了TransactionFactoryDataSource
  3. 解析日志相关标签并设置logImpl实现类
  4. Configuration对象如果不指定执行器,默认为简单的执行器SimpleExecutor
  5. 解析配置的mappers包,并将每个Mapper接口生成一个MapperProxyFactory并放到mapperRegistry 属性中,方便下次用时获取。
  6. 第五步的同时,它会去解析每个mapper.xml文件的各个增删改查标签,每个语句都会构建成一个MappedStatement对象,并缓存到mappedStatements集合中。

这里涉及到了ExecutorMapperProxyFactoryMappedStatement等组件,大家可以去组件图中再看下他们的作用,多熟悉熟悉。

1.2 Configuration对象源码分析

public class Configuration {
  //环境对象,包含了TransactionFactory、DataSource
  protected Environment environment;
  // 日志对象,我们平常配置的日志类实际就是配置这个属性
  protected Class<? extends Log> logImpl;
  //默认为简单执行器
  protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
  // 所有的Mapper接口生成的MapperProxyFactory
  protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
  // 自定义插件,说一个大家都熟悉的 PageHelper分页插件
  protected final InterceptorChain interceptorChain = new InterceptorChain();
  // mapper.xml每个增删改查标签都会生成一个MappedStatement
  protected final Map<String, MappedStatement> mappedStatements;
}

我们先看下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="mapUnderscoreToCamelCase" value="true"/>
    </settings>
    <!-- 别名 -->
    <typeAliases>
        <package name="com.demo.bean"/>
    </typeAliases>
    <!-- 插件 -->
    <plugins>
      <plugin interceptor="com.demo.MyInterceptor"></plugin>
    </plugins>
    <!-- 环境对象 -->
    <environments default="development">
        <environment id="development">
            <!-- 获取事务管理器会用到 -->
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${driver}"/>
                <property name="url" value="${url}"/>
                <property name="username" value="${username}"/>
                <property name="password" value="${password}"/>
            </dataSource>
        </environment>
    </environments>
    <databaseIdProvider type="DB_VENDOR">
        <property name="MySQL" value="mysql"/>
        <property name="SQL Server" value="sqlserver"/>
        <property name="Oracle" value="oracle"/>
    </databaseIdProvider>
  <!-- mapper接口包路径 -->
    <mappers>
        <package name="com.demo.dao"/>
    </mappers>
</configuration>

创建Configuration对象核心方法org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration
我们从这里开始分析
【源码系列】MyBatis原理源码

解析mappers节点核心方法
【源码系列】MyBatis原理源码
【源码系列】MyBatis原理源码
【源码系列】MyBatis原理源码
【源码系列】MyBatis原理源码

【源码系列】MyBatis原理源码
【源码系列】MyBatis原理源码
【源码系列】MyBatis原理源码

【源码系列】MyBatis原理源码
通过解析标签中各个子属性,通过构建者模式,各个属性构建出一个MappedStatement对象

  // 解析语句(select|insert|update|delete) 和里面各个属性标签,通过这些值去生成MappedStatement
  public void parseStatementNode() {
    // 获取SQL节点的id以及databaseId属性
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");
	// 省略部分代码
    // 根据SQL节点的名称决定其SqlCommandType
    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    //是否要缓存select结果
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    //仅针对嵌套结果 select 语句适用:如果为 true,就是假设包含了嵌套结果集或是分组了,这样的话当返回一个主结果行的时候,就不会发生有对前面结果集的引用的情况。
    //这就使得在获取嵌套的结果集的时候不至于导致内存不够用。默认值:false。
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
    
    // 在解析SQL语句之前,先处理其中的include节点
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    //参数类型
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);

    //脚本语言,mybatis3.2的新功能
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    //解析之前先解析<selectKey>
    processSelectKeyNodes(id, parameterTypeClass, langDriver);
	
	// 新增操作返回主键生成器
    KeyGenerator keyGenerator;
    // 获取selectKey节点对应的SelectKeyGenerator的id
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    // SQL节点下存在SelectKey节点
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      // 根据SQL节点的useGeneratedKeys属性值、mybatis-config.xml中全局的useGeneratedKeys配置,以及是否为insert语句,决定使用的KeyGenerator接口实现
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }

    //解析成SqlSource,一般是DynamicSqlSource
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    //语句类型, STATEMENT|PREPARED|CALLABLE 的一种
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    // 获取每次批量返回的结果行数
    Integer fetchSize = context.getIntAttribute("fetchSize");
    // 获取超时时间
    Integer timeout = context.getIntAttribute("timeout");
    // 引用外部parameterMap
    String parameterMap = context.getStringAttribute("parameterMap");
    // 结果类型
    String resultType = context.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);
    // 引用外部的resultMap
    String resultMap = context.getStringAttribute("resultMap");
    // 结果集类型,FORWARD_ONLY|SCROLL_SENSITIVE|SCROLL_INSENSITIVE 中的一种
    String resultSetType = context.getStringAttribute("resultSetType");
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    if (resultSetTypeEnum == null) {
      resultSetTypeEnum = configuration.getDefaultResultSetType();
    }

    //(仅对 insert 有用) 标记一个属性, MyBatis 会通过 getGeneratedKeys 或者通过 insert 语句的 selectKey 子元素设置它的值
    String keyProperty = context.getStringAttribute("keyProperty");
    //(仅对 insert 有用) 标记一个属性, MyBatis 会通过 getGeneratedKeys 或者通过 insert 语句的 selectKey 子元素设置它的值
    String keyColumn = context.getStringAttribute("keyColumn");
    String resultSets = context.getStringAttribute("resultSets");

    // 通过MapperBuilderAssistant创建MappedStatement对象,并添加到mappedStatements集合中保存
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }


  public MappedStatement addMappedStatement(String id,SqlSource sqlSource,StatementType statementType,SqlCommandType sqlCommandType,Integer fetchSize,Integer timeout,String parameterMap, Class<?> parameterType,String resultMap, Class<?> resultType, ResultSetType resultSetType,boolean flushCache,boolean useCache,boolean resultOrdered, KeyGenerator keyGenerator,String keyProperty,String keyColumn,String databaseId,LanguageDriver lang,String resultSets) {

	// ...

    // 为id加上namespace前缀
    id = applyCurrentNamespace(id, false);
    // 是否是select语句
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

    // 建造者模式
    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
        .resource(resource).fetchSize(fetchSize).timeout(timeout).statementType(statementType).keyGenerator(keyGenerator).keyProperty(keyProperty).keyColumn(keyColumn).databaseId(databaseId)       .lang(lang).resultOrdered(resultOrdered).resultSets(resultSets).resultMaps(getStatementResultMaps(resultMap, resultType, id)).resultSetType(resultSetType).flushCacheRequired(valueOrDefault(flushCache, !isSelect)).useCache(valueOrDefault(useCache, isSelect)).cache(currentCache);

    ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    if (statementParameterMap != null) {
      statementBuilder.parameterMap(statementParameterMap);
    }

    MappedStatement statement = statementBuilder.build();
    // 添加到configuration的mappedStatements集合中
    configuration.addMappedStatement(statement);
    return statement;
  }

到这里,创建Configuration基本完成了,我没有把所有代码都拉出来,我把我们平常用到的一些重要的东西代码拉了出来。

2.获取SqlSessionFactory对象

创建SqlSessionFactory过程就比较简单,就是将得到的Configuration对象设置给了SqlSessionFactory的属性中,方便后续用时,直接从这里拿。
【源码系列】MyBatis原理源码

这里涉及到了SqlSessionFactory组件,大家可以去组件图中再看下他们的作用,多熟悉熟悉。

3.获取SqlSession对象

 // 获取数据库的会话,创建出数据库连接的会话对象(事务工厂,事务对象,执行器,如果有插件的话会进行插件的解析)
 SqlSession sqlSession = sqlSessionFactory.openSession();


  public SqlSession openSession() {
    // 获取默认的执行器类型
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
  }

通过Configuration配置的执行器类型、隔离级别、事务自动提交来实例化SqlSession对象
【源码系列】MyBatis原理源码
创建Executor方法,我们可以看到关于插件的代码,插件放在interceptorChain的集合中,有多个会对Executor进行多次动态代理,具体插件代码需要在执行之前调用,后面调用到时我会框出来。
【源码系列】MyBatis原理源码
插件最后会调到该方法中,这里我们就能清楚的看到Jdk动态代理的代码啦
【源码系列】MyBatis原理源码

这里涉及到了SqlSessionTransactionEnvironmentExecutor组件,大家可以去组件图中再看下他们的作用,多熟悉熟悉。

4.获取Mapper接口代理对象

    EmpDao mapper = sqlSession.getMapper(EmpDao.class);

通过前面的代码,我们知道再准备Configuration对象的时候,会为EmpDao创建一个MapperProxyFactory对象并缓存起来。
【源码系列】MyBatis原理源码
我们知道MapperProxyFactory是用来生成MapperProxy,MapperProxy类实现了InvocationHandler接口,我们知道它的invoke方法是用来执行代理的逻辑,所以它的invoke方法就很重要了,算是和数据库打交道的入口。后续我们会跟这个invoke方法。
【源码系列】MyBatis原理源码
【源码系列】MyBatis原理源码

4.准备阶段总结

  1. 首先创建了Configuration对象,该对象包含了整个MyBatis运行过程中需要用到的配置
  2. 准备好Configuration对象以后,开始创建SqlSessionFactory并将Configuration对象作为参数传到SqlSessionFactory
  3. 准备好SqlSessionFactory后,开始创建SqlSession对象,我们知道所有的增删改查的交互工作都是由SqlSession来完成的,它包含了Configuration对象和Executor对象,通过组件图我们知道交付工作都是交给Executor来完成的,SqlSession只是一个装饰而已。
  4. 这些都准备好以后,开始创建代理对象,代理对象的InvocationHandlerMapperProxy类,所以后续调用代理对象的方法肯定会到MapperProxy#invoke方法中。
  5. 到这里所有准备工作都做完了

2. 执行查询过程中源码

// 具体查询代码
Emp empByEmpno = mapper.findEmpByEmpno(7369);

通过上面的讲解,该方法调用肯定会调到MapperProxy#invoke方法,我们直接进去,
通过源码我们知道进入invoke方法以后,会将方法包装成组件MapperMethod,然后再生成MapperInvoker对象。
【源码系列】MyBatis原理源码
组件MapperMethod它包含了sql类型、方法的入参、结果类型等,后面MapperMethod执行execute方法会根据sql类型来执行SqlSession对应的处理方法。
【源码系列】MyBatis原理源码
我们的语句是查询单个结果的操作,只要类型这种懂了别的类型也是一样的,就不分析了,毕竟各位同学也是很棒,相信能吃透。
【源码系列】MyBatis原理源码
这里我们遇到了组件MapperMethod,我们可以理解为用mapper的查询方法的一些属性信息组装成了MapperMethod对象,与之相对相应的mapper.xml中的增删改查标签被组装成了MappedStatement

下面我们要分析SqlSession#selectOne方法,虽然这里写着是selectOne,后续实际还是调用的selectList方法,只是最后对结果做了个判断,我们来看代码,一下就能理解了,还有我们经常遇到那个存在多个结果集的异常就是在这里抛出去的
【源码系列】MyBatis原理源码

1.插件调用相关代码

说到插件,有些同学不理解它是个啥,相信大家都用过PageHelper分页插件吧,它就是用户自定义的插件,只是别人给我们封装好了插件的逻辑,我们只需要使用就行,可能很多同学平常用的时候非常好奇,为啥它能实现我们的分页功能,实际上它拦截了所有的查询操作,执行查询语句之前,加上了limit 参数。

查询之前先通过方法名称获取了方法对应的MappedStatement,然后带着参数通过Executor去执行操作操作,如果有符合要求的插件,就会先执行插件的逻辑
【源码系列】MyBatis原理源码
插件调用会先进入下面这个方法
【源码系列】MyBatis原理源码
插件逻辑执行完成后通过反射调用Executor的query方法
【源码系列】MyBatis原理源码

2.缓存相关代码

查询前,会先生成一二级缓存的key,然后判断是否开启二级缓存,开了的话先从二级缓存中获取数据,没有再去一级缓存中取,还是没有再执行数据库的查询操作。

Mybatis缓存相关知识,我直接贴一篇博客,我觉得讲的挺好的:https://www.cnblogs.com/happyflyingpig/p/7739749.html

二级缓存代码
【源码系列】MyBatis原理源码

一级缓存代码
【源码系列】MyBatis原理源码
查询数据库代码
【源码系列】MyBatis原理源码
【源码系列】MyBatis原理源码

3.原生Jdbc查询代码

【源码系列】MyBatis原理源码

4. 结果集的处理

  1. 获取结果集ResultMap对象,根据ResultMap中定义的映射规则对ResultSet进行映射
  2. 根据ResultMap类型创建一个目标结果对象,通过目标对象创建MetaObject对象
  3. 拿到目标对象属性和数据库表中字段对应的映射集合
  4. 遍历映射集合,通过类型转换器将值转换成属性目标类型
  5. 通过MetaObject设置结果对象的该属性值
  6. 将结果对象添加到ResulHandler的结果集合中
    【源码系列】MyBatis原理源码
    【源码系列】MyBatis原理源码
    【源码系列】MyBatis原理源码
    【源码系列】MyBatis原理源码
    【源码系列】MyBatis原理源码
    【源码系列】MyBatis原理源码

5.执行查询过程中总结

  1. 调用mapper的查询方法,代理类会调到MapperProxy#invoke(这个就是动态代理传入的InvocationHandler对象)方法,这里是查询过程的入口
  2. 执行的方法会被包装成MapperMethod组件,它包含了Sql类型、方法的返回值,入参类型等信息,再将得到的MapperMethod包装成了MapperMethodInvoker
  3. 根据MapperMethod中的Sql类型和返回值类型调用对应的SqlSession的增删改查方法(实际干活的是Exector
  4. 调用查询方法之前,如果有插件的话,会先执行插件代码,插件实现原理也是通过JDK动态代理来实现的,多个插件就代理多次来实现
  5. 查询方法会先判断是否开启了二级缓存(默认关闭,作用范围SqlSessionFactory,通过装饰器模式来实现二级缓存的),先从二级缓存中取数据,没有再去一级缓存(作用范围:SqlSession)中取,还是没有的话就去数据库中查
  6. 查询数据库得到的结果,通过ResultHandler组件对结果集合目标结果对象进行值映射
  7. 将结果存入一级缓存中,开了二级缓存,也放入二级缓存
  8. 整个执行过程结束

四、总结

现在我们知道,Mybatis的核心是Jdk的动态代理和各个组件,整个运行可以分为两大步骤:

1. 准备阶段
1. 解析配置文件获取全局Configuration对象,该过程中为每个Mapper接口创建MapperProxyFactory对象,
2. 有了Configuration对象以后通过构建者模式构建SqlSessionFactory
3. 通过SqlSessionFactory创建SqlSession对象,SqlSession包含了Executor(Sql的执行器),如果有插件,Executor会织入插件逻辑(通过动态代理实现)
4. 通过SqlSession对象加上动态代理,获取Mapper的代理对象(InvocationHandler是MapperProxy),MapperProxy#invoke作为执行查询时的入口
2. 查询阶段

  1. 执行的方法会被包装成MappedMethod(包含了方法的出入参和DB操作类型),再包装成MapperMethodInvoker
  2. 根据MappedMethod中的DB类型执行SqlSession对应的增删改查方法(实际调用时Executor对象的方法)
  3. 执行Executor织入的插件代码
  4. 开始查询的具体逻辑,有二级缓存先从二级缓存中取,没有从一级缓存中取,还是没有最终再查询数据库
  5. 得到查询结果后通过ResulHanler来完成结果集和目标对象的映射

终于写完了,写了两天,累死哥了。如果有写的不对的,请大佬指出,并给出意见,如果有说错了的我会修改的,最后附上我自己画的流程图,希望对大家对MyBatis源码理解有所帮助,其实MyBatis的源码还是比较好理解的,记住这些组件及功能,多看几遍它的运行流程,基本就能吃透Mybatis啦
Mybatis执行流程图:https://processon.com/view/61e6699507912906af03f2e6?fromnew=1

版权声明:程序员胖胖胖虎阿 发表于 2022年9月23日 上午12:40。
转载请注明:【源码系列】MyBatis原理源码 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...