MyBatis源码核心处理层:MyBatis初始化流程下

1:概述

    接着上篇文章MyBatis初始化流程上,我们继续分析MyBatis的初始化流程,上篇文章讲解到配置文件的解析,本篇我们接着分析MyBatis是如何解析我们自己编写的mapper文件的,整个过程可能比较复杂,涉及到的知识点也比较多,如动态语句的解析,resultMap 节点解析,二级缓存,mapper文件与DAO的绑定,虽然比较复杂,但是耐心研究下来,还是有不少东西可以学习。对映射文件不熟悉的童鞋也可以参考官网:https://mybatis.org/mybatis-3/zh/sqlmap-xml.html 个人觉得,描述的还是比较详细的,本篇有些例子也是基于里面的样例的;如下图,在分析之前,我也简单画了一下MyBatis初始化流程图,这样也有助于理解分析整个流程:
MyBatis源码核心处理层:MyBatis初始化流程下_第1张图片
    上篇文章我们只分析到了上图的红色部分,现在我们开始分析剩下的部分,请童鞋们注意一下,在解析的过程中,每个类的一些中间产物和相关功能。

2.映射文件的解析入口

    从上篇文章可知,映射文件的解析过程也是 MyBatis 配置文件解析过程的一部分。MyBatis 的配置文件由 XMLConfigBuilder 的 parseConfiguration 进行解析,而mapper文件的解析逻辑则封装在mapperElement 方法中,如下,为该函数的代码:

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            if ("package".equals(child.getName())) {
                //(1):将包内的映射器接口实现全部注册为映射器,里面涉及到一些注解解析的过程
                String mapperPackage = child.getStringAttribute("name");
                configuration.addMappers(mapperPackage);
            } else {
                //(2):url class resource解析
                String resource = child.getStringAttribute("resource");
                String url = child.getStringAttribute("url");
                String mapperClass = child.getStringAttribute("class");
                if (resource != null && url == null && mapperClass == null) {
                    ErrorContext.instance().resource(resource);
                    //(3):解析xml文件节点,以文件相对路径表示
                    InputStream inputStream = Resources.getResourceAsStream(resource);
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                    mapperParser.parse();
                } else if (resource == null && url != null && mapperClass == null) {
                    //(4):以URL形式表示xml文件
                    ErrorContext.instance().resource(url);
                    InputStream inputStream = Resources.getUrlAsStream(url);
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
                    mapperParser.parse();
                } else if (resource == null && url == null && mapperClass != null) {
                    //(5) class文件解析
                    Class <?> mapperInterface = Resources.classForName(mapperClass);
                    configuration.addMapper(mapperInterface);
                } else {
                    throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
                }
            }
        }
    }
}

    要了解上图所示的代码可能需要对MyBatis中配置文件的mappers节点的配置要比较熟悉,该节点总共有四种配置方式,具体可参考官网配置:https://mybatis.org/mybatis-3/zh/configuration.html#mappers

代码分析(注意上面的代码注释)

  • 代码(1)处。主要是解析配置包路径的方式,该处涉及到注解一些配置,实际开发中,我们以xml文件配置为准,感兴趣的童鞋可以了解MyBatis里面的相关注解,如@select。本篇不着重分析。

  • 代码(2)处。通过XMLMapperBuilder解析xml文件,本篇会重点分析此处.

  • 代码(3)处,逻辑同代码(2)处,两者不同的是,表示xml文件位置的方式不同。

  • 代码(4)处,逻辑通代码(1)。

        从上面的代码分析可知,映射文件是由XMLMapperBuilder的Parse方法进行处理的。XMLMapperBuilder同样也是继承BaseBuilder,童鞋记得要去看一下其对应的构造函数,需要留意一下MapperBuilderAssistant这个辅助类,在后面流程会有经常看到它的身影。XMLMapperBuilder#Parse对应的方法代码如下:
    MyBatis源码核心处理层:MyBatis初始化流程下_第2张图片
    代码分析(注意上面的代码注释)

  • 代码(1)处,判断resource表示的映射文件是否已经被解析过。

  • 代码(2)处,解析映射文件,本篇重点。

  • 代码(3)处,添加到Configuration对应的资源节点中。

  • 代码(4)处,绑定,映射文件与DAO进行绑定。

  • 代码(5)处,处理解析过程的异常。

