精盡MyBatis源碼分析 - MyBatis初始化(二)之加載 Mapper 接口與 XML 映射文件

該系列文檔是本人在學習 Mybatis 的源碼過程當中總結下來的,可能對讀者不太友好,請結合個人源碼註釋(Mybatis源碼分析 GitHub 地址Mybatis-Spring 源碼分析 GitHub 地址Spring-Boot-Starter 源碼分析 GitHub 地址)進行閱讀html

MyBatis 版本:3.5.2java

MyBatis-Spring 版本:2.0.3node

MyBatis-Spring-Boot-Starter 版本:2.1.4git

MyBatis的初始化

在MyBatis初始化過程當中,大體會有如下幾個步驟:github

  1. 建立Configuration全局配置對象,會往TypeAliasRegistry別名註冊中心添加Mybatis須要用到的相關類,並設置默認的語言驅動類爲XMLLanguageDriverspring

  2. 加載mybatis-config.xml配置文件、Mapper接口中的註解信息和XML映射文件,解析後的配置信息會造成相應的對象並保存到Configuration全局配置對象中sql

  3. 構建DefaultSqlSessionFactory對象,經過它能夠建立DefaultSqlSession對象,MyBatis中SqlSession的默認實現類數據庫

由於整個初始化過程涉及到的代碼比較多,因此拆分紅了四個模塊依次對MyBatis的初始化進行分析:apache

因爲在MyBatis的初始化過程當中去解析Mapper接口與XML映射文件涉及到的篇幅比較多,XML映射文件的解析過程也比較複雜,因此才分紅了後面三個模塊,逐步分析,這樣便於理解緩存

初始化(二)之加載Mapper接口與映射文件

在上一個模塊已經分析了是如何解析mybatis-config.xml配置文件的,在最後如何解析<mapper />標籤的尚未進行分析,這個過程稍微複雜一點,由於須要解析Mapper接口以及它的XML映射文件,讓咱們一塊兒來看看這個解析過程

解析XML映射文件生成的對象主要以下圖所示:

MapperXml

主要包路徑:org.apache.ibatis.builder、org.apache.ibatis.mapping

主要涉及到的類:

  • org.apache.ibatis.builder.xml.XMLConfigBuilder:根據配置文件進行解析,開始Mapper接口與XML映射文件的初始化,生成Configuration全局配置對象
  • org.apache.ibatis.binding.MapperRegistry:Mapper接口註冊中心,將Mapper接口與其動態代理對象工廠進行保存,這裏咱們解析到的Mapper接口須要往其進行註冊
  • org.apache.ibatis.builder.annotation.MapperAnnotationBuilder:解析Mapper接口,主要是解析接口上面註解,其中加載XML映射文件內部會調用XMLMapperBuilder類進行解析
  • org.apache.ibatis.builder.xml.XMLMapperBuilder:解析XML映射文件
  • org.apache.ibatis.builder.xml.XMLStatementBuilder:解析XML映射文件中的Statement配置(<select /> <update /> <delete /> <insert />標籤)
  • org.apache.ibatis.builder.MapperBuilderAssistant:Mapper構造器小助手,用於建立ResultMapping、ResultMap和MappedStatement對象
  • org.apache.ibatis.mapping.ResultMapping:保存<resultMap />標籤的子標籤相關信息,也就是 Java Type 與 Jdbc Type 的映射信息
  • org.apache.ibatis.mapping.ResultMap:保存了<resultMap />標籤的配置信息以及子標籤的全部信息
  • org.apache.ibatis.mapping.MappedStatement:保存瞭解析<select /> <update /> <delete /> <insert />標籤內的SQL語句所生成的全部信息

解析入口

咱們回顧上一個模塊,在org.apache.ibatis.builder.xml.XMLConfigBuilder中會解析mybatis-config.xml配置文件中的<mapper />標籤,調用其parse()->parseConfiguration(XNode root)->mapperElement(XNode parent)方法,那麼咱們來看看這個方法,代碼以下:

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
        // <0> 遍歷子節點
        for (XNode child : parent.getChildren()) {
            // <1> 若是是 package 標籤,則掃描該包
            if ("package".equals(child.getName())) {
                // 得到包名
                String mapperPackage = child.getStringAttribute("name");
                // 添加到 configuration 中
                configuration.addMappers(mapperPackage);
            } else { // 若是是 mapper 標籤
                // 得到 resource、url、class 屬性
                String resource = child.getStringAttribute("resource");
                String url = child.getStringAttribute("url");
                String mapperClass = child.getStringAttribute("class");
                // <2> 使用相對於類路徑的資源引用
                if (resource != null && url == null && mapperClass == null) {
                    ErrorContext.instance().resource(resource);
                    // 得到 resource 的 InputStream 對象
                    InputStream inputStream = Resources.getResourceAsStream(resource);
                    // 建立 XMLMapperBuilder 對象
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                    // 執行解析
                    mapperParser.parse();
                // <3> 使用徹底限定資源定位符(URL)
                } else if (resource == null && url != null && mapperClass == null) {
                    ErrorContext.instance().resource(url);
                    // 得到 url 的 InputStream 對象
                    InputStream inputStream = Resources.getUrlAsStream(url);
                    // 建立 XMLMapperBuilder 對象
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url,configuration.getSqlFragments());
                    // 執行解析
                    mapperParser.parse();
                // <4> 使用映射器接口實現類的徹底限定類名
                } else if (resource == null && url == null && mapperClass != null) {
                    // 得到 Mapper 接口
                    Class<?> mapperInterface = Resources.classForName(mapperClass);
                    // 添加到 configuration 中
                    configuration.addMapper(mapperInterface);
                } else {
                    throw new BuilderException( "A mapper element may only specify a url, resource or class, but not more than one.");
                }
            }
        }
    }
}

遍歷<mapper />標籤的子節點

  1. 若是是<package />子節點,則獲取package屬性,對該包路徑下的Mapper接口進行解析

  2. 否的的話,經過子節點的resource屬性或者url屬性解析該映射文件,或者經過class屬性解析該Mapper接口

一般咱們是直接配置一個包路徑,這裏就查看上面第1種對Mapper接口進行解析的方式,第2種的解析方式其實在第1 種方式都會涉及到,它只是抽取出來了,那麼咱們就直接看第1種方式

首先將package包路徑添加到Configuration全局配置對象中,也就是往其內部的MapperRegistry註冊表進行註冊,調用它的MapperRegistryaddMappers(String packageName)方法進行註冊

咱們來看看在MapperRegistry註冊表中是如何解析的,在以前文檔的Binding模塊中有講到過這個類,該方法以下:

public class MapperRegistry {
    
    public void addMappers(String packageName) {
		addMappers(packageName, Object.class);
	}
    
    /**
	 * 用於掃描指定包中的Mapper接口,並與XML文件進行綁定
	 * @since 3.2.2
	 */
	public void addMappers(String packageName, Class<?> superType) {
		// <1> 掃描指定包下的指定類
		ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
		resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
		Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
		// <2> 遍歷,添加到 knownMappers 中
		for (Class<?> mapperClass : mapperSet) {
			addMapper(mapperClass);
		}
	}
    
	public <T> void addMapper(Class<T> type) {
		// <1> 判斷,必須是接口。
		if (type.isInterface()) {
			// <2> 已經添加過,則拋出 BindingException 異常
			if (hasMapper(type)) {
				throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
			}
			boolean loadCompleted = false;
			try {
				// <3> 將Mapper接口對應的代理工廠添加到 knownMappers 中
				knownMappers.put(type, new MapperProxyFactory<>(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.
				// <4> 解析 Mapper 的註解配置
				MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
				// 解析 Mapper 接口上面的註解和 Mapper 接口對應的 XML 文件
				parser.parse();
				// <5> 標記加載完成
				loadCompleted = true;
			} finally {
				// <6> 若加載未完成,從 knownMappers 中移除
				if (!loadCompleted) {
					knownMappers.remove(type);
				}
			}
		}
	}
}

<1>首先必須是個接口

<2>已經在MapperRegistry註冊中心存在,則會拋出異常

<3>建立一個Mapper接口對應的MapperProxyFactory動態代理工廠

<4>重要!!!】經過MapperAnnotationBuilder解析該Mapper接口與對應XML映射文件

MapperAnnotationBuilder

org.apache.ibatis.builder.annotation.MapperAnnotationBuilder:解析Mapper接口,主要是解析接口上面註解,加載XML文件會調用XMLMapperBuilder類進行解析

咱們先來看看他的構造函數和parse()解析方法:

public class MapperAnnotationBuilder {

    /**
     * 全局配置對象
     */
	private final Configuration configuration;
    /**
     * Mapper 構造器小助手
     */
	private final MapperBuilderAssistant assistant;
    /**
     * Mapper 接口的 Class 對象
     */
	private final Class<?> type;

	public MapperAnnotationBuilder(Configuration configuration, Class<?> type) {
		String resource = type.getName().replace('.', '/') + ".java (best guess)";
		this.assistant = new MapperBuilderAssistant(configuration, resource);
		this.configuration = configuration;
		this.type = type;
	}

	public void parse() {
		String resource = type.toString();
		if (!configuration.isResourceLoaded(resource)) {
		    // 加載該接口對應的 XML 文件
			loadXmlResource();
			configuration.addLoadedResource(resource);
			assistant.setCurrentNamespace(type.getName());
			// 解析 Mapper 接口的 @CacheNamespace 註解,建立緩存
			parseCache();
			// 解析 Mapper 接口的 @CacheNamespaceRef 註解,引用其餘命名空間
			parseCacheRef();
			Method[] methods = type.getMethods();
			for (Method method : methods) {
				try {
					// issue #237
					if (!method.isBridge()) { // 若是不是橋接方法
					  // 解析方法上面的註解
						parseStatement(method);
					}
				} catch (IncompleteElementException e) {
					configuration.addIncompleteMethod(new MethodResolver(this, method));
				}
			}
		}
		parsePendingMethods();
	}

	private void loadXmlResource() {
		// Spring may not know the real resource name so we check a flag
		// to prevent loading again a resource twice
		// this flag is set at XMLMapperBuilder#bindMapperForNamespace
		if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
			String xmlResource = type.getName().replace('.', '/') + ".xml";
			// #1347
			InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
			if (inputStream == null) {
				// Search XML mapper that is not in the module but in the classpath.
				try {
					inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
				} catch (IOException e2) {
					// ignore, resource is not required
				}
			}
			if (inputStream != null) {
			    // 建立 XMLMapperBuilder 對象
				XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(),
						xmlResource, configuration.getSqlFragments(), type.getName());
				// 解析該 XML 文件
				xmlParser.parse();
			}
		}
	}
}

在構造函數中,會建立一個MapperBuilderAssistant對象,Mapper 構造器小助手,用於建立XML映射文件中對應相關對象

parse()方法,用於解析Mapper接口:

  1. 獲取Mapper接口的名稱,例如interface xxx.xxx.xxx,根據Configuration全局配置對象判斷該Mapper接口是否被解析過

  2. 沒有解析過則調用loadXmlResource()方法解析對應的XML映射文件

  3. 而後解析接口的@CacheNamespace和@CacheNamespaceRef註解,再依次解析方法上面的MyBatis相關注解

註解的相關解析這裏就不講述了,由於咱們一般都是使用XML映射文件,邏輯沒有特別複雜,都在MapperAnnotationBuilder中進行解析,感興趣的小夥伴能夠看看😈😈😈

loadXmlResource()方法,解析Mapper接口對應的XML映射文件:

  1. 根據Configuration全局配置對象判斷該Mapper接口對應的XML映射文件是否被解析過,例如判斷namespace:xxx.xxx.xxx是否在已加載的資源中
  2. 獲取XML映射文件資源,例如:獲取xxx/xxx/xxx.xml文件流,與接口名稱對應
  3. 建立XMLMapperBuilder對象,調用其parse()方法解析該XML映射文件

那麼接下來咱們來看看XMLMapperBuilder是如何解析XML映射文件的

XMLMapperBuilder

org.apache.ibatis.builder.xml.XMLMapperBuilder:解析XML映射文件

繼承org.apache.ibatis.builder.BaseBuilder抽象類,該基類提供了類型轉換以及一些其餘的工具方法,比較簡單,這裏就不作展述了

構造方法

public class XMLMapperBuilder extends BaseBuilder {

	/**
	 * 基於 Java XPath 解析器
	 */
	private final XPathParser parser;
	/**
	 * Mapper 構造器助手
	 */
	private final MapperBuilderAssistant builderAssistant;
	/**
	 * 可被其餘語句引用的可重用語句塊的集合,實際上就是 Configuration 全局配置中的 sqlFragments
	 *
	 * 例如:<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>
     * <sql />可能在不少地方被引用
	 */
	private final Map<String, XNode> sqlFragments;
	/**
	 * 資源引用的地址,例如:com/aaa/bbb.xml
	 */
	private final String resource;
    
