mybatis源码,从配置到 mappedStatement —— mapper.xml 是如何被解析的?

  • Post author:
  • Post category:其他



个人博客导航页(点击

右侧

链接


即可打开个人博客):


大牛带你入门技术栈


Ext1:本文源码解析基于 mybatis-spring-boot-starter 2.1.1,即 mybatis 3.5.3 版本。

Ext2:本文主要是对源码的讲解,着重点会是在源码上。

一、从 MybatisAutoConfiguration 说开去,mapper 文件是怎么扫描的?

我们知道配置

SqlSessionFactory

是我们集成

Mybatis

时需要用到的常客,

SqlSessionFactory

顾名思义是用来创建

SqlSession

对象的,

SqlSession

对象的重要程度不言而喻。源码中提到,

SqlSession



Mybatis

运行最重要的一个接口,通过此接口,我们可以进行我们的操作指令,获取

mapper

,管理事务等操作。


官网

给出了一个简单的配置demo,通过


SqlSessionFactoryBean


进行

sqlSessionFactory

的创建。

@Bean
public SqlSessionFactory sqlSessionFactory() {
  SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
  factoryBean.setDataSource(dataSource());
  return factoryBean.getObject();
}

我们可以拿到这个

SqlSessionBean

来进行我们一些定制化操作,比如

mybatis插件

,自定义的返回处理等等。如果我们不显式声明

SqlSessionFactory

,则会使用 mybatis-spring-boot-autoconfigure 下的这个

bean

的注册:

我们可以看到在

mybatis

里的很多定制化常客,都出现在了这里。比如,配置 mapper 文件位置的配置,我们用以下的小段代码来看的话:

-- 来自代码 org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration#sqlSessionFactory --

    if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
      factory.setMapperLocations(this.properties.resolveMapperLocations());
    }

它的实现实际上非常简单:拿到我们所有的

mapperLocations

这个数组,解析成

Resource

数组。

-- 来自代码 org.mybatis.spring.boot.autoconfigure.MybatisProperties#resolveMapperLocations --

  public Resource[] resolveMapperLocations() {
    return Stream.of(Optional.ofNullable(this.mapperLocations).orElse(new String[0]))
        .flatMap(location -> Stream.of(getResources(location))).toArray(Resource[]::new);
  }

  private Resource[] getResources(String location) {
    try {
      return resourceResolver.getResources(location);
    } catch (IOException e) {
      return new Resource[0];
    }
  }

-- application.yml中的配置 --

mybatis:
  mapper-locations: classpath*:com/anur/mybatisdemo/test/mapper/*.xml
*/

纵览一下这几者的关系,

SqlSessionFactory

是根据配置

Configuration



sqlSessionFactoryBuilder

共同创建的,如果在 spring 项目中,则会由

SqlSessionFactoryBean

来替代

SqlSessionFactoryBuilder

进行创建。

二、SqlSessionFactory 的初始化与 XMLMapperBuilder

其实上面扯了那么多,只是想引入一下

XMLMapperBuilder

。我们知道,我们的配置(比如Spring中的 application.yml),最后会被解析成

Configuration

,而

mapper.xml

文件正是依据我们的配置来进行读取的,读取到的 xml 将被读取成

Resource

文件,最后在

SqlSessionFactoryBean

初始化完毕后、也就是在创建

SqlSessionFactory

之前:

会通过

XMLMapperBuilder

完成 xml 文件的解析。


XMLMapperBuilder

在完成初始化后,调用

org.apache.ibatis.builder.xml.XMLMapperBuilder#parse

来进行真正的 mapper 文件解析:

public void parse() {
  if (!configuration.isResourceLoaded(resource)) {
    configurationElement(parser.evalNode("/mapper"));
    configuration.addLoadedResource(resource);
    bindMapperForNamespace();
  }

  parsePendingResultMaps();
  parsePendingCacheRefs();
  parsePendingStatements();
}


configurationElement

就是对我们

xml

文件的解析,通过

parser.evalNode("/mapper")

拿到我们编写的

xml



<mapper>

标签进行初步的解析,源码如下:可以看到许多熟悉的身影,比如

namespace



resultMap



select|insert|update|delete

之类的。

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. The XML location is '" + resource + "'. Cause: " + e, e);
  }
}

给一个简单的

xml

看一下

mapper

标签里面的内容方便理解,就是 <mapper xxxxxxxxx> </mapper> 之间那一大段内容,

mybaits

封装的这套

XNode

可以使得我们访问

xml

像访问

map

一样轻松:

<?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.anur.mybatisdemo.test.TrackerConfigMapper">

    <select id="getAllFollower" parameterType="hashmap" resultMap="customMap">
        select *
        from tracker_config
        where in_use = 1
        <if test="followerId != null">and user_id = #{followerId}</if>
    </select>

    <select id="getFollower" resultType="com.anur.mybatisdemo.test.pojo.TrackerConfigDO">
        select *
        from tracker_config
        where in_use = 1
        <if test="followerId != null">and user_id = #{followerId}</if>
        limit 1
    </select>

    <resultMap id="customMap" type="com.anur.mybatisdemo.test.pojo.TrackerConfigDO">
        <result column="user_d" property="userId"/>
        <result column="in_use" property="inUse"/>
        <association property="config" resultMap="customMap"/>
    </resultMap>
</mapper>

三、ResultMap 是如何解析的

方才说到,

configurationElement()

方法负责对

xml

文件进行解析,我们拿几个主要的元素出来讲讲,比如

resultMap


resultMapElements(context.evalNodes("/mapper/resultMap"));

