Spring原始碼解析之ConfigurationClassPostProcessor(三)

北洛發表於2021-08-28

在上一章筆者介紹了ConfigurationClassParser.doProcessConfigurationClass(...)方法,在這個方法裡呼叫了processImports(...)方法處理配置類的@Import註解,getImports(sourceClass)能從一個配置類上獲取@Import註解配置的所有類形成一個集合,如果集合不為空則會在下面程式碼的<1>處開始遍歷處理。

如果一個類是ImportSelector介面的實現類,會進入<2>處的分支,並在<3>處建立實現類的例項,在<4>處呼叫例項的selectImports(...)方法獲取配置類列表,之後在<5>處以遞迴的方式呼叫doProcessConfigurationClass(...)將獲取到的配置類傳入。

如果一個類是ImportBeanDefinitionRegistrar介面的實現類,則會進入<6>處的分支,這裡依舊生成一個實現類的例項,只是這裡不立馬回撥該介面的方法,而是暫存在配置類中,待從ConfigurationClassParser.processImports(...)逐層返回到ConfigurationClassPostProcessor.processConfigBeanDefinitions(...)的do-while迴圈後會執行ImportBeanDefinitionRegistrar例項的回撥方法。

如果一個類僅僅是平平無奇的配置類,即不是ImportSelector的實現類,也不是ImportBeanDefinitionRegistrar的實現類,則會進入<7>處的分支,這裡會將配置類傳給processConfigurationClass(...)方法,這裡我們也可以認為是遞迴呼叫,這個方法筆者之前講過,會將配置類上的@ComponentScans、@ComponentScan註解指定的類路徑解析出來,根據類路徑掃描BeanDefinition,再判斷掃描出來的類是否有資格成為配置類,如果可以的話會再進一步解析。

class ConfigurationClassParser {
	……
	protected final SourceClass doProcessConfigurationClass(
			ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
			throws IOException {
		……
		processImports(configClass, sourceClass, getImports(sourceClass), filter, true);
		……
	}
	private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
			Collection<SourceClass> importCandidates, Predicate<String> exclusionFilter,
			boolean checkForCircularImports) {

		if (importCandidates.isEmpty()) {
			return;
		}

		if (checkForCircularImports && isChainedImportOnStack(configClass)) {
			this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
		}
		else {
			this.importStack.push(configClass);
			try {
				for (SourceClass candidate : importCandidates) {//<1>
					if (candidate.isAssignable(ImportSelector.class)) {//<2>
						// Candidate class is an ImportSelector -> delegate to it to determine imports
						Class<?> candidateClass = candidate.loadClass();
						ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
								this.environment, this.resourceLoader, this.registry);//<3>
						Predicate<String> selectorFilter = selector.getExclusionFilter();
						if (selectorFilter != null) {
							exclusionFilter = exclusionFilter.or(selectorFilter);
						}
						if (selector instanceof DeferredImportSelector) {
							this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
						}
						else {
							String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());//<4>
							Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames, exclusionFilter);
							processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter, false);//<5>
						}
					}
					else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {//<6>
						// Candidate class is an ImportBeanDefinitionRegistrar ->
						// delegate to it to register additional bean definitions
						Class<?> candidateClass = candidate.loadClass();
						ImportBeanDefinitionRegistrar registrar =
								ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
										this.environment, this.resourceLoader, this.registry);
						configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
					}
					else {//<7>
						// Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
						// process it as an @Configuration class
						this.importStack.registerImport(
								currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
						processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter);
					}
				}
			}
			catch (BeanDefinitionStoreException ex) {
				throw ex;
			}
			catch (Throwable ex) {
				throw new BeanDefinitionStoreException(
						"Failed to process import candidates for configuration class [" +
						configClass.getMetadata().getClassName() + "]", ex);
			}
			finally {
				this.importStack.pop();
			}
		}
	}
	……
}

  