    public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource,
			Map<String, XNode> sqlFragments, String namespace) {
		this(inputStream, configuration, resource, sqlFragments);
		this.builderAssistant.setCurrentNamespace(namespace);
	}

	public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource,
			Map<String, XNode> sqlFragments) {
		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);
		// 建立 MapperBuilderAssistant 對象
		this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
		this.parser = parser;
		this.sqlFragments = sqlFragments;
		this.resource = resource;
	}
}
  1. 首先會進入XPathParser的構造方法,將XML映射文件解析成org.w3c.dom.Document對象,這裏傳入了XMLMapperEntityResolver做爲解析實例對象,其中使用到本地的DTD文件

  2. 而後建立一個 Mapper 構造器助手 MapperBuilderAssistant 對象

  3. 其中一些屬性都是從Configuration全局配置對象中獲取的,例如:typeAliasRegistrytypeHandlerRegistrysqlFragments

parse方法

parse()方法用於解析XML映射文件,在MapperAnnotationBuilder中被調用,代碼以下:

public class XMLMapperBuilder extends BaseBuilder {
    public void parse() {
		// <1> 判斷當前 Mapper 是否已經加載過
		if (!configuration.isResourceLoaded(resource)) {
			// <2> 解析 `<mapper />` 節點
			configurationElement(parser.evalNode("/mapper"));
			// <3> 標記該 Mapper 已經加載過
			configuration.addLoadedResource(resource);
			// <4> 綁定 Mapper
			bindMapperForNamespace();
		}
		// <5> 解析待定的 <resultMap /> 節點
		parsePendingResultMaps();
		// <6> 解析待定的 <cache-ref /> 節點
		parsePendingCacheRefs();
		// <7> 解析待定的 SQL 語句的節點
		parsePendingStatements();
	}
}

<1> 根據Configuration全局配置判斷當前XML映射文件是否已經加載過,例如resource爲:xxx/xxx/xxx.xml

<2> 解析 <mapper /> 節點,也就是解析整個的XML映射文件,在下面的configurationElement方法中講解

<3> 標記該XML映射文件已經加載過,往Configuration全局配置添加該字段文件,例如添加:xxx/xxx/xxx.xml

<4> 綁定 Mapper 到該命名空間,避免在MapperAnnotationBuilder#loadXmlResource方法中重複加載該XML映射文件

<5> 解析待定的 <resultMap /><cache-ref /> 節點以及 Statement 對象,由於咱們配置的這些對象可能還依賴的其餘對象,在解析的過程當中這些依賴可能還沒解析出來,致使這個對象解析失敗,因此先保存在Configuration全局配置對象中,待整個XML映射文件解析完後,再遍歷以前解析失敗的對象進行初始化,這裏就不作詳細的講述了,感興趣的小夥伴能夠看一下

這裏咱們來看一下configurationElement(XNode context)方法是如何解析XML映射文件中的<mapper />節點

configurationElement方法

configurationElement(XNode context)方法就是來解析XML映射文件中咱們定義的SQL相關信息,代碼以下:

public class XMLMapperBuilder extends BaseBuilder {
    private void configurationElement(XNode context) {
		try {
			// <1> 得到 namespace 屬性
			String namespace = context.getStringAttribute("namespace");
			if (namespace == null || namespace.equals("")) {
				throw new BuilderException("Mapper's namespace cannot be empty");
			}
			builderAssistant.setCurrentNamespace(namespace);
			// <2> 解析 <cache-ref /> 節點
			cacheRefElement(context.evalNode("cache-ref"));
			// <3> 解析 <cache /> 節點
			cacheElement(context.evalNode("cache"));
			// 已廢棄!老式風格的參數映射。內聯參數是首選,這個元素可能在未來被移除,這裏不會記錄。
			parameterMapElement(context.evalNodes("/mapper/parameterMap"));
			// <4> 解析 <resultMap /> 節點
			resultMapElements(context.evalNodes("/mapper/resultMap"));
			// <5> 解析 <sql /> 節點們
			sqlElement(context.evalNodes("/mapper/sql"));
			// <6> 解析 <select /> <insert /> <update /> <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);
		}
	}
}

<1> 得到 namespace 屬性,若是 XML 映射文件中定義的 namespace 和接口名稱不相等會拋出異常

<2> 解析 <cache-ref /> 節點,調用cacheRefElement方法

<3> 解析 <cache /> 節點,調用cacheElement方法

<4> 解析 <resultMap /> 節點,調用resultMapElements方法

<5> 解析 <sql /> 節點們,調用sqlElement方法

<6> 解析 <select /> <insert /> <update /> <delete /> 節點,調用buildStatementFromContext方法

cacheRefElement方法

cacheRefElement(XNode context)方法用於解析XML映射文件中的<cache-ref />節點,代碼以下:

private void cacheRefElement(XNode context) {
    if (context != null) {
        // <1> 得到指向的 namespace 名字,並添加到 configuration 的 cacheRefMap 中
        configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
        // <2> 建立 CacheRefResolver 對象
        CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
        try {
            // 執行解析,獲取引用的緩存對象到本身這裏
            cacheRefResolver.resolveCacheRef();
        } catch (IncompleteElementException e) {
            configuration.addIncompleteCacheRef(cacheRefResolver);
        }
    }
}

解析當前XML映射文件的緩存配置,將當前namespace緩存引用其餘的namespace的緩存造成映射關係保存在Configuration全局配置對象中

獲取引用的namespace的緩存實例,將其設置到MapperBuilderAssistant構造器助手中,在後續構建相關對象時使用

cacheElement方法

cacheElement(XNode context)方法用於XML映射文件中的<cache />節點,代碼以下:

private void cacheElement(XNode context) {
    if (context != null) {
        // <1> 得到負責存儲的 Cache 實現類
        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> 得到 flushInterval、size、readWrite、blocking 屬性
        Long flushInterval = context.getLongAttribute("flushInterval");
        Integer size = context.getIntAttribute("size");
        boolean readWrite = !context.getBooleanAttribute("readOnly", false);
        boolean blocking = context.getBooleanAttribute("blocking", false);
        // <4> 得到 Properties 屬性
        Properties props = context.getChildrenAsProperties();
        // <5> 建立 Cache 對象
        builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
}

解析該節點的相關配置,而後經過MapperBuilderAssistant構造器小助手建立一個Cache緩存實例,添加到Configuration全局配置對象中,並設置到構造器助手中,在後續構建相關對象時使用

resultMapElements方法

resultMapElements(List<XNode> list)方法用於解析<resultMap />節點,最後會調用

resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType)方法逐個解析生成ResultMap對象

