From: juejin. Cn/post / 684490…

Conditional annotations are a bean-loading feature that Spring4 provides to control configuration classes and bean initialization conditions. Conditional annotations are used everywhere in the underlying source code of springBoot, springCloud, a series of frameworks.

Quite a few people have run into @conditionAlonBean annotations that don’t work because the bean they depend on has already been configured. Is @conditionalonBean related to the order in which the bean is loaded? Follow the source code to find out.

Problem demonstration:

@Configuration
public class Configuration1 {

    @Bean
    @ConditionalOnBean(Bean2.class)
    public Bean1 bean1() {
        returnnew Bean1(); }} Copy the codeCopy the code
@Configuration
public class Configuration2 {

    @Bean
    public Bean2 bean2() {returnnew Bean2(); }} Copy the codeCopy the code

Result: @conditionalonbean (bean2.class) returns false. The name defines bean2, but bean1 is not loaded.

Source code analysis

First of all, it should be clear that conditional annotation parsing must occur in the Bean Definition phase of Spring IOC, because spring bean initialization is preceded by a corresponding bean definition, Conditional annotations control whether a bean can be instantiated by determining its definition.

Source debug the above example.

From the bean definition of parsing entrance: ConfigurationClassPostProcessor

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
        int registryId = System.identityHashCode(registry);
        if (this.registriesPostProcessed.contains(registryId)) {
            throw new IllegalStateException(
                    "postProcessBeanDefinitionRegistry already called on this post-processor against " + registry);
        }
        if (this.factoriesPostProcessed.contains(registryId)) {
            throw new IllegalStateException(
                    "postProcessBeanFactory already called on this post-processor against "+ registry); } this.registriesPostProcessed.add(registryId); / / parsing bean definition entry processConfigBeanDefinitions (registry); } Duplicate codeCopy the code

Follow up processConfigBeanDefinitions method:

Public void processConfigBeanDefinitions (BeanDefinitionRegistry registry) {/ / omit unnecessary code... Parser. parse(candidates); // Run the following command to parse the candidate bean: parser.validate(); / / configuration class deposit collection Set < ConfigurationClass > configClasses = new LinkedHashSet < > (parser. GetConfigurationClasses ()); configClasses.removeAll(alreadyParsed); // Read the model and create bean definitions based on its contentif(this.reader == null) { this.reader = new ConfigurationClassBeanDefinitionReader( registry, this.sourceExtractor, this.resourceLoader, this.environment, this.importBeanNameGenerator, parser.getImportRegistry()); } / / parsing configuration class, that is, annotations parsing the conditions of entry. This reader. LoadBeanDefinitions (configClasses); alreadyParsed.addAll(configClasses); / /... } Duplicate codeCopy the code

Follow the conditional annotation parsing entry loadBeanDefinitions and start cycling through configuration classes. Here are all the custom configuration classes and autowiring configuration classes as follows:

In parsing methods loadBeanDefinitionsForConfigurationClass (), get the configuration class defined in the bean all the methods, and call the loadBeanDefinitionsForBeanMethod cycle () method to parse, The following validation methods are performed during parsing, which is the entry point for conditional annotations:

Public Boolean shouldSkip(@nullable AnnotatedTypeMetadata Metadata, @nullable ConfigurationPhase Phase) { Otherwise it just returnsif(metadata == null || ! metadata.isAnnotated(Conditional.class.getName())) {return false;
        }

        if (phase == null) {
            if (metadata instanceof AnnotationMetadata &&
                    ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) {
                return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION);
            }
            returnshouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN); List<Condition> conditions = new ArrayList<>();for (String[] conditionClasses : getConditionClasses(metadata)) {
            for(String conditionClass : conditionClasses) { Condition condition = getCondition(conditionClass, this.context.getClassLoader()); conditions.add(condition); }} / / sorted according to the Order AnnotationAwareOrderComparator. Sort (the conditions); // Iterate through the conditional annotations to start the process of executing the conditional annotationsfor (Condition condition : conditions) {
            ConfigurationPhase requiredPhase = null;
            if(condition instanceof ConfigurationCondition) { requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase(); } // The conditional annotation's condition. Matches method is used to match, returning a Boolean valueif((requiredPhase == null || requiredPhase == phase) && ! condition.matches(this.context, metadata)) {return true; }}return false; } Duplicate codeCopy the code

Follow up the matching method of conditional annotations:

Here we begin parsing the sample code
bean1Configuration:

   @Bean
    @ConditionalOnBean(Bean2.class)
    public Bean1 bean1() {
        returnnew Bean1(); } Duplicate codeCopy the code

In the getMatchOutcome method above, the parameter metadata is the target bean to parse, which is bean1. The bean on which the conditional annotation depends is encapsulated as a BeanSearchSpec, which, as the name indicates, is the object to look for. This is a static inner class constructed as follows:

BeanSearchSpec(ConditionContext context, AnnotatedTypeMetadata metadata, Class<? > annotationType) { this.annotationType = annotationType; // Read metadata set value MultiValueMap<String, Object> attributes = metadata .getAllAnnotationAttributes(annotationType.getName(),true); Collect (Attributes,"name", this.names);
			collect(attributes, "value", this.types);
			collect(attributes, "type", this.types);
			collect(attributes, "annotation", this.annotations);
			collect(attributes, "ignored", this.ignoredTypes);
			collect(attributes, "ignoredType", this.ignoredTypes);
			this.strategy = (SearchStrategy) metadata
					.getAnnotationAttributes(annotationType.getName()).get("search");
			BeanTypeDeductionException deductionException = null;
			try {
				if(this.types.isEmpty() && this.names.isEmpty()) { addDeducedBeanType(context, metadata, this.types); } } catch (BeanTypeDeductionException ex) { deductionException = ex; } validate(deductionException); } Duplicate codeCopy the code

Follow up with the method of searching beans:

MatchResult matchResult = getMatchingBeans(context, spec); Copy the codeCopy the code
private MatchResult getMatchingBeans(ConditionContext context, BeanSearchSpec beans) {
		ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
		if (beans.getStrategy() == SearchStrategy.ANCESTORS) {
			BeanFactory parent = beanFactory.getParentBeanFactory();
			Assert.isInstanceOf(ConfigurableListableBeanFactory.class, parent,
					"Unable to use SearchStrategy.PARENTS"); beanFactory = (ConfigurableListableBeanFactory) parent; } MatchResult matchResult = new MatchResult(); boolean considerHierarchy = beans.getStrategy() ! = SearchStrategy.CURRENT; List<String> beansIgnoredByType = getNamesOfBeansIgnoredByType( beans.getIgnoredTypes(), beanFactory, context, considerHierarchy); // Since the type is set in the example code, the type is iterated, according totypeGets whether the target bean existsfor (String type : beans.getTypes()) {
			Collection<String> typeMatches = getBeanNamesForType(beanFactory, type,
					context.getClassLoader(), considerHierarchy);
			typeMatches.removeAll(beansIgnoredByType);
			if (typeMatches.isEmpty()) {
				matchResult.recordUnmatchedType(type);
			}
			else {
				matchResult.recordMatchedType(type.typeMatches); }} // Follow the annotationsfor (String annotation : beans.getAnnotations()) {
			List<String> annotationMatches = Arrays
					.asList(getBeanNamesForAnnotation(beanFactory, annotation,
							context.getClassLoader(), considerHierarchy));
			annotationMatches.removeAll(beansIgnoredByType);
			if (annotationMatches.isEmpty()) {
				matchResult.recordUnmatchedAnnotation(annotation);
			}
			else{ matchResult.recordMatchedAnnotation(annotation, annotationMatches); }} // Search according to the set namefor (String beanName : beans.getNames()) {
			if(! beansIgnoredByType.contains(beanName) && containsBean(beanFactory, beanName, considerHierarchy)) { matchResult.recordMatchedName(beanName); }else{ matchResult.recordUnmatchedName(beanName); }}returnmatchResult; } Duplicate codeCopy the code