对于代码(1)处,不做过多介绍,下面直接进入代码(2)处进行分析。

3.映射文件解析

    在分析一个东西的解析原理的时候,最好是对这个东西的使用能有充分的了解,对于映射文件的配置,童鞋可事先参考一下官网(https://mybatis.org/mybatis-3/zh/sqlmap-xml.html),再回顾回顾一下映射文件的所有相关配置;如下,为configurationElement方法解析映射文件的流程代码。
MyBatis源码核心处理层:MyBatis初始化流程下_第3张图片
映射文件解析过程如下:

  • 代码<1> 处,获得 namespace 属性
  • 代码 <2> 处,设置namespae
  • 代码<3> 处,解析cache和cache-ref标签,二级缓存相关。
  • 代码<4> 处,解析resultMap节点
  • 代码<5> 处,解析 sql 节点。
  • 代码<6> 处,解析 select,insert ,update ,delete 节点。

3.1cache 节点解析-—二级缓存配置解析

    对于MyBatis的缓存,很多童鞋应该都比较熟悉,一级缓存在MyBatis是默认开启的,在上篇文章,setting节点的应用也可以发现,cacheEnabled 这个配置默认值是true。而二级缓存是要我们自己配置的,具体配置也很简单,童鞋可自己参考官网的配置:https://mybatis.org/mybatis-3/zh/sqlmap-xml.html#cache 上面也简单介绍了一些缓存配置项,如LRU,FIFO等缓存清除策略。

二级缓存对应的解析代码如下:

private void cacheElement(XNode context) {
    if (context != null) {
        //(1):cache实现,默认为PERPETUAL,为PerpetualCache的别名
        String type = context.getStringAttribute("type", "PERPETUAL");
        Class <? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
        //(2):回收策略,Cache实现类,为包装器,
        String eviction = context.getStringAttribute("eviction", "LRU");
        Class <? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
        //(3):刷新的时间间隔
        Long flushInterval = context.getLongAttribute("flushInterval");
        Integer size = context.getIntAttribute("size");
        boolean readWrite = !context.getBooleanAttribute("readOnly", false);
        boolean blocking = context.getBooleanAttribute("blocking", false);
        Properties props = context.getChildrenAsProperties();
        //(4):添加到Configuration的caches变量中
        builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
}

代码分析(注意上面的代码注释)

  • 代码(1)处,获取缓存的配置的实现,默认的别名为PERPETUAL,在上篇文章我们提到过Configuration的构造函数,谈到注册过默认别名的注册,就有该别名,该别名对应的是基础模块中的org.apache.ibatis.cache.impl.PerpetualCache类,该类实现缓存比较简单,低层主要是基于HashMap来实现,童鞋可看一下相关代码,

  • 代码(2)处,获取缓存的清除策略,默认为LRU,该别名同样在上篇文章默认的别名注册也提起过,LRU对应的类是org.apache.ibatis.cache.decorators.LruCache。实现LRU算法是通过LinkedHashMap实现的,该处需要了解一下LinkedHashMap实现原理。该类,其实是是一个装饰器模式,对于MyBatis的缓存模块,除了代码(1)提到的PerpetualCache,其它实现Cache类的实现都是装饰器。

  • 代码(3)处,设置缓存时间,刷新时间间隔等属性,对于readOnly属性,需要特别注意,读者可参考官网的描述,后面在使用时,会提到这点。blocking属性,如果设置为true ,就会有一个对应的org.apache.ibatis.cache.decorators.BlockingCache,该类也是一个装饰器,在基础模块的缓存模块有提到

  • 代码(4)处,利用构建者模式,创建cache的实例,然后添加到Configuration对象的caches属性中,该处的构建需要了基础模块的缓存模块和装饰器模式。

    至此,MyBatis的二级缓存的解析到此就结束了,如果对MyBatis基础模块的缓存模块有所理解,代码了解起来可能比较轻松。童鞋也可先阅读一下该模块,该模块比较简单,后面博主也会再写一篇关于缓存模块的文章;当然童鞋对于缓存模块的代码也可以先不理解,在MyBatis的执行流程再去细究也行。

3.2 cache-ref节点解析

在MyBatis中,如果想共用二级换成配置,则需要用cache-ref,cache-ref的配置比较简单,配置代码如下:

对应的解析代码入下所示:

private void cacheRefElement(XNode context) {
    if (context != null) {
        //(1):添加到Configuration对象的cacheRefMap属性中
        configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
        CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
        try {
            //2 解析。调用MapperBuilderAssistant解析
            cacheRefResolver.resolveCacheRef();
        } catch (IncompleteElementException e) {
            //3.可能会抛出异常,依赖问题
            configuration.addIncompleteCacheRef(cacheRefResolver);
        }
    }
}

代码分析(注意上面的代码注释)

  • 代码(1)处,将cachref添加到Configuration的cacheRefMap属性中。
  • 代码(2)处,解析cachref,在该处,其实可能会出现异常,通过对cache节点的分析可知,如果依赖的SomeMapper节点还没被解析,Configuration的caches将获取不到对应的Cache实例,该处将会抛出异常
  • 代码(3)处,将代码(2)处的异常添加到Configuration的incompleteCacheRefs属性,后面会统一解决这些运行时的特定异常。

3.3 resultMap节点解析

    ResultMap无疑不是MyBatis最常见的元素,官网也说它是MyBatis 中最重要最强大的元素,可以让使用者从 90% 的 JDBC ResultSets 数据提取代码中解放出来。并在一些情形下允许你进行一些 JDBC 不支持的操作;由此可见,resultMap的重要性;下面我们就开始ResultMap的解析,从这个开始,可能会有点慢慢变得乏,变得难了,希望读者能够耐心看下去。
     在MyBatis中,解析ResultMap节点,是通过resultMapElements循环调用resultMapElement方法的,resultMapElement代码定义如下:

private ResultMap resultMapElement(XNode resultMapNode, List <ResultMapping> additionalResultMappings, Class <?> enclosingType) throws Exception {
    ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
    //(1):获得type属性,Java类
    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();
    //(2)处理子节点
    for (XNode resultChild : resultChildren) {
        if ("constructor".equals(resultChild.getName())) {
            //(2.1)处理 constructor节点
            processConstructorElement(resultChild, typeClass, resultMappings);
        } else if ("discriminator".equals(resultChild.getName())) {
            //(2.2)处理节点
            discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
        } else {
            //(2.3)处理 association、collection等节点
            List <ResultFlag> flags = new ArrayList <>();
            if ("id".equals(resultChild.getName())) {
                flags.add(ResultFlag.ID);
            }
            resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
        }
    }
    //(3)解析id
    String id = resultMapNode.getStringAttribute("id",
        resultMapNode.getValueBasedIdentifier());
    //(4):获取 extends 和 autoMapping
    String extend = resultMapNode.getStringAttribute("extends");
    Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
    ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
    try {
        //(5)构建resultMap
        return resultMapResolver.resolve();
    } catch (IncompleteElementException e) {
        configuration.addIncompleteResultMap(resultMapResolver);
        throw e;
    }
}

    代码比较长比较复杂,童鞋们记得留意上面的代码注释,跟着我一起分析:
解析过程代码分析:

  • 代码(1)处,获取Type属性, Type属性优先级为type ,ofType,resulType,javaType。
  • 代码(2)处,解析ResultMap子节点,在下面3.3.1分析。
  • 代码(2.1)和(2.3)处,解析ResultMap其它子节点,在下面3.3.2分析。
  • 代码(4)处,获取 extends 和 autoMapping,比较简单,不在阐述
  • 代码(5)处,构造ResultMap对象。

3.3.1 ResultMap 子节点解析

    ResultMap子节点的解析最终生成的类是org.apache.ibatis.mapping.ResultMapping,所以,对于这个类,童鞋们应该有必要了解一下它里面的属性;如下,为ResultMapping的代码定义,相关属性我都加了标注,童鞋可参考研究一下:

public class ResultMapping {
    /** 全局唯一的Configuration对下岗*/
    private Configuration configuration;
    /** 配置的property值*/
    private String property;
    /** 配置的列名 */
    private String column;
    /** 属性值对应的Jav类型*/
    private Class <?> javaType;
    /** 列名对应的jdbc类型*/
    private JdbcType jdbcType;
    /** 类型处理器,它会覆盖默认的类型处理器*/
    private TypeHandler <?> typeHandler;
    /**对应节点的 resultMap 属性,该属性通过id引用了另一个节点定义,它负 责将结采集中的一部列映射
     * 成其它关联的结果对象,这样我们就可以通过join方法进行关联,然后直接映射成多个对象,并同时设置这些对象之间的组合关系*/
    private String nestedResultMapId;
    /** 对应节点的select属性,改属性可以引入另外一个select,会将指定的列作为参数进行查询,该属性可能导致n+1问题,*/
    private String nestedQueryId;
    /** 对应节点的 notNullColumn 属性拆分后的结果*/
    private Set <String> notNullColumns;
    /** 对应节点的 columnPrefix 属性*/
    private String columnPrefix;
    /**处理后的标志,标志共两 个:id 和 constructor*/
    private List <ResultFlag> flags;
    /** 对应节点的 column ,属性拆分后生成的结果 composites.size () >0 会使 column 为 null*/
    private List <ResultMapping> composites;
    /** 对应节点的 resultSet 属性*/
    private String resultSet;
    /** 对应节点的 foreignColumn 属性*/
    private String foreignColumn;
    /** 是否延迟加载*/
    private boolean lazy;
 }

    代码processConstructorElement方法定义如下:

private void processConstructorElement(XNode resultChild, Class <?> resultType, List <ResultMapping> resultMappings
    List <XNode> argChildren = resultChild.getChildren();
    for (XNode argChild : argChildren) {
        List <ResultFlag> flags = new ArrayList <>();
        flags.add(ResultFlag.CONSTRUCTOR);
        //(1)添加id标注
        if ("idArg".equals(argChild.getName())) {
            flags.add(ResultFlag.ID);
        }
        //(2)开始构建ResultMapping对象
        resultMappings.add(buildResultMappingFromContext(argChild, resultType, flags));
    }
}

    其实processConstructorElement方法比较简单,就是产生ResultFlag标志,在ResultMapping里面说到过ResultFlag的种类,童鞋可回顾看一下;在代码(2)处,通过调用buildResultMappingFromContext来构建ResultMapping对象,该方法是解析ResultMap节点的核心,需要大家耐心搞懂这个,入下,为buildResult-MappingFromContext的代码定义:

 private ResultMapping buildResultMappingFromContext(XNode context, Class <?> resultType, List <ResultFlag> flags) throws Exception {
     String property;
     //(1):constructor 获取属性名
     if (flags.contains(ResultFlag.CONSTRUCTOR)) {
         property = context.getStringAttribute("name");
     } else {
         property = context.getStringAttribute("property");
     }
     //(2)简单属性的获取
     String column = context.getStringAttribute("column");
     String javaType = context.getStringAttribute("javaType");
     String jdbcType = context.getStringAttribute("jdbcType");
     String nestedSelect = context.getStringAttribute("select");
     //(3)处理association,collection,case
     String nestedResultMap = context.getStringAttribute("resultMap",
         //association  节点解析,会将association 节点重新解析成resultMap对象
         processNestedResultMappings(context, Collections.emptyList(), resultType));
     String notNullColumn = context.getStringAttribute("notNullColumn");
     String columnPrefix = context.getStringAttribute("columnPrefix");
     String typeHandler = context.getStringAttribute("typeHandler");
     String resultSet = context.getStringAttribute("resultSet");
     String foreignColumn = context.getStringAttribute("foreignColumn");
     boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager"));
     Class <?> javaTypeClass = resolveClass(javaType);
     //(4)typeHandler解析
     Class <? extends TypeHandler <?>> typeHandlerClass = resolveClass(typeHandler);
     JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
     //(5)构建ResultMapper对象
     return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);

    对上面的的执行流程,我简单进行了划分,如下面的代码分析

  • 代码(1)处,获取属性名,只是CONSTRUCTOR配置不同。
  • 代码(2)处,简单属性的获取,代码比较简单,就不再说明
  • 代码(3)处,此处会涉及到association,collection,case等节点的解析,这些节点最终其实也是调用resultMapElement,生成ResultMap对象,然后将ResultMap的id赋值给ResultMapping对象的nestedResultMap。这些配置可以方便我们对一些join查询,pojo复杂属性的映射操作方便。具体可以参考下面的测试用例。
  • 代码(4)处,Typehandle解析,此处,并没有什么复杂,使用的话,会在结果处理提到。
  • 代码(5)处,通过MapperBuilderAsstant并结合已解析出来的属性构建ResultMapping对象。

    对单纯讲解上面的代码,可能比较枯燥,也很难讲解清楚,童鞋可参考下面的测试用例一起理解:

mapper文件配置
MyBatis源码核心处理层:MyBatis初始化流程下_第4张图片
测试查询结果用例1:
MyBatis源码核心处理层:MyBatis初始化流程下_第5张图片
查询结果,连接查询结果:
在这里插入图片描述
测试查询结果用例2:
MyBatis源码核心处理层:MyBatis初始化流程下_第6张图片
生成的ResultMap对象:
MyBatis源码核心处理层:MyBatis初始化流程下_第7张图片

3.3.2 ResultMap 其它子节点的解析

    对于ResultMap其它子节点的解析,其实核心还是在buildResultMappingFromContext(),这里就不再阐述了,需要注意的时候,在理解discriminator子节点的解析之前,希望不了解该用法的童鞋先了解一下,可以参考一下这篇博客:https://www.cnblogs.com/zwwhnly/p/11212396.html

    到这里,ResultMap节点的解析过程就分析完了。总的来说,虽然有点复杂,但是在了解了ResultMap一些子节点的配置的时候,理解起来可能就没那么困难。希望童鞋在分析的过程中结合一些测试用例,这样可能就比较轻松一点。

3.4.解析sql节点

    用sq l节点定义可重用的 SQL 语句片段,当需要重用sql节点中定义的 SQL 语句片段时,只需要使用include节点引入相应的片段即可,对于Sql节点的配置可参考官网,如下,为sql节点的解析代码:

private void sqlElement(List <XNode> list) {
    if (configuration.getDatabaseId() != null) {
        sqlElement(list, configuration.getDatabaseId());
    }
    sqlElement(list, null);
}
private void sqlElement(List <XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
        String databaseId = context.getStringAttribute("databaseId");
        String id = context.getStringAttribute("id");
        id = builderAssistant.applyCurrentNamespace(id, false);
        if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
            sqlFragments.put(id, context);
        }
    }
}

    对应sql节点的解析比较简单,就不再进行详细的分析了,整个过程就是将sql节点存入XMLMapperBuilder中的sqlFragments,其中key为sql节点id,value为对应的xNode对象。

