MyBatis 源码解析:映射文件的加载与解析

上一篇我们分析了配置文件的加载与解析过程,本文将继续对映射文件的加载与解析实现进行分析。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 使用了典型的装饰模式为缓存对象增加不同的特性,下表对这些装饰器进行了简单介绍。
在这里插入图片描述
介绍完了缓存类的基本设计,我们再回过头来继续分析 <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/> 标签以外的其它子标签配置(该标签具备自己的封装类),后者则用于封装整个 <resultMap/> 标签。

ResultMapping

public class ResultMapping {
   

    private Configuration configuration;

    /** 对应标签的 property 属性 */
    private String property;
    /** 对应标签的 column 属,配置数据表列名(or 别名) */
    private String column;
    /** 对应 java 类型,配置类型全限定名(or 别名) */
    private Class<?> javaType;
    /** 对应列的 JDBC 类型 */
    private JdbcType jdbcType;
    /** 类型处理器,会覆盖默认类型处理器 */
    private TypeHandler<?> typeHandler;
    /** 对应标签的 resultMap 属性,以 id 的方式引某个已定义的 <resultMap/> */
    private String nestedResultMapId;
    /** 对应标签的 select 属性,以 id 的方式引用某个已定义的 <select/> */
    private String nestedQueryId;
    /** 对标签的 notNullColumns 属性 */
    private Set<String> notNullColumns;
    /** 对应标签的 columnPrefix 属性 */
    private String columnPrefix;
    /** 记录处理后的标志 */
    private List<ResultFlag> flags;
    /** 记录标签 column 拆分后生成的结果 */
    private List<ResultMapping> composites;
    /** 对应标签 resultSet 属性 */
    private String resultSet;
    /** 对应标签 foreignColumn 属性 */
    private String foreignColumn;
    /** 对应标签 fetchType 属性,配置是否延迟加载 */
    private boolean lazy;

    // ... 省略构造器类定义,以及 getter 和 setter 方法
}

ResultMapping 类中定义的属性如上述代码注释。此外,还内置了一个 Builder 内部构造器类,用于封装数据构造 ResultMapping 对象,并实现了对属性值的基本校验逻辑。

ResultMap

public class ResultMap {
   

    private Configuration configuration;

    /** 对应标签的 id 属性 */
    private String id;
    /** 对应标签的 type 属性 */
    private Class<?> type;
    /** 记录除 <discriminator/> 标签以外的其它映射关系 */
    private List<ResultMapping> resultMappings;
    /** 记录带有 id 属性的映射关系 */
    private List<ResultMapping> idResultMappings;
    /** 记录带有 constructor 属性的映射关系 */
    private List<ResultMapping> constructorResultMappings;
    /** 记录带有 property 属性的映射关系 */
    private List<ResultMapping> propertyResultMappings;
    /** 记录配置中所有的 column 属性集合 */
    private Set<String> mappedColumns;
    /** 记录配置中所有的 property 属性集合 */
    private Set<String> mappedProperties;
    /** 封装 <discriminator/> 标签 */
    private Discriminator discriminator;
    /** 是否包含嵌套的结果映射 */
    private boolean hasNestedResultMaps;
    /** 是否包含嵌套查询 */
    private boolean hasNestedQueries;
    /** 是否开启自动映射 */
    private Boolean autoMapping;

    // ... 省略构造器类,以及 getter 和 setter 方法
}

ResultMap 类中定义的属性如上述代码注释。与 ResultMapping 一样,也是通过内置 Builder 内部构造器类来构造 ResultMap 对象,构造器的实现比较简单,读者可以参考源码实现。

了解了内部数据结构 ResultMapping 和 ResultMap 的定义,以及二者之间的相互依赖关系,接下来开始分析 <resultMap/> 标签的解析过程,实现位于 XMLMapperBuilder#resultMapElements 方法中:

private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) {
   
    ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
    // 获取 type 属性,支持 type、ofType、resultType,以及 javaType 类型配置
    String type = resultMapNode.getStringAttribute("type",
        resultMapNode.getStringAttribute("ofType",
            resultMapNode.getStringAttribute("resultType",
                resultMapNode.getStringAttribute("javaType"))));
    // 基于 TypeAliasRegistry 解析 type 属性对应的实体类型
    Class<?> typeClass = this.resolveClass(type);
    if (typeClass == null) {
   
        // 尝试基于 <association/> 子标签或 <case/> 子标签解析实体类型
        typeClass = this.inheritEnclosingType(resultMapNode, enclosingType);
    }
    Discriminator discriminator = null;
    // 用于记录解析结果
    List<ResultMapping> resultMappings = new ArrayList<>(additionalResultMappings);
    // 获取并遍历处理所有的子标签
    List<XNode> resultChildren = resultMapNode.getChildren();
    for (XNode resultChild : resultChildren) {
   
        // 解析 <constructor/> 子标签,封装成为 ResultMapping 对象
        if ("constructor".equals(resultChild.getName())) {
   
            this.processConstructorElement(resultChild, typeClass, resultMappings);
        }
        // 解析 <discriminator/> 子标签,封装成为 Discriminator 对象
        else if ("discriminator".equals(resultChild.getName())) {
   
            discriminator = this.processDiscriminatorElement(resultChild, typeClass, resultMappings);
        }
        // 解析 <association/><collection/>、<id/><result/> 子标签,封装成为 ResultMapping 对象
        else {
   
            List<ResultFlag> flags = new ArrayList<>();
            if ("id".equals(resultChild.getName())) {
   
                flags.add(ResultFlag.ID);
            }
            // 创建 ResultMapping 对象,并记录到 resultMappings 集合中
            resultMappings.add(this.buildResultMappingFromContext(resultChild, typeClass, flags));
        }
    }
    // 获取 id 属性(标识当前 <resultMap/> 标签),如果没有指定则基于规则生成一个
    String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier());
    // 获取 extends 属性,用于指定继承关系
    String extend = resultMapNode.getStringAttribute("extends");
    // 获取 autoMapping 属性,是否启用自动映射(自动查找与列名相同的属性名称,并执行注入)
    Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
    ResultMapResolver resultMapResolver = new ResultMapResolver(
        builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
    try {
   
        // 基于解析得到的配置构造 ResultMap 对象,记录到 Configuration#resultMaps 中
        return resultMapResolver.resolve();
    } catch (IncompleteElementException e) {
   
        // 记录解析异常的 <resultMap/> 标签,后续尝试二次解析
        configuration.addIncompleteResultMap(resultMapResolver);
        throw e;
    }
}

标签 <resultMap/> 包含 4 个属性配置,即 id、type、extends 和 autoMapping。

id:标识当前 <resultMap/> 标签,如果没有指定则会调用 XNode#getValueBasedIdentifier 方法基于规则自动生成一个,用于提升 MyBatis 的执行性能。
type:设置当前标签所关联的实体类对象,支持 type、ofType、resultType,以及 javaType 等配置方式,以尽可能用简单的配置支持更多的实体类型。
extends:指定当前标签的继承关系。
autoMapping:一个 boolean 类型的配置项,如果为 true 则表示开启自动映射功能,MyBatis 会自动查找实例类对象中与结果集列名相同的属性名,并调用 setter 方法执行注入。标签 <resultMap/> 中明确指定的映射关系优先级要高于自动映射。

标签 <resultMap/> 包含 <constructor/><id/><result/><association/><collection/>,以及 <discriminator/> 六个子标签。关于这些子标签的作用可以参阅 官方文档,除 <discriminator/> 以外,其余五个标签的解析实现大同小异,下面以 <constructor/> 标签为例对解析实现展开分析。
子标签 <constructor/> 的解析由 XMLMapperBuilder#processConstructorElement 方法实现,如下:

private void processConstructorElement(
    XNode resultChild, Class<?> resultType, List<ResultMapping> resultMappings) {
   
    // 获取并处理 <constructor/> 标签中配置的子标签列表
    List<XNode> argChildren = resultChild.getChildren();
    for (XNode argChild : argChildren) {
   
        List<ResultFlag> flags = new ArrayList<>();
        flags.add(ResultFlag.CONSTRUCTOR);
        if ("idArg".equals(argChild.getName())) {
   
            flags.add(ResultFlag.ID); // 添加 ID 标识
        }
        // 封装标签配置为 ResultMapping 对象,记录到 resultMappings 集合中
        resultMappings.add(this.buildResultMappingFromContext(argChild, resultType, flags));
    }
}

子标签 <constructor/> 用于指定实体类的构造方法以实现在构造实体类对象时注入结果值。上述方法直接遍历处理该标签的所有子标签,即 <idArg/><arg/>,并调用 XMLMapperBuilder#buildResultMappingFromContext 方法创建对应的 ResultMapping 对象,实现如下:

private ResultMapping buildResultMappingFromContext(
    XNode context, Class
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值