總體的流程圖:

ParseResultMap

例如這樣配置RresultMap:

<mapper namespace="com.mybatis3.mappers.StudentMapper">
    <!-- 學生 -->
    <resultMap id="StudentResult" type="Student">
        <result column="id" property="studentId" jdbcType="INTEGER" />
        <result column="name" property="name" jdbcType="VARCHAR" />
        <result column="age" property="age" jdbcType="INTEGER" />
        <!-- 老師 -->
        <association property="teacher" javaType="Teacher">
            <result column="teacher_id" property="id" jdbcType="INTEGER" />
        	<result column="teacher_name" property="name" jdbcType="VARCHAR" />
        	<result column="teacher_age" property="age" jdbcType="INTEGER" />
        </association>
    </resultMap>
</mapper>

resultMapElement方法代碼以下:

public class XMLMapperBuilder extends BaseBuilder {
    private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) throws Exception {
	    // 獲取當前線程的上下文
		ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
		// <1> 得到 type 屬性
		String type = resultMapNode.getStringAttribute("type", resultMapNode.getStringAttribute("ofType",
				resultMapNode.getStringAttribute("resultType", resultMapNode.getStringAttribute("javaType"))));
		// 得到 type 對應的類
		Class<?> typeClass = resolveClass(type);
		if (typeClass == null) {
            // 從 enclosingType Class 對象獲取該 property 屬性的 Class 對象
			typeClass = inheritEnclosingType(resultMapNode, enclosingType);
		}
		Discriminator discriminator = null;
		// 建立 ResultMapping 集合
		List<ResultMapping> resultMappings = new ArrayList<>();
        // 添加父 ResultMap 的 ResultMapping 集合
		resultMappings.addAll(additionalResultMappings);
		// <2> 遍歷 <resultMap /> 的子節點
		List<XNode> resultChildren = resultMapNode.getChildren();
		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 /> 節點
				discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
			} else { // <2.3> 處理其它節點
				List<ResultFlag> flags = new ArrayList<>();
				if ("id".equals(resultChild.getName())) {
				  // 爲添加該 ResultMapping 添加一個 Id 標誌
					flags.add(ResultFlag.ID);
				}
				// 生成對應的 ResultMapping 對象
				resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
			}
		}
		// 得到 id 屬性,沒有的話自動生成
		String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier());
		// 得到 extends 屬性
		String extend = resultMapNode.getStringAttribute("extends");
		// 得到 autoMapping 屬性
		Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
		// <3> 建立 ResultMapResolver 對象,執行解析
		ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend,
				discriminator, resultMappings, autoMapping);
		try {
		  // 處理 ResultMap 並添加到 Configuration 全局配置中
			return resultMapResolver.resolve();
		} catch (IncompleteElementException e) {
			configuration.addIncompleteResultMap(resultMapResolver);
			throw e;
		}
	}
}

關於<resultMap />元素的屬性配置參考MyBatis官方文檔配置說明

resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType)方法的入參分別是:

  • 當前節點Node的封裝,封裝成XNode便於操做

  • 繼承的ResultMap所對應的ResultMapping的集合,能夠經過extend屬性配置繼承哪一個ResultMap,沒有繼承的話就是空集合

  • 所屬的ResultMap的類型,例如<resultMap />中的<association />也會被解析成ResultMap,那麼它的enclosingType就是所屬ResultMap的Class對象

處理邏輯

  1. 得到 type 屬性,生成該ResultMap對應Class對象,若是沒有定義type屬性,則多是<association />標籤,嘗試從所屬ResultMap的Class對象獲取property的Class對象,由於<resultMap />標籤中配置的<association />標籤也會解析成一個ResultMap對象

  2. 遍歷 <resultMap /> 的子節點,依次處理

    1. 若是是<constructor />節點,則調用processConstructorElement方法進行解析,再獲取它的子節點生成對應的RequestMapping對象,這些RequestMapping對象會添加ResultFlag.CONSTRUCTOR標記,若是是<idArg />標籤則再添加一個ResultFlag.ID標記,這些對象會在實例化類時,注入到構造方法中
    2. 若是是<discriminator>節點,則調用processDiscriminatorElement方法進行解析,建立一個Discriminator選擇器對象,用於可使用結果值來決定這個屬性使用哪一個ResultMap,基於<case />子節點來進行映射
    3. 其餘節點,則調用buildResultMappingFromContext方法進行解析,若是是<id />則添加一個ResultFlag.ID標記,生成對應的RequestMapping對象
  3. 建立ResultMapResolver對象,調用其resolve()方法執行解析,內部調用MapperBuilderAssistant構造器小助手的addResultMap來生成ResultMap對象的

上面的2.12.2並不複雜,感興趣的小夥伴能夠查看相關方法,都已經註釋好了😈😈😈,咱們來看下2.3是如何解析成ResultMapping對象的

buildResultMappingFromContext方法

buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags)方法將<resultMap />標籤中的子標籤解析成RequestMapping對象,代碼以下:

public class XMLMapperBuilder extends BaseBuilder {
    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");
		String nestedSelect = context.getStringAttribute("select");
		// 解析 <resultMap /> 標籤中的 <association />,<collection />,<case /> 標籤,生成 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 屬性
		Class<?> javaTypeClass = resolveClass(javaType);
		// typeHandler 屬性
		Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
		// jdbcType 屬性
		JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
		// 經過上面的屬性構建一個 ResultMapping 對象
		return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum,
				nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet,
				foreignColumn, lazy);
	}
}

依次從該節點中獲取相關屬性

  • 這裏咱們看到nestedResultMap的獲取,若是這個是<association /><collection />或者<case />,則會調用processNestedResultMappings方法解析成ResultMap對象,而後返回該對象的id(沒有定義會自動生成),這樣這個RequestMapping對象就會關聯這個ResultMap對象了,這個方法內部也是調用resultMapElement方法生成ResultMap對象的,能夠回過頭再看下這個方法

  • 最後經過MapperBuilderAssistant構造器小助手的buildResultMapping方法根據這些屬性構建一個ResultMapping對象並返回

整個的ResultMap對象的解析過程到這裏就結束了,關於MapperBuilderAssistant在後續會講到,接下來咱們來看看<sql />節點的解析

sqlElement方法

sqlElement(List<XNode> list)方法用於解析全部的<sql />節點,內部調用sqlElement(List<XNode> list, String requiredDatabaseId)方法,代碼以下:

public class XMLMapperBuilder extends BaseBuilder {
    private void sqlElement(List<XNode> list, String requiredDatabaseId) {
		// <1> 遍歷全部 <sql /> 節點
		for (XNode context : list) {
			// <2> 得到 databaseId 屬性
			String databaseId = context.getStringAttribute("databaseId");
			// <3> 得到完整的 id 屬性
			String id = context.getStringAttribute("id");
			// 設置爲 `${namespace}.${id}` 格式
			id = builderAssistant.applyCurrentNamespace(id, false);
			// <4> 判斷 databaseId 是否匹配
			if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
				// <5> 添加到 sqlFragments 中
				sqlFragments.put(id, context);
			}
		}
	}
}

這裏僅僅是將該<sql />節點保存至Map<String, XNode> sqlFragments對象中(該對象保存與Configuration全局配置對象中),後續解析其餘SQL語句中會使用到,例如查詢語句中使用了<include />標籤,則須要獲取到對應的<sql />節點將其替換

buildStatementFromContext方法

buildStatementFromContext(List<XNode> list)方法用於解析<select /> <insert /> <update /> <delete /> 節點

內部調用buildStatementFromContext(List<XNode> list, String requiredDatabaseId)方法逐個解析生成MappedStatement對象,代碼以下:

public class XMLMapperBuilder extends BaseBuilder {
    private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
		// <1> 遍歷 <select /> <insert /> <update /> <delete /> 節點
		for (XNode context : list) {
			// <1> 建立 XMLStatementBuilder 對象
			final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
			try {
				// 解析成 MappedStatement 對象
				statementParser.parseStatementNode();
			} catch (IncompleteElementException e) {
				// <2> 解析失敗,添加到 configuration 中
				configuration.addIncompleteStatement(statementParser);
			}
		}
	}
}

爲該節點建立XMLStatementBuilder對象,而後調用其parseStatementNode()解析成MappedStatement對象,解析過程在下面的XMLStatementBuilder中講到

XMLStatementBuilder

org.apache.ibatis.builder.xml.XMLStatementBuilder:解析XML映射文件中的Statement配置

也就是解析<select /> <insert /> <update /> <delete /> 節點,解析過程在parseStatementNode()方法中

構造方法

public class XMLStatementBuilder extends BaseBuilder {

	private final MapperBuilderAssistant builderAssistant;
	/**
	 * 當前 XML 節點,例如:<select />、<insert />、<update />、<delete /> 標籤
	 */
	private final XNode context;
	/**
	 * 要求的 databaseId
	 */
	private final String requiredDatabaseId;

	public XMLStatementBuilder(Configuration configuration, MapperBuilderAssistant builderAssistant, XNode context) {
		this(configuration, builderAssistant, context, null);
	}

	public XMLStatementBuilder(Configuration configuration, MapperBuilderAssistant builderAssistant, XNode context,
			String databaseId) {
		super(configuration);
		this.builderAssistant = builderAssistant;
		this.context = context;
		this.requiredDatabaseId = databaseId;
	}
}

parseStatementNode方法

parseStatementNode()方法用於解析 Statement 對應節點,也就是<select /> <update /> <delete /> <insert />節點,代碼以下:

public class XMLStatementBuilder extends BaseBuilder {
    public void parseStatementNode() {
		// 得到 id 屬性,編號。
		String id = context.getStringAttribute("id");
		// 得到 databaseId , 判斷 databaseId 是否匹配
		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));
		// 是否爲 Select 語句
		boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
		// <2> 是否清空緩存
		boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
		// <3> 是否使用緩存
		boolean useCache = context.getBooleanAttribute("useCache", isSelect);
		boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

		// Include Fragments before parsing
		XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
		// <4> 將該節點的子節點 <include /> 轉換成 <sql /> 節點
		includeParser.applyIncludes(context.getNode());

		// 獲取參數類型名稱
		String parameterType = context.getStringAttribute("parameterType");
		// <5> 參數類型名稱轉換成 Java Type
		Class<?> parameterTypeClass = resolveClass(parameterType);

		// <6> 得到 lang 對應的 LanguageDriver 對象
		String lang = context.getStringAttribute("lang");
		LanguageDriver langDriver = getLanguageDriver(lang);

		// Parse selectKey after includes and remove them.
        // <7> 將該節點的子節點 <selectKey /> 解析成 SelectKeyGenerator 生成器
		processSelectKeyNodes(id, parameterTypeClass, langDriver);

		// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
		KeyGenerator keyGenerator;
		String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
		keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
        /*
         * <8> 
         * 1. 若是上面存在 <selectKey /> 子節點,則獲取上面對其解析後生成的 SelectKeyGenerator
         * 2. 不然判斷該節點是否配置了 useGeneratedKeys 屬性爲 true 而且是 插入語句,則使用 Jdbc3KeyGenerator
         */
		if (configuration.hasKeyGenerator(keyStatementId)) {
			keyGenerator = configuration.getKeyGenerator(keyStatementId);
		} else {
            keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
            configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
            ? Jdbc3KeyGenerator.INSTANCE
            : NoKeyGenerator.INSTANCE;
		}

		// <9> 建立對應的 SqlSource 對象,保存了該節點下 SQL 相關信息
		SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
		// <10> 得到 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");
		// <11> 得到返回結果類型名稱
		String resultType = context.getStringAttribute("resultType");
		// 獲取返回結果的 Java Type
		Class<?> resultTypeClass = resolveClass(resultType);
		// 獲取 resultMap
		String resultMap = context.getStringAttribute("resultMap");
		String resultSetType = context.getStringAttribute("resultSetType");
		ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
		if (resultSetTypeEnum == null) {
			resultSetTypeEnum = configuration.getDefaultResultSetType();
		}
		// 對應的 java 屬性,結合 useGeneratedKeys 使用
		String keyProperty = context.getStringAttribute("keyProperty");
        // 對應的 column 列名,結合 useGeneratedKeys 使用
		String keyColumn = context.getStringAttribute("keyColumn");
		String resultSets = context.getStringAttribute("resultSets");
        // <12> 
		builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout,
				parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache,
				resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
	}
}

