Climbing Spring Pearl mulama peak: @ Import, ImportAware use and principle analysis

@Import resolution

@Import provides a different import method from @ Bean. This article will introduce the annotation provided by Spring from the usage and principle.

Usage 1: @ Import + common class

@Configuration
@Import(Teacher.class)
public class ImportConfig {}
public class Teacher {}

@Import introduces a common class that can be added to Spring's singleton pool.

Usage 2: @ Import + implements the class of ImportSelector

@Configuration
@Import(ComputerSelector.class)
public class ImportSelectorConfig {}
public class ComputerSelector implements ImportSelector {
   @Override
   public String[] selectImports(AnnotationMetadata importingClassMetadata) {
      return new String[]{"com.slc.imp.selector.Computer"};
   }

   @Override
   public Predicate<String> getExclusionFilter() {
      return DeferredImportSelector.super.getExclusionFilter();
   }
}
package com.slc.imp.selector;
public class Computer {}

@The imported class implements ImportSelector. You can specify the full class name of the class to be imported through selectImports, and then hand over the class to Spring for management. And you can specify the parts to be excluded through getExclusionFilter.

Usage 3: @ Import + implements the class of ImportBeanDefinitionRegistrar

@Configuration
@Import(MyImportBeanDefinitionRegistrar.class)
public class ImportRegistrarConfig {}
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

	@Override
	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
		AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(School.class).getBeanDefinition();
		String beanName = importBeanNameGenerator.generateBeanName(beanDefinition, registry);
		registry.registerBeanDefinition(beanName,beanDefinition);

	}

	@Override
	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
	}
}
public class School {}

@The class imported by Import implements ImportBeanDefinitionRegistrar, which provides two rewritable methods. Spring's BeanDefinitionRegistry can be obtained in the two methods. Through this class, you can manually inject BeanDefinition, and then give it to spring to initialize BeanDefinition, and then add the classes to be injected to the spring container.

There is little difference between the two methods. The difference is that you can use BeanNameGenerator to generate beanName, or you can specify the corresponding name of the class to be injected. The ByName method can be used when obtaining.

Usage 4: @ Import and ImportAware

First define an annotation and use @ Import annotation

@Retention(RetentionPolicy.RUNTIME)@Import(SpecialConfigurationAware.class)public @interface EnableAnnotation {   String value() default "";}

Write the class and implement the ImportAware interface

public class SpecialConfigurationAware implements ImportAware {   private String value;   @Override   public void setImportMetadata(AnnotationMetadata importMetadata) {      Map<String, Object> annotationAttributes = importMetadata.getAnnotationAttributes(EnableAnnotation.class.getName());      value= annotationAttributes.get("value");      System.out.println();   }   @Bean   public Foo foo() {      return new Foo(this.value);   }}

Introduce custom annotations in any configuration class

@Configuration@EnableAnnotation("appconfig1111")public class AwareConfiguration {}

This method can get all the annotation information on the custom annotation annotation class. In this case, you can get all the annotation information on the AwareConfiguration class.

For example, the class AsyncConfigurationSelector imported by @ EnableAsync obtains the meta information of the corresponding annotation.

Analytical principle

The Spring loading process can be roughly divided into two parts: parsing all kinds of Bean information and converting it into the corresponding BeanDefinition, loading BeanDefinition, completing initialization and adding it to the Spring singleton pool.

To start Spring with annotations, the parsing process is to scan the annotations under the package, obtain the corresponding classes and package them into BeanDefinition. This paper mainly analyzes the "parsing" process.

The first is scanning. Spring completes the scanning process in refresh#invokebeanfactoryprocessors.

invokeBeanFactoryPostProcessors

protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {    PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());        if (beanFactory.getTempClassLoader() == null && beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {        beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));        beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));    }}

Delegate to PostProcessorRegistrationDelegate to complete the call to beanfactoryprocessors.

The complete code is not posted much. Here's an important part of the code.

