Spring Boot uses the @Configuration annotation to replace the applicationContext. XML file in container initialization, bypasing the previous Configuration of beans in XML files, Configuration of bean scanning, and so on. How does Spring Boot use the @Configuration annotation to do this? Starting from this problem, this paper discusses the following three aspects.

  1. When does the Configuration class (the class of the Configuration annotation) take effect?
  2. What can be configured in a configuration class? What is the parsing process like?
  3. What is the default configuration of Spring Boot?

All of the analysis below is based on Spring Boot 2.4.

What is spring BeanFactoryPostProcessor

To understand when configuration classes are in effect, first understand BeanFactoryPostProcessor. Let’s look at the Bean generation process in Spring. In simple terms, Spring becomes a BeanFactory (that is, an IOC container) that generates beans from the BeanFactory. BeanFactory is a Factory, which is an IOC container or object Factory. Its responsibilities include instantiating, locating, configuring Bean objects, and establishing dependencies between these objects. In Spring, all beans are managed by the BeanFactory.

The BeanFactory first reads various information about the Bean from an XML file or annotation configuration, which Spring calls BeanDefinition. In general, we can generate beans directly from BeanDefinition. But Spring certainly doesn’t allow such a large circle to simply generate a Bean. Spring reads the BeanDefinition information and distinguishes between special beans (beans that implement the BeanFactoryPostProcessor interface). These special beans are processed first.

BeanFactoryPostProcessor –> Plain Bean constructor. So, BeanFactoryPostProcessor is used to extend or modify a BeanDefinition that’s already loaded. This allows you to do some processing on the Bean before actually initializing it, which is an important Spring extension point.

@FunctionalInterface
public interface BeanFactoryPostProcessor {

	/**
	 * Modify the application context's internal bean factory after its standard
	 * initialization. All bean definitions will have been loaded, but no beans
	 * will have been instantiated yet. This allows for overriding or adding
	 * properties even to eager-initializing beans.
	 * @param beanFactory the bean factory used by the application context
	 * @throws org.springframework.beans.BeansException in case of errors
	 */
	void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException;

}
Copy the code

Why are there BeanDefinitionRegistryPostProcessor

Spring most of the Boot is Spring BeanFactoryPostProcessor when initializing the context instance to join, there is also a part of the form of a ApplicationContextInitializer to join in a container, See the Spring Boot extension ApplicationContextInitializer way. In simple terms, SPring Boot extends initializers that generate implementation classes for BeanFactoryPostProcessor to add to the container. Let’s not worry about which BeanFactoryPostProcessor it is.

BeanFactoryPostProcessor has a problem because it can add BeanDefinitions to BeanFactory at will. Suppose we now have two beans and two BeanFactoryPostProcessors, one BeanFactoryPostProcessor for adding beans, A BeanFactoryPostProcessor is a function added to all beans. Obviously, the BeanFactoryPostProcessor used to add beans should be executed first so that the functionality of the next BeanFactoryPostProcessor can cover all beans. In order to achieve this effect, in the Spring set a Spring BeanFactoryPostProcessor BeanDefinitionRegistryPostProcessor child interface.

public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor {

	/**
	 * Modify the application context's internal bean definition registry after its
	 * standard initialization. All regular bean definitions will have been loaded,
	 * but no beans will have been instantiated yet. This allows for adding further
	 * bean definitions before the next post-processing phase kicks in.
	 * @param registry the bean definition registry used by the application context
	 * @throws org.springframework.beans.BeansException in case of errors
	 */
	void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException;

}
Copy the code

If a spring BeanFactoryPostProcessor for containers are used to increase BeanDefinition, then BeanDefinitionRegistryPostProcessor inheritance. In this way, Spring differentiates BeanFactoryPostProcessor and ensures the order in which BeanFactoryPostProcessor is executed. Now, our order of execution looks like this.

BeanDefinitionRegistryPostProcessor – > spring BeanFactoryPostProcessor – > common Bean construction method