那麼我們來總結一下ConfigurationClassParser.processImports(...)完成的工作,這個方法主要用於處理配置類上的@Import註解,@Import註解允許我們引入多個配置類,在這個方法會遍歷處理這些配置類,如果配置類是ImportSelector的實現類,執行實現類的回撥方法能立馬拿到待引入的配置類列表,我們可以遞迴呼叫processImports(...)方法將拿到的配置類列表傳入,在這個方法會處理來自上層傳入的配置類。如果配置類是ImportBeanDefinitionRegistrar介面的實現類,這裡僅僅是例項化這個類的例項並暫存到配置類,並不立即呼叫實現類的回撥方法,因為spring不能肯定開發者會往傳入的registrar物件註冊多少個配置類,甚至有的開發者一個都不會註冊。如果一個類不是ImportSelector或ImportBeanDefinitionRegistrar介面的實現類,會呼叫processConfigurationClass(...)方法將配置類傳入,我們可以認為這裡是遞迴呼叫,因為processConfigurationClass(...)方法最終會執行到processImports(...)方法。processConfigurationClass(...)方法會解析配置類指定的類路徑,並將可以成為BeanDefinition的類註冊進spring容器,在嘗試將掃描出來的類作為配置類進行解析。

在瞭解ConfigurationClassParser.parse(...)方法完成的工作後,我們來整體過一下ConfigurationClassPostProcessor.processConfigBeanDefinitions(...)方法,這個方法在最開始的時候會獲取spring容器所有的beanName,遍歷這些beanName拿到開發者傳給容器的配置類,因為開發者可能傳入多個配置類,所以這裡會行程一個配置類列表,如果列表為空則退出該方法,如果列表不為空,則對列表中的配置類進行排序。

在對配置類列表排序完畢後,會進入一個do-while迴圈解析配置類。解析配置類是一項非常複雜的工作,解析配置類的時候不但會掃描配置類指定的類路徑,還會嘗試將掃描出來的BeanDefinition當做一個配置類再次進行解析,如果掃描出來的BeanDefinition可以作為配置類會再次進行二次解析,這裡又會重複之前所說的解析工作,在解析完畢後,才會處理@Import註解。

在瞭解完spring是如何解析配置類後,我們來再來整體過下ConfigurationClassPostProcessor.processConfigBeanDefinitions(...)的邏輯,在<1>處會先用candidateNames這個陣列存放spring容器目前所有的beanName。

candidateNames有以下兩點作用:

  1. spring會在<2>處先根據candidateNames中存放的beanName過濾出可以成為配置類的的BeanDefinition。
  2. candidateNames可以用於判斷解析配置類時是否有新的BeanDefinition被註冊進spring容器,可以認為candidateNames是解析配置類前的快照,如果解析完配置類後發現spring容器的BeanDefinition數量大於快照中beanName的數量,表示在解析的時候有引入新的BeanDefinition。

在<2>處完成遍歷後會將配置類的BeanDefinition和beanName包裝成一個BeanDefinitionHolder物件存進configCandidates列表,之後再<3>處會根據configCandidates列表生成一個配置類集合candidates,<4>處同樣會根據配置類的長度生成一個集合alreadyParsed,這個集合會存放已經解析的配置類。

之後會進入一個do-while迴圈,在迴圈中會解析、校驗配置類。<5>處會獲取所有解析出來的配置類,這裡筆者所說的不單單是我們平常用@Configuration註解標記的配置類,在解析方法裡會把類路徑下描到的BeanDefinition嘗試作為配置類進行解析,所以<5>處的方法還會將類路徑下可以作為配置類的BeanDefinition返回。我們可以在配置類上用@Import註解引入一個ImportBeanDefinitionRegistrar實現類,也可以用@ImportResource註解引入一個XML配置檔案,ImportBeanDefinitionRegistrar實現類和XML檔案在解析的時候會暫存在配置類中,等到從解析方法回到do-while迴圈時就會在<7>處配置類中暫存的ImportBeanDefinitionRegistrar例項和XML檔案,這裡可能會向spring容器註冊新的BeanDefinition。

在執行完<7>處的程式碼後,配置類才算完全被解析,在<8>會將已經被完全解析的配置類存放進alreadyParsed集合,同時我們往回看,每次執行do-while迴圈時,<5>處總會把目前獲取到的配置類返回,configClasses可能存在已經被完全解析的配置類,所以在<6>處會把已經完全解析的配置類移除,<7>處僅處理目前需要處理尚未回撥ImportBeanDefinitionRegistrar介面的例項和XML檔案。

在<8>處將完全處理完畢的配置類放到alreadyParsed集合後,會清空candidates集合,如果在<9>處判斷目前spring容器擁有的BeanDefinition數量大於解析配置類前beanName的數量,會進入分支<9>看看是否有需要再解析的配置類,如果有配置類新增到candidates集合則開啟新的一輪的迴圈。如果<9>處判斷解析完配置類後spring容器的BeanDefinition數量等於原先解析前beanName的數量,則candidates集合為空退出do-while迴圈。

