MyBatis工作流程-初始化阶段二


MyBatis源码学习系列文章目录



前言

在上一章节当中我们主要是讲解了MyBatis工作流程初始化阶段的第一步,解析配置文件。解析配置文件主要使用的XMLConfigBuilder,通过这个Builder解析xml文件然后注册相关信息到configuration当中,除mappers之外的每一个节点的解析还是非常简单的,除了前后的相互依赖关系之外,倒也没啥特别的,而到了最后一步,才是真正的繁杂之处了,因为所为的mappers引入的是另一个完整的xml文件。

// 解析mappers节点 同样支持多文件扫描和单文件查找
mapperElement(root.evalNode("mappers"));

本章就来分析MyBatis是如何解析mapper对应的xml文件。

mapper解析目标

mapper解析目标与Config配置文件的解析目标大方向上是一致的,就是读取xml内容并填充到Configuration当中,具体来说则涉及到Configuration当中的两大Map属性:resultMaps和mappedStatements。

protected final Map<String, ResultMap> resultMaps = new StrictMap<ResultMap>("Result Maps collection");
// 以下采用StrictMap保证key值不会重复注册
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>(
		"Mapped Statements collection");

可以看到这里有两个类:ResultMap和MappedStatement,这两个类是啥呢?

在这里插入图片描述
其实在mapper当中最重要的莫过于resultMap、select、delete、update、insert这五个子元素了,而后面四个在MyBatis中又可以统一为执行语句类,所以最后将resultmap解析为ResultMap对象,而其他的四类节点都解析为MappedStatement对象。比如在上面的xml文件中,就会解析出一个ResultMap对象和三个MappedStatement对象。ResultMap如下图所示
在这里插入图片描述
MappedStatement如下图所示
在这里插入图片描述
而其他的节点也会在Configuration当中对应某个属性,比如二级缓存对应org.apache.ibatis.session.Configuration#caches属性,但真实查询时使用的还是MappedStatement当中的cache属性,在Configuration#caches存储是用于解析cache-ref标签时方便查询的,可以说只是一个解析过程中的中间产物。
在这里插入图片描述
对于sql节点虽然在Configuration也有单独的记录,但是它跟caches节点一样也是一个中间产物,在通过include元素引用这个sql片段的select、delete等节点解析过程中会引用到这个属性并将节点内容替换。也就是其实初始化之后这个属性也就是没有价值了。
在这里插入图片描述
这个阶段除了xml文件的解析之外,还有一个非常重要的就是查找xml命名空间所对应的接口并注册。这里不是注册作为Configuration的某个属性,而是通过它的属性mapperRegistry进行注册的,最后相关的接口以及映射关系在org.apache.ibatis.binding.MapperRegistry#knownMappers属性当中。在个knownMappers非常重要。毕竟我们在使用MyBatis时是面向接口的。
在这里插入图片描述
所以整个mapper文件解析的最终目标最重要的三点就是:
1. 解析resultMap元素为org.apache.ibatis.mapping.ResultMap对象并注册到Configuration#resultMaps属性当中
2. 解析select、update、delete、insert元素为org.apache.ibatis.mapping.MappedStatement对象并注册到Configuration#mappedStatements属性当中。
3. 初始化mapper文件命名空间对应的类并通过Configuration#mapperRegistry进行注册

mapper解析流程

在MyBatis的配置文件当中,配置需要解析的mapper.xml文件的方式有两种方式:第一种通过扫描指定文件夹的方式,第二种通过指定扫描文件的方式。
第一种方式如下

<mappers>
    <package name="sample.mybatis.mapper"/>
</mappers>

对应的源码如下:org.apache.ibatis.builder.xml.XMLConfigBuilder#mapperElement

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            if ("package".equals(child.getName())) {
                String mapperPackage = child.getStringAttribute("name");
                configuration.addMappers(mapperPackage);
            } else {
				... 这里为第二种方式
            }
        }
    }
}

此时会直接调用mapperRegistry注册包的方式

public void addMappers(String packageName) {
	mapperRegistry.addMappers(packageName);
}

/**
 * @since 3.2.2
 */
public void addMappers(String packageName) {
	addMappers(packageName, Object.class);
}


/**
 * @since 3.2.2
 */
