Spring原始碼解析之ConfigurationClassPostProcessor(二)

北洛發表於2021-08-22

上一個章節,筆者向大家介紹了spring是如何來過濾配置類的,下面我們來看看在過濾出配置類後,spring是如何來解析配置類的。首先過濾出來的配置類會存放在configCandidates列表, 在程式碼<1>處會先根據配置類的權重做一個排序,權重越低的配置類排在越前,在解析的時候也越先解析。之後會根據configCandidates列表生成一個set集合candidates,防止configCandidates列表存在相同的元素。之後會在<3>處解析這些配置類,並在<4>處校驗解析出來的配置類。由於在解析配置類的時候,可能引入其他的配置類,所以spring這裡做了一個do-while迴圈,這個迴圈會一直持續到spring確定不會再有新的配置類引入時退出。

public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor,
		PriorityOrdered, ResourceLoaderAware, BeanClassLoaderAware, EnvironmentAware {
	……
	public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
		……
		// Sort by previously determined @Order value, if applicable
		configCandidates.sort((bd1, bd2) -> {//<1>
			int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
			int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
			return Integer.compare(i1, i2);
		});
		……
		// 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);//<2>
		Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
		do {
			parser.parse(candidates);//<3>
			parser.validate();//<4>
			……
		}
		while (!candidates.isEmpty());
		……
	}
	……	
}

  

那麼什麼情況下會出現掃描類的時候引入新的配置類呢?spring提供了@Import註解允許我們用三種不同的方式在解析配置類的時候引入新的配置類。@Import通常和@Configuration搭配使用,但也可以獨立存在。我們可以用@Import直接引入一個配置類,也可以實現ImportSelector或ImportBeanDefinitionRegistrar其中一個介面,在介面裡面返回或註冊配置類,同樣用@Import引入這兩個介面的實現類。

我們在MyConfig9這個配置類上@Import註解引入MyConfig10配置類,在測試用例中將MyConfig9傳給應用上下文,從執行結果可以看到即便MyConfig10並沒有直接傳給應用上下文,spring容器依舊會掃描出org.example.dao類路徑下的類並構造相應的bean物件。

//MyConfig9.java
@ComponentScan("org.example.service")
@Import(MyConfig10.class)
public class MyConfig9 {
}

//MyConfig10.java
@ComponentScan("org.example.dao")
public class MyConfig10 {
    public MyConfig10() {
        System.out.println("構造MyConfig10...");
    }
}

  

測試用例:

    @Test
    public void test13() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MyConfig9.class);
        System.out.println("userDao:" + ac.getBean(UserDao.class));
    }

  

執行結果:

構造MyConfig10...
userDao:org.example.dao.UserDao@707194ba

  

接下來我們看看如何通過ImportSelector實現類來引入一個配置類,MyConfig10ImportSelector實現了ImportSelector介面,這個介面會要求開發者返回一個配置類類名的列表,然後再傳給應用上下文的配置類上用@Import引入ImportSelector的實現類,從執行結果也可以看到spring容器將org.example.dao類路徑下的類掃描出來。

//MyConfig11.java
@ComponentScan("org.example.service")
@Import(MyConfig10ImportSelector.class)
public class MyConfig11 {
}

//MyConfig10ImportSelector.java
public class MyConfig10ImportSelector implements ImportSelector {
    public MyConfig10ImportSelector() {
        System.out.println("構造MyConfig10ImportSelector...");
    }

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{MyConfig10.class.getName()};
    }
}

    

測試用例:

    @Test
    public void test14() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MyConfig11.class);
        System.out.println("userDao:" + ac.getBean(UserDao.class));
    }

  

執行結果:

構造MyConfig10...
userDao:org.example.dao.UserDao@687e99d8

  

ImportBeanDefinitionRegistrar和ImportSelector有些類似,也是提供一個介面讓開發者提供配置類,只不過這個介面要求開發者必須對spring的一些實現設計有些瞭解,這個介面需要我們將配置類以BeanDefinition的形式註冊進spring容器,這種做法同樣可以讓spring將新引入的配置類指定的類路徑掃描出來。

//MyConfig12.java
@ComponentScan("org.example.service")
@Import(MyConfig10Registrar.class)
public class MyConfig12 {
}

//MyConfig10Registrar.java
public class MyConfig10Registrar implements ImportBeanDefinitionRegistrar {
    public MyConfig10Registrar() {
        System.out.println("構造MyConfig10Registrar...");
    }

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
        AnnotatedGenericBeanDefinition beanDefinition = new AnnotatedGenericBeanDefinition(MyConfig10.class);
        registry.registerBeanDefinition("myConfig10", beanDefinition);
    }
}

    

