一个mapper接口有多个mapper.xml 文件_MyBatis 源码解析:映射文件的加载与解析(上)

dd3e9863d9bdcffaac4bd0ff91287bb2.png

上一篇我们分析了配置文件的加载与解析过程,本文将继续对映射文件的加载与解析实现进行分析。MyBatis 的映射文件用于配置 SQL 语句、二级缓存,以及结果集映射等,是区别于其它 ORM 框架的主要特色之一。

在上一篇分析配置文件 <mappers/> 标签的解析实现时,了解到 MyBatis 最终通过调用 XMLMapperBuilder#parse 方法实现对映射文件的解析操作,本文我们将以此方法作为入口,探究 MyBatis 加载和解析映射文件的实现机制。

方法 XMLMapperBuilder#parse 的实现如下:

public void parse() {
    /* 1. 加载并解析映射文件 */
    if (!configuration.isResourceLoaded(resource)) {
        // 加载并解析 <mapper/> 标签下的配置
        this.configurationElement(parser.evalNode("/mapper"));
        // 标记该映射文件已被解析
        configuration.addLoadedResource(resource);
        // 注册当前映射文件关联的 Mapper 接口(标签 <mapper namespace=""/> 对应的 namespace 属性)
        this.bindMapperForNamespace();
    }

    /* 2. 处理解析过程中失败的标签 */

    // 处理解析失败的 <resultMap/> 标签
    this.parsePendingResultMaps();
    // 处理解析失败的 <cache-ref/> 标签
    this.parsePendingCacheRefs();
    // 处理解析失败的 SQL 语句标签
    this.parsePendingStatements();
}

MyBatis 在解析映射文件时首先会判断该映射文件是否被解析过,对于没有被解析过的文件则会调用 XMLMapperBuilder#configurationElement 方法解析所有配置,并注册当前映射文件关联的 Mapper 接口。对于解析过程中处理异常的标签,MyBatis 会将其记录到 Configuration 对象对应的属性中,并在方法最后再次尝试二次解析。

整个 XMLMapperBuilder#configurationElement 方法实现了对映射文件解析的核心步骤,与配置文件解析的实现方式一样,这也是一个调度方法,实现如下:

private void configurationElement(XNode context) {
    try {
        // 获取 <mapper/> 标签的 namespace 属性,设置当前映射文件关联的 Mapper 接口
        String namespace = context.getStringAttribute("namespace");
        if (namespace == null || namespace.equals("")) {
            throw new BuilderException("Mapper's namespace cannot be empty");
        }
        builderAssistant.setCurrentNamespace(namespace);
        // 解析 <cache-ref/> 子标签,多个 mapper 可以共享同一个二级缓存
        this.cacheRefElement(context.evalNode("cache-ref"));
        // 解析 <cache/> 子标签
        this.cacheElement(context.evalNode("cache"));
        // 解析 <parameterMap/> 子标签,已废弃
        this.parameterMapElement(context.evalNodes("/mapper/parameterMap"));
        // 解析 <resultMap/> 子标签,建立结果集与对象属性之间的映射关系
        this.resultMapElements(context.evalNodes("/mapper/resultMap"));
        // 解析 <sql/> 子标签
        this.sqlElement(context.evalNodes("/mapper/sql"));
        // 解析 <select/>、<insert/>、<update/> 和 <delete/> 子标签
        this.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);
    }
}

每个映射文件都关联一个具体的 Mapper 接口,而 <mapper/> 节点的 namespace 属性则用于指定对应的 Mapper 接口限定名。上述方法首先会获取 namespace 属性,然后调用相应方法对每个子标签进行解析,下面逐一展开分析。

加载与解析映射文件

下面对各个子标签的解析过程逐一展开分析,考虑到 <parameterMap/> 子标签已废弃,所以不再对其多作介绍。

解析 cache 标签

MyBatis 在设计上分为一级缓存和二级缓存(关于缓存机制将会在下一篇分析 SQL 语句执行过程时进行介绍,这里只要知道有这样两个概念即可),该标签用于对二级缓存进行配置。在具体分析 <cache/> 标签之前,我们需要对 MyBatis 的缓存类设计有一个了解,不然可能会云里雾里。MyBatis 的缓存类设计还是非常巧妙的,不管是一级缓存还是二级缓存,都实现自同一个 Cache 接口:

public interface Cache {
    /** 缓存对象 ID */
    String getId();
    /** 添加数据到缓存,一般来说 key 是 {@link CacheKey} 类型 */
    void putObject(Object key, Object value);
    /** 从缓存中获取 key 对应的 value */
    Object getObject(Object key);
    /** 从缓存中移除指定对象 */
    Object removeObject(Object key);
    /** 清空缓存 */
    void clear();
    /**
     * 获取缓存对象的个数(不是缓存的容量),
     * 该方法不会在 MyBatis 核心代码中被调用,可以是一个空实现
     */
    int getSize();
    /**
     * 缓存读写锁,
     * 该方法不会在 MyBatis 核心代码中被调用,可以是一个空实现
     */
    ReadWriteLock getReadWriteLock();
}

Cache 接口中声明的缓存操作方法中规中矩。围绕该接口,MyBatis 实现了基于 HashMap 数据结构的基本实现 PerpetualCache 类,该实现类的各项方法实现都是对 HashMap API 的封装,比较简单。在整个缓存类设计方面,MyBatis 使用了典型的装饰模式为缓存对象增加不同的特性,下表对这些装饰器进行了简单介绍。

16601491902aad366d6a7d4a4a100a98.png

介绍完了缓存类的基本设计,我们再回过头来继续分析<cache/>标签的解析过程,由XMLMapperBuilder#cacheElement方法实现:

private void cacheElement(XNode context) {
    if (context != null) {
        // 获取相应的是属性配置
        String type = context.getStringAttribute("type", "PERPETUAL"); // 缓存实现类型,可以指定自定义实现
        Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
        String eviction = context.getStringAttribute("eviction", "LRU"); // 缓存清除策略,默认是 LRU,还可以是 FIFO、SOFT,以及 WEAK
        Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
        Long flushInterval = context.getLongAttribute("flushInterval"); // 刷新间隔,单位:毫秒
        Integer size = context.getIntAttribute("size"); // 缓存大小,默认为 1024
        boolean readWrite = !context.getBooleanAttribute("readOnly", false); // 是否只读
        boolean blocking = context.getBooleanAttribute("blocking", false); // 是否阻塞
        Properties props = context.getChildrenAsProperties();
        // 创建二级缓存,并填充 Configuration 对象
        builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
}

上述方法首先会获取<cache/>标签的相关属性配置,然后调用MapperBuilderAssistant#useNewCache方法创建缓存对象,并记录到 Configuration 对象中。方法MapperBuilderAssistant#useNewCache中使用了缓存对象构造器 CacheBuilder 创建缓存对象,一起来看一下CacheBuilder#build方法实现:

public Cache build() {
    // 如果没有指定自定义缓存实现类,则设置缓存默认实现(以 PerpetualCache 作为默认实现,以 LruCache 作为默认装饰器)
    this.setDefaultImplementations();
    // 反射创建缓存对象
    Cache cache = this.newBaseCacheInstance(implementation, id);
    // 初始化缓存对象
    this.setCacheProperties(cache);
    // issue #352, do not apply decorators to custom caches
    // 如果缓存采用 PerpetualCache 实现,则遍历使用注册的装饰器进行装饰
    if (PerpetualCache.class.equals(cache.getClass())) {
        // 遍历装饰器集合,基于反射方式装饰缓存对象
        for (Class<? extends Cache> decorator : decorators) {
            cache = this.newCacheDecoratorInstance(decorator, cache);
            this.setCacheProperties(cache);
        }
        // 采用标准装饰器进行装饰
        cache = this.setStandardDecorators(cache);
    }
    // 采用日志缓存装饰器对缓存对象进行装饰
    else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
        cache = new LoggingCache(cache);
    }
    return cache;
}

构造缓存对象时首先会判断是否指定了自定义的缓存实现类,否则使用默认的缓存实现(即以 PerpetualCache 作为默认实现,以 LruCache 作为默认缓存装饰器);然后选择 String 类型参数的构造方法构造缓存对象,并基于配置对缓存对象进行初始化;最后依据缓存实现采用相应的装饰器予以装饰。

方法 CacheBuilder#setCacheProperties 除了用于设置相应属性配置外,还会判断缓存类是否实现了 InitializingObject 接口,以决定是否调用 InitializingObject#initialize 初始化方法。

解析 cache-ref 标签

标签 <cache/> 默认的作用域限定在标签所在的 namespace 范围内,如果希望能够让一个缓存对象在多个 namespace 之间共享,可以定义 <cache-ref/> 标签以引用其它命名空间中定义的缓存对象。标签 <cache-ref/> 的解析位于 XMLMapperBuilder#cacheRefElement 方法中:

private void cacheRefElement(XNode context) {
    if (context != null) {
        // 记录 <当前节点所在的 namespace, 引用缓存对象所在的 namespace> 映射关系到 Configuration 中
        configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
        // 构造缓存引用解析器 CacheRefResolver 对象
        CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
        try {
            // 从记录缓存对象的 Configuration#caches 集合中获取引用的缓存对象
            cacheRefResolver.resolveCacheRef();
        } catch (IncompleteElementException e) {
            // 如果解析出现异常则记录到 Configuration#incompleteCacheRefs 中,稍后再处理
            configuration.addIncompleteCacheRef(cacheRefResolver);
        }
    }
}

方法首先会在 Configuration#cacheRefMap 属性中记录一下当前的引用关系,其中 key 是 <cache-ref/> 所在的 namespace,value 则是引用的缓存对象所在的 namespace。然后从 Configuration#caches 属性中获取引用的缓存对象,在分析 <cache/> 标签时,我们曾提及到最终解析构造的缓存对象会记录到 Configuration#caches 属性中,这里则是一个逆过程。

解析 resultMap 标签

标签 <resultMap/> 用于配置结果集映射,建立结果集与实体类对象属性之间的映射关系。这是一个非常有用且提升开发效率的配置,如果是纯 JDBC 开发,在处理结果集与实体类对象之间的映射时还需要手动硬编码注入。对于一张字段较多的表来说,简直写到手抽筋,而 <resultMap/> 标签配置配合 mybatis-generator 工具的逆向工程可以解放我们的双手。下面是一个典型的配置,用于建立数据表 t_user 与 User 实体类之间的属性映射关系:

<resultMap id="BaseResultMap" type="org.zhenchao.mybatis.entity.User">
    <id column="id" jdbcType="BIGINT" property="id"/>
    <result column="username" jdbcType="VARCHAR" property="username"/>
    <result column="password" jdbcType="VARCHAR" property="password"/>
    <result column="age" jdbcType="INTEGER" property="age"/>
    <result column="phone" jdbcType="VARCHAR" property="phone"/>
    <result column="email" jdbcType="VARCHAR" property="email"/>
</resultMap>

在开始介绍 <resultMap/> 标签的解析过程之前,我们需要对该标签涉及到的两个主要的类 ResultMapping 和 ResultMap 有一个了解。前者用于封装除 <discriminator/> 标签以外的其它子标签配置(该标签具备自己的封装类),后者

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值