mybatis核心组件详解——MapperAnnotationBuilder

MapperAnnotationBuilder(org.apache.ibatis.builder.annotation.MapperAnnotationBuilder),mapper注解构建器。

它的职责很简单,就是解析指定的mapper接口对应的Class对象中,包含的所有mybatis框架中定义的注解,并生成Cache、ResultMap、MappedStatement三种类型对象。


MapperAnnotationBuilder是以Class.toString()方法生成的字符串,作为Class对象的唯一标识的,在解析完Class对象后,会调用Configuration.addLoadedResource()方法把这个字符串加入配置对象的内部集合中,以防止重复解析。


MapperAnnotationBuilder总会优先解析xml配置文件,并且这个xml配置文件必须与Class对象所在的包路径一致,且文件名要与类名一致。在解析完xml配置文件后,才会开始解析Class对象中包含的注解。

源码解读:

MapperAnnotationBuilder数据结构如下:

// 只有4个元素:@Select、@Insert、@Update、@Delete,sql语句保存在注解中
	private final Set<Class<? extends Annotation>> sqlAnnotationTypes = new HashSet<Class<? extends Annotation>>();
	// 只有4个元素:@SelectProvider、@InsertProvider、@UpdateProvider、@DeleteProvider
	// sql语句保存在注解指定的类的指定方法中
	private final Set<Class<? extends Annotation>> sqlProviderAnnotationTypes = new HashSet<Class<? extends Annotation>>();
	// 核心配置对象
	private Configuration configuration;
	// Mapper构建助手,用于组装解析出来的配置,生成Cache、ResultMap、MappedStatement等对象,
	// 并添加到Configuration配置对象中
	private MapperBuilderAssistant assistant;
	// 要解析的目标mapper接口的Class对象
	private Class<?> type;

MapperAnnotationBuilder解析逻辑如下:

public void parse() {
		// Class对象的唯一标识,如:
		// TopicMapper对应的字符串为"interface com.lixin.mapper.TopicMapper"
		String resource = type.toString();
		// 如果当前Class对象已经解析过,则不在解析
		if (!configuration.isResourceLoaded(resource)) {
			// 加载并解析指定的xml配置文件,Class所在的包对应文件路径,Class类名对应文件名称,如:
			// com.lixin.mapper.TopicMapper类对应的配置文件为com/lixin/mapper/TopicMapper.xml
			loadXmlResource();
			// 把Class对应的标识添加到已加载的资源列表中
			configuration.addLoadedResource(resource);
			// 设置当前namespace为接口Class的全限定名
			assistant.setCurrentNamespace(type.getName());
			// 解析缓存对象
			parseCache();
			// 解析缓存引用,会覆盖之前解析的缓存对象
			parseCacheRef();
			// 获取所有方法,解析方法上的注解,生成MappedStatement和ResultMap
			Method[] methods = type.getMethods();
			// 遍历所有获取到的方法
			for (Method method : methods) {
				try {
					// 解析一个方法生成对应的MapperedStatement对象
					// 并添加到配置对象中
					parseStatement(method);
				} catch (IncompleteElementException e) {
					configuration.addIncompleteMethod(new MethodResolver(this, method));
				}
			}
		}
		// 解析挂起的方法
		parsePendingMethods();
	}

loadXmlResource()方法,就是在解析指定的xml配置文件。

这里存在一个需要注意的命名问题:

XMLMapperBuilder每解析一个xml配置文件,都会以文件所在路径为xml文件的唯一标识,并把标识添加到已加载的资源文件列表中(如:com/lixin/mapper/topicMapper.xml)。但是loadXmlResource()方法中避免重复加载检查的key的却是"namespace:"+类全限定名的格式。

