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
bean1
Configuration:
@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 ioc
In the process of parsing@Component, @Service, @Controller
Annotated classes. Next, the configuration class is parsed, i.e@Configuration
Annotated classes. Finally start parsing what is defined in the configuration classbean
. In the sample codebean1
Is defined in the configuration class. When performing configuration class resolution,@Component,@ Service,@ Controller,@Configuration
The annotated classes have all been resolved, so theseBeanDifinition
It has been synchronized. butbean1
Conditional annotations depend onbean2
.bean2
Is defined in the configuration class because of twoBean
It’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. becauseFeignClient
The 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@ConditionalOnBean
Also depends on the configuration classBean
, the execution result is uncontrollable, and is related to the class loading sequence.