public static void invokeBeanFactoryPostProcessors(    ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> beanFactoryPostProcessors) {	...	...    List<BeanDefinitionRegistryPostProcessor> currentRegistryProcessors = new ArrayList<>();    // 1. First, call BeanDefinitionRegistryPostProcessors that implement priorityordered. String[] postProcessorNames =        beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);    for (String ppName : postProcessorNames) {        if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class))  {/ / get the class defined by the beandefinitionregistrypostprocessor interface / / the scan class defined in Spring has priority over beanfactoryprocessor. It is a subclass of beanfactoryprocessor currentregistryprocessors.add (beanfactory. GetBean (ppname, beandefinitionregistrypostprocessor. Class)); processedbeans.add (ppName);        }    }     sortPostProcessors(currentRegistryProcessors, beanFactory);     registryProcessors. addAll(currentRegistryProcessors);    // Call the processor to complete the scanning of invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry); 	...	...       String[] postProcessorNames =beanFactory. getBeanNamesForType(BeanFactoryPostProcessor.class, true, false);     List<BeanFactoryPostProcessor> priorityOrderedPostProcessors = new ArrayList<>(); 		 List<String> orderedPostProcessorNames = new ArrayList<>(); 		 List<String> nonOrderedPostProcessorNames = new ArrayList<>(); 		 for (String ppName : postProcessorNames) { 			 if (processedBeans.contains(ppName)) { 				/**				 *  skip - already processed in first phase above 				 *  It has been called first, such as ConfigurationClassPostProcessor, which implements the beandefinitionregistrypostprocessor interface 				 *  Beandefinitionregistrypostprocessor is a subclass of beanfactoryprocessor, so the name of this class will still be obtained in this collection, 				 *  So skip here (no processing) 				 */			}  else if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) { 				 priorityOrderedPostProcessors.add(beanFactory.getBean(ppName, BeanFactoryPostProcessor.class)); 			}  else if (beanFactory.isTypeMatch(ppName, Ordered.class)) { 				 orderedPostProcessorNames.add(ppName); 			}  else { 				 nonOrderedPostProcessorNames.add(ppName); 			}		}     List<BeanFactoryPostProcessor> nonOrderedPostProcessors = new ArrayList<>(nonOrderedPostProcessorNames.size());     for (String postProcessorName : nonOrderedPostProcessorNames) {        nonOrderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));    }     invokeBeanFactoryPostProcessors(nonOrderedPostProcessors, beanFactory);    ... 	...}
  • Call the processors of BeanDefinitionRegistryPostProcessor and beanfactoryprocessor interfaces respectively according to priority.
  • The former is a subclass of the latter. Both interfaces are factory level processors. The class implementing BeanDefinitionRegistryPostProcessor will be called first,

It's not hard to tell from the name. The original intention of spring is to call the processor responsible for registering BeanDefinition first, and then call other factory processors. Spring also notes in the source code that if it needs to be extended, it is recommended to implement beanfactory postprocessor instead of BeanDefinitionRegistryPostProcessor.

	private static void invokeBeanDefinitionRegistryPostProcessors(			Collection<? extends BeanDefinitionRegistryPostProcessor> postProcessors, BeanDefinitionRegistry registry) {		for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) {			postProcessor.postProcessBeanDefinitionRegistry(registry);		}	}

Let's look at the code of the scanning part. It was said in the previous startup process of Spring that Spring will register five to six internal beans at the earliest, and one of them is used here. The first BeanFactory processor to be used throughout the scanning phase is the ConfigurationClassPostProcessor

postProcessBeanDefinitionRegistry

public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor,      PriorityOrdered, ResourceLoaderAware, BeanClassLoaderAware, EnvironmentAware {......      @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);		processConfigBeanDefinitions(registry);	}......    @Override    public int getOrder() {   		 return Ordered.LOWEST_PRECEDENCE;      }}

