Spring: looking for context

A couple of months ago, a detailed post on loading classes on the JVM was published in my profile . After this talk, my colleagues asked a good question: what mechanism does Spring use to parse configurations and how does it load classes from context?




After many hours of spring source debug, my colleague experimentally got to the very simple and understandable truth.

Bit of theory


Immediately determine that ApplicationContext is the main interface in a Spring application that provides application configuration information.

Before proceeding directly to the demonstration, let’s take a look at the steps involved in creating an ApplicationContext :



In this post we will analyze the first step, since we are interested in reading configurations and creating BeanDefinition.

BeanDefinition is an interface that describes a bean, its properties, constructor arguments, and other meta-information.

Regarding the configuration of the beans themselves, Spring has 4 configuration methods:

  1. Xml configuration - ClassPathXmlApplicationContext (”context.xml”);
  2. Groovy configuration - GenericGroovyApplicationContext (”context.groovy”);
  3. Configuration via annotations indicating the package for scanning - AnnotationConfigApplicationContext (”package.name”);
  4. JavaConfig - configuration via annotations indicating the class (or class array) marked with @Configuration - AnnotationConfigApplicationContext (JavaConfig.class).

Xml configuration


We take as a basis a simple project:

public class SpringContextTest{
   private static String classFilter = "film.";
   