It is important to note that the registered BeanDefinitionRegistryPostProcessor BeanDefinition may also be a BeanDefinitionRegistryPostProcessor, so at the end of the registration, Also need to determine whether to join the new BeanDefinitionRegistryPostProcessor, if it is, that is about to perform new BeanDefinitionRegistryPostProcessor registration method, number until no longer increases, All beandefinitions are loaded into the container. The following methods to intercept method invokeBeanFactoryPostProcessors PostProcessorRegistrationDelegate class.

boolean reiterate = true;
while (reiterate) {
  reiterate = false;
  / / get the current in a container BeanDefinitionRegistryPostProcessor implementation class
  postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true.false);

  // Determine if the class is new
  for (String ppName : postProcessorNames) {
    if(! processedBeans.contains(ppName)) { currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class)); processedBeans.add(ppName); reiterate =true; }}/ / sorting
  sortPostProcessors(currentRegistryProcessors, beanFactory);
  registryProcessors.addAll(currentRegistryProcessors);
  / / execution BeanDefinitionRegistryPostProcessor postProcessBeanDefinitionRegistry method
  invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());
  currentRegistryProcessors.clear();
}
Copy the code

InvokeBeanDefinitionRegistryPostProcessors perform BeanDefinitionRegistryPostProcessor postProcessBeanDefinitionRegistry finally Method to complete the BeanDefinition registration.

private static void invokeBeanDefinitionRegistryPostProcessors( Collection
        postProcessors, BeanDefinitionRegistry registry, ApplicationStartup applicationStartup) {

  for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) {
    StartupStep postProcessBeanDefRegistry = applicationStartup.start("spring.context.beandef-registry.post-process")
        .tag("postProcessor", postProcessor::toString); postProcessor.postProcessBeanDefinitionRegistry(registry); postProcessBeanDefRegistry.end(); }}Copy the code

ConfigurationClassPostProcessor join container flow

Now that we know about BeanFactoryPostProcessor, let’s go back to how configuration classes are parsed. The Configuration is mainly used to inject new Bean in the container, must be handled by a certain BeanDefinitionRegistryPostProcessor. The BeanDefinitionRegistryPostProcessor is ConfigurationClassPostProcessor, see the name also know is used to deal with the configuration class, How the ConfigurationClassPostProcessor into container?

Spring Boot initialization, executes createApplicationContext method, is used to create context instance, is the default type AnnotationConfigServletWebServerApplicationContext, During instantiation, this class calls the following code, which registers some BeanDefinitions in the container. One of them is ConfigurationClassPostProcessor.

public static Set<BeanDefinitionHolder> registerAnnotationConfigProcessors(
			BeanDefinitionRegistry registry, @Nullable Object source) {... Set<BeanDefinitionHolder> beanDefs =new LinkedHashSet<>(8);

  if(! registry.containsBeanDefinition(CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)) { RootBeanDefinition def =newRootBeanDefinition(ConfigurationClassPostProcessor.class); def.setSource(source); beanDefs.add(registerPostProcessor(registry, def, CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)); }...return beanDefs;
}
Copy the code

It is connected to the process above, ConfigurationClassPostProcessor as a BeanDefinitionRegistryPostProcessor, after the completion of the context is initialized, Additional BeanDefinitions added to the container.

Read the @SpringBootApplication configuration

Spring Boot initialization in the context, besides the default join BeanDefinition in registerAnnotationConfigProcessors method. There is also the Boot class for SpringBoot, which is the class decorated by @SpringBootApplication. Note that since Spring does not know the scan policy at this point, it will not load the beans from under the project. You can think of the Spring container at this point as having only the startup classes and classes that are added by default during initialization.

So how did the whole project get off the ground? Let’s not ignore the @SpringBootApplication annotation. The @SpringBootApplication annotation is a composite annotation that contains several annotations, including @Configuration, to quickly configure the startup class. The @SpringBootApplication contains the default configuration for the current container-loaded Bean, which is why we rarely need to write our own configuration.

In the stage of BeanDefinitionRegistryPostProcessor execution ConfigurationClassPostProcessor traverses all current BeanDefination, to determine whether the class configuration class, if you do, It will be marked, the class will be considered a configuration class, and the rest of the process will take place.

