谈谈你对 MyBatis工作原理 的理解

 2022-08-22
原文地址:https://blog.51cto.com/u_13272527/5421559

【文章较长,建议收藏】本文解析MyBatis 工作流程源码,以及
Mybatis系列文章:
【深度好文】谈谈你对MyBatis的理解:MyBatis整体架构
【源码解析】谈谈你对 MyBatis动态SQL 的理解:Mybatis动态sql源码解析及动态sql的执行原理
【源码解析】谈谈你对 MyBatis结果集映射和参数绑定 的理解:MyBatis结果集映射源码解析,详细分析了 handleRowValuesForSimpleResultMap() 等方法实现映射的核心步骤

目录:

MyBatis 工作原理(运行流程)

MyBatis 工作流程简述

MyBatis 工作流程源码分析

1、mybatis-config.xml 解析全流程

2、Mapper.xml 映射文件解析全流程

3、SQL 语句解析全流程

使用的设计模式


MyBatis 工作原理(运行流程)

【总】 可以把 MyBatis 的运行流程分为三大阶段:
1. 初始化阶段: 读取 XML 配置文件和注解中的配置信息,创建配置对象,并完成各个模块的初始化的工作;
2. 代理封装阶段: 封装 iBatis 的编程模型,使用 mapper 接口开发的初始化工作;
3. 数据访问阶段: 通过 SqlSession 完成 SQL 的解析,参数的映射、SQL 的执行、结果的解析过程;

MyBatis 工作流程简述

202208222303094351.png

(1)读取 MyBatis 配置文件:mybatis-config.xml 为 MyBatis 的全局配置文件,配置了 MyBatis 的运行环境等信息,例如数据库连接信息。
(2)加载映射文件。映射文件即 SQL 映射文件,该文件中配置了操作数据库的 SQL 语句,需要在 MyBatis 配置文件 mybatis-config.xml 中加载。mybatis-config.xml 文件可以加载多个映射文件,每个文件对应数据库中的一张表。
(3)构造会话工厂:通过 MyBatis 的环境等配置信息构建会话工厂 SqlSessionFactory。
(4)创建会话对象:由会话工厂创建 SqlSession 对象,该对象中包含了执行 SQL 语句的所有方法。
(5)Executor 执行器:MyBatis 底层定义了一个 Executor 接口来操作数据库,它将根据 SqlSession 传递的参数动态地生成需要执行的 SQL 语句,同时负责查询缓存的维护。
(6)MappedStatement 对象:在 Executor 接口的执行方法中有一个 MappedStatement 类型的参数,该参数是对映射信息的封装,用于存储要映射的 SQL 语句的 id、参数等信息。
(7)输入参数映射:输入参数类型可以是 Map、List 等集合类型,也可以是基本数据类型和 POJO 类型。输入参数映射过程类似于 JDBC 对 preparedStatement 对象设置参数的过程。
(8)输出结果映射:输出结果类型可以是 Map、 List 等集合类型,也可以是基本数据类型和 POJO 类型。输出结果映射过程类似于 JDBC 对结果集的解析过程。

MyBatis 工作流程源码分析

这部分我们要知道 MyBatis 初始化流程源码,MyBatis是如何加载配置文件的? Mapper.xml 映射文件是如何解析的? SQL 语句是如何解析<select><insert><delete><update> 等 SQL 语句标签)

在初始化的过程中,MyBatis 会读取 mybatis-config.xml 这个全局配置文件以及所有的 Mapper 映射.xml 配置文件,同时还会加载这两个配置文件中指定的类,解析类中的相关注解,最终将解析得到的信息转换成配置对象。 完成配置加载之后,MyBatis 就会根据得到的配置对象初始化各个模块

1、mybatis-config.xml 解析全流程

下面我们正式开始介绍 MyBatis 的初始化过程(工作过程、工作原理)。

MyBatis 初始化的第一个步骤就是加载和解析 mybatis-config.xml 这个全局配置文件 ,入口是 XMLConfigBuilder 这个 Builder 对象,它由 SqlSessionFactoryBuilder.build() 方法创建。XMLConfigBuilder 会解析 mybatis-config.xml 配置文件得到对应的 Configuration 全局配置对象,然后 SqlSessionFactoryBuilder 会根据得到的 Configuration 全局配置对象创建一个 DefaultSqlSessionFactory 对象返回给上层使用。

