IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> Mybatis(四)映射文件解析流程 -> 正文阅读

[Java知识库]Mybatis(四)映射文件解析流程

本系列文章:
??Mybatis(一)Mybatis的基本使用
??Mybatis(二)Mybatis的高级使用
??Mybatis(三)配置文件解析流程
??Mybatis(四)映射文件解析流程
??Mybatis(五)SQL执行流程
??Mybatis(六)数据源、缓存机制、插件机制

??与配置文件不同,映射文件用于配置SQL语句,字段映射关系等。映射文件中包含<cache><cache-ref><resultMap><sql>、<select|insert|update|delete>等二级节点,这些节点将在接下来内容中进行分析。除了分析常规的 XML 解析过程外,还会介绍Mapper接口的绑定过程,以及其他一些知识。

1、映射文件解析解析入口

??映射文件的解析过程是配置文件解析过程的一部分,MyBatis会在解析配置文件的过程中对映射文件进行解析。解析逻辑封装在XMLConfigBuilder中的mapperElement方法中:

  	private void mapperElement(XNode parent) throws Exception {
    	if (parent != null) {
      		for (XNode child : parent.getChildren()) {
        		if ("package".equals(child.getName())) {
          			//获取<package>节点中的name属性
          			String mapperPackage = child.getStringAttribute("name");
          			//从指定包中查找mapper接口,并根据mapper接口解析映射配置
          			configuration.addMappers(mapperPackage);
        		} else {
          			// 获取resource、url、class等属性
          			String resource = child.getStringAttribute("resource");
          			String url = child.getStringAttribute("url");
          			String mapperClass = child.getStringAttribute("class");
          			//resource不为空,且其他两者为空,则从指定路径中加载配置
          			if (resource != null && url == null && mapperClass == null) {
            			ErrorContext.instance().resource(resource);
            			InputStream inputStream = Resources.getResourceAsStream(resource);
            			XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            			//解析映射文件
            			mapperParser.parse();
          				//url 不为空,且其他两者为空,则通过url加载配置
          			} else
          			 if (resource == null && url != null && mapperClass == null) {
            			ErrorContext.instance().resource(url);
            			InputStream inputStream = Resources.getUrlAsStream(url);
            			XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            			//解析映射文件
            			mapperParser.parse();
          				//mapperClass不为空,且其他两者为空,则通过mapperClass解析映射配置
          			} else if (resource == null && url == null && mapperClass != null) {
            			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.");
          			}
        		}
      		}
    	}
  	}

??代码的主要逻辑是遍历mappers的子节点,并根据节点属性值判断通过何种方式加载映射文件或映射信息。这里把配置在注解中的内容称为映射信息,以XML为载体的配置称为映射文件
??在MyBatis中,共有四种加载映射文件或映射信息的方式:

  1. 从文件系统中加载映射文件;
  2. 通过URL的方式加载映射文件;
  3. 通过mapper接口加载映射信息,映射信息可以配置在注解中,也可以配置在映射文件中;
  4. 通过包扫描的方式获取到某个包下的所有类,并使用第三种方式为每个类解析映射信息。

??在 MyBatis中,通过注解配置映射信息的方式是有一定局限性的,这一点MyBatis官方文档中描述的比较清楚:

??最初设计时,MyBatis是一个XML驱动的框架。配置信息是基于XML的,而且映射语句也是定义在XML中的。而到了MyBatis3,就有新选择了。MyBatis3构建在全面且强大的基于Java语言的配置API之上。这个配置API是基于XML的MyBatis配置的基础,也是新的基于注解配置的基础。注解提供了一种简单的方式来实现简单映射语句,而不会引入大量的开销。
??注意:Java注解的表达力和灵活性十分有限。尽管很多时间都花在调查、设计和试验上,最强大的MyBatis映射并不能用注解来构建。

??可以看出:限于Java注解的表达力和灵活性,通过注解的方式并不能完全发挥MyBatis的能力。因此,对于一些较为复杂的配置信息,还是应该通过XML 的方式进行配置。
??下面开始分析映射文件的解析过程,在分析之前,先来看一下映射文件解析入口。在上面的mapperElement方法中调用了mapperParser.parse()方法,这就是我们想要的入口,即XMLMapperBuilder中的parse方法:

  	public void parse() {
    	//检测映射文件是否已经被解析过
    	if (!configuration.isResourceLoaded(resource)) {
      		//解析mapper节点
      		configurationElement(parser.evalNode("/mapper"));
      		//添加资源路径到“已解析资源集合”中
      		configuration.addLoadedResource(resource);
      		//通过命名空间绑定Mapper接口
      		bindMapperForNamespace();
    	}
    	//处理未完成解析的节点
    	parsePendingResultMaps();
    	parsePendingCacheRefs();
    	parsePendingStatements();
  	}

??映射文件解析入口逻辑包含三个核心操作:

  1. 解析 mapper 节点。
  2. 通过命名空间绑定 Mapper 接口。
  3. 处理未完成解析的节点。

2、解析映射文件

??映射文件包含多种二级节点 , 比如<cache><resultMap><sql> 以 及<select|insert|update|delete> 等。除此之外,还包含了一些三级节点,比如 <include><if><where> 等。先来看一个映射文件配置示例:

	<mapper namespace="xyz.test.dao.AuthorDao">
		<cache/>
		<resultMap id="authorResult" type="Author">
			<id property="id" column="id"/>
			<result property="name" column="name"/>
			<!-- ... -->
		</resultMap>
		<sql id="table">
		   author
		</sql>
		<select id="findOne" resultMap="authorResult">
			 SELECT
				 id, name, age, sex, email
			 FROM
				<include refid="table"/>
			 WHERE
	 			id = #{id}
		</select>
		<!-- <insert|update|delete/> -->
	</mapper>

??以上配置中每种节点的解析逻辑都封装在了相应的方法中,这些方法由XMLMapperBuilder类的configurationElement方法统一调用:

  	private void configurationElement(XNode context) {
   	 	try {
      		//获取mapper命名空间
      		String namespace = context.getStringAttribute("namespace");
      		if (namespace == null || namespace.isEmpty()) {
        		throw new BuilderException("Mapper's namespace cannot be empty");
      		}
      		//设置命名空间到builderAssistant中
      		builderAssistant.setCurrentNamespace(namespace);
      		//解析<cache-ref>节点
      		cacheRefElement(context.evalNode("cache-ref"));
      		//解析<cache>节点
      		cacheElement(context.evalNode("cache"));
      		parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      		//解析<resultMap>节点
      		resultMapElements(context.evalNodes("/mapper/resultMap"));
      		//解析<sql>节点
      		sqlElement(context.evalNodes("/mapper/sql"));
      		//解析<select>、...、<delete>等节点
      		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);
    	}
  	}

??在阅读源码时,按部就班地分析每个方法调用即可。不过接下来在叙述的过程中会对分析顺序进行一些调整,本章将会先分析<cache>节点的解析过程,然后再分析<cache-ref>节点,之后会按照顺序分析其他节点的解析过程。

2.1 解析< cache >节点

??MyBatis 提供了一、二级缓存,其中一级缓存是 SqlSession 级别的,默认为开启状态。二级缓存配置在映射文件中,使用者需要显示配置才能开启。如果无特殊要求,二级缓存的配置很简单。

	<cache/>

??如果想修改缓存的一些属性,可以像下面这样配置:

	<cache
		eviction="FIFO"
		flushInterval="60000"
		size="512"
		readOnly="true"/>