3.5 解析SQL语句-select|insert|update|delete节点解析

3.5.1 sql语句解析入口

    压轴戏开始,回到XMLMapperBuilder中的configurationElement的代码(6)处,sql语句的解析是通过buildStatementFromContext方法进行的,该方法的定义如下:

private void buildStatementFromContext(List <XNode> list) {
    if (configuration.getDatabaseId() != null) {
        buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
}
private void buildStatementFromContext(List <XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
        final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
        try {
            statementParser.parseStatementNode();
        } catch (IncompleteElementException e) {
            configuration.addIncompleteStatement(statementParser);
        }
    }
}

    通过上面的代码可知,sql语句的解析不再是由XMLMapperBuilder解析,而是由XMLStatementBuilder负责。结合本篇开头的流程图,童鞋也可以跟我一样,把解析过程涉及到哪些类,有哪些中间产物画出来,这样对于一些复杂漫长的解析流程有助于理解和回顾。

public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");
    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
        return;
    }
    String nodeName = context.getNode().getNodeName();
    //(1)根据节点的名称来确定sql类型
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    //(2)根据节点类型,来确定二级缓存的一些默认配置,select -> flushCache=false useCache=true
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
    //(3)解析include节点
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());
     
    String parameterType = context.getStringAttribute("parameterType");
    Class <?> parameterTypeClass = resolveClass(parameterType);
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);
    //(4)解析  节点 Parse selectKey after includes and remove them.
    processSelectKeyNodes(id, parameterTypeClass, langDriver);
    //(5) 生成sql语句
    // Parse the SQL (pre:  and  were parsed and removed)
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
        keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
        keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
            configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
            ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }
    //sqlSource 是sql语句的载体,
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String resultType = context.getStringAttribute("resultType");
    Class <?> resultTypeClass = resolveClass(resultType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultSetType = context.getStringAttribute("resultSetType");
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    if (resultSetTypeEnum == null) {
        resultSetTypeEnum = configuration.getDefaultResultSetType();
    }
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    String resultSets = context.getStringAttribute("resultSets");
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

如上代码,为parseStatementNode的代码定义,整个代码流程感觉比mapper文件的解析还长,希望童鞋们静下新来分析。如下,将整个逻辑拆分了如下几个逻辑

  1. 代码(1)处,根据节点类型来判断sql语句类型,比较简单,不再详细描述。
  2. 代码(2)处,根据节点类型来设置一些二级缓存的默认值,如查询的时候,flushCache的默认值为false,userCache为true。
  3. 代码(3)处,解析include节点,各种递归,比较复杂,详细分析见3.5.2。
  4. 代码(4)处,解析selectKey的,感兴趣的童鞋可自己研究参考,因为博主平时用到这个比较少,而且篇幅原因,本篇不研究分析,后面会结合其它知识点再分析。
  5. 代码(5)处,生成sql语句,也是各种复杂骚操作,详细分析见3.5.3。

3.5.2 include标签解析

    include节点可以直接引用我们在映射文件定义的Sql节点,其解析逻辑是封装在XMLIncludeTransformer的applyIncludes方法,因为该过程涉及到递归调用,本篇会通过例子进行讲解,希望童鞋们能够结合具体例子来分析里面的过程,这样比较容易了解。applyIncludes的方法代码定义如下:

private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
    //(1)include 标签解析
    if (source.getNodeName().equals("include")) {
        //(1.1)获取sql节点的拷贝
        Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
        //(1.2)获取include标签内部定义的变量
        Properties toIncludeContext = getVariablesContext(source, variablesContext);
        applyIncludes(toInclude, toIncludeContext, true);
        //(1.3) 如果include引入的sql标签不在同一个文档上,则导入到当前文档上
        if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
            toInclude = source.getOwnerDocument().importNode(toInclude, true);
        }
        //(1.4)将include标签替换成sql标签
        source.getParentNode().replaceChild(toInclude, source);
        while (toInclude.hasChildNodes()) {
            //(1.5)将sql标签里面的字标签都插入到字标签的前面
            toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
        }
        //(1.6)删除sql标签
        toInclude.getParentNode().removeChild(toInclude);
    } else if (source.getNodeType() == Node.ELEMENT_NODE) {
        //(2):
        if (included && !variablesContext.isEmpty()) {
            //(2.1)占位符处理replace variables in attribute values
            NamedNodeMap attributes = source.getAttributes();
            for (int i = 0; i < attributes.getLength(); i++) {
                Node attr = attributes.item(i);
                attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
            }
        }
        NodeList children = source.getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
            applyIncludes(children.item(i), variablesContext, included);
        }
    } else if (included && (source.getNodeType() == Node.TEXT_NODE || source.getNodeType() == Node.CDATA_SECTION_NODE)
        && !variablesContext.isEmpty()) {
        //(3):解析文本节点,
        // replace variables in text node
        source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
    }
}