   public static void main(String[] args){
        
         printLoadedClasses(classFilter);
         /* Classloader: sun.misc.Launcher$AppClassLoader@18b4aac2
            All - 5 : 0 - Filtered      /*
        doSomething(MainCharacter.num); doSomething(FilmMaker.class);
        printLoadedClasses(classFilter);
        /* Classloader: sun.misc.Launcher$AppClassLoader@18b4aac2
               class film.MainCharacter
               class film.FilmMaker
            All - 7 : 2 - Filtered     /*

Here you should explain a little what methods and what are used:

  • printLoadedClasses (String ... filters) - the method prints to the console the name of the loader and loaded JVM classes from the package passed as a parameter. Additionally, there is information on the number of all loaded classes;
  • doSomething (Object o) is a method that does primitive work, but does not allow to exclude the mentioned classes during optimization during compilation.

We connect to the Spring project (hereinafter, Spring 4 acts as the test subject):

11 public class SpringContextTest{
12    private static String calssFilter = "film.";
13    
14    public static void main(String[] args){
15        
16        printLoadedClasses(classFilter);
17       /* Classloader: sun.misc.Launcher$AppClassLoader@18b4aac2
18           All - 5 : 0 - Filtered      /*
19        doSomething(MainCharacter.num); doSomething(FilmMaker.class);
20        printLoadedClasses(classFilter);
21        /* Classloader: sun.misc.Launcher$AppClassLoader@18b4aac2
22               class film.MainCharacter
23               class film.FilmMaker
24               All - 7 : 2 - Filtered   /*
25        ApplicationContext context = new ClassPathXmlApplicationContext(
26                  configLocation: "applicationContext.xml");
27        printLoadedClasses(classFilter);

Line 25 is the declaration and initialization of ApplicationContext through the Xml configuration .

The configuration xml file is as follows:

<beans xmlns = "http://www.spingframework.org/schema/beans" xmlns:xsi = "http..."
        <bean id = "villain" class = "film.Villain" lazy-init= "true">
                <property name = "name" value = "Vasily"/>
        </bean>
</beans> 

When configuring the bean, we specify a really existing class. Pay attention to the specified property lazy-init = ”true” : in this case, the bin will be created only after requesting it from the context.

We look at how Spring, when raising the context, will clear up the situation with the classes declared in the configuration file:

public class SpringContextTest {
    private static String classFilter = "film.";
    
    public static void main(String[] args) {
        
           printLoadedClasses(classFilter);
        /* Classloader: sun.misc.Launcher$AppClassLoader@18b4aac2
           All - 5 : 0 - Filtered      /*
        doSomething(MainCharacther.num); doSomething(FilmMaker.class);
        printLoadedClasses(classFilter);
        /* Classloader: sun.misc.Launcher$AppClassLoader@18b4aac2
               class film.MainCharacter
               class film.FilmMaker
            All - 7 : 2 - Filtered     /*
        ApplicationContext context = new ClassPathXmlApplicationContext(
                  configLocation: "applicationContext.xml");
        printLoadedClasses(classFilter);
        /* Classloader: sun.misc.Launcher$AppClassLoader@18b4aac2
               class film.MainCharacter
               class film.FilmMaker
               class film.Villain

            All - 343 : 3- Filtered     /*

Let us examine the details of Xml configuration:

- Reading the configuration file has been class XmlBeanDefinitionReader , which implements the interface BeanDefinitionReader ;

- The XmlBeanDefinitionReader at the input receives an InputStream and loads the Document through the DefaultDocumentLoader :

Document doc = doLoadDocument(inputSource, resource);
return registerBeanDefinitions(doc, resource);

- After that, each element of this document is processed and, if it is a bin, BeanDefinition is created based on the filled data (id, name, class, alias, init-method, destroy-method, etc.):

} else if (delegate.nodeNameEquals(ele, "bean")) {
    this.processBeanDefinition(ele, delegate);

- Each BeanDefinition is placed in a Map, which is stored in the DefaultListableBeanFactory class:

this.beanDefinitionMap.put(beanName, beanDefinition);
this.beanDefinitionNames.add(beanName);

In the code, Map looks like this:

/** Map of bean definition objects, keyed by bean name */
private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<String, BeanDefinition>(64);

Now in the same configuration file, add another bean declaration with the film.BadVillain class :

<beans xmlns = "http://www.spingframework.org/schema/beans" xmlns:xsi = "http..."
        <bean id = "goodVillain" class = "film.Villain" lazy-init= "true">
                <property name = "name" value = "Good Vasily"/>
        </bean>
        <bean id = "badVillain" class = "film.BadVillain" lazy-init= "true">
                <property name = "name" value = "Bad Vasily"/>
        </bean>

We ’ll see what happens if you print a list of created BeanDefenitionNames and loaded classes:

ApplicationContext context = new ClassPathXmlApplicationContext(
        configLocation: "applicationContext.xml");
System.out.println(Arrays.asList(context.getBeanDefinitionNames()));
        
printLoadedClasses(calssFilter);

Despite the fact that the film.BadVillain class specified in the configuration file does not exist, Spring works without errors:

ApplicationContext context = new ClassPathXmlApplicationContext(
        configLocation: "applicationContext.xml");
System.out.println(Arrays.asList(context.getBeanDefinitionNames()));
//  [goodVillain, badVillain]
printLoadedClasses(calssFilter);
/* Classloader: sun.misc.Launcher$AppClassLoader@18b4aac2
               class film.MainCharacter
               class film.FilmMaker
               class film.Villain
    All - 343 : 3- Filtered   /*

The BeanDefenitionNames list contains 2 elements; that is, those 2
BeanDefinition configured in our file were created.

The configurations of both bins are essentially the same. But, while the existing class loaded, no problems arose. From which we can conclude that there was also an attempt to load a nonexistent class, but a failed attempt did not break anything.

Let's try to get the beans themselves by their names:

ApplicationContext context = new ClassPathXmlApplicationContext(
        configLocation: "applicationContext.xml");
System.out.println(Arrays.asList(context.getBeanDefinitionNames()));
//  [goodVillain, badVillain]
System.out.println(context.getBean( name: "goodVillain"));

System.out.println(context.getBean( name: "badVillain"));

We get the following:



If in the first case a valid bean was received, then in the second case exception arrived.

Pay attention to stack trace: deferred loading of classes worked. All class loaders are crawled in an attempt to find the class they are looking for among previously loaded ones. And after the desired class was not found, by calling the Utils.forName method , an attempt is made to find a nonexistent class by name, which led to a natural error.

When raising the context, only one class was loaded, while an attempt to load a nonexistent file did not lead to an error. Why did it happen?

That's because we registered lazy-init: trueand forbade Spring to create an instance of the bean, where the previously received exception is generated. If you remove this property from the configuration or change its value lazy-init: false , then the error described above will also crash, but the application will not be ignored. In our case, the context was initialized, but we could not create an instance of the bean, because The specified class was not found.

Groovy configuration


When configuring a context using a Groovy file, you need to generate a GenericGroovyApplicationContext , which receives a string with the context configuration as input. In this case, the GroovyBeanDefinitionReader class is engaged in reading the context . This configuration works essentially the same as Xml, only with Groovy files. In addition, GroovyApplicationContext works well with the Xml file.

An example of a simple Groovy configuration file:

beans {
    goodOperator(film.Operator){bean - >
            bean.lazyInit = 'true' >
            name = 'Good Oleg' 
         }
    badOperator(film.BadOperator){bean - >
            bean.lazyInit = 'true' >
            name = 'Bad Oleg' / >
        }
  }

We try to do the same as with Xml: The



error crashes immediately: Groovy, like Xml, creates BeanDefenitions, but in this case the postprocessor immediately gives an error.

Configuration via annotations indicating package for scan or JavaConfig


This configuration is different from the previous two. The configuration through annotations uses 2 options: JavaConfig and annotation over classes.

The same context is used here: AnnotationConfigApplicationContext (“package” /JavaConfig.class) . It works depending on what was passed to the constructor.

In the context of AnnotationConfigApplicationContext there are 2 private fields:

  • private final AnnotatedBeanDefinitionReader reader (works with JavaConfig);
  • private final ClassPathBeanDefinitionScanner scanne r (scans the packet).

The peculiarity of AnnotatedBeanDefinitionReader is that it works in several stages:

  1. Registration of all @Configuration files for further parsing;
  2. Register a special BeanFactoryPostProcesso r, namely BeanDefinitionRegistryPostProcessor , which, using the ConfigurationClassParser class, parses JavaConfig and creates a BeanDefinition .

Consider a simple example:

@Configuration
public class JavaConfig {
    
    @Bean
    @Lazy
    public MainCharacter mainCharacter(){
        MainCharacter mainCharacter = new MainCharacter();
        mainCharacter.name = "Patric";
        return mainCharacter;        
   }
}

public static void main(String[] args) {

     ApplicationContext javaConfigContext = 
               new AnnotationConfigApplicationContext(JavaConfig.class);
     for (String str : javaConfigContext.getBeanDefinitionNames()){
          System.out.println(str);
     }
     printLoadedClasses(classFilter);

We create a configuration file with the simplest bin possible. We look at what will load:



If in the case of Xml and Groovy, as many BeanDefinition were loaded as it was announced, then in this case, both declared and additional BeanDefinition are loaded in the process of raising the context. In the case of implementation through JavaConfig, all classes are loaded immediately, including the class of JavaConfig itself, since it is itself a bean.

Another point: in the case of the Xml and Groovy configurations, 343 files were uploaded, here there was a more “heavy” load of 631 additional files. ClassPathBeanDefinitionScanner

work steps :

  • The specified package determines the list of files for scanning. All files fall into directories;
  • , InputStream org.springframework.asm.ClassReader.class;
  • 3- , org.springframework.core.type.filter.AnnotationTypeFilter. Spring , Component , Component;
  • , BeanDefinition.

All the “magic” of working with annotations, as is the case with Xml and Groovy, lies precisely in the ClassReader.class class from the springframework.asm package . The specificity of this reader is that it can work with bytecode. That is, the reader takes an InputStream from the bytecode, scans it and looks for annotations there.

Consider the scanner using a simple example.

Create your own annotation to search for the corresponding classes:

import org.springframework.stereotype.Component
import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface MyBeanLoader{
       String value() default "";

We create 2 classes: one with the standard Component annotation , the second with the custom annotation:

@Component 
public class MainCharacter {
      public static int num = 1;
      @Value("Silence")
      public String name;
      public MainCharacter() { }

MyBeanLoader("makerFilm")
@Lazy 
public class FilmMaker {
      public static int staticInt = 1;
      @Value("Silence")
      public String filmName;
      public FilmMaker(){}

As a result, we get the generated BeanDefinition for these classes and successfully loaded classes.

ApplicationContext annotationConfigContext =
       new AnnotationConfigApplicationContext(...basePackages: "film");
for (String str : annotationConfigContext.getBeanDefinitionNames()){
     System.out.println(str);
}
printLoadedClasses(classFilter);



Conclusion


From the foregoing, the questions posed can be answered as follows:

  1. Spring ?

    , . BeanDefinition, : , BeanDefinition’ . BeanDefinition, ..
  2. Spring ?

    Java: , , , .

PS I hope that in this post I was able to “open the veil of secrecy” and show in detail how the formation of the springing context occurs in the first stage. It turns out that not everything is so “scary". But this is only a small part of a large framework, which means there is still a lot of new and interesting ahead.

All Articles