Both BeanDefinitionRegistryPostProcessor and PriorityOrdered are implemented, and the priority value is ordered LOWEST_ Precidenc is integer MAX_ Value is to ensure that this is the first one to be called. It is conceivable that it is important. Inside the processor, Spring completes the parsing of the configuration class. Continue with the key parts of the code.

processConfigBeanDefinitions

public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {   List<BeanDefinitionHolder> configCandidates = new ArrayList<>();		String[] candidateNames = registry.getBeanDefinitionNames();		for (String beanName : candidateNames) {			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);				}			}			// Check whether it is a configuration class, 			//  If @ configuration is added, the corresponding beandefinition is full; 			//  If @ Bean,@Component,@ComponentScan,@Import,@ImportResource are added, it is lite. 			 else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) { 				 configCandidates.add(new BeanDefinitionHolder(beanDef, beanName)); 			}		}		//  Return immediately if no @Configuration classes were found 		 if (configCandidates.isEmpty()) { 			 return; 		}		//  Sort by previously determined @Order value, if applicable 		 configCandidates. sort((bd1, bd2) -> { 			 int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition()); 			 int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition()); 			 return Integer.compare(i1, i2); 		});		//  Detect any custom bean name generation strategy supplied through the enclosing application context 		 SingletonBeanRegistry sbr = null; 		 if (registry instanceof SingletonBeanRegistry) { 			 sbr = (SingletonBeanRegistry) registry; 			 if (!this.localBeanNameGeneratorSet) { 				 BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton( 						 AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR); 				 if (generator != null) { 					 this.componentScanBeanNameGenerator = generator; 					 this.importBeanNameGenerator = generator; 				}			}		}		 if (this.environment == null) { 			 this.environment = new StandardEnvironment(); 		}		//  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); 		 Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size()); 		 do { 			 parser.parse(candidates); 			 parser.validate(); 			 Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses()); 			 configClasses.removeAll(alreadyParsed); 			//  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); 			 alreadyParsed.addAll(configClasses); 			 candidates.clear(); 			 if (registry.getBeanDefinitionCount() > candidateNames. length) { 				 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) { 					 if (!oldCandidateNames.contains(candidateName)) { 						 BeanDefinition bd = registry.getBeanDefinition(candidateName); 						 if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) && 								! alreadyParsedClasses.contains(bd.getBeanClassName())) { 							 candidates.add(new BeanDefinitionHolder(bd, candidateName)); 						}					}				}				 candidateNames = newCandidateNames; 			}		}		 while (!candidates.isEmpty()); 		//  Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes 		 if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) { 			 sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry()); 		}		 if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) { 			//  Clear cache in externally provided MetadataReaderFactory; this is a no-op 			//  for a shared cache since it'll be cleared by the ApplicationContext. 			 ((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache(); 		}}

There are two lines of code in this method:

  • parser.parse(candidates): create a parser, let the parser parse candidates, obtain configuration classes, and mark the information of each configuration class.
  • this. reader. Loadbean definitions (configclasses): load configuration classes, including the processing of @ Import, @ ImportResource and other resources.

After parsing each annotation under the corresponding package, Spring will package it into ConfigurationClass, and continuously compare the currently resolved configuration class with the new configuration class through the do while statement to ensure that the ConfigurationClass after each parsing will not introduce a new configuration class. Finally, all configuration classes are resolved.

parser.parse(candidates)

Click the parse method to see how Spring parses.

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();}

The else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) in the previous method will determine whether the class corresponding to the current BeanDefinitionNames is a Configuration class. At the beginning, BeanDefinitionNames only included the BeanDefinition registered in Spring itself, that is, the AppConfig we manually passed. In fact, only this class will carry out subsequent processes. Because at the beginning, only AppConfig was a Configuration class. Because AppConfig is marked with @ Configuration.

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

Since it is started by annotation, the BeanDefinition corresponding to AppConfig naturally belongs to AnnotatedBeanDefinition. Follow up and skip unimportant areas. Finally, enter the doProcessConfigurationClass method.