??上面配置的意思是:

  1. 按先进先出的策略淘汰缓存项。
  2. 缓存的容量为512个对象引用。
  3. 缓存每隔60秒刷新一次。
  4. 缓存返回的对象是写安全的,即在外部修改对象不会影响到缓存内部存储对象。

??当然,除了使用Mybatis自带的缓存,也可以使用第三方缓存,比如Ehcache:

	<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
		<property name="timeToIdleSeconds" value="3600"/>
		<property name="timeToLiveSeconds" value="3600"/>
		<property name="maxEntriesLocalHeap" value="1000"/>
		<property name="maxEntriesLocalDisk" value="10000000"/>
		<property name="memoryStoreEvictionPolicy" value="LRU"/>
	</cache>

??缓存配置的解析逻辑在XMLMapperBuilder中实现:

  	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");
      		Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
      		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();
      		//构建缓存对象
      		builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    	}
  	}

??上面代码中,大段代码用来解析<cache>节点的属性和子节点,缓存对象的构建逻辑封装在 MapperBuilderAssistant类的useNewCache方法中:

  	public Cache useNewCache(Class<? extends Cache> typeClass,
      	Class<? extends Cache> evictionClass,
      	Long flushInterval,
      	Integer size,
      	boolean readWrite,
      	boolean blocking,
      	Properties props) {
    	//使用建造模式构建缓存实例
    	Cache cache = new CacheBuilder(currentNamespace)
        	.implementation(valueOrDefault(typeClass, PerpetualCache.class))
        	.addDecorator(valueOrDefault(evictionClass, LruCache.class))
        	.clearInterval(flushInterval)
        	.size(size)
        	.readWrite(readWrite)
        	.blocking(blocking)
        	.properties(props)
       	 	.build();
    	//添加缓存到Configuration对象中
    	configuration.addCache(cache);
    	//设置currentCache遍历,即当前使用的缓存
   	 	currentCache = cache;
    	return cache;
  	}

??接下来看下Cache 实例构建过程,在CacheBuilder中实现:

  	public Cache build() {
    	//设置默认的缓存类型(PerpetualCache)和缓存装饰器(LruCache)
    	setDefaultImplementations();
    	//通过反射创建缓存
    	Cache cache = newBaseCacheInstance(implementation, id);
    	setCacheProperties(cache);
    	//仅对内置缓存PerpetualCache应用装饰器
    	if (PerpetualCache.class.equals(cache.getClass())) {
      		//遍历装饰器集合,应用装饰器
      		for (Class<? extends Cache> decorator : decorators) {
        		//通过反射创建装饰器实例
        		cache = newCacheDecoratorInstance(decorator, cache);
        		//设置属性值到缓存实例中
        		setCacheProperties(cache);
      		}
      		//应用标准的装饰器,比如 LoggingCache、SynchronizedCache
      		cache = setStandardDecorators(cache);
    	} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
      		//应用具有日志功能的缓存装饰器
      		cache = new LoggingCache(cache);
    	}
    	return cache;
  	}

??上面的代码可以分为4步:

  1. 设置默认的缓存类型及装饰器。
  2. 应用装饰器到PerpetualCache对象上。
  3. 应用标准装饰器。
  4. 对非LoggingCache类型的缓存应用LoggingCache装饰器。

??最后一步的逻辑很简单,面按顺序分析前 3 个步骤:

  	private void setDefaultImplementations() {
    	if (implementation == null) {
     	 	//设置默认的缓存实现类
      		implementation = PerpetualCache.class;
      		if (decorators.isEmpty()) {
        		//添加LruCache装饰器
        		decorators.add(LruCache.class);
      		}
    	}
  	}

??以上代码主要做的事情是在implementation为空的情况下,为它设置一个默认值。其实在调用setDefaultImplementations方法之前已经进行了很多非空判断,implementation不可能为空,setDefaultImplementations 方法似乎没有存在的必要了。其实不然,如果有人不按套路写代码。比如:

	Cache cache = new CacheBuilder(currentNamespace)
		//忘记设置implementation
		.build();

??忘记设置implementation ,或人为的将implementation设为空。如果不对implementation进行判空,会导致build方法在构建实例时触发空指针异常,对于框架来说,这是一个低级错误。这种情况一定要避免,以提高框架的健壮性。
??接下来看下,如果使用第三方缓存时,其对应的配置是如何设置到缓存实例中的。

  	private void setCacheProperties(Cache cache) {
    	if (properties != null) {
      		//为缓存实例生成一个“元信息”实例,forObject方法调用层次比较深,
      		//但最终调用了MetaClass的forClass方法。
      		MetaObject metaCache = SystemMetaObject.forObject(cache);
      		for (Map.Entry<Object, Object> entry : properties.entrySet()) {
        		String name = (String) entry.getKey();
        		String value = (String) entry.getValue();
        		if (metaCache.hasSetter(name)) {
          			//获取setter方法的参数类型
          			Class<?> type = metaCache.getSetterType(name);
          			//根据参数类型对属性值进行转换,并将转换后的值
          			//通过setter方法设置到Cache实例中
          			if (String.class == type) {
            			metaCache.setValue(name, value);
          			} else if (int.class == type
              			|| Integer.class == type) {
            			/*
             			 * 此处及以下分支包含两个步骤:
             			 * 1.类型转换 → Integer.valueOf(value)
           	 			 * 2.将转换后的值设置到缓存实例中 →
            			 * metaCache.setValue(name, value)
           				 */
            			metaCache.setValue(name, Integer.valueOf(value));
         			 } else if (long.class == type
              			|| Long.class == type) {
            			metaCache.setValue(name, Long.valueOf(value));
          			} else if (short.class == type
              			|| Short.class == type) {
            			metaCache.setValue(name, Short.valueOf(value));
          			} else if (byte.class == type
              			|| Byte.class == type) {
            			metaCache.setValue(name, Byte.valueOf(value));
          			} else if (float.class == type
              			|| Float.class == type) {
            			metaCache.setValue(name, Float.valueOf(value));
          			} else if (boolean.class == type
              			|| Boolean.class == type) {
            			metaCache.setValue(name, Boolean.valueOf(value));
          			} else if (double.class == type
              			|| Double.class == type) {
            			metaCache.setValue(name, Double.valueOf(value));
          			} else {
            			throw new CacheException("Unsupported property type for cache: '" + name + "' of type " + type);
          			}
        		}
      		}
    	}
    	//如果缓存类实现了InitializingObject接口,
    	//则调用initialize方法执行初始化逻辑
    	if (InitializingObject.class.isAssignableFrom(cache.getClass())) {
      		try {
        		((InitializingObject) cache).initialize();
      		} catch (Exception e) {
        		throw new CacheException("Failed cache initialization for '"
          + cache.getId() + "' on '" + cache.getClass().getName() + "'", e);
      		}
    	}
  	}