解析方法有點長,咱們一步一步來看

  1. 根據節點名稱設置SQL語句類型,SqlCommandType:UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH
  2. 獲取flushCache屬性,是否清空緩存,非查詢語句默認都是true
  3. 獲取useCache屬性,是否開啓緩存,查詢語句默認爲true
  4. 建立XMLIncludeTransformer對象,調用其applyIncludes方法將<include />轉換成<sql />節點,大體邏輯就是從sqlFragments(前面已經將全部的<sql />節點進行解析存放在其中了)獲取對應的<sql />節點,而後替換<include />節點,具體的轉換過程這裏就不講述了,沒有特別複雜,感興趣的小夥伴能夠查看相關方法,都已經註釋好了😈😈😈
  5. 獲取parameterType屬性,參數類型,轉換成Class對象
  6. 獲取lang屬性,LanguageDriver語言驅動器,默認爲XMLLanguageDriver
  7. 將該節點的<selectKey />子節點解析成 SelectKeyGenerator 生成器,用於生成一個key設置到返回對象中,在processSelectKeyNodes方法中能夠看到,該過程也會生成一個MappedStatement對象,生成的對象的 id 爲 statementId+'!selectKey'
  8. 解析useGeneratedKeys屬性,獲取 SelectKeyGenerator 生成器,若是第7步沒有生成纔會進入這裏,直接返回Jdbc3KeyGenerator單例
  9. 根據XMLLanguageDriver語言驅動建立SqlSource對象,經過這個對象能夠獲取到對應的SQL語句,在後面的《MyBatis初始化之SQL初始化》分析該建立過程
  10. 獲取statementType屬性,Statement類型,默認PREPARED
  11. 獲取其餘的一下相關信息,例如:timeoutresultMapkeyPropertykeyColumn等屬性,其中配置的resultType也會轉換成ResultMap對象
  12. 經過MapperBuilderAssistant構造器小助手根據這些屬性信息構建一個MappedStatement對象

MapperBuilderAssistant

org.apache.ibatis.builder.MapperBuilderAssistant:Mapper構造器小助手,在前面整個XML映射文件的解析過程當中,所須要建立ResultMapping、ResultMap和MappedStatement對象都是經過這個助手來建立的,那麼咱們來看看它提供了哪些功能

構造方法

public class MapperBuilderAssistant extends BaseBuilder {
    /**
	 * 當前 Mapper 命名空間
	 */
	private String currentNamespace;
	/**
	 * 資源引用的地址
     * 解析Mapper接口:xxx/xxx/xxx.java (best guess)
     * 解析Mapper映射文件:xxx/xxx/xxx.xml
	 */
	private final String resource;
	/**
	 * 當前 Cache 對象
	 */
	private Cache currentCache;
	/**
	 * 是否未解析成功 Cache 引用
	 */
	private boolean unresolvedCacheRef; // issue #676

	public MapperBuilderAssistant(Configuration configuration, String resource) {
		super(configuration);
		ErrorContext.instance().resource(resource);
		this.resource = resource;
	}
}

😈 這個小助手是爲了 XMLMapperBuilder 和 MapperAnnotationBuilder 都能調用到一些公用方法

useCacheRef方法

前面在XMLMapperBuildercacheRefElement方法解析<cache-ref />節點的過程當中有調用這個方法,用來設置當前Cache緩存實例所引用的那個對象,代碼以下:

public Cache useCacheRef(String namespace) {
    if (namespace == null) {
        throw new BuilderException("cache-ref element requires a namespace attribute.");
    }
    try {
        unresolvedCacheRef = true; // 標記未解決
        // <1> 得到 Cache 對象
        Cache cache = configuration.getCache(namespace);
        // 得到不到,拋出 IncompleteElementException 異常
        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);
    }
}

入參namespace:所引用的緩存所在的namespace

根據該namespace獲取到其緩存實例對象,而後設置爲當前須要使用的緩存實例

useNewCache方法

前面在XMLMapperBuildercacheElement方法解析<cache />節點的過程當中有調用這個方法,用來建立一個Cache緩存實例,代碼以下:

public Cache useNewCache(Class<? extends Cache> typeClass, Class<? extends Cache> evictionClass, Long flushInterval,
        Integer size, boolean readWrite, boolean blocking, Properties props) {
    // <1> 建立 Cache 對象
    // 緩存實例默認爲 PerpetualCache 類型,Cache 裝飾器默認爲 LruCache
    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();
    // <2> 添加到 configuration 的 caches 中
    configuration.addCache(cache);
    // <3> 賦值給 currentCache
    currentCache = cache;
    return cache;
}
  1. 根據節點中的相關信息經過CacheBuilder構造器建立一個緩存實例(被裝飾的Cache實例),如何構建的代碼有點長,這裏就不講述了,感興趣的小夥伴能夠查看相關方法,都已經註釋好了😈😈😈

  2. 將緩存實例添加到Configuration全局對象中

  3. 設置爲當前須要使用的緩存實例

buildResultMapping方法

前面在XMLMapperBuilderresultMapElement方法調用的buildResultMappingFromContext方法中有調用這個方法,用來建立一個RequestMapping對象,代碼以下:

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> 解析對應的 Java Type
    Class<?> javaTypeClass = resolveResultJavaType(resultType, property, javaType);
    // 解析對應的 TypeHandler ,通常不會設置
    TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);
    List<ResultMapping> composites;
    // <2> 解析組合字段名稱成 ResultMapping 集合,涉及「關聯的嵌套查詢」
    if ((nestedSelect == null || nestedSelect.isEmpty()) && (foreignColumn == null || foreignColumn.isEmpty())) {
        composites = Collections.emptyList();
    } else {
        // RequestMapping 關聯了子查詢,若是 column 配置了多個則一一再建立 RequestMapping 對象
        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<>() : flags)
            .composites(composites).notNullColumns(parseMultipleColumnNames(notNullColumn))
            .columnPrefix(columnPrefix).foreignColumn(foreignColumn).lazy(lazy).build();
}

入參有點多,不過根據名稱能夠知道其意思,大體邏輯以下:

  1. 解析對應的 Java Type 和 TypeHandler 的 Class 對象

  2. 若是嵌套的子查詢存在組合字段,則一一解析成 ResultMapping 對象,例如須要在返回的結果集中取多個列做爲嵌套查詢的入參,那麼你須要配置多個映射關係

    例如子查詢的入參對象有兩個屬性,分別是name和age,而上一層查詢從數據庫返回的列名是studentName和studentAge,那麼你須要在嵌套查詢配置column屬性爲:{name=studentName,age=studentAge},否則沒有映射關係沒法設置子查詢的入參,這樣就會爲該屬性建立兩個ResultMapping添加到composites集合中

  3. 調用applyCurrentNamespace方法,拼接命名空間

  4. 調用parseMultipleColumnNames方法,將字符串(以逗號分隔)解析成集合,做用: 默認狀況下,在至少一個被映射到屬性的列不爲空時,子對象纔會被建立。

  5. 經過ResultMapping.Builder構建一個ResultMapping對象

addResultMap方法