doProcessConfigurationClass
protected final SourceClass doProcessConfigurationClass(      ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)      throws IOException {   if (configClass.getMetadata().isAnnotated(Component.class.getName())) {      // Recursively process any member (nested) classes first      processMemberClasses(configClass, sourceClass, filter);   }   // Process any @PropertySource annotations   for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(         sourceClass.getMetadata(), PropertySources.class,         org.Springframework.context.annotation.PropertySource.class)) {      if (this.environment instanceof ConfigurableEnvironment) {         processPropertySource(propertySource);      }      else {         logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +               "]. Reason: Environment must implement ConfigurableEnvironment");      }   }   //  Process @ componentscan annotation set < annotationattributes > componentscans = annotationconfigutils attributesForRepeatable(         sourceClass.getMetadata(), ComponentScans. class, ComponentScan. class);    if (!componentScans.isEmpty() &&         ! this. conditionEvaluator. shouldSkip(sourceClass.getMetadata(), ConfigurationPhase. REGISTER_ BEAN)) {      for (AnnotationAttributes componentScan : componentScans) {         // The config class is annotated with @ComponentScan -> perform the scan immediately         Set<BeanDefinitionHolder> scannedBeanDefinitions =               this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName() );         //  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)) {               parse(bdCand.getBeanClassName(), holder.getBeanName());            }         }      }   }   //  Process @ import annotations processimports (configclass, sourceclass, getimports (sourceclass), filter, true)// Process any @ImportResource annotations   AnnotationAttributes importResource =         AnnotationConfigUtils. attributesFor(sourceClass.getMetadata(), ImportResource. class);    if (importResource != null) {      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 @ bean set < methodmetadata > beanmethods = retrievebeanmethodmetadata (sourceclass); 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()) {      String superclass = sourceClass.getMetadata().getSuperClassName();      if (superclass != null && !superclass.startsWith("java") &&            !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;}
  • Parse @ PropertySource annotation;
  • When the configuration class contains @ ComponentScan, resolve @ ComponentScan and resolve the scanning path marked by @ ComponentScan (it will recurse continuously. If @ ComponentScan is found again, it will be scanned again)
  • Resolve @ Import
  • Resolve @ ImportResource
  • Parse @ Bean
  • Resolve the default method on the interface (new feature of jdk8: the interface can define the default method, and Spring supports injecting beans using the default method of @ Bean annotation interface)
processImports(configClass, sourceClass, getImports(sourceClass), filter, true)

When @ Import is parsed in the processImports method, there is a point to note: getImports. Before entering processImports, the getImports method will be called first.

The logic of this method is very simple: recursively obtain all annotations on the current configuration class, including meta annotations of annotations,

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) {            if (candidate.isAssignable(ImportSelector.class)) {               // Candidate class is an ImportSelector -> delegate to it to determine imports               Class<?>  candidateClass = candidate. loadClass();               // Reflection gets the class importselector of @ Import. Selector = parserstrategyutils instantiateClass(candidateClass, ImportSelector.class,                     this.environment, this.resourceLoader, this.registry);                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());                  Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames, exclusionFilter);                  processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter, false) ;               }            }  else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {               // 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 {               // Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->               // process it as an @Configuration class               this.importStack.registerImport(                     currentSourceClass.getMetadata(), candidate.getMetadata().getClassName() );                MultiValueMap<String, AnnotationMetadata> imports = this. importStack. getImports();                System. out. println(imports);                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();      }   }}

We can see that when dealing with @ Import, Spring will make the following judgments on the imported classes:

  • Whether ImportSelector has been implemented and whether DeferredImportSelector has been further implemented (DeferredImportSelector is a sub interface of ImportSelector, which has delayed loading, mainly due to the different loading timing. The processing of this part will be further analyzed later)
  • Is ImportBeanDefinitionRegistrar implemented
  • None of them are implemented. At this time, they are treated as ordinary classes;

Sort out what processImports did:

  1. First, get the @ Import imported class of the current configuration class through getImports, that is, the value value of @ Import;
  2. Use the importStack attribute to judge the cyclic dependency of the configuration class. If there is no cyclic dependency of the configuration class, continue to parse (how to judge will be described later)
  3. Because @ Import can Import more than one class, it loops through importCandidates
  4. Judge the Import class. If the Import class implements ImportSelector, first reflect and obtain the class introduced by @ Import;
  5. If the class does not implement DeferredImportSelector, directly call selectImports to obtain the array of fully qualified class names specified by the class introduced by @ Import (the real introduced class), and encapsulate the class as a configuration class;
  6. Recursively call processImports to resolve the configuration class. Because the imported configuration class (the really introduced class) may continue to implement ImportSelector or ImportBeanDefinitionRegistrar, or a configuration class using @ Bean, recursion is required.
  7. If the DeferredImportSelector is implemented, the actually introduced classes will be stored in the deferredImportSelectors collection and will not be processed for the time being
  8. If ImportBeanDefinitionRegistrar is implemented, it is stored in the importbeandefinitionregisters collection
  9. Parsing complete

The process described above is rather obscure, mainly because there will be a lot of recursion in the parsing process. You need to take a look at it in combination with the code.

After @ Import is resolved, @ ImportResource, @ Bean and interface default methods will be resolved.

Go back to parser parse(candidates)

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();
}

At this time, all the deferredImportSelector interface classes implemented before the call, and the processImports operation will be executed again

Back to processConfigBeanDefinitions

public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
 ...
 ...
    parser.parse(candidates);
    parser.validate();

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

    // 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);
 ...
 ...
}

Finally return to parser Parse (candidates) method. At this time, all @ imports are parsed

Call parse Parse is actually just the parsing operation of various annotations. It mainly sets the attributes for ConfigClass, and is not fully loaded into BeanDefinition (some configuration classes will be loaded into BeanDefinitonMap first)

  • For example, if the configuration class is imported by @ Import, the attribute: importedBy indicates which configuration class is imported,
  • For example, if the configuration class has @ Bean annotation, the attribute: beanMethods will indicate which methods are available.
  • For example, if the configuration class is @ Import and the class imported by @ Import implements importbeandefinitionregister, the attribute: importbeandefinitionregisters is the class that implements importbeandefinitionregister

loadBeanDefinitions

public void loadBeanDefinitions(Set<ConfigurationClass> configurationModel) {
   TrackedConditionEvaluator trackedConditionEvaluator = new TrackedConditionEvaluator();
   for (ConfigurationClass configClass : configurationModel) {
      loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator);
   }
}