就是解析

resultMap

的入口,同样的,先拿到

resultMap

这个 XML 节点,进入到

resultMapElements

这个方法,

resultMapElements

负责解析

xml

,最后,将解析的结果交给

ResultMapResolver

处理。

我们先忽略

ResultMapResolver

,简单看看

resultMapElement

中做了什么,对应的源码如下,大体可分为两类解析:

private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) throws Exception {
  ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
  String type = resultMapNode.getStringAttribute("type",
      resultMapNode.getStringAttribute("ofType",
          resultMapNode.getStringAttribute("resultType",
              resultMapNode.getStringAttribute("javaType"))));
  Class<?> typeClass = resolveClass(type);
  if (typeClass == null) {
    typeClass = inheritEnclosingType(resultMapNode, enclosingType);
  }
  Discriminator discriminator = null;
  List<ResultMapping> resultMappings = new ArrayList<>();
  resultMappings.addAll(additionalResultMappings);
  List<XNode> resultChildren = resultMapNode.getChildren();
  for (XNode resultChild : resultChildren) { // 循环解析子标签
    if ("constructor".equals(resultChild.getName())) {
      processConstructorElement(resultChild, typeClass, resultMappings);
    } else if ("discriminator".equals(resultChild.getName())) {
      discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
    } else {
      List<ResultFlag> flags = new ArrayList<>();
      if ("id".equals(resultChild.getName())) {
        flags.add(ResultFlag.ID);
      }
      resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
    }
  }
  String id = resultMapNode.getStringAttribute("id",
          resultMapNode.getValueBasedIdentifier());
  String extend = resultMapNode.getStringAttribute("extends");
  Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
  ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
  try {
    return resultMapResolver.resolve();
  } catch (IncompleteElementException  e) {
    configuration.addIncompleteResultMap(resultMapResolver);
    throw e;
  }
}
  • 一种是对

    resultMap

    本身属性的解析,也就是

    getStringAttribute

    ,例如当前

    resultMap



    type

    是什么,它是否开启

    autoMapping



    id

    是什么之类的。

  • 一种则是对子标签的解析,子标签的解析,则分为

    constructor



    discriminator

    、以及其他字段的解析。

如图所示:

3.1 ResultMap 中的重要成员:typeHandler



mybatis



mysql

返回的结果集

resultSet

进行解析时,

typeHandler

有着举足轻重的作用。

mysql



JdbcType

有很多,比如

BLOB



VARCHAR



DATE

等等,而我们的 java 类型(

mybatis

称之为

javaType

,或者

javaTypeClass

)也很多,还包括我们很多的 自定义的

TypeHandler

,这里就不赘述了。

那么必然存在一个问题,如何将它们一一对应上?毫无疑问,

JdbcType

可以被解析为多个

javaTypeClass

,如

VARCHAR

可以对应解析成我们的

JSON JAVA BEAN

,也可以解析为

String

等等;同样,

String

类型也可以由多个

JdbcType

解析而来,比如

DATE

类型可以经过一定规则的解析,成为

String

类型的时间。

答案就在

org.apache.ibatis.type.TypeHandlerRegistry

public TypeHandlerRegistry() {
   register(Boolean.class, new BooleanTypeHandler());
   register(boolean.class, new BooleanTypeHandler());
   register(JdbcType.BOOLEAN, new BooleanTypeHandler());
   register(JdbcType.BIT, new BooleanTypeHandler());

   register(Byte.class, new ByteTypeHandler());
   register(byte.class, new ByteTypeHandler());
   register(JdbcType.TINYINT, new ByteTypeHandler());

   register(Short.class, new ShortTypeHandler());
   register(short.class, new ShortTypeHandler());
   register(JdbcType.SMALLINT, new ShortTypeHandler());

   register(Integer.class, new IntegerTypeHandler());
   register(int.class, new IntegerTypeHandler());
   register(JdbcType.INTEGER, new IntegerTypeHandler());

   register(Long.class, new LongTypeHandler());
   register(long.class, new LongTypeHandler());

   register(Float.class, new FloatTypeHandler());
   register(float.class, new FloatTypeHandler());
   register(JdbcType.FLOAT, new FloatTypeHandler());

   register(Double.class, new DoubleTypeHandler());
   register(double.class, new DoubleTypeHandler());
   register(JdbcType.DOUBLE, new DoubleTypeHandler());
   ........


TypeHandlerRegistry

中注册了许多

javaTypeClass

->

JdbcType

的映射,内部维护了一个变量

private final Map<Type, Map<JdbcType, TypeHandler<?>>> typeHandlerMap = new ConcurrentHashMap<>();

当我们获取某个

TypeHandler

时,先根据

javaTypeClass

获取到

Map<JdbcType, TypeHandler<?>

,然后再根据

JdbcType

获取到具体的

TypeHandler

例如,对于

javaTypeClass

:

java.util.Date

来说,默认有三种映射,分别是:

null -> DateTypeHandler
"TIME" -> TimeOnlyTypeHandler
"DATE" -> DateOnlyTypeHandler

源码中,优先根据

jdbcType

获取,如果获取不到,则使用兜底的配置,也就是默认的

TypeHandler

,代码如下:

private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) {
  if (ParamMap.class.equals(type)) {
    return null;
  }
  Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = getJdbcHandlerMap(type);
  TypeHandler<?> handler = null;
  if (jdbcHandlerMap != null) {
    handler = jdbcHandlerMap.get(jdbcType); // 优先根据 `jdbcType` 获取
    if (handler == null) {
      handler = jdbcHandlerMap.get(null);// 否则获取默认的,key 为 null
    }
    if (handler == null) {
      // #591
      handler = pickSoleHandler(jdbcHandlerMap);
    }
  }
  // type drives generics here
  return (TypeHandler<T>) handler;
}