前面在XMLMapperBuilderresultMapElement方法使用ResultMapResolver生成ResultMap對象時會調用這個方法,用來解析生成ResultMap對象,代碼以下:

public ResultMap addResultMap(String id, Class<?> type, String extend, Discriminator discriminator,
        List<ResultMapping> resultMappings, Boolean autoMapping) {
    // <1> 得到 ResultMap 編號,即格式爲 `${namespace}.${id}`
    id = applyCurrentNamespace(id, false);
    // <2.1> 獲取完整的父 ResultMap 屬性,即格式爲 `${namespace}.${extend}`。從這裏的邏輯來看,貌似只能獲取本身 namespace 下的 ResultMap 。
    extend = applyCurrentNamespace(extend, true);

    // <2.2> 若是有父類,則將父類的 ResultMap 集合,添加到 resultMappings 中。
    if (extend != null) {
        // <2.2.1> 得到 extend 對應的 ResultMap 對象。若是不存在,則拋出 IncompleteElementException 異常
        // 因此說 <resultMap /> 標籤若是有繼承關係就必須有前後順序?
        if (!configuration.hasResultMap(extend)) {
            throw new IncompleteElementException("Could not find a parent resultmap with id '" + extend + "'");
        }
        ResultMap resultMap = configuration.getResultMap(extend);
        // 獲取 extend 的 ResultMap 對象的 ResultMapping 集合,並移除 resultMappings
        List<ResultMapping> extendedResultMappings = new ArrayList<>(resultMap.getResultMappings());
        extendedResultMappings.removeAll(resultMappings);
        // Remove parent constructor if this resultMap declares a constructor.
        // 判斷當前的 resultMappings 是否有構造方法,若是有,則從 extendedResultMappings 移除全部的構造類型的 ResultMapping
        boolean declaresConstructor = false;
        for (ResultMapping resultMapping : resultMappings) {
            if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
                declaresConstructor = true;
                break;
            }
        }
        if (declaresConstructor) {
            extendedResultMappings.removeIf(resultMapping -> resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR));
        }
        // 將 extendedResultMappings 添加到 resultMappings 中
        resultMappings.addAll(extendedResultMappings);
    }
    // <3> 建立 ResultMap 對象
    ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
            .discriminator(discriminator).build();
    // <4> 添加到 configuration 中
    configuration.addResultMap(resultMap);
    return resultMap;
}
  1. 調用applyCurrentNamespace方法拼接namespace與id,得到ResultMap的惟一編號,格式爲 ${namespace}.${id}

  2. 得到父ResultMap的惟一編號extend,格式爲${namespace}.${extend}

    1. extend爲null則直接忽略
    2. 不然獲取對應的ResultMap對象,則將extend的ResultMapping集合和本身的ResultMapping集合進行合併
  3. 經過ResultMap.Builder構建一個ResultMap對象,並添加到Configuration全局配置中

addMappedStatement方法

XMLStatementBuilderparseStatementNode方法中會調用該方法,用來構建一個MappedStatement對象,代碼以下:

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) {

    // <1> 若是的指向的 Cache 未解析,拋出異常
    if (unresolvedCacheRef) {
        throw new IncompleteElementException("Cache-ref not yet resolved");
    }

    // <2> 得到 id 編號,格式爲 `${namespace}.${id}`
    id = applyCurrentNamespace(id, false);
    // 是否爲查詢語句
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

    // <3> 建立 MappedStatement.Builder 對象
    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);

    // <4> 生成 ParameterMap 對象
    ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    if (statementParameterMap != null) {
        statementBuilder.parameterMap(statementParameterMap);
    }

    // <5> 建立 MappedStatement 對象
    MappedStatement statement = statementBuilder.build();
    // <6> 添加到 configuration 中
    configuration.addMappedStatement(statement);
    return statement;
}

入參有點多,這裏就不一一進行說明了,經過其名稱大體能夠知道其意思

  1. 若是的指向的 Cache 未解析,拋出異常

  2. 得到MappedStatement的惟一編號id,格式爲${namespace}.${id}

  3. 建立MappedStatement.Builder對象

  4. 建立ParameterMap對象,進入getStatementParameterMap方法能夠看到,ParameterMap的Class<?> type屬性設置爲入參類型,String id設置爲statementId

    <parameterMap />標籤已經被廢棄,因此這裏不會配置parameterMap 屬性

  5. 經過MappedStatement.Builder構建一個MappedStatement對象,並添加到Configuration全局配置中

RequestMapping

org.apache.ibatis.mapping.ResultMapping:保存ResultMap相關子節點的信息,也就是 Java Type 與 Jdbc Type 的映射信息

內部定義了Builder構造器,使用構建者模式建立實例對象,有如下屬性:

public class ResultMapping {
    private Configuration configuration;
    /**
    * Java 字段
    */
    private String property;
    /**
    * JDBC 列名
    */
    private String column;
    /**
    * Java 類型
    */
    private Class<?> javaType;
    /**
    * JDBC 類型
    */
    private JdbcType jdbcType;
    /**
    * 類型處理器
    */
    private TypeHandler<?> typeHandler;
    /**
    * 對應的 resultMapId
    * 例如 <resultMap /> 標籤中的 <association/> 標籤會生成一個 ResultMap 對象,則這個屬性對應該 ResultMap 對象的Id
    */
    private String nestedResultMapId;
    /**
    * 關聯的子查詢 Id
    */
    private String nestedQueryId;
    /**
    * 不能爲 null 的列名
    */
    private Set<String> notNullColumns;
    /**
    * 列名前綴
    */
    private String columnPrefix;
    /**
    * 具備的標記
    */
    private List<ResultFlag> flags;
    /**
    * 關聯嵌套查詢的屬性映射
    */
    private List<ResultMapping> composites;
    private String resultSet;
    private String foreignColumn;
    private boolean lazy;

    ResultMapping() {
    }
}

構建過程這裏就不列出來了,能夠根據註釋進行閱讀😈,最終會生成以上這些屬性

ResultMap

org.apache.ibatis.mapping.ResultMap:保存了<resultMap />配置的全部信息

內部定義了Builder構造器,使用了構建者模式建立實例對象,構建的時候進行了相關屬性的分類,有如下屬性:

public class ResultMap {
	/**
	 * Configuration 對象
	 */
	private Configuration configuration;