上面几乎每个过程的代码我都打上了注释,读者可根据注释和下面的测试例子进行跟踪。
MyBatis源码核心处理层:MyBatis初始化流程下_第8张图片
    如上解析流程,将解析逻辑分为三处,每行代码的核心逻辑我都有标注,解析的步骤和调用顺序都已经在流程图中用序号表名,童鞋借助参考理解。总的来说就是一个递归调用过程,文本也是一个文本节点,总的来说,就是先把include标签换成sql标签,然后将sql标签的内容插入到sql标签之前,最终删除sql标签;通过这三步,就完成了include标签的转换。
    最终生成的sql如下图所示:
在这里插入图片描述

3.5.3 sql语句生成

    看到这里,可能有些童鞋对org.apache.ibatis.scripting.LanguageDriver 这个接口不熟悉,这个接口是语言驱动接口,在MyBatis项目里,这个接口只有唯一的实现,那就是XMLLanguageDriver ,所以大家直接看这个类的实现就行,如下,为XMLLanguageDriver的createSqlSource代码实现:
在这里插入图片描述
    如上,SQL 语句的解析逻辑被封装在了 XMLScriptBuilder 类的 parseScriptNode 方法中,同样,该类也是继承BaseBuilder,在分析Sql语句之前,我们需要先解一下与该章节相关的两个类:org.apache.ibatis.mapping.SqlSource和org.apache.ibatis.scripting.xmltags.SqlNode,具体可参考MyBatis动态sql解析,SqlNode在MyBatis中,可以表示我们写的动态sql标签。如if标签体现的是ifSqlNode,for标签体现的是ForEachSqlNode,而SqlSource表示的是我们的一条原始sql语句,里面包含有动态的sql语句。在了解了这两个类之后,我们再来看parseScriptNode这个方法:

public SqlSource parseScriptNode() {
    //(1)sql解析
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    if (isDynamic) {
        //(2)动态sql语句
        sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
        //(3)非动态sql语句
        sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
}

代码分析

  • 代码(1)处,sql解析,将XNode形式的动态sql语句转为以SqlNode形式的动态sql。
  • 代码(2)处,如果是动态sql,则返回DynamicSqlSource。
  • 代码(3)处。如果非动态sql语句,则返回RawSqlSourc
        通过上面的代码分析,核心是利用parseDynamicTags生成sqlNode对象,对于parseDynamicTags方法,整体流程不难,里面也有递归调用,但是要对sqlNode相关接口熟悉一下,然后还需要看一下NodeHandler相关的接口实现,如下,为parseDynamicTags的代码定义:
protected MixedSqlNode parseDynamicTags(XNode node) {
    List <SqlNode> contents = new ArrayList <>();
    NodeList children = node.getNode().getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
        XNode child = node.newXNode(children.item(i));
        //(1)解析文本节点
        if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
            String data = child.getStringBody("");
            TextSqlNode textSqlNode = new TextSqlNode(data);
            //(2)判断是否是动态sql
            if (textSqlNode.isDynamic()) {
                contents.add(textSqlNode);
                isDynamic = true;
            } else {
                contents.add(new StaticTextSqlNode(data));
            }
        } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
            //(3)解析动态sql,通过不同标签的NodeHandler接口处理
            String nodeName = child.getNode().getNodeName();
            NodeHandler handler = nodeHandlerMap.get(nodeName);
            if (handler == null) {
                throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
            }
            handler.handleNode(child, contents);
            isDynamic = true;
        }
    }
    return new MixedSqlNode(contents);
}