3.2 mapper.xml 配置与 typeHandler

上面只是说了

typeHandler

的获取,那么又是如何从

mapper

文件解析出我们需要的

typeHandler

呢?这里直接上结论,再一一解析。

上图看起来好像复杂,实际上解析过程十分简单,遵循以下几个获取顺序:

  • 优先获取 xml 配置的

    typeHandler

    ,自己配置的

    typeHandler

    优先级最高
  • 若果没有配置,则需要从刚才讲的那个

    TypeHandlerRegistry

    中,通过

    javaTypeClass

    +

    jdbcType

    获取
  • 如果

    javaTypeClass

    为空则使用

    Object.class

    类型作为

    javaTypeClass
  • 如果

    jdbcType

    为空则获取默认的

    typeHandler


javaTypeClass

也有自己的获取权重,顺序如下:

  • 优先获取 xml 配置的

    javaType
  • 否则根据

    property

    +

    resultType

    根据反射来获取

3.2.1 typeHandler 的获取顺序

源码也很容易看明白,先通过 xml 文件获取

typeHandler

== org.apache.ibatis.builder.xml.XMLMapperBuilder#buildResultMappingFromContext ==

String typeHandler = context.getStringAttribute("typeHandler");
Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);

比如对于下面这个xml,update_time 这一属性,会优先使用

Date2StrTypeHandler

来进行解析。

<resultMap id="customMap" type="com.anur.mybatisdemo.test.pojo.TrackerConfigDO">
    <result column="user_id" property="userId"/>
    <result column="update_time" property="updateTime" typeHandler="com.anur.mybatisdemo.Date2StrTypeHandler"/>
    <result column="in_use" property="inUse"/>
    <association property="config" resultMap="customMap"/>
</resultMap>

紧接着,如果 xml 中指定了

typeHandler

,则创建一个

typeHandler

实例,如果没有指定,则

typeHandler

会在下一步骤进行创建。


== org.apache.ibatis.builder.BaseBuilder#resolveTypeHandler(java.lang.Class<?>, java.lang.Class<? extends org.apache.ibatis.type.TypeHandler<?>>) ==

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;
}

没有手动指定

TypeHandler

,那么则会使用

javaTypeClass

+

JdbcType

共同来定位一个

TypeHandler

,也就是调用

typeHandlerRegistry.getTypeHandler(resultMapping.javaType, resultMapping.jdbcType)

这个方法,此方法在本文 2.1.1 有提到过

== org.apache.ibatis.mapping.ResultMapping.Builder#resolveTypeHandler ==

private void resolveTypeHandler() {
  if (resultMapping.typeHandler == null && resultMapping.javaType != null) {
    Configuration configuration = resultMapping.configuration;
    TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
    resultMapping.typeHandler = typeHandlerRegistry.getTypeHandler(resultMapping.javaType, resultMapping.jdbcType);
  }
}

3.2.2 javaTypeClass 的获取顺序


javaTypeClass

也是一样的道理,先是优先从 xml 中获取:

    String javaType = context.getStringAttribute("javaType");
    Class<?> javaTypeClass = resolveClass(javaType);

比如下面这个 xml 的 userId 这一属性,

javaTypeClass

就是

Integer

<resultMap id="customMap" type="com.anur.mybatisdemo.test.pojo.TrackerConfigDO">
     <result column="user_id" property="userId" javaType="java.lang.Integer"/>
     <result column="update_time" property="updateTime" typeHandler="com.anur.mybatisdemo.Date2StrTypeHandler"/>
     <result column="in_use" property="inUse"/>
     <association property="config" resultMap="customMap"/>
 </resultMap>

但是如果我们不指定,源码中则是这么处理的,通过

property

这个 xml 配置,配合我们的

resultType

,共同进行解析,还是拿上面那个 xml 为例, in_use 这个属性,由于我们没有指定

javaType

,它会通过

resultMap

中我们指定的那个 javaBean ,也就是 TrackerConfigDO 连同

property = inUse

,通过反射来进行解析。

private Class<?> resolveResultJavaType(Class<?> resultType, String property, Class<?> javaType) {
  if (javaType == null && property != null) {
    try {
      MetaClass metaResultType = MetaClass.forClass(resultType, configuration.getReflectorFactory());
      javaType = metaResultType.getSetterType(property);
    } catch (Exception e) {
      //ignore, following null check statement will deal with the situation
    }
  }
  if (javaType == null) {
    javaType = Object.class;
  }
  return javaType;
}

3.3 ResultMap 中的另一个常用属性: resultMap嵌套


resultMap

嵌套,包括使用

association

一对一的关联、

collection

一对多的管理与

discriminator

+

case

的魔幻sql语句 (感觉这么写很蛋疼)


resultMap

嵌套大体分为两种情况

  • 其一是指定直接嵌套,比如下面所示 xml 中的

    collection

    标签
  • 其二是指定另一个

    resultMap

    进行嵌套,如下所示

    association

    标签
  • (或者上面两种互相嵌套组合)

3.3.1 resultMap嵌套中的两种解析规则

3.3.1.1 其一,如果指定了显式的

resultMap

,则直接拿到它的名字

这种情况,内嵌

ResultMap

十分简单,就是直接拿到名字:

String nestedResultMap = context.getStringAttribute("resultMap",

    // 下面会运行,但是不会生成的 id 和当前的 nestedResultMap 没关系,因为我们指定了 resultMap
    processNestedResultMappings(context, Collections.emptyList(), resultType));

最后,如果你没指定

resultMap

是哪个包来的,则会给你加上前面的

namespace

public String applyCurrentNamespace(String base, boolean isReference) {
  if (base == null) {
    return null;
  }
  if (isReference) {
    // is it qualified with any namespace yet?
    if (base.contains(".")) {
      return base;
    }
  } else {
    // is it qualified with this namespace yet?
    if (base.startsWith(currentNamespace + ".")) {
      return base;
    }
    if (base.contains(".")) {
      throw new BuilderException("Dots are not allowed in element names, please remove it from " + base);
    }
  }
  return currentNamespace + "." + base;
}

例如指定了一个一对一关联:

<association property="config" resultMap="customMap"/>

,我们拿到的

id

不是

customMap

,而是

com.anur.mybatisdemo.test.TrackerConfigMapper.customMap

这种情况比较简单,这里就不赘述了(注意,这种情况也会递归解析(下面的这一小节)此标签,但是名字拿的是我们指定的名字)。

3.3.1.2 其二,如果未指定显式

resultMap

,则递归解析,拿到其 ValueBasedIdentifier ,即 id

它的递归解析上大体如下图所示:


// 方法一,如何解析一个 resultMap,以及其子节点
== org.apache.ibatis.builder.xml.XMLMapperBuilder#resultMapElement(org.apache.ibatis.parsing.XNode, java.util.List<org.apache.ibatis.mapping.ResultMapping>, java.lang.Class<?>) ==

private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) throws Exception {
   List<XNode> resultChildren = resultMapNode.getChildren();
   for (XNode resultChild : resultChildren) {// 调用方法二循环解析子节点
       resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
   }
   ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
   return resultMapResolver.resolve();
 }

// 方法二,如何解析一个子节点,如果子节点中包含 resultMap,或者 association、collection、case 等,调用方法三
 private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) throws Exception {
   String nestedResultMap = context.getStringAttribute("resultMap",
       processNestedResultMappings(context, Collections.emptyList(), resultType));
   return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
 }

// 方法三,调用方法一
 private String processNestedResultMappings(XNode context, List<ResultMapping> resultMappings, Class<?> enclosingType) throws Exception {
   if ("association".equals(context.getName())
       || "collection".equals(context.getName())
       || "case".equals(context.getName())) {
     if (context.getStringAttribute("select") == null) {
       validateCollection(context, enclosingType);
       ResultMap resultMap = resultMapElement(context, resultMappings, enclosingType);
       return resultMap.getId();
     }
   }
   return null;
 }

用图表示则为以下三个方法:


为了避免看起来很混乱,


下面将第一个解析

resultMap

标签的方法称为

resultMap解析方法



将第二个解析子标签

resultMapping

的方法称为

子标签解析方法



将第三个判断子标签有无内嵌

resultMap

如果有,则调用第一个方法的方法称为

内嵌解析方法

还是拿出我们的 xml 文件来举栗子:

<resultMap id="customMap" type="com.anur.mybatisdemo.test.pojo.TrackerConfigDO">
    <association property="config" resultMap="customMap"/>
    <collection property="configDOList" ofType="com.anur.mybatisdemo.test.pojo.TrackerConfigDO">
        <result column="role" property="role"/>
        <result column="in_use" property="inUse"/>
    </collection>
</resultMap>
  • 首先调用

    resultMap解析方法

    解析我们当前的最外层,即

    customMap

    ,id为

    xxx略xxx.mapper_customMap
  • 发现有子标签,遍历子标签

    • 子标签

      <collection>

      存在内嵌

      resultMap

      • 调用

        resultMap解析方法

        解析其内嵌

        resultMap

        (略)
      • 子标签

        <collection>

        由于显式指定了

        resultMap

        ,所以其

        内嵌id



        xxx略xxx.mapper_customMap
    • 子标签

      <association>

      没有显式指定

      resultMap

      ,故其内嵌id 从调用

      resultMap解析方法

      中来

      • 调用

        resultMap解析方法

        解析其内嵌

        resultMap
      • 发现有子标签,遍历子标签

        • 子标签

          role

          没有内嵌

          resultMap
        • 子标签

          inUse

          没有内嵌

          resultMap
      • 解析完毕,此

        resultMap


        Id



        xxx略xxx.mapper_resultMap[customMap]_collection[configDOList]
      • 子标签

        <association>

        没有显式指定

        resultMap



        内嵌id



        xxx略xxx.mapper_resultMap[customMap]_collection[configDOList]

3.3.2 ValueBasedIdentifier

刚才提到的

内嵌id

,或者

id

实际上是“一个东西”,可以理解为地址和引用之间的关系。比如说,这个

resultMap



id

叫做

customMap

,它的子标签中内嵌了一个

resultMap



内嵌id



customMap


从上面的解析我们也可以看出,我们的

ResultMap

只有持有一层结构,即使,

ResultMap

持有其所有子标签

resultMapping

,而子标签

resultMapping

对另外

ResultMap

,是通过记录其

id

的形式持有的。

这些

ValueBasedIdentifier

或者

id

,生成规则如下:

public String getValueBasedIdentifier() {
  StringBuilder builder = new StringBuilder();
  XNode current = this;
  while (current != null) {
    if (current != this) {
      builder.insert(0, "_");
    }
    String value = current.getStringAttribute("id",
        current.getStringAttribute("value",
            current.getStringAttribute("property", null)));
    if (value != null) {
      value = value.replace('.', '_');
      builder.insert(0, "]");
      builder.insert(0,
          value);
      builder.insert(0, "[");
    }
    builder.insert(0, current.getName());
    current = current.getParent();
  }
  return builder.toString();
}