??上面的大段代码用于对属性值进行类型转换,和设置转换后的值到Cache实例中。
??最后,看一下设置标准装饰器的过程。

  	private Cache setStandardDecorators(Cache cache) {
    	try {
      		//创建“元信息”对象
      		MetaObject metaCache = SystemMetaObject.forObject(cache);
      		if (size != null && metaCache.hasSetter("size")) {
        		//设置size属性
        		metaCache.setValue("size", size);
      		}
      		if (clearInterval != null) {
        		//clearInterval不为空,应用ScheduledCache装饰器
        		cache = new ScheduledCache(cache);
        		((ScheduledCache) cache).setClearInterval(clearInterval);
      		}
      		if (readWrite) {
        		//readWrite为true,应用SerializedCache装饰器
        		cache = new SerializedCache(cache);
      		}
      		/*
	   		 * 应用LoggingCache,SynchronizedCache装饰器,
       		 * 使原缓存具备打印日志和线程同步的能力
       		 */
      		cache = new LoggingCache(cache);
      		cache = new SynchronizedCache(cache);
      		if (blocking) {
        		//blocking为true,应用BlockingCache装饰器
        		cache = new BlockingCache(cache);
      		}
      		return cache;
    	} catch (Exception e) {
      		throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
    	}
  	}

??以上代码用于为缓存应用一些基本的装饰器,除了LoggingCache和SynchronizedCache这两个是必要的装饰器,其他的装饰器应用与否,取决于用户的配置。

2.2 解析< cache-ref >节点

??在MyBatis中,二级缓存是可以共用的。这需要通过<cache-ref>节点为命名空间配置参照缓存,示例:

	<!-- Mapper1.xml -->
	<mapper namespace="xyz.coolblog.dao.Mapper1">
		<!-- Mapper1 与 Mapper2 共用一个二级缓存 -->
		<cache-ref namespace="xyz.coolblog.dao.Mapper2"/>
	</mapper>
	<!-- Mapper2.xml -->
	<mapper namespace="xyz.coolblog.dao.Mapper2">
		<cache/>
	</mapper>

??cache-ref的解析过程还是在XMLMapperBuilder中实现的:

  	private void cacheRefElement(XNode context) {
    	if (context != null) {
      		configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
      		//创建CacheRefResolver实例
      		CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
      		try {
        		//解析参照缓存
        		cacheRefResolver.resolveCacheRef();
      		} catch (IncompleteElementException e) {
        		//捕捉IncompleteElementException异常,并将cacheRefResolver
        		//存入到Configuration的incompleteCacheRefs集合中
        		configuration.addIncompleteCacheRef(cacheRefResolver);
      		}
    	}
  	}

??<cache-ref>节点的解析逻辑封装在了CacheRefResolver的resolveCacheRef方法中:

  	public Cache resolveCacheRef() {
    	//调用builderAssistant的useNewCache(namespace)方法
    	return assistant.useCacheRef(cacheRefNamespace);
  	}

??上述代码又调用了MapperBuilderAssistant中的useCacheRef方法:

  	public Cache useCacheRef(String namespace) {
    	if (namespace == null) {
      		throw new BuilderException("cache-ref element requires a namespace attribute.");
    	}
    	try {
      		unresolvedCacheRef = true;
      		//根据命名空间从全局配置对象(Configuration)中查找相应的缓存实例
      		Cache cache = configuration.getCache(namespace);
      		/*
	   		 * 若未查找到缓存实例,此处抛出异常。这里存在两种情况导致未查找到
	  		 * cache实例,分别如下:
	 		 * 1.使用者在<cache-ref>中配置了一个不存在的命名空间,
	 		 * 导致无法找到cache实例
	 		 * 2.使用者所引用的缓存实例还未创建
	 		 */
      		if (cache == null) {
        		throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
      		}
      		//设置cache为当前使用缓存
      		currentCache = cache;
      		unresolvedCacheRef = false;
      		return cache;
    	} catch (IllegalArgumentException e) {
      		throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
    	}
  	}

2.3 解析< resultMap >节点

??resultMap是MyBatis框架中常用的特性,主要用于映射结果。
??resultMap配置的解析过程也是在XMLMapperBuilder中实现的:

  	private void resultMapElements(List<XNode> list) {
    	//遍历<resultMap>节点列表
    	for (XNode resultMapNode : list) {
      		try {
        		//解析resultMap节点
        		resultMapElement(resultMapNode);
      		} catch (IncompleteElementException e) {
      		}
    	}
  	}

  	private ResultMap resultMapElement(XNode resultMapNode) {
    	//调用重载方法
    	return resultMapElement(resultMapNode, Collections.emptyList(), null);
  	}

  	private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) {
    	ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
    	//获取type属性
    	String type = resultMapNode.getStringAttribute("type",
        	resultMapNode.getStringAttribute("ofType",
            	resultMapNode.getStringAttribute("resultType",
                	resultMapNode.getStringAttribute("javaType"))));
    	//解析type属性对应的类型
    	Class<?> typeClass = resolveClass(type);
    	if (typeClass == null) {
      		typeClass = inheritEnclosingType(resultMapNode, enclosingType);
    	}
    	Discriminator discriminator = null;
    	List<ResultMapping> resultMappings = new ArrayList<>(additionalResultMappings);
    	//获取并遍历<resultMap>的子节点列表
    	List<XNode> resultChildren = resultMapNode.getChildren();
    	for (XNode resultChild : resultChildren) {
      		if ("constructor".equals(resultChild.getName())) {
        		//解析constructor节点,并生成相应的ResultMapping
        		processConstructorElement(resultChild, typeClass, resultMappings);
      		} else if ("discriminator".equals(resultChild.getName())) {
        		//解析discriminator节点
        		discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
      		} else {
        		List<ResultFlag> flags = new ArrayList<>();
        		if ("id".equals(resultChild.getName())) {
          			//添加ID到flags集合中
          			flags.add(ResultFlag.ID);
        		}
        		//解析id和property节点,并生成相应的ResultMapping
        		resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
      		}
    	}
    	//获取id属性
    	String id = resultMapNode.getStringAttribute("id",
            resultMapNode.getValueBasedIdentifier());
    	//获取extends和autoMapping
    	String extend = resultMapNode.getStringAttribute("extends");
    	Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
    	ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
    	try {
      		//根据前面获取到的信息构建ResultMap对象
      		return resultMapResolver.resolve();
    	} catch (IncompleteElementException e) {
     	 	/*
	   		 * 如果发生IncompleteElementException异常,
	  		 * 这里将resultMapResolver添加到incompleteResultMaps集合中
	  		 */
      		configuration.addIncompleteResultMap(resultMapResolver);
      		throw e;
    	}
  	}

??上面的代码做的事情:

  1. 获取<resultMap>节点的各种属性。
  2. 遍历<resultMap>的子节点,并根据子节点名称执行相应的解析逻辑。
  3. 构建 ResultMap 对象。
  4. 若构建过程中发生异常,则将resultMapResolver添加到incompleteResultMaps 集合中。

??第2步和第3步分别是<resultMap>节点的子节点解析过程,以及ResultMap对象的构建过程,这两个过程比较重要。

  • 1、 解析< id >和< result >节点
    ??在<resultMap>节点中,子节点<id><result>都是常规配置,这两个节点的解析是在XMLMapperBuilder中进行的:
  	private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) {
    	String property;
    	//根据节点类型获取name或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");
    	String nestedSelect = context.getStringAttribute("select");
    	//解析resultMap属性,该属性出现在<association>和<collection>节点中。
    	//若这两个节点不包含resultMap属性,则调用processNestedResultMappings方法
    	//解析嵌套resultMap。
    	String nestedResultMap = context.getStringAttribute("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"));
    	//解析javaType、typeHandler 的类型以及枚举类型JdbcType
    	Class<?> javaTypeClass = resolveClass(javaType);
    	Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
    	JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
    	//构建ResultMapping对象
    	return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
  	}

