mybatis核心組件詳解——MapperAnnotationBuilder

MapperAnnotationBuilder(org.apache.ibatis.builder.annotation.MapperAnnotationBuilder),mapper註解構建器。java

它的職責很簡單,就是解析指定的mapper接口對應的Class對象中,包含的全部mybatis框架中定義的註解,並生成Cache、ResultMap、MappedStatement三種類型對象。
sql


MapperAnnotationBuilder是以Class.toString()方法生成的字符串,做爲Class對象的惟一標識的,在解析完Class對象後,會調用Configuration.addLoadedResource()方法把這個字符串加入配置對象的內部集合中,以防止重複解析。apache


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解析邏輯以下:mybatis

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配置文件。
app

這裏存在一個須要注意的命名問題:框架

XMLMapperBuilder每解析一個xml配置文件,都會以文件所在路徑爲xml文件的惟一標識,並把標識添加到已加載的資源文件列表中(如:com/lixin/mapper/topicMapper.xml)。可是loadXmlResource()方法中避免重複加載檢查的key的倒是"namespace:"+類全限定名的格式。ide

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。

相關文章
相關標籤/搜索