public void addMappers(String packageName, Class<?> superType) {
    1. 解析包路径下符合条件的类 这个的IsA是要求目标类都必须是superType的子类
	ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
	resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
	2. 获取符合以上条件的类
	Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
	3. 遍历以上的类 并执行注册
	for (Class<?> mapperClass : mapperSet) {
		addMapper(mapperClass);
	}
}

// 解析mapper.xml文件时或者mapper接口时增加
public <T> void addMapper(Class<T> type) {
    1. 首先这个类必须是个接口
	if (type.isInterface()) {
	    2. 如果这个类已经解析过 会报错 不允许重复注册
		if (hasMapper(type)) {
			throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
		}
		boolean loadCompleted = false;
		try {
			3. 保存mapper和代理工厂的映射关系
			knownMappers.put(type, new MapperProxyFactory<T>(type));
			// It's important that the type is added before the parser is run
			// otherwise the binding may automatically be attempted by the
			// mapper parser. If the type is already known, it won't try.   // mapper parser可能会尝试绑定
			MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
			4. 解析mapper接口上的注解并尝试读取默认的xml文件
			parser.parse();
			loadCompleted = true;
		} finally {
			if (!loadCompleted) {
				knownMappers.remove(type);
			}
		}
	}
}

如果对上一章VFS还有印象的话,不难看出这里其实就是扫描指定包路径下面的所有类,然后尝试解析这个类。其实这种方式是在3.2.2版本才加入的,主要的目的是因为要在接口上面增加注解,通过完全使用注解的方式可以不需要xml文件了,而执行注解解析操作是通过MapperAnnotationBuilder这个类来完成的。当然了这里还是会支持xml文件的,在MapperAnnotationBuilder#parse方法当中会通过loadXmlResource来加载按照一定规则存放和命名的文件的。比如接口名称为sample.mybatis.mapper.HotelMapper,那么对应的xml资源路径为sample/mybatis/mapper/HotelMapper.xml。如果将这个文件存放在其他路径下,就不会找到对应xml文件了。如果既没有注解,也不按照规则存放xml文件,在初始化的过程中并不会报错,直到真正运行无法初始化mapper接口方法的时候就会以下的异常

org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): sample.mybatis.mapper.CityMapper.findAll

	at org.apache.ibatis.binding.MapperMethod$SqlCommand.<init>(MapperMethod.java:228)
	at org.apache.ibatis.binding.MapperMethod.<init>(MapperMethod.java:49)
	at org.apache.ibatis.binding.MapperProxy.cachedMapperMethod(MapperProxy.java:66)
	at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:59)
	at com.sun.proxy.$Proxy8.findAll(Unknown Source)
	at sample.mybatis.MyBatisTest.testBuilder(MyBatisTest.java:34)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)

所以,第一种按照包路径加载的方式不是指定xml资源的路径,而是指定mapperInterface的包名。
第二种方式,通过扫描指定文件的方式,既可以指定xml资源的路径,也可以指定对应mapper接口。

<mappers>
    1. 指定mapper接口的方式
	<mapper class="sample.mybatis.mapper.CityMapper"/>
	2. 通过resource相对路径指定mapper.xml文件的路径
	<mapper resource="sample/mybatis/mapper/CityMapper.xml"/>
	3. 通过url指定mapper.xml文件的路径
	<mapper url="file:D:\mybatis\parent-mybatis-parent-29\spring-boot-starter-mybatis-spring-boot-1.3.2\mybatis-spring-boot-samples\mybatis-spring-boot-sample-xml\src\main\resources\sample\mybatis\mapper\HotelMapper.xml"/>
</mappers>

源码如下

String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
    1. 按照resource资源路径加载文件并解析
    ErrorContext.instance().resource(resource);
    InputStream inputStream = Resources.getResourceAsStream(resource);
    // mapper文件解析器 此时会构造一个XPathParser 并解析当前xml为一个Document对象
    // 同时会设置一个解析该xml过程中唯一的一个builderAssistant对象
    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource,
            configuration.getSqlFragments());
    mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
    2. 按照url路径加载文件并解析
    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) {
	3. 按照直接注册mapper接口的方式
    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.");
}

第三种与上面通过扫描包的方式其实是一样的,而通过resource和通过url的差别只不过是获取流的方式不同,真正解析的流程是一致的。如下所示:

1. 创建mapper文件的解析构造器
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource,
        configuration.getSqlFragments());
2. 进行解析操作
mapperParser.parse();