private void loadXmlResource() {
		// 如果已加载资源列表中指定key已存在,则不再解析xml文件
		// 资源名称为namepace:全限定名
		if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
			// 根据Class对象生成xml配置文件路径
			String xmlResource = type.getName().replace('.', '/') + ".xml";
			InputStream inputStream = null;
			try {
				// 获取文件字节流
				inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
			} catch (IOException e) {
				// ignore, resource is not required
			}
			// 如果xml文件存在,则创建XMLMapperBuilder进行解析
			if (inputStream != null) {
				XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(),
						xmlResource, configuration.getSqlFragments(), type.getName());
				xmlParser.parse();
			}
		}
	}

之所以使用这种格式,是因为当前并不知道xml文件的真实名称是什么,与Class全限定名只是约定(换句话说,xml文件路径完全可以不遵循这种约定)。因此,为了避免重复加载,XMLMapperBuilder在解析完配置文件后,会调用bindMapperForNamespace()方法,尝试加载配置文件中根元素的namespace属性获取Class对象,并且添加"namespace:"+全限定名格式的额外的key到已加载资源列表中,来通知MapperAnnotationBuilder。


解析缓存对象:

private void parseCache() {
		// 获取接口上的@CacheNamespace注解
		CacheNamespace cacheDomain = type.getAnnotation(CacheNamespace.class);
		// 如果存在该注解,则调用构建助手创建缓存对象
		if (cacheDomain != null) {
			assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), cacheDomain.flushInterval(),
					cacheDomain.size(), cacheDomain.readWrite(), null);
		}
	}

@CacheNamespace注解对应mapper.xml配置文件中的<cache>元素,但是注解方式不支持properties自定义属性的配置。

解析缓存引用:

private void parseCacheRef() {
		// 获取接口上的@CacheNamespaceRef注解
		CacheNamespaceRef cacheDomainRef = type.getAnnotation(CacheNamespaceRef.class);
		// 如果存在该注解,则调用构建助手添加引用关系,
		// 以Class对象的全限定名为目标namespace
		if (cacheDomainRef != null) {
			assistant.useCacheRef(cacheDomainRef.value().getName());
		}
	}

@CacheNamespaceRef注解对应mapper.xml配置文件中的<cache-ref namespace=""/>元素。


解析MappedStatement和ResultMap:

MapperAnnotationBuilder会遍历Class对象中的所有方法,一个Method对象对应一个MappedStatement对象,ResultMap的定义与xml配置文件方式不同,配置文件由单独的<resultMap>元素定义,而注解方式则定义在方法上。每个方法可以创建新的ResultMap对象,也可以引用已经存在的ResultMap对象的id。