在 SqlSessionFactoryBuilder.build() 方法中也可以看到,XMLConfigBuilder.parse() 方法触发了 mybatis-config.xml 配置文件的解析, 其中的 parseConfiguration() 方法定义了解析 mybatis-config.xml 配置文件的完整流程 ,核心步骤如下:

解析 标签;
解析 标签;
处理日志相关组件;
解析 标签;
解析 标签;
解析 标签;
解析 标签;
解析 标签;
解析 标签;
解析 标签;
解析 标签;
解析 标签。

    public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
        try {
          XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
          // 触发了 mybatis-config.xml 配置文件的解析
          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.
          }
        }
      }

这里 创建的 XMLConfigBuilder 对象的核心功能就是解析 mybatis-config.xml 配置文件 。XMLConfigBuilder 有一部分能力继承自 BaseBuilder 抽象类,具体继承关系如下图所示:

202208222303112512.png

configuration (Configuration 类型):MyBatis 的初始化过程就是围绕 Configuration 对象展开的,我们可以认为 Configuration 是一个单例对象,MyBatis 初始化解析到的全部配置信息都会记录到 Configuration 对象中。
typeAliasRegistry (TypeAliasRegistry 类型):别名注册中心。
typeHandlerRegistry (TypeHandlerRegistry 类型):TypeHandler 注册中心。除了定义别名之外,我们在 mybatis-config.xml 配置文件中,还可以使用 <typeHandlers> 标签添加自定义 TypeHandler 实现,实现数据库类型与 Java 类型的自定义转换,这些自定义的 TypeHandler 都会记录在这个 TypeHandlerRegistry 对象中。

resolveAlias() 方法 :解析别名 ,核心逻辑是在中实现的,主要依赖于 TypeAliasRegistry 对象
resolveTypeHandler() 方法:解析 TypeHandler ,主要依赖于 TypeHandlerRegistry 对象。

    public abstract class BaseBuilder {
      protected final Configuration configuration;
      protected final TypeAliasRegistry typeAliasRegistry;
      protected final TypeHandlerRegistry typeHandlerRegistry;
    
      protected Class<?> resolveAlias(String alias) {
        return typeAliasRegistry.resolveAlias(alias);
      }
    
      protected TypeHandler<?> resolveTypeHandler(Class<?> javaType, String typeHandlerAlias) {
        if (typeHandlerAlias == null) {
          return null;
        }
        Class<?> type = resolveClass(typeHandlerAlias);
        if (type != null && !TypeHandler.class.isAssignableFrom(type)) {
          throw new BuilderException("Type " + type.getName() + " is not a valid TypeHandler because it does not implement TypeHandler interface");
        }
        @SuppressWarnings( "unchecked" ) // already verified it is a TypeHandler
        Class<? extends TypeHandler<?>> typeHandlerType = (Class<? extends TypeHandler<?>>) type;
        return resolveTypeHandler(javaType, typeHandlerType);
      }
    
      protected TypeHandler<?> resolveTypeHandler(Class<?> javaType, Class<? extends TypeHandler<?>> typeHandlerType) {
        if (typeHandlerType == null) {
          return null;
        }
        // javaType ignored for injected handlers see issue #746 for full detail
        TypeHandler<?> handler = typeHandlerRegistry.getMappingTypeHandler(typeHandlerType);
        if (handler == null) {
          // not in registry, create a new one
          handler = typeHandlerRegistry.getInstance(javaType, typeHandlerType);
        }
        return handler;
      }
    }

我们回来看 XMLConfigBuilder 这个 Builder 实现类,看看它是如何解析 mybatis-config.xml 配置文件的。