測試用例:

    @Test
    public void test15() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MyConfig12.class);
        System.out.println("userDao:" + ac.getBean(UserDao.class));
    }

  

執行結果:

構造MyConfig10...
userDao:org.example.dao.UserDao@45b4c3a9

 

我們總結下三種用@Import引入配置類的方式:如果確定了要引入的配置類,我們可以簡單地用@Import引入。如果在引入配置類的時候,需要加一些業務邏輯,可以實現ImportSelector或ImportBeanDefinitionRegistrar介面,然後用@Import將其實現類的類物件引入。這兩個介面的區別在筆者看來ImportSelector對開發者的要求是少一些,因為僅需要開發者提供待引入的配置類的類名,我們不需要去管spring是如何將配置類包裝成BeanDefinition。而ImportBeanDefinitionRegistrar介面對開發者的要求會更高一些,畢竟我們要自己將配置類包裝成BeanDefinition註冊進spring容器,但這個介面的靈活性也更高,我們可以從這個介面傳進來的BeanDefinitionRegistry物件獲取其他BeanDefinition,修改這個BeanDefinition的屬性從而改變這個BeanDefinition的行為,比如:我們獲取到一個BeanDefinition後再獲取其類物件,然後用cglib技術生成該類物件的子類,這個子類在呼叫父類的方法時可以加一個列印呼叫父類方法的耗時日誌。

那麼我們來看看在ConfigurationClassParser.parse(...)方法中是如何完成遞迴解析的,在parse(...)方法中會遍歷配置類的BeanDefinition,根據不同情況呼叫其他parse(...)的過載方法,從其他3個過載的parse(...)方法來看,最終都會呼叫到ConfigurationClassParser.processConfigurationClass(...)方法。

class ConfigurationClassParser {
	……

	public void parse(Set<BeanDefinitionHolder> configCandidates) {
		for (BeanDefinitionHolder holder : configCandidates) {
			BeanDefinition bd = holder.getBeanDefinition();
			try {
				if (bd instanceof AnnotatedBeanDefinition) {
					parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
				}
				else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
					parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
				}
				else {
					parse(bd.getBeanClassName(), holder.getBeanName());
				}
			}
			catch (BeanDefinitionStoreException ex) {
				throw ex;
			}
			catch (Throwable ex) {
				throw new BeanDefinitionStoreException(
						"Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
			}
		}

		this.deferredImportSelectorHandler.process();
	}

	protected final void parse(@Nullable String className, String beanName) throws IOException {
		Assert.notNull(className, "No bean class name for configuration class bean definition");
		MetadataReader reader = this.metadataReaderFactory.getMetadataReader(className);
		processConfigurationClass(new ConfigurationClass(reader, beanName), DEFAULT_EXCLUSION_FILTER);
	}

	protected final void parse(Class<?> clazz, String beanName) throws IOException {
		processConfigurationClass(new ConfigurationClass(clazz, beanName), DEFAULT_EXCLUSION_FILTER);
	}

	protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException {
		processConfigurationClass(new ConfigurationClass(metadata, beanName), DEFAULT_EXCLUSION_FILTER);
	}
	……
}

  

在processConfigurationClass(...)方法中我們又看到一個do-while迴圈,之所以存在這個迴圈,是淫威可能存在配置類繼承配置類的情況,比如我們宣告瞭一個MyConfig13的配置類,並繼承MyConfig9,我們將MyConfig13傳給應用上下文,在<1>處的程式碼處理完MyConfig13後,會判斷其父類是否有被處理的需要,如果有的話會進行第二次迴圈。在處理完配置類後,會在<2>處將配置類放進configurationClasses這個集合。

class ConfigurationClassParser {
	……
	private final Map<ConfigurationClass, ConfigurationClass> configurationClasses = new LinkedHashMap<>();
	……
	protected void processConfigurationClass(ConfigurationClass configClass, Predicate<String> filter) throws IOException {
		……
		// Recursively process the configuration class and its superclass hierarchy.
		SourceClass sourceClass = asSourceClass(configClass, filter);
		do {
			sourceClass = doProcessConfigurationClass(configClass, sourceClass, filter);//<1>
		}
		while (sourceClass != null);

		this.configurationClasses.put(configClass, configClass);//<2>
	}
	……
}

   

