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

本文详细介绍了Mybatis中mapper.xml文件的解析过程,从MybatisAutoConfiguration开始,讲解SqlSessionFactory的初始化,XMLMapperBuilder的角色,ResultMap的解析,以及sql语句与MappedStatement的关联。通过源码分析,揭示了mapper.xml中配置的ResultMap、SqlSessionFactory、SqlSource和MappedStatement的生成机制,帮助读者理解Mybatis的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

个人博客导航页(点击右侧链接即可打开个人博客):大牛带你入门技术栈 

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>标签进行初步的解析,源码如下:可以看到许多熟悉的身影,比如 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,例如当前 resultMap 的 type 是什么,它是否开启 autoMapping, id 是什么之类的。

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

如图所示: 

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());

### Spring 中配置 MyBatisMapper.xml 文件到指定目录 在 Spring 和 MyBatis 结合使用的场景下,可以通过多种方式来配置 `Mapper.xml` 文件的位置。以下是几种常见的实现方法: #### 1. 使用 `<mappers>` 标签显式加载 XML 映射器 可以在 MyBatis 的核心配置文件(通常命名为 `mybatis-config.xml` 或其他自定义名称)中使用 `<mappers>` 标签手动指定映射器文件的路径。 ```xml <mappers> <!-- 方式一:通过具体路径 --> <mapper resource="com/sun/dao/UserMapper.xml"/> <!-- 方式二:通过类名 --> <mapper class="com.sun.dao.UserMapper"/> <!-- 方式三:通过包扫描 --> <package name="com.sun.dao"/> </mappers> ``` 上述代码片段展示了三种不同的加载方式[^1]。其中第三种方式最为常用,它允许自动扫描指定包下的所有接口及其对应的 `Mapper.xml` 文件。 --- #### 2.Mapper.xml 放入 resources 资源目录 为了使 MyBatis 正确读取 `Mapper.xml` 文件,建议将其放置于项目的 `resources` 目录下。如果 `Mapper.xml` 文件位于 Java 源码目录而非标准资源目录,则需要调整其所在文件夹的属性为 “Resources Folder”,以便编译时能够被正确识别并打包[^2]。 例如: - 如果 `UserMapper.xml` 存在于 `src/main/resources/com/sun/dao/` 下,则可以直接通过如下方式进行引用: ```xml <mapper resource="com/sun/dao/UserMapper.xml"/> ``` --- #### 3. 基于 Spring Boot 自动化配置 当项目基于 Spring Boot 构建时,可以利用框架提供的自动化功能简化配置过程。只需确保以下条件满足即可完成默认配置: - 所有 `Mapper.xml` 文件均存放在 `resources/mapper` 或类似的约定位置; - 接口所在的包已注册至 MyBatis 的扫描范围。 示例代码如下所示: ```yaml # application.yml 配置 mybatis: mapper-locations: classpath:/mapper/*.xml type-aliases-package: com.example.domain ``` 此 YAML 配置指定了 `Mapper.xml` 文件的具体存储路径以及实体类别名的包路径[^4]。 --- #### 4. 动态生成 SqlSessionFactory 并设置 Mapper Locations 对于更复杂的场景,可能需要程序化地创建 `SqlSessionFactory` 实例并向其传递所需的参数。下面是一个典型的例子: ```java @Bean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean(); sessionFactory.setDataSource(dataSource); // 设置 Mapper.xml 文件的位置 PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); sessionFactory.setMapperLocations(resolver.getResources("classpath:/mapper/**/*.xml")); return sessionFactory.getObject(); } ``` 在此代码段中,调用了 `setMapperLocations()` 方法动态指定了多个匹配模式下的 `Mapper.xml` 文件地址[^3]。 --- ### 总结 无论采用何种策略,在实际开发过程中都应遵循良好的实践原则——即保持清晰一致的命名规则与合理的分层结构设计,从而提升系统的可维护性和扩展性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值