Conditional analysis

role

Indicates whether the Component is eligible to be added to the BeanFactory.

When a Conditional annotation is used, the Conditional annotation class is called to the Condition implementation class in the Conditional value. If these are true, the bean is added to the BeanFactory. If one of them is false, it will not be added.

Because of this special feature, you can use it to check your bean before registering it with the BeanFactory. Such as ConditionalOnClass…

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
	Class<? extends Condition>[] value();

}
Copy the code

use

  1. @Component used directly or indirectly. Like @Configuration. (The Configuration meta annotation is an @Component, but Conditional annotations will still work if you use it with Conditional annotations ona class)

  2. As a meta-annotation, you can use custom annotations. ConditionalOnClass in Springboot, for example.

    @Target({ ElementType.TYPE, ElementType.METHOD })
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Conditional(OnClassCondition.class)   // as a meta-annotation
    public @interfaceConditionalOnClass { Class<? >[] value()default {};
    	String[] name() default {};
    
    }
    
    Copy the code
  3. It can also be used for methods with @bean annotations.

    The following example ConditionalOnMissingBean and ConditionalOnClass is the same as the above said, Conditional annotations are their yuan notes, it is also telling.

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(MongoTemplate.class)
    @ConditionalOnBean(MongoTemplate.class)
    @ConditionalOnEnabledHealthIndicator("mongo")
    @AutoConfigureAfter({ MongoAutoConfiguration.class, MongoDataAutoConfiguration.class, MongoReactiveHealthContributorAutoConfiguration.class })
    public class MongoHealthContributorAutoConfiguration
    		extends CompositeHealthContributorConfiguration<MongoHealthIndicator.MongoTemplate> {
    
    	@Bean
    	@ConditionalOnMissingBean(name = { "mongoHealthIndicator", "mongoHealthContributor" })
    	public HealthContributor mongoHealthContributor(Map<String, MongoTemplate> mongoTemplates) {
    		returncreateContributor(mongoTemplates); }}Copy the code

Special circumstances:

  1. If an @Configuration class has an @Conditional annotation on it, all the @Bean-annotated methods, the @import annotation, and the @ComponentScan annotation in the Bean are associated to the @Conditional annotation. In other words, it is determined by @Conditional.

Condition interface Analysis

Condition interface Analysis

This interface is used for judgment. In the value of @Conditional is the class that implements Condition, which is instantiated and whose matches method is called.

In the matches method, you pass two parameters.

  1. ConditionContext context: the context object of the current Condition.

    The Condition interface has operations that operate directly on BeanFactory or Environment and, of course, ResourceLoader. So the ConditionContext interface defines these methods, and the ConditionContext implementation class holds these objects.

    public interface ConditionContext {
    	BeanDefinitionRegistry getRegistry(a);
    	@Nullable
    	ConfigurableListableBeanFactory getBeanFactory(a);
    	Environment getEnvironment(a);
    	ResourceLoader getResourceLoader(a);
    	@Nullable
    	ClassLoader getClassLoader(a);
    }
    Copy the code
  2. AnnotatedTypeMetadata metadata

    Class, and this information is wrapped by Spring, wrapped into this object.

A return value of true indicates a match, and the current bean can be added to the BeanFactory. A return value of false indicates that the current bean does not need to be added to the BeanFactory.

@FunctionalInterface
public interface Condition {
   boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
Copy the code

The class diagram

ConfigurationCondition Indicates interface analysis

Inheriting from the Condition interface, it has more detailed control over @Configuration than the Condition does.

Condition is processed at different stages of configuration. In short, it is called configuration parsing, which is divided into two stages. Different things can be done at different stages, and certain things can be allowed at different stages.

It is divided into the following two stages

  1. ConfigurationPhase.PARSE_CONFIGURATION

    Condition is evaluated when parsing the @Configuration annotation class. If there is no match, the @Configuration will not be added.

  2. ConfigurationPhase.REGISTER_BEAN

    The matching is not done until a rule bean is added.

public interface ConfigurationCondition extends Condition {
	ConfigurationPhase getConfigurationPhase(a);