代码很好理解,优先获取标签的

id

属性、其次则是

value

属性、最后是

property

属性, 如果不为空,替换一下

.

符号,避免它把你命名里面的

.

当成路径来解析,然后在左右套一个

[]

(

insert(0,val)

+

while

相当于递归 + 按顺序

append

,逻辑也更清楚)

总结规则为

_标签名[命名(可能为空)]

最后再在前面塞一个当前标签的标签名,然后

while

循环上层来向更上层命名。

也就是说打个比方,下面的 xml 最里层的这个关联标签会生成一个

mapper_resultMap[customMap]_collection[configDOList]_association[config]



ValueBasedIdentifier

<resultMap id="customMap" type="com.anur.mybatisdemo.test.pojo.TrackerConfigDO">
    <collection property="configDOList" ofType="com.anur.mybatisdemo.test.pojo.TrackerConfigDO">
        <association property="config" resultMap="customMap"/>
    </collection>
</resultMap>

3.3.3 ResultMap 解析总结

其实到现在已经很明了了,所有的

ResultMap

都会被生成一个独立的数据结构,所以无论怎么嵌套,起码在解析层面,是不会出问题的,它只会保存自己的所有子标签,用

List<ResultMapping>

表示,如果子标签中存在内嵌的

ResultMap

,则仅仅保存其

id

,并另外(递归)解析此

ResultMap

比如下面这个xml的解析结果:

    <resultMap id="customMap" type="com.anur.mybatisdemo.test.pojo.TrackerConfigDO">
        <association property="config" resultMap="customMap"/>
        <collection property="configDOList" ofType="com.anur.mybatisdemo.test.pojo.TrackerConfigDO">
            <result column="role" property="role"/>
            <result column="in_use" property="inUse"/>
        </collection>
    </resultMap>

如此图所示,虚线不是真正的关联,只是保存了一个叫做

nestedResultMapId

的属性,即

内嵌id

3.4 在解析 resultSet(查询结果集) 时是如何实现的,会不会死循环?

我们已经对

resultMap

的解析建立起了清晰的认知,那么此时还有另外一个问题,

mybatis

在对查询结果集进行解析的时候,是如何使用

resultMap

的?

虽然此部分与

mapper.xml

无关,但如果无法建立起体系,单纯的

resultMap

分析只会让人一头雾水。

实际上



resultMap

解析完成后,mybatis 会将其保存在

configuration





configuration

前面也提到过,里面保存了 mybatis 的配置,但它不仅如此,它还承担了我们

mybtais

上下文对象的作用。类似于

spring

框架中的

applicationContext

    ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
        .discriminator(discriminator)
        .build();
    configuration.addResultMap(resultMap);

目光来到我们的

ResultHandler

,我们知道

ResultHandler



SqlSession

,也是 mybatis 的核心组件之一,它负责对

ResultSet

进行解析。


解析的核心代码如下(有所删减,后续会有文章专门分析

ResultSetHandler

,所以这里只是简单提一下)

 private Object (ResultSetWrapper rsw, ResultMap resultMap, CacheKey combinedKey, String columnPrefix, Object partialObject) throws SQLException {
    final String resultMapId = resultMap.getId(); // 这个id 就是我们前面说了很久的那个 ValueBasedIdentifier
    Object rowValue = partialObject;
    if (rowValue != null) {
      final MetaObject metaObject = configuration.newMetaObject(rowValue);
      ancestorObjects.put(resultMapId, resultObject);
      applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, false);
      ancestorObjects.remove(resultMapId);
    } else {
      final ResultLoaderMap lazyLoader = new ResultLoaderMap();
      rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix); // 根据反射构建出当前resultMap的承载对象
      if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
        final MetaObject metaObject = configuration.newMetaObject(rowValue);// metaObject 是 mybatis 对对象的一套类似反射的封装,但不仅仅是反射这么简单
        boolean foundValues = this.useConstructorMappings;
        foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) 
			|| foundValues;// 解析普通的子标签属性
        ancestorObjects.put(resultMapId, resultObject);
        foundValues = applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, true)
			|| foundValues; // 解析内嵌 resultMap
        ancestorObjects.remove(resultMapId);
        foundValues = lazyLoader.size() > 0 || foundValues;
        rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
      }
      if (combinedKey != CacheKey.NULL_CACHE_KEY) {
        nestedResultObjects.put(combinedKey, rowValue);
      }
    }
    return rowValue;
  }


  // 解析内嵌 resultMap
  private boolean applyNestedResultMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String parentPrefix, CacheKey parentRowKey, boolean newObject) {
    boolean foundValues = false;
    for (ResultMapping resultMapping : resultMap.getPropertyResultMappings()) {
      final String nestedResultMapId = resultMapping.getNestedResultMapId();
      if (nestedResultMapId != null && resultMapping.getResultSet() == null) { // 如果子标签有内嵌resultMap,才会继续解析
        try {
          final String columnPrefix = getColumnPrefix(parentPrefix, resultMapping);
          final ResultMap nestedResultMap = 
			  getNestedResultMap(rsw.getResultSet(), nestedResultMapId, columnPrefix);// 拿到内嵌的resultMap
          if (resultMapping.getColumnPrefix() == null) {
            // try to fill circular reference only when columnPrefix
            // is not specified for the nested result map (issue #215)
            // 解决循环引用的核心代码
            Object ancestorObject = ancestorObjects.get(nestedResultMapId);
            if (ancestorObject != null) {
              if (newObject) {
                linkObjects(metaObject, resultMapping, ancestorObject); // issue #385
              }
              continue;
            }
          }
          Object rowValue = nestedResultObjects.get(combinedKey);
          boolean knownValue = rowValue != null;
          instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject); // mandatory
          if (anyNotNullColumnHasValue(resultMapping, columnPrefix, rsw)) {

          final CacheKey rowKey = createRowKey(nestedResultMap, rsw, columnPrefix);
          final CacheKey combinedKey = combineKeys(rowKey, parentRowKey);
            // 套娃递归~~
            rowValue = getRowValue(rsw, nestedResultMap, combinedKey, columnPrefix, rowValue);
            if (rowValue != null && !knownValue) {
              linkObjects(metaObject, resultMapping, rowValue);
              foundValues = true;
            }
          }
        } catch (SQLException e) {
          throw new ExecutorException("Error getting nested result map values for '" + resultMapping.getProperty() + "'.  Cause: " + e, e);
        }
      }
    }
    return foundValues;
  }