在執行ConfigurationClassParser.doProcessConfigurationClass(...)的時候,會先在<1>處獲取配置類的掃描路徑,還是以上面MyConfig13繼承MyConfig10為例,sourceClass初始的類物件為MyConfig13,所以這裡會先獲取到MyConfig13指向的類路徑,之後會在<2>處遍歷MyConfig3指向的類路徑,將類路徑下的類封裝成BeanDefinition,再將BeanDefinition及beanName封裝成BeanDefinitionHolder物件存到一個集合內並返回,即<3>處的集合。

當掃描並獲取到類路徑下的BeanDefinitionHolder,會遍歷這些物件執行<4>和<5>這兩個方法,相信這兩個方法大家一定不會覺得陌生,<4>處是判斷一個BeanDefinition是能可以成為配置類,可以的話是full模式還是lite模式,<5>處會再次呼叫ConfigurationClassParser.parse(...)解析配置類。看到這裡也許有些人心裡有些猜測,沒錯,這裡會遞迴解析,ConfigurationClassParser.parse(...)在根據配置類指定的類路徑掃描處類後,又會將嘗試將一個類當做配置類進行解析。假設配置類的類路徑為org.example.service,在這個類路徑下有一個用@Component註解標記的TestService類,那麼這個類就是一個lite模式的配置類,不管這個類有沒有用@ComponentScan或@ComponentScans註解指定類路徑,都會嘗試對TestService類進行解析,如果有配置類路徑,則再一次掃描新的類路徑下的類。需要注意一點的是:註解具有繼承性,@Repository、@Service、@Controller和@Configuration都繼承了@Component,也就是說只要類上標記了這幾個註解,spring都會認為這個類是配置類。

如果配置類有加@Import註解,則會在<6>處進行處理。如果配置類有加@@ImportResource註解,則會在<7>處將註解指定的檔名存到configClass物件內,待後續解析XML檔案。同樣類種有@Bean註解,在<8>處會解析出類裡帶@Bean註解的方法,存放到configClass物件內。

最後在<9>處判斷sourceClass的父類是否需要處理,之前筆者拿MyConfig13繼承MyConfig10的例子說過,在初始執行ConfigurationClassParser.doProcessConfigurationClass(...)的時候,configClass和sourceClass都是指向MyConfig13類物件,所以在<9>處會判斷MyConfig13有父類,之後獲取其父類MyConfig10的類名,在<10>處判斷這個類的並不是java.*包下的類,這一步主要是為了防止解析java.lang.Object,大部分開發者都知道Object在Java中是所有類的父類。只要一個類的父類不為null,按類名不是java.*包下的類,且父類沒有被處理過(即:不在knownSuperclasses集合),這裡會將其父類放到knownSuperclasses集合並返回父類,這裡會返回封裝了MyConfig10的sourceClass物件。ConfigurationClassParser.doProcessConfigurationClass(...) 判斷返回的sourceClass不為null,則會再次處理。如果判斷是java.*包下的類,則會返回空,ConfigurationClassParser.doProcessConfigurationClass(...) 在判斷返回的sourceClass為null就會退出迴圈。

class ConfigurationClassParser {
	……
	private final ComponentScanAnnotationParser componentScanParser;
	……
	protected final SourceClass doProcessConfigurationClass(
			ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
			throws IOException {
		……
		// Process any @ComponentScan annotations
		Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
				sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);//<1>
		if (!componentScans.isEmpty() &&
				!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
			for (AnnotationAttributes componentScan : componentScans) {//<2>
				// The config class is annotated with @ComponentScan -> perform the scan immediately
				Set<BeanDefinitionHolder> scannedBeanDefinitions =
						this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());//<3>
				// Check the set of scanned definitions for any further config classes and parse recursively if needed
				for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
					BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
					if (bdCand == null) {
						bdCand = holder.getBeanDefinition();
					}
					if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {//<4>
						parse(bdCand.getBeanClassName(), holder.getBeanName());//<5>
					}
				}
			}
		}

		// Process any @Import annotations
		processImports(configClass, sourceClass, getImports(sourceClass), filter, true);//<6>

		// Process any @ImportResource annotations
		AnnotationAttributes importResource =
				AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
		if (importResource != null) {//<7>
			String[] resources = importResource.getStringArray("locations");
			Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
			for (String resource : resources) {
				String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
				configClass.addImportedResource(resolvedResource, readerClass);
			}
		}

		// Process individual @Bean methods
		Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);//<8>
		for (MethodMetadata methodMetadata : beanMethods) {
			configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
		}

		// Process default methods on interfaces
		processInterfaces(configClass, sourceClass);

		// Process superclass, if any
		if (sourceClass.getMetadata().hasSuperClass()) {//<9>
			String superclass = sourceClass.getMetadata().getSuperClassName();
			if (superclass != null && !superclass.startsWith("java") &&//<10>
					!this.knownSuperclasses.containsKey(superclass)) {
				this.knownSuperclasses.put(superclass, configClass);
				// Superclass found, return its annotation metadata and recurse
				return sourceClass.getSuperClass();
			}
		}

		// No superclass -> processing is complete
		return null;
	}
	……
}

  