private void loadBeanDefinitionsForConfigurationClass(
			ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {

		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());
}

Call this reader. Loadbean definitions (configclasses). At this time, the operations corresponding to all annotations will be processed, that is, loaded into the BeanDefinitionMap.

Annotated @ Import: make a switch for the Import class

I don't know how to get the title of this content. Generally speaking, it is a special use of @ Import, but it is used a lot. In the Spring source code, we can see many @ EnableXXX annotations. When this annotation is used, some classes will be imported into the Spring singleton pool (container). If this annotation is not used, it will not be imported.

In many articles and comments on articles, there has been such a paragraph: "when @ Import is used as a meta annotation to modify other annotations, do you need to add @ Configuration?"

I don't think so.

To make this clear, one method needs to be emphasized again: getImports: when Spring scans a configuration class, it will recursively judge all meta annotations on the configuration class.

When @ EnableXXX is used, the class marked with @ EnableXXX is also marked with @ Configuration. These two annotations are usually used together. Because of getImports, @ Import in @ EnableXXX can also be resolved to.

About the role of DeferredImportSelector

It plays the role of delaying loading. The ordinary ImportSelector has long been parsed into ConfigurationClass and added to the configurationClasses of ConfigurationClassParser. Until all configuration classes are resolved, the collection storing DeferredImportSelector will not be called, and then the loop processing will be carried out.

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);
			}
		}
		//It will not be called until all configuration classes have been resolved
		this.deferredImportSelectorHandler.process();
	}