对我们的对象进行赋值,实际上就是来来回回调用这两个方法,那么它如何防止resultMap套自己引起的无限解析呢?

3.4.1 借助额外的 Map映射 来解决 resultMap 套娃

    <resultMap id="customMap" type="com.anur.mybatisdemo.test.pojo.TrackerConfigDO">
        <association property="config" resultMap="customMap"/>
        <collection property="configDOList" ofType="com.anur.mybatisdemo.test.pojo.TrackerConfigDO">
            <result column="role" property="role"/>
            <result column="in_use" property="inUse"/>
        </collection>
    </resultMap>

实际上很简单,我们的

resultMap

都有一个唯一的

id

,也就是我们所提到的

ValueBasedIdentifier

,在第一次进入到

getRowValue

方法时,会通过反射创建我们的

resultMap

所表示的对象,比如上面

customMap

这个

resultMap

,会创建一个

TrackerConfigDO

对象,然后在解析内嵌

resultMap

之前,有一个关键动作,将创建的对象放进

ancestorObjects

        ancestorObjects.put(resultMapId, resultObject);
        foundValues = applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, true)
			|| foundValues; // 解析内嵌 resultMap

是的,就是

ancestorObjects

这个map映射,在解析内嵌

resultMap

子标签时,发现 config 这个子标签所指向的

resultMap

是它的父亲,也就是 config 这个子标签所引用的

resultMap



ValueBasedIdentifier

与外面

id = customMap

的相同,它会把我们刚才放进

ancestorObjects

里的那个对象拿出来,然后直接continue,不再继续向下解析了。

也就是打个比方

TrackerConfigDO@9999

这个对象中,有一个成员变量叫

config

,它也指向

TrackerConfigDO@9999

            // try to fill circular reference only when columnPrefix
            // is not specified for the nested result map (issue #215)
            // 解决循环引用的核心代码
            Object ancestorObject = ancestorObjects.get(nestedResultMapId);
            if (ancestorObject != null) {
              if (newObject) {
                linkObjects(metaObject, resultMapping, ancestorObject); // issue #385
              }
              continue;
            }

如果发现不是同一个

ValueBasedIdentifier

,则是一个递归解析,它会递归调用刚才的

resultMap

解析方法:

rowValue = getRowValue(rsw, nestedResultMap, combinedKey, columnPrefix, rowValue);

和前面说的三个方法递归很像,就是它把解析的主体定义为一个

resultMap

,在

resultMap

的子标签中如果发现了内嵌

resultMap

,则进行递归,我这里就不啰嗦了。


Extra:

既然说到了这个

ancestorObjects

map,顺便提一嘴。我们知道,一对多的映射,比如一条主数据对应10条从数据,在 mysql 中查询出来时,实际上是有10条的。是我们 mybatis 把这十条合成了一条,那么它是怎么做的呢?

实际上也是通过

ancestorObject

来完成的,不过这里主要讲的是

mapper

部分相关源码,后续会有文章专门讲

ResultSetHandler

四、sql语句与 mappedStatment

饶了一大圈,视线回到我们的

XMLMapperBuilder

  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. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }

同样是拿到我们的 insert,update 等节点们:

  private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }

它的主要方法就是

statementParser.parseStatementNode();

,构造函数里面什么都没有,就是对几个成员变量进行赋值,这里就不啰嗦了。

我们看到

parseStatementNode()

,代码很长,但是我们不着急,现将它拆解成几个部分:

  • 1、属性获取,大部分代码实际上都是在对属性(attr)进行获取,比如 resultMap,resultType之类的。
  • 2、对内嵌语句 sqlFragment、SelectKey 的解析
  • 3、生成 SqlSource
  • 4、创建 mappedStatment


第一部分太简单,这里不啰嗦

。可以理解为给你一个map,或者json,各种获取值,把它存起来,除此之外没别的了,这里主要对后面几个部分进行讲解。

  public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }

    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // Include Fragments before parsing
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);

    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    // Parse selectKey after includes and remove them.
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }

    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String resultType = context.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultSetType = context.getStringAttribute("resultSetType");
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    if (resultSetTypeEnum == null) {
      resultSetTypeEnum = configuration.getDefaultResultSetType();
    }
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    String resultSets = context.getStringAttribute("resultSets");

    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

4.1 sql 内嵌语句 sqlFragment

这两个标签相信大家都熟悉,就是提高sql复用率(个人不喜欢这种写法)。有了前面