代码分析:

  • 码(1)处,解析文本节点,在代码(2)处,会判断文本节点是否有动态sql,判断标志是文本是否包含有${}的字符串,如果有,则为动态sql语句,其实判断是否有${}字符串,里面还是用到我们在解析器模块提到的方法,具体可看一下实现。
  • 代码(3)处,利用对应的NodeHandler接口实现解析对应的sql标签,如if标签,则是ifHandler解析,解析完成之后会生成ifSqlNode,然后将ifSqlNode添加到对应的contents集合中去。此处,不知道童鞋发现没,sqlNode的构建有点像树形结构,此处,MyBatis也是利用了组合模式进行构建的,童鞋们可以好好揣摩揣摩。

    致此,sqlSource的解析流程就结束了,整个过程也是比较冗长复杂,希望童鞋们可以静下心来慢慢分析,其实到了此处,童鞋只需要知道sqlSource和sqlNode里面表示的是什么就行。后面在再结合执行流程,就会更加清晰。至于后面利用构建者模式生成MappedStatement就不再分析了,比较简单,童鞋们可以自己看一下。

4.mapper接口绑定

     每个映射配置文件的命名空间可以绑定一个 Mapper 接口,井注册到 MapperRegis中,此处是由MyBatis的绑定模块处理完的,过程比较简单。没有特别绕的地方,主要是利用动态代理为接口类生成Mapper-Proxy,此处就不再分析了,后面在执行流程再进行分析MapperProxy的使用。

