美文网首页Mybatis
Mybatis源码之美:3.5.6.resultMap元素的解析

Mybatis源码之美:3.5.6.resultMap元素的解析

作者: 吃竹子的程序熊 | 来源:发表于2020-04-24 17:51 被阅读0次

    解析resultMap元素的过程

    我来了

    在前面几篇文章中,我们详细的了解了resultMap元素的属性和子元素定义,过程比较复杂,但总算还是有些收获.

    我也是写到resultMap元素的时候,才忽然想明白一个道理:

    只有实实在在的了解每个属性和元素的作用,我们才能更好的去理解mybatis解析resultMap时所做的一些判断和处理.

    所以,虽然介绍resultMap元素的用法着实费了很大的功夫,但我觉得还是值得的.

    辛苦了那么久,今天来点不一样的,换一个方式看看能不能更有意思的去学习源码,

    那么,让我们开始吧~

    一番操作

    闪亮登场环节

    如果我们将mybatis比作一个创业公司,resultMap元素就好比是mybatis的一个项目部,那么resultMap元素的各个子元素就是形形色色的员工,角色不同,职责也不同.

    resultMap项目负责对接外部数据,并完成将外部数据转换为java对象的业务.

    mybatis公司初创时,针对resultMap项目部,招进来许许多多的result元素,刚开始时间短,看不出来什么不同,大家都负责最基础的普通属性转换工作.

    result

    但是随着公司业务的扩张,慢慢地,部分优秀的result元素开始展露头角,因为在一些问题上总是能提出一些具有建设性的意见,于是得到了老板的赏识,升级成了id元素.

    虽然id元素干的还是和result元素一样的活,但是,多多少少身份是不一样了,至少在开会时,id元素开始拥有了决策投票权,甚至所有的id元素在一起就能拍板决定resultMap项目部的大小事宜.

    id元素

    除此之外呢,还有一些result元素深耕自己的岗位,日积月累的,在某一方面取得了不菲的成就,成为了能够独当一面的大牛,比如collectionassociation元素,他们分别成为了处理复杂对象和处理集合对象的业务领域专家,被公司委以重任,根据公司需要,他们随时可以拉起团队,成立一个和resultMap职能一样的项目部.

    collection和association元素

    公司内还有一个比较特殊的discriminator元素,他是公司老板的亲大爷,见多识广,位高权重,现担任传达室大爷一职,你别看大爷上年纪了,但是本事可不小,一些比较复杂多变的问题基本都得靠discriminator大爷来解决,当然大爷不会真动手干活,基本就是动动嘴皮子,分析分析业务,把业务合情合理的分配给整天屁颠屁颠的跟在他屁股后面的case元素来处理.

    discriminator

    case元素也是从result元素的位置上一点点摸爬滚打走上来的,他们虽然是discriminator元素的儿子,但你可不要一厢情愿的以为case元素是不学无术的富二代,这些case元素也是个顶个儿的人才,个个都能独当一面.

    case

    甚至在resultMap内部,大家将collection,association以及case三元素称为嵌套映射的三巨头,人送外号小resultMap.

    除此之外呢,传说在resultMap内部还有一个神龙见首不见尾的技术总监,名为constructor,constructor元素可不简单,他控制着resultMap项目部工作成果的交付,公司可以没有他的身影,但是却一定流传着他的大名,他一现身,如何将数据转换为java对象这件事就得按照他的想法来了.

    constructor

    constructor手下有一个单独的部门用来完成java对象的构建工作,在这个部门完成java对象的构建操作之前,其他元素啥事都不能干,都得等着他们.

    这个部门里待的元素虽然也是从result这个职位上走过来的,但是地位却有所不同.

    公司的元老级元素arg就是其中之一,公司初创的时候他就在了,一步步的从result元素走到这个位置,虽然本事没啥长进,职位也没往上升,但多多少少算是个老人了,大家或多或少的也给他点面子.

    arg

    但是另一个元素,idArg就不一样了,他是arg元素中的精英,精英中的精英,人称小技术总监,他可是和id元素一样,具有公司的决策投票权的.

    idArg

    所以你别看idArgid两个元素表面上只能干最普通的活,但是人家可以参与项目部决策,拿项目部分红的,甚至在一些人眼里,他俩能代表整个resultMap项目部.

    物以类聚,元素以群分

    虽然resultMap项目部不大,在mybaits公司内部,干的也是数据清洗的脏话累活,手底下干活的元素也不多,但是他的的确确是mybaits公司的核心项目部.

    通常来说,你要是有数据清洗的项目,你就找mybaits公司的前台填个表,上面详细的列上你需要哪些人,干哪些事.

    你要是不知道这个单子应该怎么填,你就看看前面的几篇文章,或者看看resultMap项目部的员工介绍:

    比如id,result这俩哥们,他俩就非常擅长把数据转换为简单的属性值,一般情况下,简单的属性转换操作,找这哥俩准没错.

    id和result

    要是有些数据的构建比较复杂,那就得找技术总监constructor元素处理了,constructor人狠话不多,做事干净利索,拿个小本本稍微记一下,就把数据下放到idArgarg手里去了.

    看戏

    前面咱也说了idArg,arg,id,result这四个哥们干的都是些简单的数据处理操作.你要是非得拿复杂数据来找他们处理,idArgarg俩哥们倒是也能解决,他俩直接把数据转包给其他resultMap项目部,让转包公司处理就行了.

    ok

    但是idresult这俩哥们就不一样了,他俩要是能搭理你一下都算我输.

    给你看个宝贝

    所以说,你要是真有些数据要转换为复杂对象,还是得找collectionassociation元素,这俩哥们全能,简单的活他俩能干,如果是复杂的活,他俩转包也好,自己拉团队也好,都能给你整的明明白白的.

    悠闲

    要是有些数据比较复杂,涉及到的场景是会发生变化的,那这时候你就得找年纪大,经验足,察言观色能力的强的discriminator大爷了,大爷手一背,墨镜一戴,就为涉及到的每一种场景,都分配了一个case元素.

    大爷码数据

    跟屁虫case元素接到数据之后,转包也好,自己拉团队也好,处理起来那也是一点也不含糊的,毫不逊色于collectionassociation元素.

    你看,这就是resultMap公司,能把数据给你安排的明明白白.

    真棒

    现在你知道单子怎么填了吧,你把单子填上之后,mybatis就开始根据你的单子给你立项,成立一个单独的resultMap项目部来完成你的需求.

    解析resultMap元素的基本流程

    要知道,在一个resultMap项目里面,真正干活的元素也就那么几个,像discriminatorconstructor这种元素,他们属于领导型,主要是负责分配活.

    真正干活的是那些从result位置上一步一步摸爬滚打上来的元素:id,result,idArg,arg,association,collection,以及case.

    比如id,result,idArg,arg这四个元素就是脚踏实地,老老实实的负责简单数据转换的工作,虽然职位名称不一样吧,但是干的活基本一致.

    剩下的嵌套映射三巨头的工作和上面四个又有点不一样,因为三巨头是能带团队的,所以他们自己可以根据工作需要单独成立一个子resultMap项目部,或者找个其他的resultMap项目部来帮他们完成工作.

    下面是resultMap的所有子元素定义:

    resultMap的子元素

    我们将上面的思维导图去重之后重新整理:

    思维导图

    娱乐时间结束,言归正传.

    我们可以大致将resultMap的子元素分为两类:一类是不负责实际属性转换操作的标志性的元素,一类是实际完成属性转换的元素.

    标志性元素discriminatorconstructor因为定义和作用完全不同,所以在解析时需要单独处理,剩余的其他元素则可以放在一起处理.

    从这个角度上出发,我们可以将resultMap的子元素分为三部分来解析,分别是constuctor元素,discriminator元素以及剩余的其他元素.

    实际上,mybatis也是这样解析的.

    resultMap元素的解析入口

    XMLMapperBuilderconfigurationElement()方法是resultMap元素的解析入口:

    private void configurationElement(XNode context) {
        try {
            // 省略...
            // 解析并注册resultMap元素
            resultMapElements(context.evalNodes("/mapper/resultMap"));
            // 省略...
    
        } catch (Exception e) {
            throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
        }
    }
    

    configurationElement()方法将从mapper文件中获取到的所有resultMap元素定义一股脑的交给了resultMapElements()方法来完成解析操作:

    private void resultMapElements(List<XNode> list) throws Exception {
        for (XNode resultMapNode : list) {
            try {
                // 解析ResultMap节点
                resultMapElement(resultMapNode);
            } catch (IncompleteElementException e) {
                // ignore, it will be retried
                // 在内部实现中,未完成解析的节点将会被放至Configuration#incompleteResultMaps中
            }
        }
    }
    

    resultMapElements()方法则依次将每个resultMap元素定义交给resultMapElement()方法来完成resultMap元素的解析注册操作:

    /**
        * 解析ResultMap节点
        *
        * @param resultMapNode ResultMap节点
        */
    private ResultMap resultMapElement(XNode resultMapNode) throws Exception {
        return resultMapElement(resultMapNode, Collections.<ResultMapping>emptyList(), null);
    }
    

    resultMapElement方法简介

    不过,实际完成解析工作的方法是resultMapElement()方法的另一个重载实现:

    /**
     * 解析ResultMap元素,关于ResultMap的各个子元素的作用可以参考文档{@link https://blog.csdn.net/u012702547/article/details/54599132}
     * <p>
     * 该方法并不是单纯的只用于解析ResultMap元素,而是用于解析具有ResultMap性质的元素,该方法的调用方,目前 有两个,一个是用来解析`ResultMap`元素,
     * 另一个使用该方法来解析association/collection/discriminator的case元素。
     *
     * @param resultMapNode            resultMapNode节点
     * @param additionalResultMappings 现有的resultMapping结合
     * @param enclosingType            返回类型
     */
    private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) throws Exception {
        ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
        // 获取唯一标志,有趣的是association/collection/discriminator的case元素都没有ID属性,所以该ID会根据嵌套的上下文来生成。
        String id = resultMapNode.getStringAttribute("id",
                resultMapNode.getValueBasedIdentifier());
    
        // 获取返回类型 依次读取:【type】>【ofType】>【resultType】>【javaType】
        String type = resultMapNode.getStringAttribute("type",
                resultMapNode.getStringAttribute("ofType",
                        resultMapNode.getStringAttribute("resultType",
                                resultMapNode.getStringAttribute("javaType"))));
    
        // 获取当前ResultMap是否继承了其他ResultMap
        String extend = resultMapNode.getStringAttribute("extends");
    
        // 获取自动映射标志
        Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
    
        // 解析出返回类型
        Class<?> typeClass = resolveClass(type);
        if (typeClass == null) {
            // 嵌套映射时,外部对象属性类型定义优先级较低
            typeClass = inheritEnclosingType(resultMapNode, enclosingType);
        }
    
        Discriminator discriminator = null;
    
        // 返回结果定义
        List<ResultMapping> resultMappings = new ArrayList<>();
        // 添加所有额外的ResultMap集合
        resultMappings.addAll(additionalResultMappings);
    
        List<XNode> resultChildren = resultMapNode.getChildren();
        // 开始处理ResultMap中的每一个子节点
        for (XNode resultChild : resultChildren) {
            // 获取每一个ResultMap的子节点 处理constructor节点,该节点用来配置构造方法
            if ("constructor".equals(resultChild.getName())) {
                // 处理constructor节点
                processConstructorElement(resultChild, typeClass, resultMappings);
    
            } else if ("discriminator".equals(resultChild.getName())) {
                // 处理discriminator节点(鉴别器)
                // 通过配置discriminator节点可以实现根据查询结果动态生成查询语句的功能
                discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
    
            } else {
                // 获取ID标签
                List<ResultFlag> flags = new ArrayList<>();
                if ("id".equals(resultChild.getName())) {
                    // 添加ID标记
                    flags.add(ResultFlag.ID);
                }
    
                // 添加ResultMapping配置
                resultMappings.add(
                        buildResultMappingFromContext(
                                resultChild
                                , typeClass
                                , flags
                        )
                );
            }
        }
    
        // 构建ResultMap解析器
        ResultMapResolver resultMapResolver = new ResultMapResolver(
                builderAssistant
                , id /*resultMap的ID*/
                , typeClass /*返回类型*/
                , extend /*继承的ResultMap*/
                , discriminator /*鉴别器*/
                , resultMappings /*内部的ResultMapping集合*/
                , autoMapping /*自动映射*/
        );
    
        try {
            // 解析ResultMap
            return resultMapResolver.resolve();
        } catch (IncompleteElementException e) {
            // 解析ResultMap发生异常,将奖盖ResultMap放入未完成解析的ResultMap集合.
            configuration.addIncompleteResultMap(resultMapResolver);
            throw e;
        }
    }
    

    resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType)方法最终会返回一个ResultMap对象,该方法的实现比较长,我们一点一点的看.

    该方法返回的ResultMap对象维护了整个resultMap元素中的所有配置,他的属性很多,功能也很强大,具体的作用我们会在后续的解析过程中给出。

    我们先要了解的第一点是:resultMapElement方法解析的resultMap元素是指所有具有resultMap元素性质的元素,因此resultMapElement方法还被用来解析association,collection以及case元素.

    resultMapElement方法的入参介绍

    resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType)方法有三个入参,分别是resultMapNode,additionalResultMappings以及enclosingType.

    resultMapNode

    其中resultMapNode比较好理解,他表示所有具有resultMap元素性质的元素定义,注意,他不是单纯的只代表resultMap元素,而是表示所有具有resultMap性质的元素,
    比如他还可以表示associationcollection以及discriminatorcase元素:

    <!ELEMENT resultMap   (constructor?,id*,result*,association*,collection*, discriminator?)>
    <!ELEMENT association (constructor?,id*,result*,association*,collection*, discriminator?)>
    <!ELEMENT collection  (constructor?,id*,result*,association*,collection*, discriminator?)>
    <!ELEMENT case        (constructor?,id*,result*,association*,collection*, discriminator?)>
    

    additionalResultMappings

    additionalResultMappings表示现有的ResultMapping集合,该参数只有在解析discriminator元素时才有数据,其他时候均为空集合.

    更多关于additionalResultMappings参数的介绍,我们放在解析discriminator子元素的内容中来讲.

    因为根据DTD定义来看,为具有resultMap性质的元素配置discriminator子元素时,discriminator子元素必须声明在元素的尾部:

    resultMap性质的元素

    这样我们在解析时,必须是先解析出其余的元素配置,才会解析discriminator子元素.

    enClosingType

    enClosingType表示当前正在解析resultMap所属resultMap对应的java类型,该参数有可能为空.

    假设我们现有如下resultMap配置:

    <resultMap id="userWithNotNullColumn" type="org.apache.learning.result_map.association.User" autoMapping="true">
        <association property="role" column="role_id" resultMap="role" columnPrefix="role_" notNullColumn="name"/>
    </resultMap>
    

    在我们解析iduserWithNotNullColumnresultMap元素时,因为resultMap不是嵌套的结果映射配置,他没有所属的resultMap,所以在解析该元素是enclosingType参数为null.

    但是当我们解析嵌套在resultMap内部的association元素时,因为该元素属于iduserWithNotNullColumnresultMap元素,所以enclosingType参数的值是org.apache.learning.result_map.association.User.

    总结

    additionalResultMappingsenClosingType这两个属性可能现在比较难理解,但是当我们深入到resultMap元素的解析过程之后,我们就会很容易的理解这两个参数的含义.

    resultMapElement方法的详解(解析resultMap元素)

    了解了方法入参之后,我们回到resultMapElement()方法的解析过程中来:

    // 获取唯一标志,有趣的是association/collection/discriminator的case元素都没有ID属性,所以该ID会根据嵌套的上下文来生成。
    String id = resultMapNode.getStringAttribute("id",
            resultMapNode.getValueBasedIdentifier());
    

    在解析这种具有resultMap性质的元素的时候,mybatis首先会读取他的id属性,这个id属性的值将会作为生成该元素唯一标志的依据.

    不过,有一点需要注意,除了resultMap元素以外,associationcollection以及discriminatorcase元素都没有提供id属性的定义,他们唯一标志的生成是根据元素定义在DOM树中实际所处的位置来确定的,大致实现原理就是递归拼接元素类型和名称直到顶层元素为止,具体实现代码如下:

    /**
        * 基于元素的层级结构生成唯一标志,
        * 大概就是这样:
        * mapper_resultMap[test_resultMap]_collection[arrays]
        */
    public String getValueBasedIdentifier() {
        StringBuilder builder = new StringBuilder();
        XNode current = this;
        // 一直递归处理到顶级元素
        while (current != null) {
            if (current != this) {
                builder.insert(0, "_");
            }
            // 按照优先级一次读取 id, value ,property属性
            String value = current.getStringAttribute(
                    "id",
                    current.getStringAttribute(
                            "value",
                            current.getStringAttribute(
                                    "property"
                                    , null
                            )
                    )
            );
            // 将value值中所有的.都替换成下划线
            // [value]
            if (value != null) {
                value = value.replace('.', '_');
                builder.insert(0, "]");
                builder.insert(0,
                        value);
                builder.insert(0, "[");
            }
            // 元素名称[value]
            builder.insert(0, current.getName());
            current = current.getParent();
        }
        return builder.toString();
    }
    

    在完成了ResultMap对象的唯一标志的生成工作之后,Mybatis接着会获取该元素所对应的java类型,因为不同类型的元素对于java类型定义的属性名称也有些许的不同之处,因此Mybatis在获取java类型的时候会按照顺序依次读取type,ofType,resultType,javaType属性:

    // 获取返回类型 优先级:【type】>【ofType】>【resultType】>【javaType】
    String type = resultMapNode.getStringAttribute("type",
            resultMapNode.getStringAttribute("ofType",
                    resultMapNode.getStringAttribute("resultType",
                            resultMapNode.getStringAttribute("javaType"))));
    

    在获取到该元素的java类型之后,Mybatis会通过读取该元素定义的extends属性,获取当前元素所继承的resultMap元素的定义。

    之后再通过autoMapping属性来获取当前resultMap的自动映射行为:

    // 获取当前ResultMap是否继承了其他ResultMap
    String extend = resultMapNode.getStringAttribute("extends");
    
    // 获取自动映射标志
    Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
    
    

    然后解析类型别名,尝试获取当前resultMap对应的java类型:

    // 解析出返回类型
    Class<?> typeClass = resolveClass(type);
    if (typeClass == null) {
        // 嵌套映射时,外部对象属性类型定义优先级较低
        typeClass = inheritEnclosingType(resultMapNode, enclosingType);
    }
    

    如果当前resultMap没有直接指定对应的java类型,mybatis会尝试通过上下文来推断出合适的类型,负责推断类型的方法是inheritEnclosingType()方法:

    protected Class<?> inheritEnclosingType(XNode resultMapNode, Class<?> enclosingType) {
        // 一对一集合映射
        if ("association".equals(resultMapNode.getName()) && resultMapNode.getStringAttribute("resultMap") == null) {
            // 通过属性定义推断java类型
            String property = resultMapNode.getStringAttribute("property");
            if (property != null && enclosingType != null) {
                MetaClass metaResultType = MetaClass.forClass(enclosingType, configuration.getReflectorFactory());
                return metaResultType.getSetterType(property);
            }
        } else if ("case".equals(resultMapNode.getName()) && resultMapNode.getStringAttribute("resultMap") == null) {
            // 鉴别器
            return enclosingType;
        }
        return null;
    }
    

    inheritEnclosingType()方法的实现并不复杂,首先他不会处理配置了resultMap属性的元素,因为无论是association元素也好还是case元素也好,如果他们配置了resultMap属性,那就意味着该元素对应属性的类型转换处理操作是由被引用的resultMap来处理的,当前resultMap无需处理,因此,当前方法也就无需进行类型推断操作.

    针对association元素,因为在设计上association元素的作用就是为某一对象属性配置一个复杂对象的映射,因此我们可以借助于属性名称属性所属对象的类型通过反射获取到association元素所对应的类型定义.

    而针对case元素,其父元素discriminatorjavaType属性是必填的,这个属性直接就表明了当前鉴别器所对应的java类型,discriminatorjavaType属性定义在解析时是作为enclosingType参数传递进来,因此从理论上来讲直接返回enclosingType参数即可.

    <!ATTLIST discriminator
    column CDATA #IMPLIED
    javaType CDATA #REQUIRED
    jdbcType CDATA #IMPLIED
    typeHandler CDATA #IMPLIED
    >
    
    

    那么同为嵌套映射三巨头collection为什么没有进行类型推断操作呢?

    这是因为collection元素用于配置集合映射,所以解析collection时,enclosingType参数对应的是一个集合类型,根据mybatis现有配置,我们无法为集合指定泛型,因此除非用户明确指出,否则我们无法通过反射或者其他方式获取集合中元素的类型.

    在得到当前resultMap元素对应的java类型之后,mybatis创建了一个用于存放ResultMapping对象的resultMappings集合:

    // 返回结果定义
    List<ResultMapping> resultMappings = new ArrayList<>();
    // 添加所有额外的ResultMap集合
    resultMappings.addAll(additionalResultMappings);
    

    之后会依次将当前resultMap的所有子元素全部转换为ResultMapping对象,并保存到resultMappings集合中.

    探究resultMap子元素的解析操作

    resultMap的子元素转换为ResultMapping对象的操作,根据子元素的类型和作用,基本可以分为三类:构造参数配置,鉴别器配置,以及标准属性映射配置.

    List<XNode> resultChildren = resultMapNode.getChildren();
    // 开始处理ResultMap中的每一个子节点
    for (XNode resultChild : resultChildren) {
        // 获取每一个ResultMap的子节点 处理constructor节点,该节点用来配置构造方法
        if ("constructor".equals(resultChild.getName())) {
            // 处理constructor节点
            processConstructorElement(resultChild, typeClass, resultMappings);
    
        } else if ("discriminator".equals(resultChild.getName())) {
            // 处理discriminator节点(鉴别器)
            // 通过配置discriminator节点可以实现根据查询结果动态生成查询语句的功能
            discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
    
        } else {
            // 获取ID标签
            List<ResultFlag> flags = new ArrayList<>();
            if ("id".equals(resultChild.getName())) {
                // 添加ID标记
                flags.add(ResultFlag.ID);
            }
    
            // 添加ResultMapping配置
            resultMappings.add(
                    buildResultMappingFromContext(
                            resultChild
                            , typeClass
                            , flags
                    )
            );
        }
    }
    

    这三类的解析操作则分别对应着上面代码中的三条分支语句.

    解析标准属性映射配置

    其中最基本的的操作是标准属性映射配置,它对应的代码块是上面代码中的最后一个else语句:

    // 获取ID标签
    List<ResultFlag> flags = new ArrayList<>();
    if ("id".equals(resultChild.getName())) {
        // 添加ID标记
        flags.add(ResultFlag.ID);
    }
    
    // 添加ResultMapping配置
    resultMappings.add(
            buildResultMappingFromContext(
                    resultChild
                    , typeClass
                    , flags
            )
    );
    

    这一部分代码主要用来处理id,result,association以及collection四个元素.

    方法实现比较简单,除了会针对id元素的配置单独添加一个ResultFlag.ID标记之外,剩下的操作都交给了buildResultMappingFromContext()方法来完成.

    ResultFlag是一个枚举对象,他有两个实现:IDCONSTRUCTOR,分别用来为当前配置的元素添加ID和构造参数标识.

    public enum ResultFlag {
      ID, CONSTRUCTOR
    }
    

    解析构造参数配置

    构造参数配置的处理逻辑和标准属性映射配置的处理逻辑非常相似,它对应着第一个if语句分支:

     // 获取每一个ResultMap的子节点 处理constructor节点,该节点用来配置构造方法
    if ("constructor".equals(resultChild.getName())) {
        // 处理constructor节点
        processConstructorElement(resultChild, typeClass, resultMappings);
    
    }
    

    processConstructorElement()方法中,mybatis读取出constructor元素的所有argidArg子元素定义.

    因为这两个元素用于配置构造参数,所以需要为他们添加上ResultFlag.CONSTRUCTOR标记,针对idArg还要额外增加ResultFlag.ID标记.

    添加完标记之后,剩下的操作就和标准属性映射配置的处理一样了,殊途同归,将剩余的操作都交给了buildResultMappingFromContext()方法来完成:

    private void processConstructorElement(XNode resultChild, Class<?> resultType, List<ResultMapping> resultMappings) throws Exception {
        // 获取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);
            }
            resultMappings.add(
                    buildResultMappingFromContext(
                            argChild /*constructor元素的idArg或者arg子元素*/
                            , resultType/*构造方法对应的java对象*/
                            , flags /*参数类型标记*/
                    )
            );
        }
    }
    

    因此buildResultMappingFromContext()方法实际处理的是id,result,association,collection,arg以及idArg六个子元素.

    简单理解buildResultMappingFromContext方法的入参

    buildResultMappingFromContext方法是用来构建ResultMapping对象实例的,他有三个参数content,resultType以及flags.

    其中content是一个XNode对象实例,他表示一个resultMap元素的子元素,他可以是arg,idArg,collection,association,result以及id.

    resultType参数则表示这个元素对应的java类型。

    flags表示这个元素的性质,比如这个元素是不是一个构造参数,或者这个元素是不是一个数据库主键。

    buildResultMappingFromContext方法的解析操作

    buildResultMappingFromContext()方法的实现谈不上复杂与否,基本就是简单逻辑的堆砌,我们先总览一下代码实现:

    /**
        * 构建resultMapping对象
        *
        * @param context    ResultMapping代码块
        * @param resultType 返回类型
        * @param flags      参数类型标记(构造?主键?)
        */
    private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) throws Exception {
        String property;
        if (flags.contains(ResultFlag.CONSTRUCTOR)) {
            // 如果当前节点定义的是一个构造参数,那么读取的是其name属性(形参名称)。
            property = context.getStringAttribute("name");
        } else {
            // 不是构造参数,读取property属性
            property = context.getStringAttribute("property");
        }
    
        // 对应的JDBC列名称
        String column = context.getStringAttribute("column");
        // 对应的java类型
        String javaType = context.getStringAttribute("javaType");
        // 对应的jdbc类型
        String jdbcType = context.getStringAttribute("jdbcType");
    
        // 是否引用了其他select语句
        String nestedSelect = context.getStringAttribute("select");
        /*
            resultMap中可以包含association或者collection这种复合节点,这些复合类型可以使用外部定义的resultMap或者内嵌的resultMap,
            因此针对这里的处理逻辑是:如果有resultMap就获取,没有则动态生成一个,动态生成的resultMap的唯一标志是基于XNode#getValueBasedIdentifier计算得来的。
            */
        String nestedResultMap = context.getStringAttribute(
                "resultMap",/*使用指定的resultMap*/
                processNestedResultMappings(context, Collections.<ResultMapping>emptyList(), resultType)/*这里表示默认值,如果没有则动态生成一个ResultMap*/
        );
    
        // 默认情况下,子对象仅在至少一个列映射到其属性非空时才创建。
        // 通过对这个属性指定非空的列将改变默认行为,这样做之后Mybatis将仅在这些列非空时才创建一个子对象。
        // 可以指定多个列名,使用逗号分隔。默认值:未设置(unset)。
        String notNullColumn = context.getStringAttribute("notNullColumn");
    
        // 当连接多表时,你将不得不使用列别名来避免ResultSet中的重复列名。
        // 因此你可以指定columnPrefix映射列名到一个外部的结果集中。
        String columnPrefix = context.getStringAttribute("columnPrefix");
    
        // 类型转换处理器
        String typeHandler = context.getStringAttribute("typeHandler");
    
        // 获取resultSet集合
        String resultSet = context.getStringAttribute("resultSet");
    
        // 标识出包含foreign keys的列的名称
        String foreignColumn = context.getStringAttribute("foreignColumn");
    
        // 懒加载
        boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager"));
    
        // 解析java类型
        Class<?> javaTypeClass = resolveClass(javaType);
        // 解析类型处理器
        Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
        // 解析出jdbc类型
        JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
    
        // 创建最终的resultMap
        return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
    }
    

    buildResultMappingFromContext()方法首先会根据元素的不同使用不同的方法来获取元素对应的属性名称.

    因为idArgarg两个元素特殊的DTD定义,所以在获取这两个元素的属性名称时,不能通过property属性,而是要通过name属性:

    String property;
    if (flags.contains(ResultFlag.CONSTRUCTOR)) {
        // 如果当前节点定义的是一个构造参数,那么读取的是其name属性(形参名称)。
        property = context.getStringAttribute("name");
    } else {
        // 不是构造参数,读取property属性
        property = context.getStringAttribute("property");
    }
    

    前面的文章中提到过,argidArg元素的name属性,对应的是java构造方法的形参名称,基于形参名称来配置构造参数,我们就可以忽略掉具体的构造参数的顺序了。

    // 对应的JDBC列名称
    String column = context.getStringAttribute("column");
    // 对应的java类型
    String javaType = context.getStringAttribute("javaType");
    // 对应的jdbc类型
    String jdbcType = context.getStringAttribute("jdbcType");
    

    获取java字段名称之后,buildResultMappingFromContext()方法会进行一些基础属性的获取工作,比如获取对应的java类型,对应的数据库列名称,对应的数据库类型等等.

    // 是否引用了其他select语句
    String nestedSelect = context.getStringAttribute("select");
    

    之后,会判断当前元素有没有配置select属性.

    idArg,arg,association,collection这四个元素都可以配置select属性,select属性可以引用一个现有的映射声明语句。

    处理完select属性之后,开始处理resultMap属性的配置.

    /*
        resultMap中可以包含association或者collection这种复合节点,这些复合类型可以使用外部定义的resultMap或者内嵌的resultMap,
        因此针对这里的处理逻辑是:如果有resultMap就获取,没有则动态生成一个,动态生成的resultMap的唯一标志是基于XNode#getValueBasedIdentifier计算得来的。
        */
    String nestedResultMap = context.getStringAttribute(
            "resultMap",/*使用指定的resultMap*/
            processNestedResultMappings(context, Collections.<ResultMapping>emptyList(), resultType)/*这里表示默认值,如果没有则动态生成一个ResultMap*/
    );
    
    

    虽然arg,idArg,collection以及association这四个元素都能够配置resultMap属性,但是argidArg只能引用现有的结果映射配置,而collection,association这两个元素还能直接用来配置嵌套结果映射.

    因此针对collectionassociation这两个元素,还会调用processNestedResultMappings()方法解析嵌套的结果映射配置.

    processNestedResultMappings()方法是用来解析嵌套映射三巨头的,因此除了collectionassociation元素之外,case元素也能被该方法处理,他负责将嵌套结果映射配置解析成相应的ResultMap对象,并返回ResultMap对象的全局引用ID.

    /**
        * 处理嵌套的ResultMap,作用于处理association或者collection节点、
        * <p>
        * resultMap中可以包含association或者collection这种复合节点,这些复合类型可以使用外部定义的resultMap或者内嵌的resultMap,
        * 因此针对这里的处理逻辑是:如果有resultMap就获取,
        * 没有则动态生成一个,动态生成的resultMap的唯一标志是基于XNode#getValueBasedIdentifier计算得来的。
        *
        * @param context        父级XML代码块
        * @param resultMappings 已有的resultMapping集合
        * @param enclosingType  返回类型
        */
    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())) {
            // association和collection property有select属性,这里只处理非select参数的代码块
            /*
                * select可以指定另外一个映射语句的ID,加载这个属性映射需要的复杂类型。
                * 在列属性中指定的列值将会被传递给目标select语句中作为参数。
                */
            if (context.getStringAttribute("select") == null) {
                // 没有指定select属性
                validateCollection(context, enclosingType);
                // 解析嵌套的resultMap元素
                ResultMap resultMap =
                        resultMapElement(context, resultMappings, enclosingType);
                return resultMap.getId();
            }
        }
        return null;
    }
    

    processNestedResultMappings()方法不会处理指定了select属性的元素,这是因为同一条属性映射配置不能在指定select属性的同时配置嵌套映射.

    processNestedResultMappings()方法的实现并不复杂,在单独对collection元素做了校验之后,就把创建嵌套结果映射的任务交给resultMapElement()方法完成,这时候,我们可以看到processNestedResultMappings()方法和resultMapElement()方法二者之间构成了递归调用的关系:

    @startuml
    autonumber
    participant "resultMapElement()" as rme
    participant "buildResultMappingFromContext()" as brmfc
    participant "processNestedResultMappings()" as pnrm
    
    activate rme
    opt 处理标准属性映射配置
        rme -> brmfc ++ : 处理元素定义
            opt 未引用其他结果映射
                brmfc -> pnrm ++ : 处理嵌套结果映射
                opt 未配置select属性
                == 开始递归调用 ==
                    pnrm -[#red]> rme ++ #red: 解析处理嵌套结果映射 <color:red> <b> [递归调用]
                    return
                == 结束递归调用 ==
                end
            return
            end
        return
    end
    
    @enduml
    
    递归图示

    processNestedResultMappings()方法为什么需要单独校验collection元素呢?

    我们需要先明确一点,调用processNestedResultMappings()方法的前提是子元素没有配置resultMap属性,在这个前提下,association元素和case元素的类型早就在前面的处理过程中加载或者推断出来了.

    类型获取

    因此,此时只有collection元素才有可能无法获取对应的集合类型.所以在真正解析collection元素之前,我们需要校验collection元素是否定义了对应的java类型.

    validateCollection()方法实现比较简单,因为如果collection元素配置了resultMap或者resultType属性,mybatis是可以根据这两个属性间接得到集合类型的,因此validateCollection()方法主要是校验在没有配置这些属性的时候,能否通过反射来获取集合类型:

    /**
        * 验证集合
        *
        * @param context       XML代码块
        * @param enclosingType 返回类型
        */
    protected void validateCollection(XNode context, Class<?> enclosingType) {
        if ("collection".equals(context.getName()) /*处理Collection集合*/
                && context.getStringAttribute("resultMap") == null/*没有定义resultMap*/
                && context.getStringAttribute("resultType") == null/*没有定义resultType*/
        ) {
            // 解析collection内部块
    
            // 获取将要返回类型的类型元数据
            MetaClass metaResultType = MetaClass.forClass(enclosingType, configuration.getReflectorFactory());
    
            /*获取其property值,该值对应返回类的字段*/
            String property = context.getStringAttribute("property");
            if (!metaResultType.hasSetter(property)) {
                throw new BuilderException(
                        "Ambiguous collection type for property '" + property + "'. You must specify 'resultType' or 'resultMap'.");
            }
        }
    }
    

    相关文章

      网友评论

        本文标题:Mybatis源码之美:3.5.6.resultMap元素的解析

        本文链接:https://www.haomeiwen.com/subject/cxfqwhtx.html