當未指定且存在多個構造器,例項化物件時Spring如何選擇?

努力的小雨發表於2024-03-08

前言

在前面的講解中,我們瞭解瞭如何獲取構造器。當只有一個符合條件的構造器時,自然會選擇它作為初始化的構造器。然而,在上一節中,我們遇到了一種特殊情況:當有多個符合條件的構造器時,返回的是一個陣列。在這種情況下,Spring又是如何從多個構造器中選擇最合適的呢?今天,我們將討論的主題是:autowireConstructor方法。

autowireConstructor

讓我們首先深入研究一下該方法的主要原始碼,畢竟原始碼是最好的老師。

public BeanWrapper autowireConstructor(String beanName, RootBeanDefinition mbd,
		@Nullable Constructor<?>[] chosenCtors, @Nullable Object[] explicitArgs) {

	BeanWrapperImpl bw = new BeanWrapperImpl();
	this.beanFactory.initBeanWrapper(bw);

	Constructor<?> constructorToUse = null;
	ArgumentsHolder argsHolderToUse = null;
	Object[] argsToUse = null;

	// 如果getBean()傳入了args,那構造方法要用的入參就直接確定好了
	if (explicitArgs != null) {
		argsToUse = explicitArgs;
	}
	else {
		Object[] argsToResolve = null;
		synchronized (mbd.constructorArgumentLock) {
			constructorToUse = (Constructor<?>) mbd.resolvedConstructorOrFactoryMethod;
			if (constructorToUse != null && mbd.constructorArgumentsResolved) {
				// Found a cached constructor...
				argsToUse = mbd.resolvedConstructorArguments;
				if (argsToUse == null) {
					argsToResolve = mbd.preparedConstructorArguments;
				}
			}
		}
		if (argsToResolve != null) {
			argsToUse = resolvePreparedArguments(beanName, mbd, bw, constructorToUse, argsToResolve);
		}
	}

	// 如果沒有確定要使用的構造方法,或者確定了構造方法但是所要傳入的引數值沒有確定
	if (constructorToUse == null || argsToUse == null) {

		// Take specified constructors, if any.
		// 如果沒有指定構造方法,那就獲取beanClass中的所有構造方法所謂候選者
		Constructor<?>[] candidates = chosenCtors;
		if (candidates == null) {
			Class<?> beanClass = mbd.getBeanClass();
			try {
				candidates = (mbd.isNonPublicAccessAllowed() ?
						beanClass.getDeclaredConstructors() : beanClass.getConstructors());
			}
			catch (Throwable ex) {
				throw new BeanCreationException(mbd.getResourceDescription(), beanName,
						"Resolution of declared constructors on bean Class [" + beanClass.getName() +
						"] from ClassLoader [" + beanClass.getClassLoader() + "] failed", ex);
			}
		}

		// 如果只有一個候選構造方法,並且沒有指定所要使用的構造方法引數值,並且該構造方法是無參的,那就直接用這個無參構造方法進行例項化了
		if (candidates.length == 1 && explicitArgs == null && !mbd.hasConstructorArgumentValues()) {
			Constructor<?> uniqueCandidate = candidates[0];
			if (uniqueCandidate.getParameterCount() == 0) {
				synchronized (mbd.constructorArgumentLock) {
					mbd.resolvedConstructorOrFactoryMethod = uniqueCandidate;
					mbd.constructorArgumentsResolved = true;
					mbd.resolvedConstructorArguments = EMPTY_ARGS;
				}
				bw.setBeanInstance(instantiate(beanName, mbd, uniqueCandidate, EMPTY_ARGS));
				return bw;
			}
		}

		// Need to resolve the constructor.
		boolean autowiring = (chosenCtors != null ||
				mbd.getResolvedAutowireMode() == AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR);
		ConstructorArgumentValues resolvedValues = null;

		// 確定要選擇的構造方法的引數個數的最小值,後續判斷候選構造方法的引數個數如果小於minNrOfArgs,則直接pass掉
		int minNrOfArgs;
		if (explicitArgs != null) {
			// 如果直接傳了構造方法引數值,那麼所用的構造方法的引數個數肯定不能少於
			minNrOfArgs = explicitArgs.length;
		}
		else {
			// 如果透過BeanDefinition傳了構造方法引數值,因為有可能是透過下標指定了,比如0位置的值,2位置的值,雖然只指定了2個值,但是構造方法的引數個數至少得是3個
			ConstructorArgumentValues cargs = mbd.getConstructorArgumentValues();
			resolvedValues = new ConstructorArgumentValues();
			// 處理RuntimeBeanReference
			minNrOfArgs = resolveConstructorArguments(beanName, mbd, bw, cargs, resolvedValues);
		}

		// 對候選構造方法進行排序,public的方法排在最前面,都是public的情況下引數個數越多越靠前
		AutowireUtils.sortConstructors(candidates);
		int minTypeDiffWeight = Integer.MAX_VALUE;
		Set<Constructor<?>> ambiguousConstructors = null;
		Deque<UnsatisfiedDependencyException> causes = null;

		// 遍歷每個構造方法,進行篩選
		for (Constructor<?> candidate : candidates) {
			// 引數個數
			int parameterCount = candidate.getParameterCount();

			// 本次遍歷時,之前已經選出來了所要用的構造方法和入參物件,並且入參物件個數比當前遍歷到的這個構造方法的引數個數多,則不用再遍歷,退出迴圈
			if (constructorToUse != null && argsToUse != null && argsToUse.length > parameterCount) {
				// Already found greedy constructor that can be satisfied ->
				// do not look any further, there are only less greedy constructors left.
				break;
			}
			// 如果引數個數小於所要求的引數個數,則遍歷下一個,這裡考慮的是同時存在public和非public的構造方法
			if (parameterCount < minNrOfArgs) {
				continue;
			}

			ArgumentsHolder argsHolder;
			Class<?>[] paramTypes = candidate.getParameterTypes();
			// 沒有透過getBean()指定構造方法引數值
			if (resolvedValues != null) {
				try {
					// 如果在構造方法上使用了@ConstructorProperties,那麼就直接取定義的值作為構造方法的引數名
					String[] paramNames = ConstructorPropertiesChecker.evaluate(candidate, parameterCount);

					// 獲取構造方法引數名
					if (paramNames == null) {
						ParameterNameDiscoverer pnd = this.beanFactory.getParameterNameDiscoverer();
						if (pnd != null) {
							paramNames = pnd.getParameterNames(candidate);
						}
					}

					// 根據引數型別、引數名找到對應的bean物件
					argsHolder = createArgumentArray(beanName, mbd, resolvedValues, bw, paramTypes, paramNames,
							getUserDeclaredConstructor(candidate), autowiring, candidates.length == 1);
				}
				catch (UnsatisfiedDependencyException ex) {
					// 當前正在遍歷的構造方法找不到可用的入參物件,記錄一下
					if (logger.isTraceEnabled()) {
						logger.trace("Ignoring constructor [" + candidate + "] of bean '" + beanName + "': " + ex);
					}
					// Swallow and try next constructor.
					if (causes == null) {
						causes = new ArrayDeque<>(1);
					}
					causes.add(ex);
					continue;
				}
			}
			else {
				// Explicit arguments given -> arguments length must match exactly.
				// 在調getBean方法時傳入了引數值,那就表示只能用對應引數個數的構造方法
				if (parameterCount != explicitArgs.length) {
					continue;
				}
				// 不用再去BeanFactory中查詢bean物件了,已經有了,同時當前正在遍歷的構造方法就是可用的構造方法
				argsHolder = new ArgumentsHolder(explicitArgs);
			}

			// 當前遍歷的構造方法所需要的入參物件都找到了,根據引數型別和找到的引數物件計算出來一個匹配值,值越小越匹配
			// Lenient表示寬鬆模式
			int typeDiffWeight = (mbd.isLenientConstructorResolution() ?
					argsHolder.getTypeDifferenceWeight(paramTypes) : argsHolder.getAssignabilityWeight(paramTypes));
			// Choose this constructor if it represents the closest match.
			// 值越小越匹配
			if (typeDiffWeight < minTypeDiffWeight) {
				constructorToUse = candidate;
				argsHolderToUse = argsHolder;
				argsToUse = argsHolder.arguments;
				minTypeDiffWeight = typeDiffWeight;
				ambiguousConstructors = null;
			}
			// 值相等的情況下,記錄一下匹配值相同的構造方法
			else if (constructorToUse != null && typeDiffWeight == minTypeDiffWeight) {
				if (ambiguousConstructors == null) {
					ambiguousConstructors = new LinkedHashSet<>();
					ambiguousConstructors.add(constructorToUse);
				}
				ambiguousConstructors.add(candidate);
			}
		}
		// 遍歷結束   x

		// 如果沒有可用的構造方法,就取記錄的最後一個異常並丟擲
		if (constructorToUse == null) {
			if (causes != null) {
				UnsatisfiedDependencyException ex = causes.removeLast();
				for (Exception cause : causes) {
					this.beanFactory.onSuppressedException(cause);
				}
				throw ex;
			}
			throw new BeanCreationException(mbd.getResourceDescription(), beanName,
					"Could not resolve matching constructor on bean class [" + mbd.getBeanClassName() + "] " +
					"(hint: specify index/type/name arguments for simple parameters to avoid type ambiguities)");
		}
		// 如果有可用的構造方法,但是有多個
		else if (ambiguousConstructors != null && !mbd.isLenientConstructorResolution()) {
			throw new BeanCreationException(mbd.getResourceDescription(), beanName,
					"Ambiguous constructor matches found on bean class [" + mbd.getBeanClassName() + "] " +
					"(hint: specify index/type/name arguments for simple parameters to avoid type ambiguities): " +
					ambiguousConstructors);
		}

		// 如果沒有透過getBean方法傳入引數,並且找到了構造方法以及要用的入參物件則快取
		if (explicitArgs == null && argsHolderToUse != null) {
			argsHolderToUse.storeCache(mbd, constructorToUse);
		}
	}

	Assert.state(argsToUse != null, "Unresolved constructor arguments");
	bw.setBeanInstance(instantiate(beanName, mbd, constructorToUse, argsToUse));
	return bw;
}