In short, the difference between import selector and import selector is that the loading time is different.

  • ImportSelector is the judgment that will be made when parsing any configuration class. It belongs to the in progress state of parsing configuration class;
  • DeferredImportSelector is the final state of resolving configuration classes, which is resolved only after all configuration classes have been resolved;

Just saying the difference, not the application scenario, is playing hooligans.

Application scenario: the default configuration class of SpringBoot.

When introducing various starters, SpringBoot will import many basic classes to programmers by default. If programmers do not redefine the basic classes, they can directly use the basic classes provided in the starter.

SpringBoot does this through DeferredImportSelector (plus @ Condition)

Referring to each default configuration class in the starter, you will find a large number of @ conditions. The annotation is used to judge whether a Bean defined by the programmer already exists in the singleton pool. If it already exists, the default configuration class of the starter will not be loaded.

But there is a problem: how to ensure that the class defined by the programmer is registered into the singleton pool before the configuration provided by the starter?

Another perspective: how to ensure that the registration time of the configuration provided by starter must be later than the class defined by the programmer?

Map<ConfigurationClass, ConfigurationClass> configurationClasses = new LinkedHashMap<>()

Because DeferredImportSelector is the last resolved configuration class, it is the last one added to the map collection of the configuration class. And this map set, it's ordered.

Therefore, the Bean defined by the programmer is always resolved to ConfigurationClass earlier than the DeferredImportSelector, and then resolved to BeanDefiniton earlier, and then registered in the spring singleton pool earlier (L1 cache)

How to generate and solve the circular dependency of the configuration file?

That is, how isChainedImportOnStack works, a method called in processImports.

Let's talk about how the circular dependency of the configuration file is generated: (the reason why we emphasize the circular dependency of the configuration file is to distinguish the circular dependency of automatic loading)

For example:

@Configuration
@Import(ConfiguationB.class)
public class ConfiguationA {
}

@Configuration
@Import(ConfiguationC.class)
public class ConfiguationB {
}

@Configuration
@Import(ConfiguationA.class)
public class ConfiguationC {
}

In this case, configuration a introduces configuration B, configuration B introduces configuration C, and configuration C introduces configuration A. in this way, a loop is constructed, and problems will occur during parsing (continuous recursion). Even the configuration class a Imoprt itself will also have problems.

Solution: store it in the ImportStack of ConfigurationClassParser, and use this class to judge.

Click the ImportStack class:

private static class ImportStack extends ArrayDeque<ConfigurationClass> implements ImportRegistry {
   private final MultiValueMap<String, AnnotationMetadata> imports = new LinkedMultiValueMap<>();
   ...  
}

As you can see, this is a double ended queue. And maintain a MultiValueMap internally

Starting from parsing the first configuration class, each time @ Import is parsed, the currently parsed configuration class is added to the map of importStack.

this.importStack.registerImport(
      currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());

Take the above situation as an example:

  • Parsing A, adding B class to map of importStack, calling processConfigurationClass to parse B class,
  • Parsing B, adding C class to map of importStack, calling processConfigurationClass to parse C class,
  • Parsing C, adding class A to importStack's map, calling processConfigurationClass to resolve the C class, isChainedImportOnStack will then determine the cyclic dependency.
private boolean isChainedImportOnStack(ConfigurationClass configClass) {
		if (this.importStack.contains(configClass)) {
			String configClassName = configClass.getMetadata().getClassName();
			AnnotationMetadata importingClass = this.importStack.getImportingClassFor(configClassName);
			while (importingClass != null) {
				if (configClassName.equals(importingClass.getClassName())) {
					return true;
				}
				importingClass = this.importStack.getImportingClassFor(importingClass.getClassName());
			}
		}
		return false;
	}