There are two different flags CONFIGURATION_CLASS_FULL and CONFIGURATION_CLASS_LITE. CONFIGURATION_CLASS_FULL is well understood, with the @configuration annotation. “CONFIGURATION_CLASS_LITE”?

It’s not just @Configuration that can add a new BeanDefination. A normal @Component annotated class can actually generate a new BeanDefination if one of its methods uses an @Bean annotation. Or classes with @ComponentScan, @import, @importResource annotations will generate a new class that is CONFIGURATION_CLASS_LITE, which you can think of as a configuration class.

The following code is used to determine if a class is a configuration class.

. Map<String, Object> config = metadata.getAnnotationAttributes(Configuration.class.getName());// The @configuration annotation is marked CONFIGURATION_CLASS_FULL
// proxyBeanMethods is the @configuration parameter, which defaults to true
if(config ! =null && !Boolean.FALSE.equals(config.get("proxyBeanMethods"))) {
  beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL);
}

// one of the @Component, @ComponentScan, @import, @importResource annotations
// Or there is a method that contains @bean, marked CONFIGURATION_CLASS_LITE
else if(config ! =null || isConfigurationCandidate(metadata)) {
  beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE);
}
else {
  return false;
}
Copy the code

The following code determines the configuration class of type CONFIGURATION_CLASS_LITE. The @Configuration class is not the only Configuration class. Whenever a class has one of the @Component, @ComponentScan, @import, @importResource annotations, or a method that contains the @Bean annotation, Is a configuration class

Here is the code that determines the configuration class of type CONFIGURATION_CLASS_LITE.

static {
  candidateIndicators.add(Component.class.getName());
  candidateIndicators.add(ComponentScan.class.getName());
  candidateIndicators.add(Import.class.getName());
  candidateIndicators.add(ImportResource.class.getName());
}

public static boolean isConfigurationCandidate(AnnotationMetadata metadata) {
  if (metadata.isInterface()) {
    return false;
  }

  for (String indicator : candidateIndicators) {
    if (metadata.isAnnotated(indicator)) {
      return true; }}// let's look for @Bean methods...
  try {
    return metadata.hasAnnotatedMethods(Bean.class.getName());
  }
  catch (Throwable ex) {
    return false; }}Copy the code

@Component can achieve the same effect as @Configuration for Configuration purposes. Of course, there are some implementation differences between the two (the way concrete objects are generated).

Anyway, once the code starts, the class annotated by @SpringBootApplication is considered a configuration class, the default configuration is read, the class file is scanned and loaded, the configuration is read, and so on until the number of classes scanned is no longer changed.

Configure the class resolution process

ConfigurationClassPostProcessor parsing process is more complex configuration class.

1. Handle inner classes

Configure classes that contain inner classes first. An inner class can also be a configuration class in the same way as the inner class above, which is a recursive process.

2. Handle @propertysource annotations

PropertySource annotation to load the specified configuration file, such as the demo.properties file in the Resources folder. Use it as a configuration source. I will not go into details about configuration sources in the Envirnoment article.

@PropertySource(value = {"classpath:demo.properties"})
Copy the code

The configuration source added in this way has a lower priority than the application. Properties configuration file.

3. Handle @ComponentScan annotations

@ComponentScan assembles classes that meet the scanning rules into a container according to the configured scanning path. By default, this annotation scans all configuration classes in the package that the class belongs to.

If you need to specify which packages to scan, you do so using the Valule property of @ComponentScan. Use excludeFilters to rule out certain packet scans. Use includeFilters to include only certain packet scans by rule. The specific logic will be looked at separately.

4. Handle @import annotations

There are three ways to Import @import. The simplest is to Import a class directly, which will be loaded into the Spring container.

@Import({ Cat.class})
class InnerConfiguration {}
Copy the code

The first approach is more rigid, so Spring also provides the ImportSelector interface, from which we can dynamically specify class names.

public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{"com.shenmax.xiang.configuration.Cat"}; }}Copy the code

At this point, we simply replace the @import argument with the ImportSelector implementation class.

@Import({ MyImportSelector.class})
class InnerConfiguration {}
Copy the code

The third and the second is similar, to implement ImportBeanDefinitionRegistrar interface, the usage of custom registration BeanDefination, rarely used.