那麼在分支<9>中又是如何判斷是否應該往candidates集合新增新的配置類呢?在分支<9>內會先把spring容器目前所有的beanName存放到newCandidateNames這個陣列中,oldCandidateNames用於存放解析前的beanName,alreadyParsedClasses用於存放解析後的所有配置類。之後會在<10>處遍歷spring容器目前所有的beanName,如果beanName在oldCandidateNames集合中,表示其BeanDefinition在本次迴圈或之前的迴圈已經被解析,這裡不會進入<11>處的分支。如果beanName不在oldCandidateNames集合中,那麼這個beanName對應的BeanDefinition有可能是在解析時註冊進spring容器,也可能是解析配置類完畢後在<7>處註冊進spring容器的,如果是在解析時引入,那麼alreadyParsedClasses會包含這個配置類,這裡不會進入<12>處的分支。如果是在<7>處處理暫存在配置類的ImportBeanDefinitionRegistrar例項和XML檔案,那麼alreadyParsedClasses不會包含BeanDefinition對應的類,這裡只要ConfigurationClassUtils.checkConfigurationClassCandidate(...)判斷BeanDefinition對應的類是配置類就會把BeanDefinition和beanName包裝成BeanDefinitionHolder物件存進candidates。最後在<13>處將spring目前所有的beanName存進陣列candidateNames,作為下一輪迴圈解析前的快照。如果下一輪迴圈在<9>處判斷spring容器中的BeanDefinition數量等於上一輪的快照數量,就會退出迴圈。另外進入了<9>處的分支並不意味著一定會有下個迴圈,do-while退出迴圈的條件是candidates為空,只要新引入的BeanDefinition都是解析出來的配置類,或者新引入的BeanDefinition都不是配置類,則不會進入分支<12>,則candidates為空,會退出do-while迴圈。

public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor,
		PriorityOrdered, ResourceLoaderAware, BeanClassLoaderAware, EnvironmentAware {
	……
	public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
		List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
		String[] candidateNames = registry.getBeanDefinitionNames();//<1>

		for (String beanName : candidateNames) {//<2>
			BeanDefinition beanDef = registry.getBeanDefinition(beanName);
			if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
				if (logger.isDebugEnabled()) {
					logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
				}
			}
			else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
				configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
			}
		}
		……
		// Parse each @Configuration class
		ConfigurationClassParser parser = new ConfigurationClassParser(
				this.metadataReaderFactory, this.problemReporter, this.environment,
				this.resourceLoader, this.componentScanBeanNameGenerator, registry);

		Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);//<3>
		Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());//<4>
		do {
			parser.parse(candidates);
			parser.validate();

			Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());//<5>
			configClasses.removeAll(alreadyParsed);//<6>

			// Read the model and create bean definitions based on its content
			if (this.reader == null) {
				this.reader = new ConfigurationClassBeanDefinitionReader(
						registry, this.sourceExtractor, this.resourceLoader, this.environment,
						this.importBeanNameGenerator, parser.getImportRegistry());
			}
			this.reader.loadBeanDefinitions(configClasses);//<7>
			alreadyParsed.addAll(configClasses);//<8>

			candidates.clear();
			if (registry.getBeanDefinitionCount() > candidateNames.length) {//<9>
				String[] newCandidateNames = registry.getBeanDefinitionNames();
				Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));
				Set<String> alreadyParsedClasses = new HashSet<>();
				for (ConfigurationClass configurationClass : alreadyParsed) {
					alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
				}
				for (String candidateName : newCandidateNames) {//<10>
					if (!oldCandidateNames.contains(candidateName)) {//<11>
						BeanDefinition bd = registry.getBeanDefinition(candidateName);
						if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
								!alreadyParsedClasses.contains(bd.getBeanClassName())) {//<12>
							candidates.add(new BeanDefinitionHolder(bd, candidateName));
						}
					}
				}
				candidateNames = newCandidateNames;//<13>
			}
		}
		while (!candidates.isEmpty());
		……
	}
	……
}

  

至此,我們瞭解在執行ConfigurationClassPostProcessor.processConfigBeanDefinitions(...)的時候是如何完成掃描類路徑,筆者再介紹在上面的方法是如何完成配置類的校驗,如何執行配置類中暫存的ImportBeanDefinitionRegistrar例項引入新的BeanDefinition,就結束本章對ConfigurationClassPostProcessor的介紹。

