MyBatis - 初始化(九)加载Mapper接口与XML映射文件

MyBatis - 初始化(九)加载Mapper接口与XML映射文件

该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对各位读者会不太友好,阅读前需要对 MyBatis 和 Spring 有一定的了解。比较适合刚接触,会使用但是一直没去探究底层的同学。

MyBatis 版本:3.5.6

MyBatis-Spring 版本:2.0.3

MyBatis-Spring-Boot-Starter 版本:2.1.4

该系列其他文档请查看:《 MyBatis 系列 - 导读》

一、MyBatis的初始化

在MyBatis初始化过程中,大致会有以下几个步骤:

  1. 创建Configuration全局配置对象,会往TypeAliasRegistry别名注册中心添加Mybatis需要用到的相关类,并设置默认的语言驱动类为XMLLanguageDriver
  2. 加载mybatis-config.xml配置文件、Mapper接口中的注解信息和XML映射文件,解析后的配置信息会形成相应的对象并保存到Configuration全局配置对象中
  3. 构建DefaultSqlSessionFactory对象,通过它可以创建DefaultSqlSession对象,MyBatis中SqlSession的默认实现类

因为整个初始化过程涉及到的代码比较多,所以拆分成了四个模块依次对MyBatis的初始化进行分析:

  • 《MyBatis - 初始化(八)加载 mybatis-config.xml 》
  • 《MyBatis - 初始化(九)加载Mapper接口与XML映射文件》
  • 《MyBatis - 初始化(十)SQL初始化(上)》
  • 《MyBatis - 初始化(十一)SQL初始化(下)》

由于在MyBatis的初始化过程中去解析Mapper接口与XML映射文件涉及到的篇幅比较多,XML映射文件的解析过程也比较复杂,所以才分成了后面三个模块,逐步分析,这样便于理解

初始化(二)之加载Mapper接口与映射文件

在上一个模块已经分析了是如何解析mybatis-config.xml配置文件的,在最后如何解析标签的还没有进行分析,这个过程稍微复杂一点,因为需要解析Mapper接口以及它的XML映射文件,让我们一起来看看这个解析过程

解析XML映射文件生成的对象主要如下图所示:

MyBatis - 初始化(九)加载Mapper接口与XML映射文件_第1张图片

主要包路径: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配置( 标签内的SQL语句所生成的所有信息

解析入口

我们回顾上一个模块,在org.apache.ibatis.builder.xml.XMLConfigBuilder中会解析mybatis-config.xml配置文件中的标签,调用其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.");
                }
            }
        }
    }
}

遍历标签的子节点

  1. 如果是子节点,则获取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
	 *
	 * 例如: ${alias}.id,${alias}.username,${alias}.password 
     * 可能在很多地方被引用
	 */
	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> 解析 `` 节点
			configurationElement(parser.evalNode("/mapper"));
			// <3> 标记该 Mapper 已经加载过
			configuration.addLoadedResource(resource);
			// <4> 绑定 Mapper
			bindMapperForNamespace();
		}
		// <5> 解析待定的  节点
		parsePendingResultMaps();
		// <6> 解析待定的  节点
		parsePendingCacheRefs();
		// <7> 解析待定的 SQL 语句的节点
		parsePendingStatements();
	}
}

<1> 根据Configuration全局配置判断当前XML映射文件是否已经加载过,例如resource为:xxx/xxx/xxx.xml

<2> 解析 节点,也就是解析整个的XML映射文件,在下面的configurationElement方法中讲解

<3> 标记该XML映射文件已经加载过,往Configuration全局配置添加该字段文件,例如添加:xxx/xxx/xxx.xml

<4> 绑定 Mapper 到该命名空间,避免在MapperAnnotationBuilder#loadXmlResource方法中重复加载该XML映射文件

<5> 解析待定的 节点以及 Statement 对象,因为我们配置的这些对象可能还依赖的其他对象,在解析的过程中这些依赖可能还没解析出来,导致这个对象解析失败,所以先保存在Configuration全局配置对象中,待整个XML映射文件解析完后,再遍历之前解析失败的对象进行初始化,这里就不做详细的讲述了,感兴趣的小伙伴可以看一下

这里我们来看一下configurationElement(XNode context)方法是如何解析XML映射文件中的节点

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> 解析  节点
			cacheRefElement(context.evalNode("cache-ref"));
			// <3> 解析  节点
			cacheElement(context.evalNode("cache"));
			// 已废弃!老式风格的参数映射。内联参数是首选,这个元素可能在将来被移除,这里不会记录。
			parameterMapElement(context.evalNodes("/mapper/parameterMap"));
			// <4> 解析  节点
			resultMapElements(context.evalNodes("/mapper/resultMap"));
			// <5> 解析  节点们
			sqlElement(context.evalNodes("/mapper/sql"));
			// <6> 解析     节点,调用`buildStatementFromContext方法