The judgment logic of this method is very simple: judge whether the currently parsed configuration class has appeared in the map of importStack. If it has, it proves that circular dependency has occurred.

Briefly summarize the principle: a map is used to store the processed configuration classes. If it is found that the currently processed configuration class has been resolved at a certain stage of recursive parsing, circular dependency occurs.

Principle of ImportAware

The parsing principle of ImportAware is not in the processImports method. It is solved through the post processor of spring.

First, when calling postProcessBeanFactory, spring will register ImportAwareBeanPostProcessor, which is used to process ImportAware.

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
   int factoryId = System.identityHashCode(beanFactory);
   if (this.factoriesPostProcessed.contains(factoryId)) {
      throw new IllegalStateException(
            "postProcessBeanFactory already called on this post-processor against " + beanFactory);
   }
   this.factoriesPostProcessed.add(factoryId);
   if (!this.registriesPostProcessed.contains(factoryId)) {
      // BeanDefinitionRegistryPostProcessor hook apparently not supported...
      // Simply call processConfigurationClasses lazily at this point then.
      processConfigBeanDefinitions((BeanDefinitionRegistry) beanFactory);
   }
   //Enhance the Configuration class
   enhanceConfigurationClasses(beanFactory);
   //Add an ImportAware bean postprocessor post processor to support the parsing of ImportAware,
   beanFactory.addBeanPostProcessor(new ImportAwareBeanPostProcessor(beanFactory));
}

Call Post Processor

protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {
    ...
    Object wrappedBean = bean;
    if (mbd == null || !mbd.isSynthetic()) {
       wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
    }
    ...
}


public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName)
    throws BeansException {

    Object result = existingBean;
    //getBeanPostProcessors() will get the ImportAwareBeanPostProcessor added earlier
    for (BeanPostProcessor processor : getBeanPostProcessors()) {
        Object current = processor.postProcessBeforeInitialization(result, beanName);
        if (current == null) {
            return result;
        }
        result = current;
    }
    return result;
}

//postProcessBeforeInitialization in ImportAwareBeanPostProcessor
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
    if (bean instanceof ImportAware) {
        ImportRegistry ir = this.beanFactory.getBean(IMPORT_REGISTRY_BEAN_NAME, ImportRegistry.class);
        AnnotationMetadata importingClass = ir.getImportingClassFor(ClassUtils.getUserClass(bean).getName());
        if (importingClass != null) {
            ((ImportAware) bean).setImportMetadata(importingClass);
        }
    }
    return bean;
}

When calling the processor, you will get the ImportRegistry, which is actually the ImportStack in the previous parser

When did ImportStack register into the spring container?

Answer: when all configurations have just been parsed, spring will manually register into the singleton pool for use by ImportAwareBeanPostProcessor.

public void processConfigBeanDefinitions(BeanDefinitionRegistry registry){
	...
    // Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes
    if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {
       sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
    }
	...
}

When ImportAwareBeanPostProcessor obtains the ImportRegistry (i.e. ImportStack) in the container, it can obtain the value in the map stored in ImportStack. This value is all the annotation information on the imported ImportAware class.

Points to note 1: multiple references make it impossible to obtain meta information

Note that there is a pit here: let's first check how getImportingClassFor gets the annotation meta information.

public AnnotationMetadata getImportingClassFor(String importedClass) {
   List<AnnotationMetadata> annotationMetadata = this.imports.get(importedClass);
   return CollectionUtils.lastElement(annotationMetadata);
}

CollectionUtils.lastElement: take the last element.

It should be noted that Spring uses MultiValueMap to store the imported configuration classes.

private final MultiValueMap<String, AnnotationMetadata> imports = new LinkedMultiValueMap<>();

MultiValueMap is a special map implemented by spring itself. A key can correspond to multiple values (in fact, list is used as the value, and when adding a new value, it can be added to the list).