	/**
	 * ResultMap 對象
	 */
	private String id;
	/**
	 * 類型
	 */
	private Class<?> type;
	/**
	 * ResultMapping 集合
	 */
	private List<ResultMapping> resultMappings;
	/**
	 * ID ResultMapping 集合
	 *
	 * 當 idResultMappings 爲空時,使用 {@link #resultMappings} 賦值
	 */
	private List<ResultMapping> idResultMappings;
	/**
	 * 構造方法的入參 ResultMapping 集合
   * 根據參數名稱已經排序好了
	 *
	 * 和 {@link #propertyResultMappings} 不存在相同元素
	 */
	private List<ResultMapping> constructorResultMappings;
	/**
	 * 屬性 ResultMapping 集合
	 */
	private List<ResultMapping> propertyResultMappings;
	/**
	 * 數據庫的字段集合(所有大寫)
	 */
	private Set<String> mappedColumns;
	/**
	 * Java 對象的屬性集合
	 */
	private Set<String> mappedProperties;
	/**
	 * Discriminator 選擇器對象
	 */
	private Discriminator discriminator;
	/**
	 * 是否有內嵌的 ResultMap
	 */
	private boolean hasNestedResultMaps;
	/**
	 * 是否有嵌套關聯的子查詢
	 */
	private boolean hasNestedQueries;
	/**
	 * 是否開啓自動匹配
	 *
	 * 若是設置這個屬性,MyBatis將會爲這個ResultMap開啓或者關閉自動映射。這個屬性會覆蓋全局的屬性
	 * autoMappingBehavior。默認值爲:unset。
	 */
	private Boolean autoMapping;

	private ResultMap() {
	}
}

構建過程這裏就不列出來了,能夠根據註釋進行閱讀😈,最終會生成以上這些屬性

MappedStatement

org.apache.ibatis.mapping.MappedStatement:保存瞭解析<select /> <update /> <delete /> <insert />標籤內的SQL語句所生成的全部信息

內部定義了Builder構造器,使用了構建者模式構建對象,有如下屬性:

public final class MappedStatement {
    /**
     * XML 映射文件路徑,例如:xxx/xxx/xxx.xml
     */
    private String resource;
    /**
     * 全局配置對象
     */
    private Configuration configuration;
    /**
     * 惟一編號:`${namespace}.${id}`
     */
    private String id;
    /**
     * 這是一個給驅動的建議值,嘗試讓驅動程序每次批量返回的結果行數等於這個設置值
     * 默認值爲未設置(unset)(依賴驅動)
     */
    private Integer fetchSize;
    /**
     * 這個設置是在拋出異常以前,驅動程序等待數據庫返回請求結果的秒數
     * 默認值爲未設置(unset)(依賴數據庫驅動)
     */
    private Integer timeout;
    /**
     * Statement 的類型:STATEMENT PREPARED CALLABLE,默認 PREPARED
     * 分別對應:Statement PreparedStatement  CallableStatement
     */
    private StatementType statementType;
    private ResultSetType resultSetType;
    /**
     * SQL 相關信息
     */
    private SqlSource sqlSource;
    /**
     * 緩存對象
     */
    private Cache cache;
    private ParameterMap parameterMap;
    /**
     * ResultMap對象
     * 配置多個時須要加上 namespace 並以逗號分隔
     */
    private List<ResultMap> resultMaps;
    /**
     * 是否清空緩存
     */
    private boolean flushCacheRequired;
    /**
     * 是否使用緩存
     */
    private boolean useCache;
    /**
     * 這個設置僅針對嵌套結果 select 語句,默認值:false
     * 若是爲 true,將會假設包含了嵌套結果集或是分組,當返回一個主結果行時,就不會產生對前面結果集的引用
     * 這就使得在獲取嵌套結果集的時候不至於內存不夠用
     */
    private boolean resultOrdered;
    /**
     * SQL 語句類型
     */
    private SqlCommandType sqlCommandType;
    /**
     * key 的生成器
     */
    private KeyGenerator keyGenerator;
    /**
     * key 的生成器的 Java 屬性
     */
    private String[] keyProperties;
    /**
     * key 的生成器的 column 列名
     */
    private String[] keyColumns;
    /**
     * 是否有內嵌的 ResultMap
     */
    private boolean hasNestedResultMaps;
    /**
     * 數據庫表示
     */
    private String databaseId;
    /**
     * 日誌對象
     */
    private Log statementLog;
    /**
     * 語言驅動,默認爲XMLLanguageDriver
     */
    private LanguageDriver lang;
    /**
     * 這個設置僅適用於多結果集的狀況
     * 它將列出語句執行後返回的結果集並賦予每一個結果集一個名稱,多個名稱之間以逗號分隔
     */
    private String[] resultSets;

    MappedStatement() {
        // constructor disabled
    }
}

構建過程這裏就不列出來了,能夠根據註釋進行閱讀😈,最終會生成以上這些屬性

其中SqlSource是經過XMLLanguageDriver語言驅動建立的,能夠回到XmlStatementBuilderparseStatementNode()方法看看,在後面的《MyBatis初始化之SQL初始化》分析整個建立過程

總結

本分分析了MyBatis在初始化時加載Mapper接口與XML映射文件的整個過程

  1. XMLConfigBuilder中將用戶配置的Mapper接口所在包路徑package添加到MapperRegistry註冊表中

  2. MapperRegistry註冊表中會對包下的全部Mapper接口進行解析,每一個接口都會建立對應的MapperProxyFactory動態代理對象工廠,並保存,也會經過MapperAnnotationBuilder對該接口進行解析,解析過程:

    1. 首先經過該Mapper接口的名稱獲取對應的XML映射文件,獲取到該文件資源進行加載解析,解析後的對象都會跟XML映射文件中配置的namespace屬性關聯,因此XML映射文件的名稱要與Mapper接口的名稱保持一致,配置的namespace屬性要與接口的全名保持一致
    2. 而後解析Mapper接口的MyBatis相關注解
  3. 解析XML映射文件的過程當中是在XMLMapperBuilder中進行的,會使用到MapperBuilderAssistant小助手用於建立ResultMappingResultMapMappedStatement對象

  4. 其中解析<select /> <update /> <delete /> <insert />標籤的解析過程又在XMLStatementBuilder對象中進行

XMLStatementBuilder解析上面標籤的時候須要經過XMLLanguageDriver語言驅動建立SqlSource對象,這個過程涉及到的篇幅有點多,會在接下來的《MyBatis初始化(三)之SQL初始化(上)》中開始分析

最終全部的MyBatis配置、Mapper接口和XML映射文件生成的相應對象都保存在了Configuration全局配置對象中,那麼接下來咱們來看看SQL語句在MyBatis中是如何初始化的

參考文章:芋道源碼《精盡 MyBatis 源碼分析》

相關文章
相關標籤/搜索