	enum ConfigurationPhase {

		PARSE_CONFIGURATION,

		REGISTER_BEAN
	}

}
Copy the code

Where exactly are the PARSE_CONFIGURATION and REGISTER_BEAN phases in Spring?

Why are there two stages

Normally, parsing is loading, because once parsing is done, the next step is loaded directly, no problem. This is not the case with Spring’s Configuration.

For a configuration class, you can import a normal class or a configuration class, which is divided into two stages. After reading, you need to load.

  1. Read @import, @Bean, @ComponentScan, etc. annotations to add beans to the container.
  2. Load. The load here is to parse common annotations, such as @role. @scope, and those init and destroy methods on beans, and so on. These annotations do not add specific bean information to Spring.

Based on the above reasons, it is divided into two stages. Splitting into two nodes allows more granular control, such as the getConfigurationPhase return value REGISTER_BEAN for a configuration class. This bean will then be added when the Configuration is parsed. Its Mathch methods are not called, which means the results of the Match method are ignored at this stage.

For example, if a configuration class needs a class to exist, but that class is imported from another configuration class, then this scenario is suitable.

PARSE_CONFIGURATION phase

Start parsing the classes annotated by Configuration. This phase is mainly about parsing @import, @Bean, and @ComponentScan.

Mainly in the ConfigurationClassParser#parse method, the following sections focus on some of the key code. The actual code is too long, so I won’t write it here.

The following is just a simple code listing, because the logic of parsing is the same, along with the analysis of the REGISTER_BEAN phase below.

ConfigurationClassParser#processConfigurationClass
    ConditionEvaluator is called to calculate whether the current class is a Condition and the stage passed is PARSE_CONFIGURATION.
    if (thisShouldSkip (configClass.getMetadata(), configurationPhase.parse_configuration)) {return;
	}
Copy the code

REGISTER_BEAN phase

After parsing above, this phase will parse @Role, which does not add beans to Spring, build a BeanDefinition, and add it to the BeanFactory, where it will actually be added.

Mainly in ConfigurationClassBeanDefinitionReader# loadBeanDefinitions method, mainly analyzes some key code below. The actual code is too long, so I won’t write it here

ConfigurationClassParser#processConfigurationClass

 ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForConfigurationClass
    	private void loadBeanDefinitionsForConfigurationClass( ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {
        // If an object is imported, the return value is determined by the imported class
     // Conditional judgment is also called here
		if (trackedConditionEvaluator.shouldSkip(configClass)) {
			String beanName = configClass.getBeanName();
			if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
				this.registry.removeBeanDefinition(beanName);
			}
			this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());
			return;
		}

		if (configClass.isImported()) {
			registerBeanDefinitionForImportedConfigurationClass(configClass);
		}
		for (BeanMethod beanMethod : configClass.getBeanMethods()) {
			loadBeanDefinitionsForBeanMethod(beanMethod);
		}

		loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());
		loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
	}
Copy the code

ConditionEvaluator analysis

Note that it is not public; it is intended for internal use for processing Conditional annotations. ConditionContextImpl is its static inner class and requires a context object to use it. This context is primarily used to encapsulate common objects, passed to the Condition’s matches method.

The main method is shouldSkip, which needs to pass two arguments:

  1. AnnotatedTypeMetadata Metadata: Annotated meta-information for the current class.
  2. ConfigurationPhase phase: the previous ConfigurationPhase.

Let’s focus on the shouldSkip method

The return value of the method:

True: Indicates that the current bean does not need to be added.

False: The current bean needs to be added.

There are mainly the following steps:

  1. This method determines whether the current class has Conditional annotations.

  2. If ConfigurationPhase is not passed, there is a phase enhancement process, null->PARSE_CONFIGURATION->REGISTER_BEAN;

  3. The value of values in the Conditional annotation is instantiated first (these implement the Condition interface).

  4. Sorting.

  5. Loop calls. The important thing is to look at this method

    If the Condition is ConfigurationCondition, we get ConfigurationPhase. Note that the following comparison is consistent with the ConfigurationPhase (the phase to be activated is compared from the method’s input parameter to the ConfigurationCondition return value). .

    for (Condition condition : conditions) {
    			ConfigurationPhase requiredPhase = null;
    			if (condition instanceof ConfigurationCondition) {
    				requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
    			}
        
    			if ((requiredPhase == null|| requiredPhase == phase) && ! condition.matches(this.context, metadata)) {
    				return true; }}Copy the code