parsed(boolean 类型): 状态标识字段,记录当前 XMLConfigBuilder 对象是否已经成功解析完 mybatis-config.xml 配置文件。
parser(XPathParser 类型): XPathParser 对象是一个 XML 解析器,这里的 parser 对象就是用来解析 mybatis-config.xml 配置文件的。
environment(String 类型): 标签定义的环境名称。
localReflectorFactory(ReflectorFactory 类型): ReflectorFactory 接口的核心功能是实现对 Reflector 对象的创建和缓存。

    public class XMLConfigBuilder extends BaseBuilder {
      private boolean parsed;
      private final XPathParser parser;
      private String environment;
      private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory();
    
      public Configuration parse() {
        if (parsed) {
          throw new BuilderException("Each XMLConfigBuilder can only be used once.");
        }
        parsed = true;
        parseConfiguration(parser.evalNode("/configuration"));
        return configuration;
      }
    
      private void parseConfiguration(XNode root) {
        try {
          //issue #117 read properties first
          propertiesElement(root.evalNode("properties"));
          Properties settings = settingsAsProperties(root.evalNode("settings"));
          loadCustomVfs(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);
        }
      }
    }

我们可以看到 parseConfiguration(XNode root) 就硬核处理,简单介绍几个常用的

处理<properties>标签:<properties> 标签中解析出来的 KV 信息会被记录到一个 Properties 对象(也就是 Configuration 全局配置对象的 variables 字段),在后续解析其他标签的时候,MyBatis 会使用这个 Properties 对象中记录的 KV 信息替换匹配的占位符。

处理<settings>标签: 是否使用二级缓存、是否开启懒加载功能等,这些都是通过 mybatis-config.xml 配置文件中的 <settings> 标签进行配置的。XMLConfigBuilder.settingsAsProperties() 方法的核心逻辑就是解析 <settings> 标签,并将解析得到的配置信息记录到 Configuration 这个全局配置对象的同名属性中。

处理<mappers>标签: <mappers> 标签中会指定 Mapper.xml 映射文件的位置,通过解析 <mappers>标签,MyBatis 就能够知道去哪里加载这些 Mapper.xml 文件了。mapperElement() 方法就是 XMLConfigBuilder 处理 <mappers> 标签的具体实现,其中会初始化 XMLMapperBuilder 对象来加载各个 Mapper.xml 映射文件。同时,还会扫描 Mapper 映射文件相应的 Mapper 接口,处理其中的注解并将 Mapper 接口注册到 MapperRegistry 中。

2、Mapper.xml 映射文件解析全流程

在 mybatis-config.xml 配置文件中可以定义多个 <mapper> 标签指定 Mapper 配置文件的地址, MyBatis 会为每个 Mapper.xml 映射文件创建一个 XMLMapperBuilder 实例完成解析

与 XMLConfigBuilder 类似,XMLMapperBuilder也是具体构造者的角色,继承了 BaseBuilder 这个抽象类,解析 Mapper.xml 映射文件的入口是 XMLMapperBuilder.parse() 方法,其核心步骤如下:

1、执行 configurationElement() 方法解析整个Mapper.xml 映射文件的内容;
2、获取当前 Mapper.xml 映射文件指定的 Mapper 接口,并进行注册;
3、处理 configurationElement() 方法中解析失败的 标签;
4、处理 configurationElement() 方法中解析失败的 标签;
5、处理 configurationElement() 方法中解析失败的SQL 语句标签。

    public void parse() {
        if (!configuration.isResourceLoaded(resource)) {
          configurationElement(parser.evalNode("/mapper"));
          configuration.addLoadedResource(resource);
          bindMapperForNamespace();
        }
    
        parsePendingResultMaps();
        parsePendingCacheRefs();
        parsePendingStatements();
      }
    
      private void configurationElement(XNode context) {
        try {
          String namespace = context.getStringAttribute("namespace");
          if (namespace == null || namespace.equals("")) {
            throw new BuilderException("Mapper's namespace cannot be empty");
          }
          builderAssistant.setCurrentNamespace(namespace);
          cacheRefElement(context.evalNode("cache-ref"));
          cacheElement(context.evalNode("cache"));
          parameterMapElement(context.evalNodes("/mapper/parameterMap"));
          resultMapElements(context.evalNodes("/mapper/resultMap"));
          sqlElement(context.evalNodes("/mapper/sql"));
          buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
        } catch (Exception e) {
          throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
        }
      }