??上面的代码主要用于获取<id><result>节点的属性。resultMap属性的解析过程要相对复杂一些。该属性存在于<association><collection>节点中。下面以<association>节点为例,演示该节点的两种配置方式。
??第一种配置方式是通过resultMap属性引用其他的<resultMap>节点:

	<resultMap id="articleResult" type="Article">
		<id property="id" column="id"/>
		<result property="title" column="article_title"/>
		<association property="article_author" column="article_author_id"
			resultMap="authorResult"/>
	</resultMap>
	<resultMap id="authorResult" type="Author">
		<id property="id" column="author_id"/>
		<result property="name" column="author_name"/>
	</resultMap>

??第二种配置方式是采取resultMap嵌套的方式进行配置:

	<resultMap id="articleResult" type="Article">
		<id property="id" column="id"/>
		<result property="title" column="article_title"/>
		<association property="article_author" javaType="Author">
			<id property="id" column="author_id"/>
			<result property="name" column="author_name"/>
		</association>
	</resultMap>

??<association>的子节点是一些结果映射配置,这些结果配置最终也会被解析成ResultMap。解析过程仍在XMLMapperBuilder中:

  	private String processNestedResultMappings(XNode context, List<ResultMapping> resultMappings, Class<?> enclosingType) {
    	//判断节点名称
    	if (Arrays.asList("association", "collection", "case").contains(context.getName())
        	&& context.getStringAttribute("select") == null) {
      		validateCollection(context, enclosingType);
      		//resultMapElement是解析ResultMap入口方法
      		ResultMap resultMap = resultMapElement(context, resultMappings, enclosingType);
      		//返回resultMap id
      		return resultMap.getId();
    	}
    	return null;
  	}

??<association>的子节点由resultMapElement方法解析成ResultMap,并在最后返回resultMap.id。对于<resultMap>节点,id的值配置在该节点的id属性中。但<association>节点无法配置id属性,那么该id如何产生的呢?答案在XNode类的getValueBasedIdentifier方法中。
??接下来分析ResultMapping的构建过程,该过程是在MapperBuilderAssistant中实现的:

  	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) {
    	//若javaType为空,这里根据property的属性进行解析。方法中的参数说明:
    	// - resultType:即<resultMap type="xxx"/>中的type属性
    	// - property:即<result property="xxx"/>中的property属性
    	Class<?> javaTypeClass = resolveResultJavaType(resultType, property, javaType);
    	//解析TypeHandler
    	TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);
    	//解析column = {property1=column1, property2=column2}的情况,
    	//这里会将column拆分成多个ResultMapping
    	List<ResultMapping> composites;
    	if ((nestedSelect == null || nestedSelect.isEmpty()) && (foreignColumn == null || foreignColumn.isEmpty())) {
      		composites = Collections.emptyList();
    	} else {
      		composites = parseCompositeColumnName(column);
    	}
    	//通过建造模式构建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<>() : flags)
        	.composites(composites)
        	.notNullColumns(parseMultipleColumnNames(notNullColumn))
       	 	.columnPrefix(columnPrefix)
        	.foreignColumn(foreignColumn)
        	.lazy(lazy)
        	.build();
  	}

??接着调用了ResultMapping中的build方法:

    public ResultMapping build() {
      	//将flags和composites两个集合变为不可修改集合
      	resultMapping.flags = Collections.unmodifiableList(resultMapping.flags);
      	resultMapping.composites = Collections.unmodifiableList(resultMapping.composites);
      	//从TypeHandlerRegistry中获取相应TypeHandler
      	resolveTypeHandler();
      	validate();
      	return resultMapping;
    }

??ResultMapping的构建过程不是很复杂,首先是解析javaType类型,并创建typeHandler实例。然后处理复合column。最后通过建造器构建ResultMapping实例。

  • 2、解析< constructor >节点
    ??有时开发时会用到特殊些的POJO,比如把POJO的setter方法移除,增加构造方法用于初始化成员变量。对于这种不可变的Java类,需要通过带有参数的构造方法进行初始化(反射也可以达到同样目的)。此时会用到<constructor>节点,示例:
	<constructor>
		<idArg column="id" name="id"/>
		<arg column="title" name="title"/>
		<arg column="content" name="content"/>
	</constructor>

??该节点的解析是在XMLMapperBuilder中的:

  	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中添加CONSTRUCTOR标志
      		flags.add(ResultFlag.CONSTRUCTOR);
      		if ("idArg".equals(argChild.getName())) {
        		//向flags中添加ID标志
        		flags.add(ResultFlag.ID);
      		}
      		//构建ResultMapping
      		resultMappings.add(buildResultMappingFromContext(argChild, resultType, flags));
    	}
  	}

??首先是获取并遍历子节点列表,然后为每个子节点创建flags集合,并添加CONSTRUCTOR标志。对于idArg节点,额外添加ID标志。最后一步则是构建ResultMapping。

  • 3、ResultMap 对象构建过程分析
    ??通过前面的分析,可知<id><result>等节点最终都被解析成了ResultMapping。在得到这些ResultMapping后,紧接着要做的事情是构建ResultMap。
    ??ResultMap构建的入口:
	private ResultMap resultMapElement(XNode resultMapNode,
  		List<ResultMapping> additionalResultMappings) throws Exception {
		//获取resultMap节点中的属性
		// ...
		//解析resultMap对应的类型
		// ...
		//遍历resultMap节点的子节点,构建ResultMapping对象
		// ...
		//创建ResultMap解析器
		ResultMapResolver resultMapResolver = new ResultMapResolver(
			builderAssistant, id, typeClass, extend, discriminator,
			resultMappings, autoMapping);
		try {
			//根据前面获取到的信息构建ResultMap对象
			return resultMapResolver.resolve();
	 	} catch (IncompleteElementException e) {
			configuration.addIncompleteResultMap(resultMapResolver);
			throw e;
	 	}
	}

??ResultMap的构建逻辑封装在ResultMapResolver的resolve方法中:

  	public ResultMap resolve() {
    	return assistant.addResultMap(this.id, this.type, this.extend, 	
    		this.discriminator, this.resultMappings, this.autoMapping);
  	}

??上面的方法将构建ResultMap实例的任务委托给了MapperBuilderAssistant的addResultMap:

  	public ResultMap addResultMap(
      	String id,Class<?> type,String extend,Discriminator discriminator,
      	List<ResultMapping> resultMappings,Boolean autoMapping) {
    	//为ResultMap的id和extend属性值拼接命名空间
    	id = applyCurrentNamespace(id, false);
    	extend = applyCurrentNamespace(extend, true);

   	 	if (extend != null) {
      		if (!configuration.hasResultMap(extend)) {
        		throw new IncompleteElementException("Could not find a parent resultmap with id '" + extend + "'");
     		}
      		ResultMap resultMap = configuration.getResultMap(extend);
      		List<ResultMapping> extendedResultMappings = new ArrayList<>(resultMap.getResultMappings());
      		//为拓展ResultMappings取出重复项
      		extendedResultMappings.removeAll(resultMappings);
      		boolean declaresConstructor = false;
      		//检测当前resultMappings集合中是否包含CONSTRUCTOR标志的元素
      		for (ResultMapping resultMapping : resultMappings) {
        		if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
          			declaresConstructor = true;
          			break;
        		}
      		}
      		//如果当前<resultMap>节点中包含<constructor>子节点,
      		//则将拓展ResultMapping 集合中的包含CONSTRUCTOR标志的元素移除
      		if (declaresConstructor) {
        		extendedResultMappings.removeIf(resultMapping -> resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR));
      		}
      		//将扩展resultMappings集合合并到当前resultMappings集合中
      		resultMappings.addAll(extendedResultMappings);
    	}
    	//构建ResultMap
    	ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
        	.discriminator(discriminator)
        	.build();
    	configuration.addResultMap(resultMap);
    	return resultMap;
  	}