构造mapperParser的过程源码如下

public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource,
                        Map<String, XNode> sqlFragments) {
    1. 首先创建XPathParser并创建document
    this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver()),
            configuration, resource, sqlFragments);
}

private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource,
                         Map<String, XNode> sqlFragments) {
    super(configuration);
    1. 每个XMLMapperBuilder对应一个builderAssistant
    this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
    this.parser = parser;
    this.sqlFragments = sqlFragments;
    this.resource = resource;
}

通过以上的构造器我们不难得出以下的结论:每一个xml文件都对应一个XMLMapperBuilder对象,而这个对象内部都包含一个XPathParser对象和一个MapperBuilderAssistant对象,XPathParser在上一章中我们已经介绍过,用于解析xml文件为Document,并且在读取时将节点转为XNode,所以mapper文件同样支持占位符模式(每个XMLMapperBuilder对象之间是共享configuration,所以也共享在配置文件中定义的变量)。而MapperBuilderAssistant这里是首次出现,但是在后面解析的过程中会起到相当重要的作用。在mapper文件的解析过程中,通过XPathParser读取文件创建好Document之后,通过XMLMapperBuilder读取节点的属性,然后再通过MapperBuilderAssistant进行一定的处理然后再注册到configuration当中。以下为XMLMapperBuilder解析的主流程

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
        1. 解析mapper节点 也就是解析mapper.xml的关键 解析mapper节点 填充要素到configuration当中
        configurationElement(parser.evalNode("/mapper"));
        2. 记录已经解析过的mapper资源
        configuration.addLoadedResource(resource);
        3. 根据namespace查找对应的接口 并通过mapperRegistry进行注册
        bindMapperForNamespace();
    }
    4. 尝试解析尚未解析完全的ResultMap信息
    parsePendingResultMaps();
    5. 尝试解析尚未解析完全的Cache-Ref信息
    parsePendingCacheRefs();
    6. 尝试解析尚未解析完全的XMLStatementBuilder
    parsePendingStatements();
}

其中的重点是第一步(解析mapper文件并注册)和第三步(注册mapper接口)。
- 解析mapper节点
解析xml文件无非就是按照节点一个一个读取,并根据节点的不同封装成不用对象,然后再填充到Configuration当中。

// 解析mapper文件
private void configurationElement(XNode context) {
    try {
  		1. 获取命名空间 设置当前命名空间 保证全局唯一性
        String namespace = context.getStringAttribute("namespace");
        if (namespace == null || namespace.equals("")) {
            throw new BuilderException("Mapper's namespace cannot be empty");
        }
        builderAssistant.setCurrentNamespace(namespace);
        2. 解析二级缓存引用 引用另一个mapper的缓存 因此由可能两个mapper共享一个二级缓存
        cacheRefElement(context.evalNode("cache-ref"));
        3. 解析二级缓存 最后将二级缓存保存到builderAssistant的currentCache属性和Configuration的caches属性当中
        cacheElement(context.evalNode("cache"));
        parameterMapElement(context.evalNodes("/mapper/parameterMap"));
        4. 解析resultMap节点 因为涉及内嵌resultMap的情形(collection或者association)
        resultMapElements(context.evalNodes("/mapper/resultMap"));
        5.    解析sql片段
        //    <sql id="baseStatement">
        //        ci.city_id, ci.name as city_name, ci.state
        //    </sql>
        sqlElement(context.evalNodes("/mapper/sql"));
        6. 解析Statement
        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);
    }
}

以上步骤中最复杂也最关键的是第四步和第六步,分别为解析resultMap和select|insert|update|delete标签。在XMLMapperBuilder方法中相关的方法中我们分析其中的buildResultMappingFromContext

/**
 * <resultMap id="BaseResultMap" type="sample.mybatis.domain.City">
 * <id property="id" column="id" jdbcType="INTEGER"/>
 * <result property="name" column="name" jdbcType="VARCHAR"/>  javaType和jdbcType用于解析TypeHandler
 * <result property="state" column="state" jdbcType="VARCHAR"/>
 * <result property="country" column="country" jdbcType="VARCHAR"/>
 * </resultMap>
 *
 * @param context    resultMap中的子节点 id和result
 * @param resultType sample.mybatis.domain.City
 * @param flags      比如id为ID 或者构造器参数 constructor
 * @return <result property="name" column="name" jdbcType="VARCHAR"/> -> ResultMapping
 * @throws Exception
 */
