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

本文详细解析了Mybatis中mapper.xml的解析过程,从MybatisAutoConfiguration的配置开始,探讨了mapper文件的扫描、SqlSessionFactory的初始化、XMLMapperBuilder的角色,以及ResultMap和Sql语句的解析。重点介绍了ResultMap的解析,包括typeHandler的获取顺序、resultMap嵌套的解析规则,并解释了解析resultSet时如何避免死循环。最后,文章总结了在Mybatis中解析xml文件的一些常用技巧,如递归、id的使用以及configuration类的重要性。
摘要由CSDN通过智能技术生成
Ext1:本文源码解析基于 mybatis-spring-boot-starter 2.1.1,即 mybatis 3.5.3 版本。
Ext2:本文主要是对源码的讲解,着重点会是在源码上。

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

我们知道配置 SqlSessionFactory 是我们集成 Mybatis 时需要用到的常客,SqlSessionFactory 顾名思义是用来创建 SqlSession 对象的,SqlSession 对象的重要程度不言而喻。源码中提到,SqlSessionMybatis 运行最重要的一个接口,通过此接口,我们可以进行我们的操作指令,获取 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 是根据配置 ConfigurationsqlSessionFactoryBuilder 共同创建的,如果在 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> 标签进行初步的解析,源码如下:可以看到许多熟悉的身影,比如 namespaceresultMapselect|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,例如当前 resultMaptype 是什么,它是否开启 autoMappingid 是什么之类的。

  • 一种则是对子标签的解析,子标签的解析,则分为 constructordiscriminator、以及其他字段的解析。

如图所示:

3.1 ResultMap 中的重要成员:typeHandler

mybatismysql 返回的结果集 resultSet 进行解析时,typeHandler 有着举足轻重的作用。mysqlJdbcType 有很多,比如 BLOBVARCHARDATE 等等,而我们的 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(JdbcTy
  • 11
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我与错

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值