??上面的方法主要用于处理resultMap节点的extend属性,extend不为空的话,这里将当前resultMappings集合和扩展resultMappings集合合二为一。随后,通过建造模式,在ResultMap中构建ResultMap实例。

    public ResultMap build() {
      	if (resultMap.id == null) {
        	throw new IllegalArgumentException("ResultMaps must have an id");
      	}	
      	resultMap.mappedColumns = new HashSet<>();
      	resultMap.mappedProperties = new HashSet<>();
      	resultMap.idResultMappings = new ArrayList<>();
      	resultMap.constructorResultMappings = new ArrayList<>();
      	resultMap.propertyResultMappings = new ArrayList<>();
      	final List<String> constructorArgNames = new ArrayList<>();
      	for (ResultMapping resultMapping : resultMap.resultMappings) {
        	//检测<association>或<collection>节点
        	//是否包含select和resultMap属性
        	resultMap.hasNestedQueries = resultMap.hasNestedQueries || resultMapping.getNestedQueryId() != null;
        	resultMap.hasNestedResultMaps = resultMap.hasNestedResultMaps || (resultMapping.getNestedResultMapId() != null && resultMapping.getResultSet() == null);
        	final String column = resultMapping.getColumn();
        	if (column != null) {
          		//将colum转换成大写,并添加到mappedColumns集合中
          		resultMap.mappedColumns.add(column.toUpperCase(Locale.ENGLISH));
        	} else if (resultMapping.isCompositeResult()) {
          		for (ResultMapping compositeResultMapping : resultMapping.getComposites()) {
            		final String compositeColumn = compositeResultMapping.getColumn();
            		if (compositeColumn != null) {
              			resultMap.mappedColumns.add(compositeColumn.toUpperCase(Locale.ENGLISH));
            		}
          		}
        	}
        	//添加属性property到mappedProperties集合中
        	final String property = resultMapping.getProperty();
        	if (property != null) {
          		resultMap.mappedProperties.add(property);
        	}
        	//检测当前resultMapping是否包含CONSTRUCTOR标志
        	if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
          		//添加resultMapping到constructorResultMappings中
          		resultMap.constructorResultMappings.add(resultMapping);
          		//添加属性(constructor节点的name属性)到constructorArgNames中
          		if (resultMapping.getProperty() != null) {
            		constructorArgNames.add(resultMapping.getProperty());
          		}
        	} else {
          		//添加resultMapping到propertyResultMappings中
          		resultMap.propertyResultMappings.add(resultMapping);
        	}
        	if (resultMapping.getFlags().contains(ResultFlag.ID)) {
          		//添加resultMapping到idResultMappings中
          		resultMap.idResultMappings.add(resultMapping);
        	}
      	}
      	if (resultMap.idResultMappings.isEmpty()) {
        	resultMap.idResultMappings.addAll(resultMap.resultMappings);
      	}
      	if (!constructorArgNames.isEmpty()) {
        	//获取构造方法参数列表
        	final List<String> actualArgNames = argNamesOfMatchingConstructor(constructorArgNames);
        	if (actualArgNames == null) {
          		throw new BuilderException("Error in result map '" + resultMap.id
              		+ "'. Failed to find a constructor in '"
              		+ resultMap.getType().getName() + "' by arg names " + constructorArgNames
              		+ ". There might be more info in debug log.");
        	}
        	resultMap.constructorResultMappings.sort((o1, o2) -> {
          		int paramIdx1 = actualArgNames.indexOf(o1.getProperty());
          		int paramIdx2 = actualArgNames.indexOf(o2.getProperty());
          		return paramIdx1 - paramIdx2;
        	});
      	}
      	//将以下这些集合变为不可修改集合
      	resultMap.resultMappings = Collections.unmodifiableList(resultMap.resultMappings);
      	resultMap.idResultMappings = Collections.unmodifiableList(resultMap.idResultMappings);
      	resultMap.constructorResultMappings = Collections.unmodifiableList(resultMap.constructorResultMappings);
      	resultMap.propertyResultMappings = Collections.unmodifiableList(resultMap.propertyResultMappings);
      	resultMap.mappedColumns = Collections.unmodifiableSet(resultMap.mappedColumns);
      	return resultMap;
    }

??以上代码主要做的事情就是将ResultMapping实例及属性分别存储到不同的集合中,仅此而已。ResultMap中定义了五种不同的集合:

集合名称用途
mappedColumns用于存储 <id><result><idArg><arg> 节点column属性
mappedProperties用于存储 <id><result> 节点的property属性,或 <idArgs><arg>节点的name属性
idResultMappings用于存储 <id><idArg> 节点对应的ResultMapping对象
propertyResultMappings用于存储 <id><result> 节点对应的ResultMapping对象
constructorResultMappings用于存储 <idArgs><arg> 节点对应的ResultMapping对象

2.4 解析< sql >节点

??<sql>节点用来定义一些可重用的SQL语句片段,比如表名,或表的列名等。在映射文件中,我们可以通过<include>节点引用<sql>节点定义的内容。示例:

	<sql id="table">
		 article
	</sql>
	<select id="findOne" resultType="Article">
		 SELECT id, title FROM <include refid="table"/> WHERE id = #{id}
	</select>

??<sql>解析的过程还是从XMLMapperBuilder中开始:

  	private void sqlElement(List<XNode> list) {
    	if (configuration.getDatabaseId() != null) {
      		//调用sqlElement解析<sql>节点
      		sqlElement(list, configuration.getDatabaseId());
    	}
    	//再次调用sqlElement,不同的是,这次调用,该方法的第二个参数为null
    	sqlElement(list, null);
  	}

??第一次传入具体的databaseId,用于解析带有databaseId属性,且属性值与此相等的<sql>节点。第二次传入的databaseId为空,用于解析未配置databaseId属性的<sql>节点。

  	private void sqlElement(List<XNode> list, String requiredDatabaseId) {
    	for (XNode context : list) {
      		//获取id和databaseId属性
      		String databaseId = context.getStringAttribute("databaseId");
      		String id = context.getStringAttribute("id");
      		//id = currentNamespace + "." + id
      		id = builderAssistant.applyCurrentNamespace(id, false);
      		//检测当前databaseId和requiredDatabaseId是否一致
      		if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
        		//将<id, XNode>键值对缓存到sqlFragments中
        		sqlFragments.put(id, context);
      		}
    	}
  	}

??上面的逻辑:首先是获取<sql>节点的id和databaseId属性,然后为id属性值拼接命名空间。最后,通过检测当前databaseId和requiredDatabaseId是否一致,来决定保存还是忽略当前的<sql>节点。接下来看databaseId的匹配逻辑:

  	private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {
    	if (requiredDatabaseId != null) {
      		//当前databaseId和目标databaseId不一致时,返回false
      		return requiredDatabaseId.equals(databaseId);
    	}
    	//如果目标databaseId为空,但当前databaseId不为空。两者不一致,返回false
    	if (databaseId != null) {
      		return false;
    	}
    	//如果当前<sql>节点的id与之前的<sql>节点重复,且先前节点
    	//databaseId不为空。则忽略当前节点,并返回false
    	if (!this.sqlFragments.containsKey(id)) {
      		return true;
    	}
    
    	XNode context = this.sqlFragments.get(id);
    	return context.getStringAttribute("databaseId") == null;
  	}