The getBeanNamesForType() method eventually delegates to the getNamesForType method of the BeanTypeRegistry class to get the corresponding bean name of the specified type:

Set<String> getNamesForType(Class<? >type) {// Synchronize the bean updateTypesIfNecessary() in the Spring container; // Returns a bean of the specified typereturnthis.beanTypes.entrySet().stream() .filter((entry) -> entry.getValue() ! = null && type.isAssignableFrom(entry.getValue())) .map(Map.Entry::getKey) .collect(Collectors.toCollection(LinkedHashSet::new)); } Duplicate codeCopy the code

Here’s the point. The first step in the above method is to synchronize the beans, which fetch all beanDifinition in the Spring container at that time. Only then does the conditional annotation make sense.

UpdateTypesIfNecessary () :

	private void updateTypesIfNecessary() {// lastBeanDefinitionCount means the number of synchronized views, if different from the number in the container. // Otherwise, get the beanFactory iterator and start synchronization.if(this.lastBeanDefinitionCount ! = this.beanFactory.getBeanDefinitionCount()) { Iterator<String> names = this.beanFactory.getBeanNamesIterator();while (names.hasNext()) {
				String name = names.next();
				if(! this.beanTypes.containsKey(name)) { addBeanType(name); }} // After the synchronization, update the number of beanDefinitions that have been synchronized. this.lastBeanDefinitionCount = this.beanFactory.getBeanDefinitionCount(); }} Copy the codeCopy the code

So we’re just one step away from the answer, which BeanDefinitions are iterated over from the beanFactory?

Follow up the beanFactory. GetBeanNamesIterator (); Methods:

@Override
	public Iterator<String> getBeanNamesIterator() {
		CompositeIterator<String> iterator = new CompositeIterator<>();
		iterator.add(this.beanDefinitionNames.iterator());
		iterator.add(this.manualSingletonNames.iterator());
		returniterator; } Duplicate codeCopy the code

Look at them separately:

  • BeanDefinitionNames are beans that are stored for automatic resolution and assembly, our startup class, configuration class, Controller, Service, and so on.

  • ManualSingletonNames, as you can see from the name, manualSingletonNames. What does that mean? During the spring IOC process, registration of some beans is triggered manually. In springboot startup process, for example, will show the registration of some configuration beans, such as: springBootBanner, systemEnvironment, systemProperties, etc.

Let’s examine why the above example bean1 was not instantiated.

inspring iocIn the process of parsing@Component, @Service, @ControllerAnnotated classes. Next, the configuration class is parsed, i.e@ConfigurationAnnotated classes. Finally start parsing what is defined in the configuration classbean. In the sample codebean1Is defined in the configuration class. When performing configuration class resolution,@Component,@ Service,@ Controller,@ConfigurationThe annotated classes have all been resolved, so theseBeanDifinitionIt has been synchronized. butbean1Conditional annotations depend onbean2.bean2Is defined in the configuration class because of twoBeanIt’s all in the configuration classBean, so the sequence of configuration class parsing cannot be guaranteed, and it will not take effect.

Same thing if you depend on thetaFeignClient, it is also possible that the situation will not take effect. becauseFeignClientThe configuration class is ultimately triggered, and the order of parsing is not guaranteed.

To solve

There are two ways:

  • Most of the classes that conditional annotations depend on in your project are handed over to the Spring container, so @conditionalonClass (bean2.class) can be used instead if you want to make the configuration Bean dependent on the configured Bean via @conditionalonBean.
  • If you must distinguish between the two configuration classes in order, you can hand them over to EnableAutoConfiguration for management and triggering. Factories (@autoConfigureBefore, AutoConfigureAfter, AutoConfigureOrder). Because these three annotations only apply to auto-configuration classes.

conclusion

Defined in the configuration classBean, if using@ConditionalOnBeanAlso depends on the configuration classBean, the execution result is uncontrollable, and is related to the class loading sequence.