resultMap

的基础,我们很容易猜得到,这些标签在解析完以后,会生成自己一个唯一的

id

,然后存到

configuration

里面。

是的,这里也确实是这么操作的。

首先,

sqlFragment

的解析代码如下:

== org.apache.ibatis.builder.xml.XMLIncludeTransformer#applyIncludes(org.w3c.dom.Node, java.util.Properties, boolean) ==
  /**
   * Recursively apply includes through all SQL fragments.
   * @param source Include node in DOM tree
   * @param variablesContext Current context for static variables with values
   */
  private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
    if (source.getNodeName().equals("include")) {
      Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
      Properties toIncludeContext = getVariablesContext(source, variablesContext);
      applyIncludes(toInclude, toIncludeContext, true);
      if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
        toInclude = source.getOwnerDocument().importNode(toInclude, true);
      }
      source.getParentNode().replaceChild(toInclude, source);
      while (toInclude.hasChildNodes()) {
        toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
      }
      toInclude.getParentNode().removeChild(toInclude);
    } else if (source.getNodeType() == Node.ELEMENT_NODE) {
      if (included && !variablesContext.isEmpty()) {
        // replace variables in attribute values
        NamedNodeMap attributes = source.getAttributes();
        for (int i = 0; i < attributes.getLength(); i++) {
          Node attr = attributes.item(i);
          attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
        }
      }
      NodeList children = source.getChildNodes(); 
      for (int i = 0; i < children.getLength(); i++) {
        applyIncludes(children.item(i), variablesContext, included);
      }
    } else if (included && (source.getNodeType() == Node.TEXT_NODE || source.getNodeType() == Node.CDATA_SECTION_NODE)
        && !variablesContext.isEmpty()) {
      // replace variables in text node
      source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
    }
  }

我们一步步解析,这段代码有三个大的分支

  • 第一个分支顾名思义,解析

    include

    标签用的,
  • 第二个则是解析普通节点用的,
  • 第三个 else 则是代表解析

    Text

    节点、

    CDATASection

    节点。

先看看怎么解析普通节点,普通节点将节点拆成子节点,然后循环递归调用自己,这个没什么好说的,前面已经说了各种递归,这个也是一样的道理,

mybatis

解析套娃,核心就是递归。


text

节点的解析也很简单,唯一值得注意的就是

variablesContext

这个东西,它实际上是从配置文件中来,我们可以通过配置全局

variables

,它会在这个时候将其填充进去。

打个比方,下面这个

testValue

,我们在配置里面将其指定为全局变量,在写sql时,会将配置的值注入(不推荐)。

mybatis:
  configuration-properties:
    testValue: 43
	
  <select id="getAllFollower" parameterType="hashmap" resultMap="customMap">
 	 select role,in_use, id as inner_id from tracker_config where id &lt;= ${testValue}
  </select>


后面两个解析很简单,主要是我们的第一个分支,就是如何去解析

include

标签:

看到我们这块分支的代码,上来第一步,是根据名字去

configuration



include

,这个很好理解。紧接着就是一个递归,直接忽略它,前面的几个小结讲了太多递归,这里懒得再讲了。

      Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
      Properties toIncludeContext = getVariablesContext(source, variablesContext);
      applyIncludes(toInclude, toIncludeContext, true);// 递归解析 include标签

      if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
        toInclude = source.getOwnerDocument().importNode(toInclude, true);// 进行资源的引入
      }
      source.getParentNode().replaceChild(toInclude, source);// 将sql进行替换
      while (toInclude.hasChildNodes()) {
        toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
      }
      toInclude.getParentNode().removeChild(toInclude);// 移出引入sql的外标签

我们还是将其分为四个步骤

  • 1、递归调用,解析 include 标签里面可能含有的 include 标签
  • 2、判断 include 元素与当前这个节点是否是同一个文件, 如果不是同一个文件,则将其引入。这个没什么好说的 = =,一些 xml 的 api
  • 3、将include标签进行替换,也就是

    source.getParentNode().replaceChild(toInclude, source);

    ,也就是将真正的那段sql移过来。
  • 4、一个while循环 +

    toInclude.getParentNode().removeChild(toInclude);

    ,这部分实际上就是将刚才移过来那段sql的外层标签去掉,内容(childNode)拿出来。(主要是它没有 removeNodeWrapper(开玩笑的)这种方法,所以它这里采取了一种让人疑惑的写法)

我们拿这样的一段 xml 来模拟一下这个过程:

    <sql id="select">select</sql>

    <sql id="including">
        <include refid="select"/>
        *
        from tracker_config
    </sql>

    <select id="getAllFollower" parameterType="hashmap" resultMap="customMap">
	<include refid="including"/> 
	where id &lt;= ${hashmap.id}
    </select>

解析

id = getAllFollower

这个节点,它是一个普通节点,所以循环它的所有子节点,第一个子节点就是

<include refid="including"/>

,首先根据

refid

拿到真正的资源,先给阿姨倒一杯卡布奇诺,先给即将引入的这段 sql 来一个解析套餐。

解析

id="including"

,一样的规则,它是个普通节点,循环它所有子节点,第一个子节点是

<include refid="select"/>

,和上面一样,即将引入的这另一段 sql 也会被奉上解析套餐。

解析套餐三大分支都没什么可以对

<sql id="select">select</sql>

做的,于是进入到我们的 include 四个步骤的后续步骤,先是将其替换,如下所示:

    <sql id="select">select</sql>

    <sql id="including">
        <sql id="select">select</sql>
        *
        from tracker_config
    </sql>

    <select id="getAllFollower" parameterType="hashmap" resultMap="customMap">
	<include refid="including"/> 
	where id &lt;= ${hashmap.id}
    </select>