??databaseId 的匹配规则:

  1. databaseId与requiredDatabaseId不一致,返回 false。
  2. 当前节点与之前的节点出现id重复的情况,若之前的<sql>节点databaseId属性不为空,返回false。
  3. 若以上两条规则均匹配失败,此时返回true。

??。databaseId用于标明数据库厂商的身份,不同厂商有自己的 SQL 方言,MyBatis 可以根据 databaseId 执行不同 SQL 语句。databaseId 在<sql>节点中有什么用呢?这个问题也不难回答。<sql>节点用于保存 SQL 语句片段,如果 SQL 语句片段中包含方言的话,那么该<sql>节点只能被同一databaseId 的查询语句或更新语句引用。

2.5 解析 SQL 语句节点

??前面分析了<cache><cache-ref><resultMap>以及<sql>节点,从这一节开始,我们来分析映射文件中剩余的几个节点,分别是<select><insert><update>以及<delete>等。这几个节点中存储的是相同的内容,都是SQL语句,所以这几个节点的解析过程也是相同的。

??在进行代码分析之前,这里需要特别说明一下:为了避免和<sql>节点混淆,同时也为了描述方便,这里把<select><insert><update>以及<delete>等节点统称为SQL语句节点。

??解析过程依旧从XMLMapperBuilder开始:

  	private void buildStatementFromContext(List<XNode> list) {
    	if (configuration.getDatabaseId() != null) {
      		//调用重载方法构建Statement
      		buildStatementFromContext(list, configuration.getDatabaseId());
    	}
    	//调用重载方法构建Statement,requiredDatabaseId参数为空
    	buildStatementFromContext(list, null);
  	}

  	private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    	for (XNode context : list) {
      		//创建Statement建造类
      		final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      		try {
        		//解析Statement节点,并将解析结果存储到
        		//configuration的mappedStatements集合中
        		statementParser.parseStatementNode();
      		} catch (IncompleteElementException e) {
        		//解析失败,将解析器放入Configuration的incompleteStatements集合中
        		configuration.addIncompleteStatement(statementParser);
      		}
    	}
  	}

??顺着代码继续看XMLStatementBuilder:

  	public void parseStatementNode() {
    	//获取id和databaseId属性
    	String id = context.getStringAttribute("id");
    	String databaseId = context.getStringAttribute("databaseId");
    	//根据databaseId进行检测
    	if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      		return;
    	}
    	//获取节点的名称,比如<select>节点名称为select
    	String nodeName = context.getNode().getNodeName();
    	//根据节点名称解析SqlCommandType
    	SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    	boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    	boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    	boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    	boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    	//解析<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);

    	//解析<selectKey>节点
    	processSelectKeyNodes(id, parameterTypeClass, langDriver);

    	KeyGenerator keyGenerator;
    	String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    	keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    	if (configuration.hasKeyGenerator(keyStatementId)) {
      		//获取KeyGenerator实例
      		keyGenerator = configuration.getKeyGenerator(keyStatementId);
    	} else {
      		//创建KeyGenerator实例
      		keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          	configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    	}
   	 	//解析SQL语句
    	SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    	//解析Statement类型,默认为PREPARED
    	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");
    	//通过别名解析resultType对应的类型
    	Class<?> resultTypeClass = resolveClass(resultType);
    	String resultMap = context.getStringAttribute("resultMap");
    	//解析ResultSetType
    	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");
    	//构建MappedStatement对象,并将该对象存储到
    	//Configuration的mappedStatements集合中
    	builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        	fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        	resultSetTypeEnum, flushCache, useCache, resultOrdered,
        	keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  	}

??上面的代码中起码有一半的代码是用来获取节点属性,以及解析部分属性等。抛去这部分代码,以上代码做的事情:

  1. 解析<include>节点。
  2. 解析<selectKey>节点。
  3. 解析SQL,获取SqlSource。
  4. 构建MappedStatement实例。
  • 1、解析< include >节点
    ??<include>节点的解析逻辑封装在 XMLIncludeTransformer的applyIncludes方法 中:
  	public void applyIncludes(Node source) {
    	Properties variablesContext = new Properties();
    	Properties configurationVariables = configuration.getVariables();
    	//将configurationVariables中的数据添加到variablesContext中
    	Optional.ofNullable(configurationVariables)
    		.ifPresent(variablesContext::putAll);
    	//调用重载方法处理<include>节点
    	applyIncludes(source, variablesContext, false);
  	}

??上面代码中创建了一个新的Properties对象,并将全局Properties添加到其中。这样做的原因是applyIncludes的重载方法会向Properties中添加新的元素,如果直接将全局Properties传给重载方法,会造成全局Properties被污染。

  	private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
    	if ("include".equals(source.getNodeName())) {
      		//获取<sql>节点。若refid中包含属性占位符${},
      		//则需先将属性占位符替换为对应的属性值
      		Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
      		//解析<include>的子节点<property>,并将解析结果与variablesContext融合,
      		//然后返回融合后的Properties。若<property>节点的value属性中存在
      		//占位符${},则将占位符替换为对应的属性值
      		Properties toIncludeContext = getVariablesContext(source, variablesContext);
      		/*
	  		 * 这里是一个递归调用,用于将<sql>节点内容中出现的属性占位符${}
	  		 * 替换为对应的属性值。这里要注意一下递归调用的参数:
	   		 * - toInclude:<sql>节点对象
	  		 * - toIncludeContext:<include>子节点<property>的解析结果与
	  		 * 全局变量融合后的结果
	  		 */
      		applyIncludes(toInclude, toIncludeContext, true);
      		//如果<sql>和<include>节点不在一个文档中,
	  		//则从其他文档中将<sql>节点引入到<include>所在文档中
      		if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
        		toInclude = source.getOwnerDocument().importNode(toInclude, true);
      		}
      		//将<include>节点替换为<sql>节点
      		source.getParentNode().replaceChild(toInclude, source);
      		while (toInclude.hasChildNodes()) {
        		//将<sql>中的内容插入到<sql>节点之前
        		toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
      		}
      		//前面已经将<sql>节点的内容插入到dom中了,
	  		//现在不需要<sql>节点了,这里将该节点从dom中移除
      		toInclude.getParentNode().removeChild(toInclude);
    	} else if (source.getNodeType() == Node.ELEMENT_NODE) {
      		if (included && !variablesContext.isEmpty()) {
        		NamedNodeMap attributes = source.getAttributes();
        		for (int i = 0; i < attributes.getLength(); i++) {
          			Node attr = attributes.item(i);
          			//将source节点属性中的占位符${}替换成具体的属性值
          			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()) {
      		//将文本(text)节点中的属性占位符${}替换成具体的属性值
      		source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
    	}
  	}
  • 2、解析< selectKey >节点
    ??对于一些不支持自增主键的数据库来说,我们在插入数据时,需要明确指定主键数据。以Oracle数据库为例,Oracle数据库不支持自增主键,但它提供了自增序列工具。<selectKey>可以用来获取主键值。
	<insert id="saveAuthor">
		<selectKey keyProperty="id" resultType="int" order="BEFORE">
		 	select author_seq.nextval from dual
		</selectKey>
	 insert into Author
		 (id, name, password)
	 values
		 (#{id}, #{username}, #{password})
	</insert>

??在上面的配置中,查询语句会先于插入语句执行,这样我们就可以在插入时获取到主键的值。该节点的解析是从XMLStatementBuilder中开始的:

  	private void processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver) {
    	List<XNode> selectKeyNodes = context.evalNodes("selectKey");
    	if (configuration.getDatabaseId() != null) {
      		//解析<selectKey>节点,databaseId不为空
      		parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());
    	}
    	//解析<selectKey>节点,databaseId为空
    	parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);
    	//将<selectKey>节点从dom树中移除
    	removeSelectKeyNodes(selectKeyNodes);
  	}