void parseStatement(Method method) {
		// 获取输入参数的类型,这个参数没用,
		// 因为现在都是以输入参数对象为根,通过ognl表达式寻值的
		Class<?> parameterTypeClass = getParameterType(method);
		// 通过方法上的@Lang注解获取语言驱动
		LanguageDriver languageDriver = getLanguageDriver(method);
		// 通过方法上的@Select等注解获取SqlSource
		SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
		// 如果成功创建了SqlSource,则继续
		if (sqlSource != null) {
			// 获取方法上的@Options注解
			Options options = method.getAnnotation(Options.class);
			// 映射语句id为类的全限定名.方法名
			final String mappedStatementId = type.getName() + "." + method.getName();
			Integer fetchSize = null;
			Integer timeout = null;
			StatementType statementType = StatementType.PREPARED;
			ResultSetType resultSetType = ResultSetType.FORWARD_ONLY;
			// 通过注解获取Sql命令类型
			SqlCommandType sqlCommandType = getSqlCommandType(method);
			boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
			boolean flushCache = !isSelect;
			boolean useCache = isSelect;

			KeyGenerator keyGenerator;
			String keyProperty = "id";
			String keyColumn = null;
			// 如果是insert或update命令
			if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
				// 首先检查@SelectKey注解 ,它会覆盖任何其他的配置
				// 获取方法上的SelectKey注解
				SelectKey selectKey = method.getAnnotation(SelectKey.class);
				// 如果存在@SelectKey注解
				if (selectKey != null) {
					keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method),
							languageDriver);
					keyProperty = selectKey.keyProperty();
				} else {
					if (options == null) {
						keyGenerator = configuration.isUseGeneratedKeys() ? new Jdbc3KeyGenerator()
								: new NoKeyGenerator();
					} else {
						keyGenerator = options.useGeneratedKeys() ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
						keyProperty = options.keyProperty();
						keyColumn = options.keyColumn();
					}
				}
			} else {
				// 其他sql命令均没有键生成器
				keyGenerator = new NoKeyGenerator();
			}

			if (options != null) {
				flushCache = options.flushCache();
				useCache = options.useCache();
				fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize()
						: null; // issue #348
				timeout = options.timeout() > -1 ? options.timeout() : null;
				statementType = options.statementType();
				resultSetType = options.resultSetType();
			}
			
			// 处理方法上的@ResultMap注解
			String resultMapId = null;
			// 获取注解,@ResultMap注解代表引用已经存在的resultMap对象的id
			ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
			// 如果方法上存在@ResultMap注解,则生成引用id即可
			if (resultMapAnnotation != null) {
				// 获取指定的多个resultMapId
				String[] resultMaps = resultMapAnnotation.value();
				StringBuilder sb = new StringBuilder();
				// 遍历String数组,拼接为一个String,逗号分隔
				for (String resultMap : resultMaps) {
					if (sb.length() > 0)
						sb.append(",");
					sb.append(resultMap);
				}
				resultMapId = sb.toString();
				// 不存在@ResultMap注解,且语句为select类型,
				// 则通过解析@Args、@Results等注解生成新的ResultMap对象
			} else if (isSelect) {
				// 生成新的ResultMap对象,加入Configuration配置对象
				// 同时返回resultMapId
				resultMapId = parseResultMap(method);
			}
			// 构建MappedStatement并添加到配置对象中
			assistant.addMappedStatement(mappedStatementId, sqlSource, statementType, sqlCommandType, fetchSize,
					timeout, null, // ParameterMapID
					parameterTypeClass, resultMapId, // ResultMapID
					getReturnType(method), resultSetType, flushCache, useCache, false, // TODO
																						// issue
																						// #577
					keyGenerator, keyProperty, keyColumn, null, languageDriver, null);
		}
	}

从代码逻辑可以看出,如果不存在@Select、@Insert、@Update、@Delete或者对应的@xxxProvider中的任何一个,则后续注解全是无效,只有@Lang会起作用。当使用@ResultMap注解引用已存在的结果映射时,后续关于创建新的结果映射的注解将失效。

解析注解生成新的ResultMap对象逻辑如下:

private String parseResultMap(Method method) {
		// 获取目标bean的类型
		Class<?> returnType = getReturnType(method);
		// 获取方法上的@ConstructorArgs注解
		ConstructorArgs args = method.getAnnotation(ConstructorArgs.class);
		// 获取方法上的@Results
		Results results = method.getAnnotation(Results.class);
		// 获取方法上的@TypeDiscriminator注解
		TypeDiscriminator typeDiscriminator = method.getAnnotation(TypeDiscriminator.class);
		// 根据方法生成resultMap的唯一标识,格式为:类全限定名.方法名-参数类型简单名称
		String resultMapId = generateResultMapName(method);
		// resultMapId和返回值类型已经解析完毕,
		// 再解析剩下的构造方法映射、属性映射和鉴别器,之后添加结果映射到配置对象中
		applyResultMap(resultMapId, returnType, argsIf(args), resultsIf(results), typeDiscriminator);
		return resultMapId;
	}

注解方式只支持属性映射时使用另外的select语句,不支持嵌套的属性映射的配置。

源码的重点逻辑已经讲解完毕,具体的注解与配置文件元素的一一对应关系,并没有逐一解释,如果读者有疑问可以留言。另外关于注解的配置,我将会单独讲解。

最后给一点建议,注解配置是xml方式配置的子集,且配置后不易修改和查看,因此还是建议用户使用xml的方式来配置mapper。

转载于:https://my.oschina.net/lixin91/blog/625903

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值