然后来到第四步,也就是去“头“,如下:

    <sql id="select">select</sql>

    <sql id="including">
        select
        *
        from tracker_config
    </sql>

    <select id="getAllFollower" parameterType="hashmap" resultMap="customMap">
	<include refid="including"/> 
	where id &lt;= ${hashmap.id}
    </select>

最里层的递归出栈,来到上层递归,也就是

<include refid="including"/>

的解析,一样的,先替换:

    <sql id="select">select</sql>

    <sql id="including">
        select
        *
        from tracker_config
    </sql>

    <select id="getAllFollower" parameterType="hashmap" resultMap="customMap">
    	<sql id="including">
      	  select
      	  *
      	  from tracker_config
  	</sql>
	where id &lt;= ${hashmap.id}
    </select>

然后去头:

    <sql id="select">select</sql>

    <sql id="including">
        select
        *
        from tracker_config
    </sql>

    <select id="getAllFollower" parameterType="hashmap" resultMap="customMap">
      	  select
      	  *
      	  from tracker_config
	where id &lt;= ${hashmap.id}
    </select>

递归完毕,

<sql id="including">

解析完毕 。

4.2 selectKey 解析

个人不太推荐

selectKey

的使用,个人感觉类似存储过程…,在一个sql里面做各种事情,例如将某个值赋值为另一个sql的结果,比如插入自增id,或者在插入完毕后,

SELECT LAST_INSERT_ID() AS xxxx

将插入主键拿到,这种需求更加推荐通过多个mapper + 业务控制、通过插件、或者修改源码的方式去写。

从代码也很容易看出来,实际上它就是构建了一个新的查询类型的

mappedStatement

,将它存到

configuration



KeyGenerator

中。

    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);

    id = builderAssistant.applyCurrentNamespace(id, false);

    MappedStatement keyStatement = configuration.getMappedStatement(id, false);
    configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));

有一点需要注意的是,就是它有一个执行顺序:

boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));

是在本sql之前执行,还是在之后执行,其实就是简单指定一下。

它保存的map

keyGenerators

,id 为当前sql的 id,我们执行这条sql语句之前,只需要根据当前执行sql的id,就可以拿到

selectKey

语句。

  public void addKeyGenerator(String id, KeyGenerator keyGenerator) {
    keyGenerators.put(id, keyGenerator);
  }

在生成执行语句

mappedStatement

之后,它会从 sql 中被移除,都很简单,我就不啰嗦了:

  private void removeSelectKeyNodes(List<XNode> selectKeyNodes) {
    for (XNode nodeToHandle : selectKeyNodes) {
      nodeToHandle.getParent().getNode().removeChild(nodeToHandle.getNode());
    }
  }

4.3 mappedStatement 的生成

实际上到了这里已经没什么可以讲的了,mappedStatement 就是一个存放解析对象的一个容器

 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.addMappedStatement(statement);
    return statement;

它的实例化是一个比较纯粹的构造器模式,拿到各种配置的

attribute

,解析出来的

resultMap

等,加以拼装,它并不是我们理解

mybatis

的重点,我们应该更加关注参与构造的这些参数是怎么来的,由于篇幅有限,再加上博主也懒得讲解一些不常用,或者比较简单的配置。如果哪里分析的有问题,或者希望博主对哪个部分进行比较深入的分析欢迎评论~

后续会有更多 mybatis 源码讲解~~

五、小结

小小的总结一下,在 mybatis 对 xml 的解析中,常用的几个技巧:

  • 递归,递归来解决嵌套问题,同时也可以避免生成的java类层级过深
  • id,几乎所有的 node 节点,都会有一个自己的全限定名,配合递归来使用事半功倍
  • 统一的上下文,configuration 类,基本什么东西都可以往里面塞,就像是一个哆啦A梦的四次元口袋,它本身也比较纯粹,基本不参与什么业务操作,就是存了很多解析生成的东西
  • 大量的兜底配置,实际上文章里很少提到的,但是代码里很常见,即兜底配置,实际上就是一种约定优于配置的思想,许多的额外支持仅仅暴露出来作为可选项,默认是有一套自己的实现的。


附Java/C/C++/机器学习/算法与数据结构/前端/安卓/Python/程序员必读/书籍书单大全:


(点击右侧 即可打开个人博客内有干货):

技术干货小栈


=====>>

①【Java大牛带你入门到进阶之路】

<<====

=====>>

②【算法数据结构+acm大牛带你入门到进阶之路】

<<===

=====>>

③【数据库大牛带你入门到进阶之路】

<<=====

=====>>

④【Web前端大牛带你入门到进阶之路】

<<====

=====>>

⑤【机器学习和python大牛带你入门到进阶之路】

<<====

=====>>

⑥【架构师大牛带你入门到进阶之路】

<<=====

=====>>

⑦【C++大牛带你入门到进阶之路】

<<====

=====>>

⑧【ios大牛带你入门到进阶之路】

<<====

=====>>

⑨【Web安全大牛带你入门到进阶之路】

<<=====

=====>>

⑩【Linux和操作系统大牛带你入门到进阶之路】

<<=====

天下没有不劳而获的果实,望各位年轻的朋友,想学技术的朋友,在决心扎入技术道路的路上披荆斩棘,把书弄懂了,再去敲代码,把原理弄懂了,再去实践,将会带给你的人生,你的工作,你的未来一个美梦。



版权声明:本文为JKX_geek原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。