??<selectKey>节点在解析完成后,会被从dom树中移除。这样后续可以更专注的解析<insert><update>节点中的SQL,无需再额外处理<selectKey>节点。继续跟踪:

  	private void parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId) {
    	for (XNode nodeToHandle : list) {
      		//id = parentId + !selectKey,比如saveUser!selectKey
      		String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
      		//获取<selectKey>节点的databaseId属性
      		String databaseId = nodeToHandle.getStringAttribute("databaseId");
      		//匹配databaseId
      		if (databaseIdMatchesCurrent(id, databaseId, skRequiredDatabaseId)) {
        		//解析<selectKey>节点
        		parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, langDriver, databaseId);
      		}
    	}
  	}

  	private void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId) {
    	//获取各种属性
    	String resultType = nodeToHandle.getStringAttribute("resultType");
    	Class<?> resultTypeClass = resolveClass(resultType);
    	StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    	String keyProperty = nodeToHandle.getStringAttribute("keyProperty");
    	String keyColumn = nodeToHandle.getStringAttribute("keyColumn");
    	boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));

    	//设置默认值
    	boolean useCache = false;
    	boolean resultOrdered = false;
    	KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
    	Integer fetchSize = null;
    	Integer timeout = null;
    	boolean flushCache = false;
    	String parameterMap = null;
    	String resultMap = null;
    	ResultSetType resultSetTypeEnum = null;
    	//创建SqlSource
    	SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);
    	//<selectKey>节点中只能配置SELECT查询语句,
    	//因此sqlCommandType为SqlCommandType.SELECT
    	SqlCommandType sqlCommandType = SqlCommandType.SELECT;
    	//构建MappedStatement,并将MappedStatement
    	//添加到Configuration的mappedStatements map中
    	builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        	fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        	resultSetTypeEnum, flushCache, useCache, resultOrdered,
        	keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);
    	//id = namespace + "." + id
    	id = builderAssistant.applyCurrentNamespace(id, false);

    	MappedStatement keyStatement = configuration.getMappedStatement(id, false);
    	//创建SelectKeyGenerator,并添加到keyGenerators map中
    	configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
  	}

??以上代码比较重要的一些步骤:

  1. 创建SqlSource实例。
  2. 构建并缓存MappedStatement实例。
  3. 构建并缓存SelectKeyGenerator实例。
  • 3、解析SQL语句
    ??前面分析了<include><selectKey>节点的解析过程,这两个节点解析完成后,都会以不同的方式从dom树中消失。所以目前的SQL语句节点由一些文本节点和普通节点组成,比如<if><where>等。下面我们来看一下移除掉<include><selectKey>节点后的SQL语句节点是如何解析的。
    ??先从XMLLanguageDriver开始:
  	public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    	XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    	return builder.parseScriptNode();
  	}

??接着到了XMLScriptBuilder:

  	public SqlSource parseScriptNode() {
    	//解析SQL语句节点
    	MixedSqlNode rootSqlNode = parseDynamicTags(context);
    	SqlSource sqlSource;
    	//根据isDynamic状态创建不同的SqlSource
    	if (isDynamic) {
      		sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    	} else {
      		sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    	}
    	return sqlSource;
  	}

??SQL语句的解析逻辑被封装在了XMLScriptBuilder类的parseScriptNode方法中。该方法首先会调用parseDynamicTags解析SQL语句节点。在解析过程中,会判断节点是是否包含一些动态标记,比如${}占位符以及动态SQL节点等。若包含动态标记,则会将isDynamic设为true。后续可根据isDynamic创建不同的SqlSource。
??在XMLScriptBuilder构造方法中,会调用initNodeHandlerMap:

  	/** 该方法用于初始化nodeHandlerMap集合,该集合后面会用到 */
  	private void initNodeHandlerMap() {
    	nodeHandlerMap.put("trim", new TrimHandler());
    	nodeHandlerMap.put("where", new WhereHandler());
    	nodeHandlerMap.put("set", new SetHandler());
    	nodeHandlerMap.put("foreach", new ForEachHandler());
    	nodeHandlerMap.put("if", new IfHandler());
    	nodeHandlerMap.put("choose", new ChooseHandler());
    	nodeHandlerMap.put("when", new IfHandler());
    	nodeHandlerMap.put("otherwise", new OtherwiseHandler());
    	nodeHandlerMap.put("bind", new BindHandler());
  	}

??接下来继续跟踪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));
      		if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
        		//获取文本内容
        		String data = child.getStringBody("");
        		TextSqlNode textSqlNode = new TextSqlNode(data);
        		//若文本中包含${}占位符,也被认为是动态节点
        		if (textSqlNode.isDynamic()) {
          			contents.add(textSqlNode);
          			isDynamic = true;
        		} else {
          			//创建StaticTextSqlNode
          			contents.add(new StaticTextSqlNode(data));
        		}
      		//child节点是ELEMENT_NODE类型,比如<if>、<where>等
      		} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
        		//获取节点名称,比如if、where、trim等
        		String nodeName = child.getNode().getNodeName();
        		//根据节点名称获取NodeHandler
        		NodeHandler handler = nodeHandlerMap.get(nodeName);
        		//如果handler为空,表明当前节点对与MyBatis来说,是未知节点。
        		//MyBatis无法处理这种节点,故抛出异常
        		if (handler == null) {
          			throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
        		}
        		//处理child节点,生成相应的SqlNode
        		handler.handleNode(child, contents);
        		isDynamic = true;
      		}
    	}
    	return new MixedSqlNode(contents);
  	}

??以上方法主要是用来判断节点是否包含一些动态标记,比如${}占位符以及动态SQL节点等。这里,不管是动态SQL节点还是静态SQL节点,我们都可以把它们看成是SQL片段,一个SQL语句由多个SQL片段组成。在解析过程中,这些SQL片段被存储在contents集合中。最后,该集合会被传给MixedSqlNode构造方法,用于创建MixedSqlNode 实例。从MixedSqlNode类名上可知,它会存储多种类型的SqlNode。
??SqlNode的不同实现类用于处理不同的动态SQL逻辑,这些SqlNode是由各种NodeHandler生成。之前代码中的handler.handleNode(child, contents);就是用于处理动态SQL节点,并生成相应的 SqlNode。
??看一个XMLScriptBuilder中的WhereHandler例子:

  	private class WhereHandler implements NodeHandler {
    	public WhereHandler() {
    	}

    	@Override
    	public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
      		//调用parseDynamicTags解析<where>节点
      		MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
      		//创建WhereSqlNode
      		WhereSqlNode where = new WhereSqlNode(configuration, mixedSqlNode);
      		//添加到targetContents
      		targetContents.add(where);
    	}
  	}