cacheRefElement方法

cacheRefElement(XNode context)方法用于解析XML映射文件中的节点,代码如下:

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映射文件中的节点,代码如下:

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 list)方法用于解析节点,最后会调用

resultMapElement(XNode resultMapNode, List additionalResultMappings, Class enclosingType)方法逐个解析生成ResultMap对象

整体的流程图:

MyBatis - 初始化(九)加载Mapper接口与XML映射文件_第2张图片

例如这样配置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> 遍历  的子节点
		List<XNode> resultChildren = resultMapNode.getChildren();
		for (XNode resultChild : resultChildren) {
			if ("constructor".equals(resultChild.getName())) { // <2.1> 处理  节点
				processConstructorElement(resultChild, typeClass, resultMappings);
			} else if ("discriminator".equals(resultChild.getName())) { // <2.2> 处理  节点
				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;
		}
	}
}

关于元素的属性配置参考MyBatis官方文档配置说明

resultMapElement(XNode resultMapNode, List additionalResultMappings, Class enclosingType)方法的入参分别是:

  • 当前节点Node的封装,封装成XNode便于操作
  • 继承的ResultMap所对应的ResultMapping的集合,可以通过extend属性配置继承哪个ResultMap,没有继承的话就是空集合
  • 所属的ResultMap的类型,例如中的也会被解析成ResultMap,那么它的enclosingType就是所属ResultMap的Class对象

处理逻辑

  1. 获得 type 属性,生成该ResultMap对应Class对象,如果没有定义type属性,则可能是标签,尝试从所属ResultMap的Class对象获取property的Class对象,因为标签中配置的标签也会解析成一个ResultMap对象
  2. 遍历 的子节点,依次处理
    1. 如果是节点,则调用processConstructorElement方法进行解析,再获取它的子节点生成对应的RequestMapping对象,这些RequestMapping对象会添加ResultFlag.CONSTRUCTOR标记,如果是标签则再添加一个ResultFlag.ID标记,这些对象会在实例化类时,注入到构造方法中
    2. 如果是节点,则调用processDiscriminatorElement方法进行解析,创建一个Discriminator选择器对象,用于可以使用结果值来决定这个属性使用哪个ResultMap,基于子节点来进行映射
    3. 其他节点,则调用buildResultMappingFromContext方法进行解析,如果是则添加一个ResultFlag.ID标记,生成对应的RequestMapping对象
  3. 创建ResultMapResolver对象,调用其resolve()方法执行解析,内部调用MapperBuilderAssistant构造器小助手的addResultMap来生成ResultMap对象的

上面的2.12.2并不复杂,感兴趣的小伙伴可以查看相关方法,我们来看下2.3是如何解析成ResultMapping对象的

buildResultMappingFromContext方法

buildResultMappingFromContext(XNode context, Class resultType, List flags)方法将标签中的子标签解析成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 对象
		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的获取,如果这个是或者,则会调用processNestedResultMappings方法解析成ResultMap对象,然后返回该对象的id(没有定义会自动生成),这样这个RequestMapping对象就会关联这个ResultMap对象了,这个方法内部也是调用resultMapElement方法生成ResultMap对象的,可以回过头再看下这个方法
  • 最后通过MapperBuilderAssistant构造器小助手的buildResultMapping方法根据这些属性构建一个ResultMapping对象并返回

整个的ResultMap对象的解析过程到这里就结束了,关于MapperBuilderAssistant在后续会讲到,接下来我们来看看节点的解析

sqlElement方法

sqlElement(List list)方法用于解析所有的节点,内部调用sqlElement(List list, String requiredDatabaseId)方法,代码如下:

public class XMLMapperBuilder extends BaseBuilder {
    private void sqlElement(List<XNode> list, String requiredDatabaseId) {
		// <1> 遍历所有  节点
		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);
			}
		}
	}
}

这里仅仅是将该节点保存至Map sqlFragments对象中(该对象保存与Configuration全局配置对象中),后续解析其他SQL语句中会使用到,例如查询语句中使用了标签,则需要获取到对应的节点将其替换

buildStatementFromContext方法

buildStatementFromContext(List list)方法用于解析 节点 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配置

也就是解析 标签 */ 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 对应节点,也就是 标签内的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语言驱动创建的,可以回到XmlStatementBuilder的**parseStatementNode()**方法看看,在后面的《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. 其中解析