private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags)
        throws Exception {
    String property;
    if (flags.contains(ResultFlag.CONSTRUCTOR)) {
        property = context.getStringAttribute("name");
    } else {
        property = context.getStringAttribute("property");// 对象属性名称
    }
    String column = context.getStringAttribute("column");// 数据库字段
    String javaType = context.getStringAttribute("javaType");
    String jdbcType = context.getStringAttribute("jdbcType");
    // 当前元素包含有select属性 <collection property="posts" column="id" ofType="Post" select="selectPostsForBlog"/>
    String nestedSelect = context.getStringAttribute("select");
    // 解析association和collection元素 但不包含select属性
    //    <association property="country" javaType="sample.mybatis.domain.Country">
    //            <id property="id" column="country_id" jdbcType="INTEGER"/>
    //            <result property="name" column="country_name" jdbcType="VARCHAR"/>
    //            <result property="continent" column="continent" jdbcType="VARCHAR"/>
    //    </association>
    String nestedResultMap = context.getStringAttribute("resultMap",
            processNestedResultMappings(context, Collections.<ResultMapping>emptyList()));// association collection
    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);
    @SuppressWarnings("unchecked")
    Class<? extends TypeHandler<?>> typeHandlerClass = (Class<? extends TypeHandler<?>>) resolveClass(typeHandler);
    JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
    return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum,
            nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet,
            foreignColumn, lazy);
}

一个resultMap其实就是对一个类的描述,而其中的属性定义则是resultMap中的各个节点来表示,比如id、result、constructor、association和collection,在MyBatis当中resultMap最后会抽象为ResultMap对象,而这些子元素会抽象为ResultMapping对象,上面的方法就是用于解析resultMap的这些子节点为ResultMapping对象的。其中的id和constructor给对应的属性赋予了额外的含义,所以在flags属性当中会记录相关信息。

id 和 result 元素都将一个列的值映射到一个简单数据类型(String, int, double, Date 等)的属性或字段。这两者之间的唯一不同是,id 元素对应的属性会被标记为对象的标识符,在比较对象实例时使用。 这样可以提高整体的性能,尤其是进行缓存和嵌套结果映射(也就是连接映射)的时候。

在这里插入图片描述
而association和collection元素通常代表的不是一个普通的java属性,要么包含了子元素,要么包含了其他的查询语句,也就是说嵌套结果或者嵌套查询,在MyBatis中同样也将这两种节点作为ResultMap处理,同时也是作为一个ResultMapping对象记录到父resultMap中。而在ResultMapping的属性nestedResultMapId或者nestedQueryId中记录相关信息。前者记录的嵌套结果信息,后者记录的是嵌套查询的信息。
在这里插入图片描述在上面的buildResultMappingFromContext方法的主要做的就是读取节点的属性,除了processNestedResultMappings又会递归调用按照resultMap的标签处理之外,没啥复杂的,就是收集相关的信息,最后交给builderAssistant来处理,

/**
 * 对应resultMap中的result子元素 包含ID, CONSTRUCTOR
 * <p>
 * <id property="id" column="id" javaType="long" jdbcType="BIGINT" typeHandler="org.apache.ibatis.type
 * .LongTypeHandler"/>
 * <result property="name" column="name" jdbcType="VARCHAR"/>
 *
 * @param resultType      类的类型
 * @param property        属性名称
 * @param column          属性名称对应的数据库字段名称
 * @param javaType        属性类型
 * @param jdbcType        数据库字段类型
 * @param nestedSelect    是否嵌套查询
 * @param nestedResultMap 嵌套的结果集 比如association和collection元素
 * @param notNullColumn
 * @param columnPrefix    数据库字段前缀 column映射数据库查询语句返回字段包含的前缀
 * @param typeHandler     用户设置的typeHandler 用于setParameter和getResult时使用
 * @param flags           ID, CONSTRUCTOR信息
 * @param resultSet
 * @param foreignColumn
 * @param lazy
 * @return
 */