class ConditionEvaluator {
	private final ConditionContextImpl context;
	
	public boolean shouldSkip(AnnotatedTypeMetadata metadata) {
		return shouldSkip(metadata, null);
	}
 
	public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) {
		if (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);
			}
			return shouldSkip(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);
			}
		}

		AnnotationAwareOrderComparator.sort(conditions);

		for (Condition condition : conditions) {
			ConfigurationPhase requiredPhase = null;
			if (condition instanceof ConfigurationCondition) {
				requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
			}
			if ((requiredPhase == null|| requiredPhase == phase) && ! condition.matches(this.context, metadata)) {
				return true; }}return false;
	}


	private static class ConditionContextImpl implements ConditionContext {

		@Nullable
		private final BeanDefinitionRegistry registry;

		@Nullable
		private final ConfigurableListableBeanFactory beanFactory;

		private final Environment environment;

		private final ResourceLoader resourceLoader;

		@Nullable
		private finalClassLoader classLoader; }}Copy the code

TrackedConditionEvaluator analysis

TrackedConditionEvaluator is ConfigurationClassBeanDefinitionReader inner classes, main is to use the decorator design pattern. A few more operations were added before the normal conditionEvaluator call.

The handling of Import has been added to cascade @Conditionality above Configuration annotations, that is, Conditional annotations ona Configuration class are associated with beans imported from that Configuration class.

How does that work?

  1. First of all, this is a recursive operation. The essence of this method is to determine whether the current bean should not be added to the BeanFactory. (Remember this is the essence).
  2. Let’s say A is imported by B. Now A is calling this method.
    1. So let’s see if it’s in the cache, and then let’s see if A is imported, and then when we get A who imported it.
    2. So let’s go through the set, so this is B.
    3. B starts the way up here. Here we go again. But it turns out that B wasn’t imported. And then B is going to continue to go down
    4. B conditionEvaluator has arrived. ShouldSkip (). Suppose B now returns true. B does not need to be added to the BeanFactory. B will also be in cache
    5. So we’re back to 2. 2. To be skipped over or skipped. 3. To be deflected or skipped is still the default value true. You don’t go to matches and skip it. Put it in cache. Go straight back to skip.
  3. Through the analysis of the above steps, A will not be added because B does not meet the requirements.
private class TrackedConditionEvaluator {
       // This is a cache, key is the class, and value indicates whether the current beA should be skipped (i.e. not added to the beanFactory).
		private final Map<ConfigurationClass, Boolean> skipped = new HashMap<>();

		public boolean shouldSkip(ConfigurationClass configClass) {
            // Get it from the cache.
			Boolean skip = this.skipped.get(configClass);
			if (skip == null) {
                 // The importedBy attribute for ConfigurationClass holds the class importing the current bean.
				if (configClass.isImported()) {
                    
					boolean allSkipped = true;
					for (ConfigurationClass importedBy : configClass.getImportedBy()) {
                        // importedBy.
						if(! shouldSkip(importedBy)) { allSkipped =false;
							break; }}if (allSkipped) {
						To be deflected or skipped about only in the presence of a shouldSkip
						skip = true; }}if (skip == null) {
                    // This calls the above judgment
					skip = conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN);
				}
                / / slow
				this.skipped.put(configClass, skip);
			}
			returnskip; }}Copy the code

use