The entire code process is very clear, first of all determine whether the imported class implements ImportSelector ImportBeanDefinitionRegistrar then judgment, is not, the direct import.

for (SourceClass candidate : importCandidates) {
  if (candidate.isAssignable(ImportSelector.class)) {
    ...
    if (selector instanceofDeferredImportSelector) { ... }... }else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
    ...
  }
  else{... }}Copy the code

5. Handle the @importResource annotation

The @import object is one or more classes, and the @importResource object is one or more configuration file paths. The essence is to read the XML file and load the Bean configuration items from the configuration file into the container.

@ImportResource(value = "xxx.xml")
Copy the code

6. Handle the @bean method

Previous annotations were placed on classes; @beans were placed on methods. ConfigurationClassPostProcessor will scan configuration method of a class, as long as found @ Bean annotations, will return the value of the method loading container.

@Configuration
public class MyConfiguration {
  @Bean
  public Persion persion(a) {
      return  newPersion(); }}Copy the code

7. Handle parent classes

Get the parent of the current class, and then go through the above process again. Of course, whether you’re dealing with a parent class or a child class, you need to determine whether it’s already been dealt with, otherwise it’s an infinite loop.

Spring Boot is configured by default

The @SpringBootApplication annotation only defines @ComponentScan for the configuration class, so we just need to figure out what that configuration means.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
  ...
}
Copy the code

@ ComponentScan annotations by ComponentScanAnnotationParser parse method in processing. BasePackages basePackageClasses @ComponentScan basePackages basePackageClasses The default root is ClassUtils getPackageName (declaringClass), which is the current @ ComponentScan classes in the package path directly.

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

Once you have this path, you need to read the file from this path. Here is the code for reading the file.

 String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
      resolveBasePackage(basePackage) + '/' + this.resourcePattern;
Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
Copy the code

The final packageSearchPath value is CLASspath *: XXX/XXX/XXX /**/*.class. Indicates scanning files with the.class suffix at any level under the current path. This isn’t the end of the story. You’ll need to filter the classes you read.

if (isCandidateComponent(metadataReader)) {
  ScannedGenericBeanDefinition sbd = newScannedGenericBeanDefinition(metadataReader); sbd.setSource(resource); . }Copy the code

According to the excludeFilters and includeFilters we configured. Note that includeFilters have a default value. If useDefaultFilters = false is not declared, the default includeFilters will be used. The default Component and ManagedBean are scanned. @Controller, @Service, and @Repository all inherit @Component and will be scanned as well.

protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
  for (TypeFilter tf : this.excludeFilters) {
    if (tf.match(metadataReader, getMetadataReaderFactory())) {
      return false; }}for (TypeFilter tf : this.includeFilters) {
    if (tf.match(metadataReader, getMetadataReaderFactory())) {
      returnisConditionMatch(metadataReader); }}return false;
}
Copy the code

By default, Spring Boot scans.class files at any level of the package in which the entry file is located. This class needs to be annotated by @Component or @ManagedBean. And can’t be TypeExcludeFilter or AutoConfigurationExcludeFilter.

conclusion

  1. Spring provides BeanFactoryPostProcessor extensions that allow you to modify BeadDefination even after it has been loaded into the container.

  2. BeanDefinitionRegistryPostProcessor is a special spring BeanFactoryPostProcessor, used to register new BeadDefination into the container.

  3. ConfigurationClassPostProcessor is a BeanDefinitionRegistryPostProcessor, used to resolve the configuration class, according to the configuration of the configuration class is loaded into the container.

  4. The configuration classes are full and Lite.

  5. A class is not a Configuration class only if it has @Component, @ComponentScan, @import, @importResource, or a method that contains an @Bean annotation

  6. The parse flow for a configuration class goes like this: inner class -> @propertysource -> @ComponentScan -> @import -> @importResource -> @Bean -> parent class.

  7. The @SpringBootApplication annotation is a composite annotation that contains @Configuration and @ComponentScan annotations. The default path for @ComponentScan is the package path for the configuration class.

If you think you’ve learned something, please give it a thumbs up!