5.处理解析过程的异常

    不知道童鞋是否还记得,我们在分析cache-ref节点的过程中,就提到过,会出现异常,当时MyBatis是将该异常放入 incomplet* 集合中;重新回到XMLMapperBuilder的parse方法,在configurationElement调用完毕之后,会调用 parsePendingResultMaps()方法、 parsePendingChacheRefs()方法、parsePendingStatements()方法 三 个 parsePending*()方法处理 incomplet* ,因为解析逻辑处理逻辑比较相似,我们以parsePendingChacheRefs为例进行分析,另外两个童鞋可自行参考源码进行分析,如下,为parsePendingCacheRefs方法定义:

private void parsePendingCacheRefs() {
    Collection <CacheRefResolver> incompleteCacheRefs = configuration.getIncompleteCacheRefs();
    synchronized (incompleteCacheRefs) {
        Iterator <CacheRefResolver> iter = incompleteCacheRefs.iterator();
        while (iter.hasNext()) {
            try {
                //调用CacheRefResolver解析
                iter.next().resolveCacheRef();
                iter.remove();
            } catch (IncompleteElementException e) {
                // Cache ref is still missing a resource...
            }
        }
    }
}

    其实看到这块代码,在看完本篇的cache-ref节点解析的童鞋应该非常清楚,此处也是调用MapperBuilderAssistant的useCacheRef尝试获取cache的实例,此处,有可能会继续失败,但是因为incomplet*集合是在configuration中的,所以,在解析完最后一个mapper的时候,该异常类型的集合一定可以处理完毕。

6.总结

    本篇文章对映射文件的解析过程进行了较为详细的分析,因为全文篇幅比较大,有的地方只是稍微提及,仅仅下篇文章就用了一天半的时间进行撰写,如有错误之处,希望各位童鞋留下评论进行探讨。初始化流程上篇连接:MyBatis初始化流程上

你可能感兴趣的:(MyBatis)