首先我們來看配置類是如何完成校驗的,在執行ConfigurationClassParser.validate()方法的時候會遍歷已經獲取到的所有配置類ConfigurationClass.validate(...)方法,在配置類校驗的時候,會檢查如果配置類是full模式(有標記@Configuration註解且proxyBeanMethods為true),這個full模式的配置類是否有使用fina修飾符,如果是full模式的配置類,那麼類中是否有同時使用了@Bean註解和fina修飾符的方法?之所以做這樣的校驗:是因為一個full模式的配置類是要使用cglib技術代理的,如果配置類本身使用了final修飾符是無法被繼承的,則不能通過校驗。如果full模式的配置類有用@Bean註解標記的方法,方法本身也使用了final修飾符,這個方法是不能被重寫的,就不允許通過校驗。

但如果大家將@Configuration、@Bean和final修飾符搭配使用,會發現java會出現編譯錯誤,既然連編譯這關都過不了,為何還要校驗呢?spring這裡是防止編譯出來的位元組碼被篡改,有人特意在編譯後的full模式配置類位元組碼加上final修飾符,在在這個方法裡再次對配置類進行校驗。

class ConfigurationClassParser {
	……
	public void validate() {
		for (ConfigurationClass configClass : this.configurationClasses.keySet()) {
			configClass.validate(this.problemReporter);
		}
	}
	……
}

final class ConfigurationClass {
	……
	public void validate(ProblemReporter problemReporter) {
		// A configuration class may not be final (CGLIB limitation) unless it declares proxyBeanMethods=false
		Map<String, Object> attributes = this.metadata.getAnnotationAttributes(Configuration.class.getName());
		if (attributes != null && (Boolean) attributes.get("proxyBeanMethods")) {
			if (this.metadata.isFinal()) {//校驗full模式的配置類是否使用final修飾符
				problemReporter.error(new FinalConfigurationProblem());
			}
			for (BeanMethod beanMethod : this.beanMethods) {//校驗full模式配置類的方法
				beanMethod.validate(problemReporter);
			}
		}
	}
	……
}

final class BeanMethod extends ConfigurationMethod {
	……
	public void validate(ProblemReporter problemReporter) {
		if (getMetadata().isStatic()) {
			// static @Bean methods have no constraints to validate -> return immediately
			return;
		}

		if (this.configurationClass.getMetadata().isAnnotated(Configuration.class.getName())) {
			if (!getMetadata().isOverridable()) {//校驗方法是否允許被重寫
				// instance @Bean methods within @Configuration classes must be overridable to accommodate CGLIB
				problemReporter.error(new NonOverridableMethodError());
			}
		}
	}
	……
}

  

最後,我們來了解下ConfigurationClassBeanDefinitionReader.loadBeanDefinitions(...)是如何在解析完配置類後,又從配置類載入新的BeanDefinition到spring容器裡。這個方法會呼叫loadBeanDefinitionsForConfigurationClass(...)方法遍歷傳入的配置類,在程式碼<1>處會遍歷配置類中加了@Bean註解的方法,這裡會呼叫loadBeanDefinitionsForBeanMethod(...)將@Bean方法包裝成一個BeanDefinition註冊進spring容器,因為我們知道spring會根據@Bean方法例項化一個bean物件,而BeanDefinition是生產bean物件的原料。

在根據@Bean方法生成BeanDefinition時,會先在<2>處獲取方法名,然後在<3>處獲取bean物件的別名,並在<4>處建立beanName和別名的對映。之後會在<5>處根據配置類、後設資料和方法名建立一個ConfigurationClassBeanDefinition物件,建立bean物件的方法是否是靜態決定了控制流是進入<6>處還是<7>處的分支。如果是用靜態方法來建立bean物件,在<6>處就設定靜態方法對應的類以及方法名。如果是例項方法建立bean物件則進入<7>處的分支,在<7>處的分支會設定可以建立當前bean物件的工廠beanName,即配置類的beanName,再設定建立bean物件的方法名。

在<8>處會設定@Bean方法的BeanDefinition的預設自動裝配模型為AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR,但@Bean註解預設的自動裝配模型為Autowire.NO,在<9>處會獲取@Bean註解配置的自動裝配模型並存放到BeanDefinition。在<10>處會獲取@Bean註解設定的是否自動參與候選,預設為true。之後在<11>處獲取初始化方法,在<12>處獲取銷燬方法,如果這兩個欄位有設定的話。最後在<13>處將beanName和BeanDefinition的對映註冊進spring容器中。這裡我們又看到一個BeanDefinition的實現型別。