public ResultMapping buildResultMapping(Class<?> resultType, String property, String column, Class<?> javaType,
                                        JdbcType jdbcType, String nestedSelect, String nestedResultMap, String notNullColumn, String columnPrefix,
                                        Class<? extends TypeHandler<?>> typeHandler, List<ResultFlag> flags, String resultSet, String foreignColumn,
                                        boolean lazy) {
    1. 根据反射类和属性名称解析属性类型
    Class<?> javaTypeClass = resolveResultJavaType(resultType, property, javaType);
    2. 尝试解析出TypeHandler
    TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);
    List<ResultMapping> composites = parseCompositeColumnName(column);
    3. 根据收集的信息构造ResultMapping对象
    return new ResultMapping.Builder(configuration, property, column, javaTypeClass).jdbcType(jdbcType)
            .nestedQueryId(applyCurrentNamespace(nestedSelect, true))
            .nestedResultMapId(applyCurrentNamespace(nestedResultMap, true)).resultSet(resultSet)
            .typeHandler(typeHandlerInstance).flags(flags == null ? new ArrayList<ResultFlag>() : flags)
            .composites(composites).notNullColumns(parseMultipleColumnNames(notNullColumn))
            .columnPrefix(columnPrefix).foreignColumn(foreignColumn).lazy(lazy).build();
}

builderAssistant做的事情就是将以上收集的信息再处理一下,比如解析属性的类型,根据属性类型查找对应的TypeHandler,最后通过建造者模式构造一个目标对象。其实处理resultMap的流程和上面是一模一样的,首先在XMLMapperBuilder#resultMapElements中收集属性信息和子节点,最后构造一个ResultMapResolver对象,这个对象也不过是对收集的属性信息、builderAssistant的简单包装,而调用resolve方法真实调用的就是MapperBuilderAssistant#addResultMap方法。
在这里插入图片描述
在这里插入图片描述
MapperBuilderAssistant的处理跟上面的ResultMapping的方式也差不多,适度解析,然后构造目标对象,这里是ResultMap对象,同样是通过建造者模式。不过比前面要多一步,将构造好的ResultMap对象记录到configuration当中。
在这里插入图片描述
通过以上的步骤,最终完成了resultMap的解析。在上面我们说过,对于association、collection也会作为一个ResultMap进行处理,在ResultMapping中只是通过一个属性关联,而真正的信息也是通过跟以上同样的方式注册到configuration当中的。
在这里插入图片描述
接下来分析一下select|insert|update|delete这些标签的处理方式。MyBatis主要是通过XMLStatementBuilder来处理这些标签的,通过构造一个MappedStatement对象。整个过程中有如下几个点比较复杂,首先就是要将其中的sql语句解析放到MappedStatement的sqlSource当中,而且将其中包含的#{}替换为,必要的情况下还要存储传入的参数映射。
在这里插入图片描述
而这一操作是通过LanguageDriver来完成的,而LanguageDriver又实际上交给了XMLScriptBuilder来处理,为什么要这么复杂,因为实际解析过程中涉及到很多动态语句,比如if、trim这些标签。除了这些复杂性之外,还有include标签。在语句当中可以通过include标签引入其他sql定义的语句片段,MyBatis在解析过程中首先通过XMLIncludeTransformer将include片段替换掉,而处理过程中替换的操作又必须在上面解析为sqlSource之前完成。如下所示
在这里插入图片描述
处理include还是简单的,通过refid找到对应的sql片段然后替换到对应的元素当中即可。
在这里插入图片描述
替换的完整结果如下图
在这里插入图片描述
另外,在上上图的源码当中,我们还可以发现在处理include和解析为sqlSource之间还有一个需要处理的是selectKey标签。MyBatis的处理方式是将这个selectKey对应的元素也作为一个MapperdStatement对象。
在这里插入图片描述
父标签元素对应
在这里插入图片描述
要掌握MappedStatement的解析过程其实就是对这个对象属性的认知,在上面我们已经介绍了sqlSource、keyGenerator、lang,另外在前面篇章介绍缓存时也介绍过二级缓存cache,在日志增强章节也介绍了statementLog。其实在MappedStatement中还有一个更重要的属性resultMaps,对于查询语句无论是定义了resultMap或是resultType属性,都会在这个属性当中存放关联的ResultMap对象。比如不存在resultMap属性但存在resultType属性的
在这里插入图片描述
已经存在resultMap属性的直接关联即可。
在这里插入图片描述
这一块的实现参考源码org.apache.ibatis.builder.MapperBuilderAssistant#getStatementResultMaps