在進入這個方法之前,還存在一個快取層,因為原型BeanDefinition可能會多次建立Bean,但無需每次都重新尋找構造器。因此,當第一次找到構造器時,會被快取起來。如果快取中已經存在構造方法,那麼可以直接進行例項化,無需再次執行推斷方法。如果在之前的步驟中沒有找到其他構造器,那麼將會使用無參構造器來例項化Bean。

推斷方法判斷

我們現在來仔細觀察一下autowireConstructor方法的整體流程,這樣我們可以更清楚地理解其運作方式。

如果沒有明確確定要使用的構造方法,或者已確定構造方法但其所需傳入引數值尚未確定。

  1. 當沒有確定要使用的構造方法時,可以遍歷類中的所有構造方法。
  2. 當類中只存在一個無參構造方法時,可以直接使用該無參構造方法進行例項化,無需額外的選擇操作。
  3. 在選擇構造方法時,需要確定所需引數個數的最小值。若已傳入構造方法引數值,則所選構造方法的引數個數必不少於傳入值的個數;若未傳入引數值,則需檢查BeanDefinition中是否指定了某個下標的值,確保最小值大於該下標。

比如這樣配置:

public class UserServiceBeanPostProcessor implements BeanFactoryPostProcessor {

	@Override
	public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
		BeanDefinition userService = beanFactory.getBeanDefinition("userService");
		//這裡我瞎寫的null,正常應該是物件。
		userService.getConstructorArgumentValues().addIndexedArgumentValue(1,null);
	}
}
  1. 對候選構造方法進行排序。首先,將public修飾的構造方法排在最前面;若所有構造方法均為public,那麼引數個數越多的構造方法越靠前。

  2. 遍歷每個構造方法

  3. 在呼叫getBean()方法時,如果不指定構造方法的引數值,系統會根據構造器中的引數型別和引數名來匹配相應的bean物件。

  4. 在呼叫getBean()方法時,如果指定了構造方法的引數值,系統會直接利用這些引數值來例項化bean物件

  5. 在確定構造方法時,儘管找到了匹配的構造方法引數值,但並不意味著這個構造方法是最佳選擇。因此,需要考慮是否存在多個構造方法匹配了相同的值。在這種情況下,系統將會根據值和構造方法型別之間的匹配程度進行評分,以找到最佳匹配的構造方法。