在<1>處的方法遍歷完所有的beanMethod將其BeanDefinition註冊進spring容器後,會分別在<14>和<15>處處理暫存在配置類的XML配置檔案和ImportBeanDefinitionRegistrar例項。在<15>處loadBeanDefinitionsFromRegistrars(...)方法的實現我們也看到在這個方法內會遍歷配置類中所有的ImportBeanDefinitionRegistrar例項,回撥其registerBeanDefinitions(...)方法,在這個方法中開發者可以向spring容器註冊新的BeanDefinition。

class ConfigurationClassBeanDefinitionReader {
	……
	public void loadBeanDefinitions(Set<ConfigurationClass> configurationModel) {
		TrackedConditionEvaluator trackedConditionEvaluator = new TrackedConditionEvaluator();
		for (ConfigurationClass configClass : configurationModel) {
			loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator);
		}
	}
	private void loadBeanDefinitionsForConfigurationClass(
			ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {
		……
		for (BeanMethod beanMethod : configClass.getBeanMethods()) {//<1>
			loadBeanDefinitionsForBeanMethod(beanMethod);
		}

		loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());//<14>
		loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());//<15>
	}
	……
	private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) {
		ConfigurationClass configClass = beanMethod.getConfigurationClass();
		MethodMetadata metadata = beanMethod.getMetadata();
		String methodName = metadata.getMethodName();//<2>
		……
		AnnotationAttributes bean = AnnotationConfigUtils.attributesFor(metadata, Bean.class);
		Assert.state(bean != null, "No @Bean annotation attributes");

		// Consider name and any aliases
		List<String> names = new ArrayList<>(Arrays.asList(bean.getStringArray("name")));//<3>
		String beanName = (!names.isEmpty() ? names.remove(0) : methodName);

		// Register aliases even when overridden
		for (String alias : names) {//<4>
			this.registry.registerAlias(beanName, alias);
		}

		……
		ConfigurationClassBeanDefinition beanDef = new ConfigurationClassBeanDefinition(configClass, metadata, beanName);//<5>
		beanDef.setSource(this.sourceExtractor.extractSource(metadata, configClass.getResource()));

		if (metadata.isStatic()) {//<6>
			// static @Bean method
			if (configClass.getMetadata() instanceof StandardAnnotationMetadata) {
				beanDef.setBeanClass(((StandardAnnotationMetadata) configClass.getMetadata()).getIntrospectedClass());
			}
			else {
				beanDef.setBeanClassName(configClass.getMetadata().getClassName());
			}
			beanDef.setUniqueFactoryMethodName(methodName);
		}
		else {//<7>
			// instance @Bean method
			beanDef.setFactoryBeanName(configClass.getBeanName());
			beanDef.setUniqueFactoryMethodName(methodName);
		}

		if (metadata instanceof StandardMethodMetadata) {
			beanDef.setResolvedFactoryMethod(((StandardMethodMetadata) metadata).getIntrospectedMethod());
		}

		beanDef.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR);//<8>
		beanDef.setAttribute(org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor.
				SKIP_REQUIRED_CHECK_ATTRIBUTE, Boolean.TRUE);

		AnnotationConfigUtils.processCommonDefinitionAnnotations(beanDef, metadata);

		Autowire autowire = bean.getEnum("autowire");//<9>
		if (autowire.isAutowire()) {
			beanDef.setAutowireMode(autowire.value());
		}

		boolean autowireCandidate = bean.getBoolean("autowireCandidate");//<10>
		if (!autowireCandidate) {
			beanDef.setAutowireCandidate(false);
		}

		String initMethodName = bean.getString("initMethod");//<11>
		if (StringUtils.hasText(initMethodName)) {
			beanDef.setInitMethodName(initMethodName);
		}

		String destroyMethodName = bean.getString("destroyMethod");//<12>
		beanDef.setDestroyMethodName(destroyMethodName);
		……
		BeanDefinition beanDefToRegister = beanDef;
		……
		this.registry.registerBeanDefinition(beanName, beanDefToRegister);//<13>
	}
	……
	private void loadBeanDefinitionsFromRegistrars(Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> registrars) {
		registrars.forEach((registrar, metadata) ->
				registrar.registerBeanDefinitions(metadata, this.registry, this.importBeanNameGenerator));
	}
	……
}

  

相關文章