在上面程式碼的<3>處會完成掃描類路徑返回一個BeanDefinitionHolder物件列表,BeanDefinitionHolder物件包含BeanDefinition及其beanName,那麼我們來看看在<3>處是如何來完成掃描的。

在這個方法裡我們又看到ClassPathBeanDefinitionScanner這個類,不知道大家還有沒有印象,筆者曾經在Spring原始碼解析之BeanFactoryPostProcessor(二)這一章節中介紹這個類,在初始化註解應用上下文時(AnnotationConfigApplicationContext),註解上下文的預設構造方法會初始化一個ClassPathBeanDefinitionScanner物件,當時筆者說過這個物件的作用就是用來傳遞一個類路徑,根據類路徑掃描BeanDefinition,並且筆者在這個章節也說了,雖然應用上下文物件裡的ClassPathBeanDefinitionScanner物件可以根據類路徑掃描BeanDefinition,但我們在配置類上指定的類路徑並不是應用上下文內部的ClassPathBeanDefinitionScanner物件來掃描的,這裡我們也看到了將類路徑下的類掃描成BeanDefinition是在執行ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(...)方法時,執行到ComponentScanAnnotationParser.parse(...)這裡會初始化一個ClassPathBeanDefinitionScanner物件來解析掃描類路徑。

@ComponentScan註解允許我們設定一些掃描規則,比如:includeFilters、excludeFilters這兩個屬性可以規定哪些類可以掃描,哪些類不可以掃描,lazyInit可以決定類路徑下的類是否是懶載入的,如果我們希望某個類路徑下的類只有在需要的時候才去建立bean物件,並不需要將類路徑下的每個類都加上@Lazy註解,直接針對指定該類路徑@ComponentScan註解設定其lazyInit屬性為true即可。下面這段程式碼根據我們在@ComponentScan註解裡設定的屬性相應的修改scanner物件的屬性,比如<1>處增加將類解析為BeanDefinition的條件,<2>處增加判斷一個類不是BeanDefinition的條件,<3>處設定這個類路徑的類是否都是懶載入。最後會在<4>處呼叫scanner.doScan(...) 方法掃描出類路徑下可以成為BeanDefinition的類。scanner.doScan(...) 方法方法的實現在Spring原始碼解析之BeanFactoryPostProcessor(二)章節已經講過,這裡就不再贅述。

class ComponentScanAnnotationParser {
	……
	public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {
		ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,
				componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);
		……
		for (AnnotationAttributes filter : componentScan.getAnnotationArray("includeFilters")) {
			for (TypeFilter typeFilter : typeFiltersFor(filter)) {
				scanner.addIncludeFilter(typeFilter);//<1>
			}
		}
		for (AnnotationAttributes filter : componentScan.getAnnotationArray("excludeFilters")) {
			for (TypeFilter typeFilter : typeFiltersFor(filter)) {
				scanner.addExcludeFilter(typeFilter);//<2>
			}
		}

		boolean lazyInit = componentScan.getBoolean("lazyInit");
		if (lazyInit) {
			scanner.getBeanDefinitionDefaults().setLazyInit(true);//<3>
		}

		Set<String> basePackages = new LinkedHashSet<>();
		String[] basePackagesArray = componentScan.getStringArray("basePackages");
		for (String pkg : basePackagesArray) {
			String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg),
					ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
			Collections.addAll(basePackages, tokenized);
		}
		for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) {
			basePackages.add(ClassUtils.getPackageName(clazz));
		}
		if (basePackages.isEmpty()) {
			basePackages.add(ClassUtils.getPackageName(declaringClass));
		}

		scanner.addExcludeFilter(new AbstractTypeHierarchyTraversingFilter(false, false) {
			@Override
			protected boolean matchClassName(String className) {
				return declaringClass.equals(className);
			}
		});
		return scanner.doScan(StringUtils.toStringArray(basePackages));//<4>
	}
	……
}

  

相關文章