Take a simple example from the above analysis. Simulate the function of @conditiononBean. Except, this is beanName

  1. Custom annotations

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Conditional(SimpleCondition.class)
    @Order(Ordered.LOWEST_PRECEDENCE)
    public @interface LcConditionalOnBeanName {
    	String beanName(a);
    }
    Copy the code
  2. Two simple entity classes

    public class A  {
    	private String name;
    	private Integer age;
    
    
    
    	public A(String name, Integer age) {
    		this.name = name;
    		this.age = age;
    	}
    
    	@Override
    	public String toString(a) {
    		return "A{" +
    				"name='" + name + '\' ' +
    				", age=" + age +
    				'} '; }}public class B {
    	private String name;
    	private Integer age;
    
    
    	public B(String name, Integer age) {
    		this.name = name;
    		this.age = age;
    	}
    
    
    	@Override
    	public String toString(a) {
    		return "A{" +
    				"name='" + name + '\' ' +
    				", age=" + age +
    				'} '; }}Copy the code
  3. Two interacting configuration classes

    @Configuration(proxyBeanMethods = false)
    @LcConditionalOnBeanName(beanName = "b")
    public class SimpleConfig {
    	@Bean
    	public A a(a){
    		return new A("a".12); }}@Configuration(proxyBeanMethods = false)
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public class SimpleConfig2 {
    
    	@Bean
    	public B b (a){
    		return new B("a".12); }}Copy the code
  4. ConfigurationCondition implementation class

    public class SimpleCondition implements ConfigurationCondition {
    	@Override
    	public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    		Map<String, Object> annotationAttributes = metadata.getAnnotationAttributes(LcConditionalOnBeanName.class.getName());
    		if (Objects.isNull(annotationAttributes)) {
    			return false;
    		}
    
    		String beanName = (String) annotationAttributes.get("beanName");
    		Assert.notNull(beanName, "beanName not null");
    		boolean contains = Arrays.asList(context.getRegistry().getBeanDefinitionNames()).contains(beanName);
    		return contains;
    	}
    
    	@Override
    	public ConfigurationPhase getConfigurationPhase(a) {
    		returnConfigurationPhase.REGISTER_BEAN; }}Copy the code
  5. The main start class

    	public static void test(a) {
    		try {
    			AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SimpleConfig.class, SimpleConfig2.class);
    			A bean = context.getBean(A.class);
    			printLog(bean);
    			context.close();
    		} catch(Throwable e) { e.printStackTrace(); }}public static void printLog(Object s) {
    		SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    		System.out.println(Thread.currentThread().getName() + "-" + simpleDateFormat.format(new Date()) + ":" + s);
    	}
    Copy the code
  6. The results of

Generally, a bean acquires bean information

instructions

In the example above, there are two configuration classes,

SimpleConfig has a custom annotation on it that says that the current find bean must have a bean named B in order for it to work.

SimpleConfig2 injects a bean of type B with the name “b”.

SimpleCondition instructions

Get the LcConditionalOnBeanName annotation on the bean, get the value, and get the bean from the beanFactory.

LcConditionalOnBeanName instructions

The @conditional and @Order annotations are its meta-annotation information.

The problem?

  1. Why do I need the @Order annotation

    This is a clever way to do it, because with the order annotation to control the loading order of the Configuration class, I need to make sure that the B in SimpleConfig is loaded into the BeanFactory before parsing and loading SimpleConfig.

  2. Is the Ordered interface allowed or not?

    Can’t.

    Specific code logic in ConfigurationClassUtils# checkConfigurationClassCandidate inside. This will contain the following code

    Gets the Order annotation annotated on the Bean and gets the value. Will this value in the attribute (ORDER_ATTRIBUTE = Conventions. GetQualifiedAttributeName (ConfigurationClassPostProcessor. Class, “order”);)

    public static Integer getOrder(AnnotationMetadata metadata) {
    		Map<String, Object> orderAttributes = metadata.getAnnotationAttributes(Order.class.getName());
    		return(orderAttributes ! =null ? ((Integer) orderAttributes.get(AnnotationUtils.VALUE)) : null);
    	}
    Copy the code

    The Configuration is sorted first. If the Configuration is not sorted, the default value is minimum. So, this is why the two configuration classes are annotated.

    	configCandidates.sort((bd1, bd2) -> {
    			int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
    			int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
    			return Integer.compare(i1, i2);
    		});
    Copy the code