private List<ResultMap> getStatementResultMaps(String resultMap, Class<?> resultType, String statementId) {
    resultMap = applyCurrentNamespace(resultMap, true);

    List<ResultMap> resultMaps = new ArrayList<ResultMap>();
    1. resultMap属性不为空 通过configuration查找指定id的ResultMap对象
    if (resultMap != null) {
        String[] resultMapNames = resultMap.split(",");
        for (String resultMapName : resultMapNames) {
            try {
                resultMaps.add(configuration.getResultMap(resultMapName.trim()));
            } catch (IllegalArgumentException e) {
                throw new IncompleteElementException("Could not find result map " + resultMapName, e);
            }
        }
    } else if (resultType != null) {
        2. resultType属性不为空 则创建一个内联的ResultMap对象 
        ResultMap inlineResultMap = new ResultMap.Builder(configuration, statementId + "-Inline", resultType,
                new ArrayList<ResultMapping>(), null).build();
        resultMaps.add(inlineResultMap);
    }
    return resultMaps;
}

通过以上步骤之后,完成了MappedStatement对象的构造之后,就可以注册到Configuration的mappedStatements属性当中了。

总结一下,针对整个mapperXml文件的解析首先都是由对应的Builder读取参数、然后适度的解析,然后交给builderAssistant构造成目标对象然后注册到Configuration中的指定属性中,这其中最重要的无非就是ResultMap和MappedStatement对象。
在这里插入图片描述

- 注册mapper接口
初始化阶段走到了现在,绝大部分工作已经完成了,但是还差临门一脚。为啥这么说呢?我们平时在使用MyBatis的时候其实是面向接口的,如果我们解析完了mapperXml文件,但我们的接口该如何关联到对应的xml呢?所以在org.apache.ibatis.builder.xml.XMLMapperBuilder#parse中会调用bindMapperForNamespace方法,根据xml的命名空间查找目标类,并添加到configuration当中。

private void bindMapperForNamespace() {
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
        Class<?> boundType = null;
        try {
            1. 根据命名空间查找目标类
            boundType = Resources.classForName(namespace);
        } catch (ClassNotFoundException e) {
            //ignore, bound type is not required
        }
        if (boundType != null) {
            2. 如果还没有注册到configuration当中 则进行注册
            if (!configuration.hasMapper(boundType)) {
                // Spring may not know the real resource name so we set a flag
                // to prevent loading again this resource from the mapper interface
                // look at MapperAnnotationBuilder#loadXmlResource
                configuration.addLoadedResource("namespace:" + namespace);
                3. 添加mapper接口 此时会在org.apache.ibatis.binding.MapperRegistry.knownMappers添加接口与MapperProxyFactory的映射
                configuration.addMapper(boundType);
            }
        }
    }
}

而configuration.addMapper恰好和扫描文件夹注册接口的方式其实就是一样的。通过以上步骤最后将一个mapper接口与MapperProxyFactory关联,在实际使用时可以通过MapperProxyFactory来获取对应接口的具体实现进行操作了,当然这些不是在初始化阶段完成的。
在这里插入图片描述

总结

MyBatis对于mapperXml文件的解析过程其实是简单的,跟流水线一样,只是细节比较多。整体流程是通过对应的Builder读取xml中的节点,读取节点属性和子节点,进行适度解析,关联其他元素,最后交给builderAssistant构造成目标对象并注册到configuration中。完成了xml文件的解析,还需要将接口也注册到configuration当中。结合上一章,无论是配置文件还是mapper文件,包括对应的接口信息,最后都存放到了configuration属性当中,后续的任何操作我们就只用面对configuration属性了。另外在MyBatis针对xml解析并构造对象的过程中大量使用了建造者模式,这种模式与工厂模式都可以创建对象并为客户端屏蔽复杂度,但是使用的场景不同,用一句话形容二者的差别就是:**建造者模式生产定制版,而工厂模式生产大众版**。建造者的应用场景如下:

  1. 需要生成的对象具有复杂的内部结构,实例化对象需要屏蔽对象内部的细节,让上层代码与复杂对象的实例化过程解耦,可以使用建造者模式;简而言之,如果遇到多个构造器参数时要考虑建造者模式。
  2. 对象的实例化需要依赖各个组件的生产以及装配顺序,关注的是一步一步地组装出目标对象,可以使用建造者模式。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lang20150928

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值