Review the function of this map: it is used to store which configuration class is imported by the imported class.

for instance:

@Configuration
@Import(Teacher.class)
public class ImportConfig {}
public class Teacher {}

When these two classes are parsed and stored in the map, they will become key: com slc. imp.base. Teacher ,value: All annotation information on the ImportConfig class

Why does spring use MultiValueMap to store imports?

This is because the import ed class may be introduced multiple times. For example, the Teacher class is introduced by ConfigB. At this time, a meta annotation will be added to the value corresponding to the Teacher in the map.

Namely: key: com slc. imp.base. Teacher ,value: All annotation information on ImportConfig class, all annotation information on ConfigB class,

Then, getImportingClassFor calls collectionutils When using lastelement, the last element is always obtained, which will result in the failure to obtain the meta information of the annotation class introduced earlier.

  • For example, @ EnableA1 and @ EnableA2 are defined. Both annotations are @ import(A.class),
  • Class A implements the ImportAware interface
  • @EnableA1 and @ EnableA2 are introduced on different configuration classes.

At this time, @ EnableA2 is resolved later than @ @ EnableA1, so the class A importMetadata is the annotation meta information on the configuration class using @ EnableA2.

Therefore: it is recommended that when using ImportAware, one @ EnableXXX corresponds to one ImportAware class

Note 2: why does ImportAware need to be used with @ import?

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
    if (bean instanceof ImportAware) {
        ImportRegistry ir = this.beanFactory.getBean(IMPORT_REGISTRY_BEAN_NAME, ImportRegistry.class);
        AnnotationMetadata importingClass = ir.getImportingClassFor(ClassUtils.getUserClass(bean).getName());
        if (importingClass != null) {
            ((ImportAware) bean).setImportMetadata(importingClass);
        }
    }
    return bean;
}

In the post method of ImportAwareBeanPostProcessor, it only determines whether the current class is a sub interface of ImportAware, and does not determine whether it is the relevant logic of @ Import. It seems that as long as the Bean implements ImportAware, it can be processed logically.

If you don't Import Teacher through @ Import, can you Import Teacher instead of @ Bean?

Importstack is a sub interface of ImportRegistry. Check the spring source code's explanation of ImportRegistry: Registry of imported class, which translates to Import class registry. It seems that ImportRegistry specifically supports @ Import parsing. Check spring's use of importstack. It can be found that data will be added to the importstack only when @ Import is parsed.

From the above example, if you add @ Bean to inject Teacher, you will not follow the parsing route of @ Import. There is no corresponding value in importstack. Therefore, importingClass is empty and Teacher's setImportMetadata is not called at all.

Make a summary: only the ImportAware implementation class imported by @ Import will be called back.

Note 3: what is the annotation metadata of ImportAware?

Import the information of all annotations on the ImportAware class (encapsulated as AnnotationMetadata)

  • @Enablexxx+@Import is all the annotation information above the class using @ enablexxx

Generally, we use @ Enablexxx only to obtain the property value of @ Enablexxx, but actually we get more than the property content of @ Enablexxx. And other notes.

@Abc("1")
@Def("2")
@Configuation
@Enablexxx("3")
pubic  Class A{
    
}

For example, in this case, annotation metadata contains all annotation information of @ Abc, @ Def, @ configuration, @ Enablexxx("3").

Therefore, the example in "places to note 1" clearly specifies that @ EnableA1 and @ EnableA2 are introduced on different configuration classes. If they are introduced on the same configuration class, the AnnotationMetadata can still obtain the information of the two annotations, and it seems that there is no coverage problem. (in fact, it's just a coincidence)

Core concern: instead of just getting @ enablexxx, AnnotationMetadata imports all annotations of this class, except that all annotations of the latter contain @ enablexxx

Keywords: Java Spring

Added by jordanwb on Thu, 30 Dec 2021 18:53:06 +0200