configurationElement() 方法才是真正解析 Mapper.xml 映射文件的地方 ,其中定义了处理 Mapper.xml 映射文件的核心流程:
获取 标签中的 namespace 属性,同时会进行多种边界检查;
解析 标签;
解析 标签;
解析 标签;
解析 标签;
解析
等 SQL 语句标签。虽然定义在 Mapper.xml 映射文件中,但是 这些标签是由 XMLStatementBuilder 进行解析的 ,而不再由 XMLMapperBuilder 来完成解析。

XMLStatementBuilder 解析 SQL 标签的入口方法 parseStatementNode() 方法,在该方法中首先会根据 id 属性和 databaseId 属性决定加载匹配的 SQL 标签,然后解析其中的 标签和 标签,相关的代码片段如下:

    public void parseStatementNode() {
        // 获取SQL标签的id以及databaseId属性
        String id = context.getStringAttribute("id");
        String databaseId = context.getStringAttribute("databaseId");
        // 若databaseId属性值与当前使用的数据库不匹配,则不加载该SQL标签
        // 若存在相同id且databaseId不为空的SQL标签,则不再加载该SQL标签
        if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
            return;
        }
        // 根据SQL标签的名称决定其SqlCommandType
        String nodeName = context.getNode().getNodeName();
        SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
        // 获取SQL标签的属性值,例如,fetchSize、timeout、parameterType、parameterMap、
        // resultMap、resultType、lang、resultSetType、flushCache、useCache等。
        // 这些属性的具体含义在MyBatis官方文档中已经有比较详细的介绍了,这里不再赘述
        ... ...
    
        // 在解析SQL语句之前,先处理其中的<include>标签
        XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
        includeParser.applyIncludes(context.getNode());
        // 获取SQL标签的parameterType、lang两个属性
        ... ...
    
        // 解析<selectKey>标签
        processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
        // 前面是解析<selectKey>和<include>标签的逻辑,这里不再展示
        // 当执行到这里的时候,<selectKey>和<include>标签已经被解析完毕,并删除掉了
        // 下面是解析SQL语句的逻辑,也是parseStatementNode()方法的核心
        // 通过LanguageDriver.createSqlSource()方法创建SqlSource对象
        SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    
        // 获取SQL标签中配置的resultSets、keyProperty、keyColumn等属性,以及前面解析<selectKey>标签得到的KeyGenerator对象等,
        // 这些信息将会填充到MappedStatement对象中
    
        // 根据上述属性信息创建MappedStatement对象,并添加到Configuration.mappedStatements集合中保存
        builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
                fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
                resultSetTypeEnum, flushCache, useCache, resultOrdered,
                keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
    }
    }

使用的设计模式

MyBatis 在加载配置文件、创建配置对象的时候,会使用到经典设计模式中的 建造者模式(Builder Pattern) ,正文部分(谈谈你对MyBatis的理解【建议收藏】)也有提到,在这里我们详细聊一下

202208222303139714.png

构造者模式的四个核心组件。
Product 接口: 复杂对象的接口,定义了要创建的目标对象的行为。
ProductImpl 类: Product 接口的实现,它真正要创建的复杂对象,其中实现了我们需要的复杂业务逻辑。
Builder 接口: 定义了构造 Product 对象的每一步行为。
BuilderImpl 类: Builder 接口的具体实现,其中具体实现了构造一个 Product 的每一个步骤,例如上图中的 setPart1()、setPart2() 等方法,都是用来构造 ProductImpl 对象的各个部分。在完成整个 Product 对象的构造之后,我们会通过 build() 方法返回这个构造好的 Product 对象。

使用构造者模式一般有两个目的。
第一个目的是将使用方与复杂对象的内部细节隔离,从而实现解耦的效果。使用方提供的所有信息,都是由 Builder 这个“中间商”接收的,然后由 Builder 消化这些信息并构造出一个完整可用的 Product 对象。
第二个目的是简化复杂对象的构造过程。在很多场景中,复杂对象可能有很多默认属性,这时我们就可以将这些默认属性封装到 Builder 中,这样就可以简化创建复杂对象所需的信息。

通过构建者模式的类图我们还可以看出,每个 BuilderImpl 实现都是能够独立创建出对应的 ProductImpl 对象,那么在程序需要扩展的时候,我们只需要添加新的 BuilderImpl 和 ProductImpl,就能实现功能的扩展,这完全符合“开放-封闭原则”