分值越低越匹配

打分規則是基於分值越低越匹配的原則。要確定分值,我們需要將程式碼提取出來進行執行,因為底層的邏輯相當複雜,需要仔細分析。

public int getTypeDifferenceWeight(Class<?>[] paramTypes) {
	// 最終值和型別的匹配程度
	int typeDiffWeight = MethodInvoker.getTypeDifferenceWeight(paramTypes, this.arguments);
	// 原始值和型別的匹配程度,並減掉1024,使得原始值的匹配值更優先,意思就是優先根據原始值來算匹配值
	int rawTypeDiffWeight = MethodInvoker.getTypeDifferenceWeight(paramTypes, this.rawArguments) - 1024;
	// 取最小值
	return Math.min(rawTypeDiffWeight, typeDiffWeight);
}

那麼,讓我們來檢視一下getTypeDifferenceWeight方法能夠輸出怎樣的數值。

首先,我們定義一個A類,該類繼承自B類,而B類又繼承自C類,同時A類實現了介面D。

Object[] objects = new Object[]{new A()};
// 0
System.out.println(MethodInvoker.getTypeDifferenceWeight(new Class[]{A.class}, objects));
// 2
System.out.println(MethodInvoker.getTypeDifferenceWeight(new Class[]{B.class}, objects));
// 4
System.out.println(MethodInvoker.getTypeDifferenceWeight(new Class[]{C.class}, objects));
// 1
System.out.println(MethodInvoker.getTypeDifferenceWeight(new Class[]{D.class}, objects));

透過仔細觀察,我們可以明白為什麼分值越低越具有匹配性。

總結

在本文中,我們深入研究了Spring框架中的autowireConstructor方法。該方法用於在存在多個構造器時選擇最合適的構造器進行例項化Bean。透過分析原始碼和推斷方法判斷的流程,我們瞭解到系統是如何根據引數個數、型別和數值的匹配程度來選擇最佳構造器的。

在實際應用中,我們需要注意遍歷構造方法、引數個數的最小值、排序規則、引數值的匹配等細節。

總的來說,autowireConstructor方法是Spring框架中一個關鍵的方法,它為我們提供了靈活且智慧的構造器選擇機制,幫助我們更好地管理Bean的例項化過程。透過學習和掌握這一方法,我們能夠更好地運用Spring框架。

相關文章