??handleNode方法内部会再次调用parseDynamicTags解析节点中的内容,这样又会生成一个MixedSqlNode对象。最终,整个SQL语句节点会生成一个具有树状结构的MixedSqlNode。
??到此,SQL语句的解析过程就分析完了。现在,我们已经将XML配置解析了SqlSource,但这还没有结束。SqlSource中只能记录SQL语句信息,除此之外,这里还有一些额外的信息需要记录。因此,需要一个类能够同时存储SqlSource和其他的信息,这个类就是MappedStatement。

  • 4、构建 MappedStatement
    ??SQL语句节点可以定义很多属性,这些属性和属性值最终存储在MappedStatement中。MappedStatement的构建过程在MapperBuilderAssistant:
  	public MappedStatement addMappedStatement(
      	String id,SqlSource sqlSource,StatementType statementType,
      	SqlCommandType sqlCommandType,Integer fetchSize,Integer timeout,
      	String parameterMap,Class<?> parameterType,String resultMap,
      	Class<?> resultType,ResultSetType resultSetType,boolean flushCache,
      	boolean useCache,boolean resultOrdered,KeyGenerator keyGenerator,
      	String keyProperty,String keyColumn,String databaseId,
      	LanguageDriver lang,String resultSets) {

    	if (unresolvedCacheRef) {
      		throw new IncompleteElementException("Cache-ref not yet resolved");
    	}

    	id = applyCurrentNamespace(id, false);
    	boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    	//创建建造器,设置各种属性
    	MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
       	 	.resource(resource)
        	.fetchSize(fetchSize)
        	.timeout(timeout)
        	.statementType(statementType)
        	.keyGenerator(keyGenerator)
        	.keyProperty(keyProperty)
        	.keyColumn(keyColumn)
        	.databaseId(databaseId)
        	.lang(lang)
        	.resultOrdered(resultOrdered)
        	.resultSets(resultSets)
        	.resultMaps(getStatementResultMaps(resultMap, resultType, id))
        	.resultSetType(resultSetType)
        	.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
        	.useCache(valueOrDefault(useCache, isSelect))
        	.cache(currentCache);
    	//获取或创建ParameterMap
    	ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    	if (statementParameterMap != null) {
      		statementBuilder.parameterMap(statementParameterMap);
    	}
    	//构建MappedStatement
    	MappedStatement statement = statementBuilder.build();
    	//添加MappedStatement到configuration的mappedStatements集合中
    	configuration.addMappedStatement(statement);
    	return statement;
  	}

??上面就是MappedStatement的构建过程。

3、Mapper接口绑定过程

??映射文件解析完成后,并不意味着整个解析过程就结束了。此时还需要通过命名空间绑定mapper接口,这样才能将映射文件中的SQL语句和mapper接口中的方法绑定在一起,后续可直接通过调用mapper接口方法执行与之对应的SQL语句。
??mapper接口的绑定过程从XMLMapperBuilder开始:

  	private void bindMapperForNamespace() {
    	//获取映射文件的命名空间
    	String namespace = builderAssistant.getCurrentNamespace();
    	if (namespace != null) {
      		Class<?> boundType = null;
      		try {
        		//根据命名空间解析mapper类型
        		boundType = Resources.classForName(namespace);
      		} catch (ClassNotFoundException e) {
      		}
      		//检测当前mapper类是否被绑定过
     		if (boundType != null && !configuration.hasMapper(boundType)) {
        		configuration.addLoadedResource("namespace:" + namespace);
        		//绑定mapper类
        		configuration.addMapper(boundType);
      		}
    	}
  	}

??接着继续追踪到Configuration:

  	public <T> void addMapper(Class<T> type) {
    	//通过MapperRegistry绑定mapper类
    	mapperRegistry.addMapper(type);
  	}

??接着继续追踪到MapperRegistry:

  	public <T> void addMapper(Class<T> type) {
    	if (type.isInterface()) {
     	 	if (hasMapper(type)) {
        		throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      		}
      		boolean loadCompleted = false;
      		try {
        		//将type和MapperProxyFactory进行绑定,
        		//MapperProxyFactory可为mapper接口生成代理类
        		knownMappers.put(type, new MapperProxyFactory<>(type));
        		//创建注解解析器。在MyBatis中,有XML和注解两种配置方式可选
        		MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        		//解析注解中的信息
        		parser.parse();
        		loadCompleted = true;
      		} finally {
        		if (!loadCompleted) {
          			knownMappers.remove(type);
        		}
      		}
    	}
  	}

??Mapper 接口的绑定过程,这里简单总结一下:

  1. 获取命名空间,并根据命名空间解析mapper类型。
  2. 将type和MapperProxyFactory实例存入knownMappers中。
  3. 解析注解中的信息。

4、处理未完成解析的节点

??在解析某些节点的过程中,如果这些节点引用了其他一些未被解析的配置,会导致当前节点解析工作无法进行下去。对于这种情况,MyBatis的做法是抛出IncompleteElementException。外部逻辑会捕捉这个异常,并将节点对应的解析器放入incomplet*集合中。先看XMLMapperBuilder:

	public void parse() {
		//...
		//解析mapper节点
		configurationElement(parser.evalNode("/mapper"));
		//处理未完成解析的节点
		parsePendingResultMaps();
		parsePendingCacheRefs();
		parsePendingStatements();
	}

??parse方法是映射文件的解析入口。上面三个以parsePending开头的方法逻辑一致,所以下面我只会分析其中一个方法的源码。简单起见,选择分析parsePendingCacheRefs的源码。先看个<cache-ref>节点无法完成解析的例子:

	<!-- 映射文件 1 -->
	<mapper namespace="xyz.coolblog.dao.Mapper1">
		<!-- 引用映射文件 2 中配置的缓存 -->
		<cache-ref namespace="xyz.coolblog.dao.Mapper2"/>
	</mapper>
	<!-- 映射文件 2 -->
	<mapper namespace="xyz.coolblog.dao.Mapper2">
		<cache/>
	</mapper>

??假设MyBatis先解析映射文件1,然后再解析映射文件2。按照这样的解析顺序,映射文件1 中的<cache-ref>节点就无法完成解析,因为它所引用的缓存还未被解析。当映射文件2解析完成后,MyBatis会调用parsePendingCacheRefs方法处理在此之前未完成解析的<cache-ref>节点。

  	private void parsePendingCacheRefs() {
    	//获取CacheRefResolver列表
    	Collection<CacheRefResolver> incompleteCacheRefs = configuration.getIncompleteCacheRefs();
    	synchronized (incompleteCacheRefs) {
      		Iterator<CacheRefResolver> iter = incompleteCacheRefs.iterator();
      		//通过迭代器遍历列表
      		while (iter.hasNext()) {
        		try {
          			//尝试解析<cache-ref>节点,若解析失败,则抛出
          			//IncompleteElementException,此时下面的删除操作不会被执行
          			iter.next().resolveCacheRef();
          			//移除CacheRefResolver对象。如果代码能执行到此处,
		  			//表明已成功解析了<cache-ref>节点
          			iter.remove();
        		} catch (IncompleteElementException e) {
          			//如果再次发生IncompleteElementException,表明当前
		  			//映射文件中并没有<cache-ref>所引用的缓存。有可能所引用的缓存
		  			//在后面的映射文件中,所以这里不能将解析失败的CacheRefResolver
		  			/从集合中删除
        		}
      		}
    	}
  	}

??上面的逻辑比较简单,这里简单总结一下:

  1. 获取获取 CacheRefResolver 列表,并进行遍历。
  2. 尝试解析<cache-ref>节点,若解析失败再次抛出异常。
  3. 若解析成功则列表中移除相关节点。
  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-05-09 12:26:14  更:2022-05-09 12:29:45 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 0:29:39-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码