Spring: Searching For Context

A couple of months ago in our blog, we shared a detailed guide about classloading on the Java Virtual Machine. After that, IntexSoft developers started to wonder about what mechanism Spring uses to parse configurations and how it loads classes from context.
Spring: Searching For Context
undefined
IntexSoft

A couple of months ago in our blog, we shared a detailed guide about classloading on the Java Virtual Machine. After that, IntexSoft developers started to wonder about what mechanism Spring uses to parse configurations and how it loads classes from context.

After many hours of Spring source code debugging, IntexSoft Java developer Andrew Zenov experimentally got to the bottom of the very simple and clear truth.

A bit of theory

ApplicationContext is the main interface in Spring app that provides configuration information to an application.

Before proceeding directly to the demonstration, let us look at the steps of ApplicationContext creating:

Spring context steps

In this post, we will analyze the first stage, since we are interested in reading configuration files and creating BeanDefinition.

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

As for the configuration of the beans themselves, Spring has 4 configuration methods:

  1. XML configuration - ClassPathXmlApplicationContext(“context.xml”);
  2. Groovy configuration - GenericGroovyApplicationContext(“context.groovy”);
  3. Annotation-Based Configuration where you indicate the package to scan - AnnotationConfigApplicationContext(“package.name”);
  4. JavaConfig - annotation-based configuration that indicates (Java) class (or an array of classes) annotated with @Configuration - AnnotationConfigApplicationContext(JavaConfig.class).

XML configuration

We take a simple project as a basis:

    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 we should explain what methods are used and what they are used for:

  • printLoadedClasses(String… filters) method displays the name of the loader and JVM classes loaded from the package, transmitted as a parameter into the console. Additionally, there is information on the number of all loaded classes
  • doSomething(Object o) is a method which performs primitive work but does not allow to exclude the mentioned classes during optimization at the stage of compilation

We connect Spring to our project (hereinafter, Spring 4 is used):

    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

Configuration XML-file looks as follows:

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

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

Let us look at how Spring will manage the situation with classes declared in the configuration file when parsing the context:

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     /*

After executing printLoadedClasses(calssFilter), 343 classes were loaded along with the context, 3 of which are the classes from our package. It means one more class has been added to the previously used ones and it was mentioned in the configuration XML-file as film.Villian class.

Let’s look into the details of XML configuration:

  • The configuration file is read by the XmlBeanDefinitionReader class, which implements the BeanDefinitionReader interface.
  • The XmlBeanDefinitionReader receives InputStream at the input and loads the Document through DefaultDocumentLoader:
  • Document doc = doLoadDocument(inputSource, resource);
        return registerBeanDefinitions(doc, resource);
  • After that, each element of this document is processed and, if it is a bean, 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 Map, which is stored in the DefaultListableBeanFactory class:
  • this.beanDefinitionMap.put(beanName, beanDefinition);
            this.beanDefinitionNames.add(beanName);

Map looks the following way in the code:

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

Now let’s add another bean declaration containing film.BadVillain class to the same configuration file:

    <beans xmlns = "http://www.spingframework.org/schema/beans" xmlns:xsi = "link">
    <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 will see what happens if to 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 class film.BadVillain specified in the configuration file does not exist, Spring worked 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 list of BeanDefenitionNames contains 2 elements; i.e those 2 bean definitions configured in our file were created.

Configurations of both beans are essentially the same. But, the existing class was loaded, no problems arose. That is why we can conclude that there was also an attempt to load a nonexistent class, but a failed attempt did not affect anything.

Let us try to get the beans themselves using 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"));

Here is what we get:

spring code exception

If in the first case a valid bean was received, in the second case we had an exception.

Pay your attention to the stack trace: lazy loading came into action. All class loaders were bypassed in an attempt to find the class we are looking for among previously loaded ones. And after the necessary class was not found, there was an attempt to find a nonexistent class using Utils.forName method, which led us to a logical error.

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

That is because we stated lazy-init:true and forbade Spring to create a bean instance, where the previously received exception is generated. If we delete this property from the configuration or change its value to lazy-init:false, then the error described above will also arise, but will not be ignored and the application will stop. In our case, the context was initialized, but we could not create a bean instance as the specified class was not found.

Groovy configuration

When configuring the context with the help of Groovy-file, it is necessary to form GroovyBeanDefinitionReader, which receives the string with the context configuration on the input. In this case, the GroovyBeanDefinitionReader class is engaged in the reading of the context. In fact, this configuration works the same way as the XML, but with Groovy files. In addition, GroovyApplicationContext works well with the XML-file as well.

Here is the example of a simple configuration Groovy-file:

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

Let us try to do the same we did with XML:

spring code exception

The error comes up immediately: Groovy, like XML, creates bean definitions, but in this case, the post-processor gives an error right away.

Annotation-Based Configuration indicating the package to scan or JavaConfig

This kind of configuration differs from the previous ones. Two variants are used in the annotation-based configuration, i.e. JavaConfig and annotations on classes.

The same context is used here:

AnnotationConfigApplicationContext(“package”/JavaConfig.class). It works depending on what was transferred to the constructor.

In AnnotationConfigApplicationContext there are 2 private fields:

  • private final AnnotatedBeanDefinitionReader reader (works with JavaConfig)
  • private final ClassPathBeanDefinitionScanner scanner (scans the package)

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

  1. Registration of @Configuration-files for further parsing;
  2. Registration of special BeanFactoryPostProcessor, namely BeanDefinitionRegistryPostProcessor, which parses the JavaConfig using ConfigurationClassParser class and creates BeanDefinition.

Here is 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 bean possible. And watch what will load:

spring context

If in the case with XML and Groovy, as many BeanDefinitions were loaded as it was declared, then, in this case, both the declared and additional bean definitions 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 a bean.

Moreover, here is one more thing. In the case of XML and Groovy configurations, 343 files were uploaded; here a more “heavy” load of 631 additional files occurred.

ClassPathBeanDefinitionScanner operation stages:

  • The specified package determines the list of files to scan. All files go to directories;
  • The scanner goes through each file, receives InputStream and scans them using the org.springframework.asm.ClassReader.class;
  • At the 3rd stage, the scanner checks whether the found objects pass through the filters of org.springframework.core.type.filter.AnnotationTypeFilter annotation. Spring searches for classes that are annotated with @Component or any other annotation that includes @Component by default
  • If the verification is successful, new BeanDefinition will be created and registered.

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

Let us look at how the scanner works using a simple example. We create a custom annotation to find the appropriate classes:

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

Then we create 2 classes: one with a standard @Component annotation, and another one with custom annotation.

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

As a result, we get generated bean definitions for these classes and successfully loaded classes as well.

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

Conclusion

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

1. What mechanism does Spring use to parse configurations?

Each context implementation has its own toolkit, but scanning is mainly used. Until the BeanDefinition is created, we do not try to load classes: first, scanning is carried out according to the specified parameters, and then suitable bean definitions are created based on the scan results. Then, post-processors try to ‘‘tune’’ BeanDefinition themselves, load a class to it and so on.

2. How does Spring load classes from the context?

The standard Java class loading mechanism is used: class loaders are bypassed in an attempt to find the desired class among previously loaded ones, and if the class cannot be found, an attempt is made to load it.


The authors

Andrey Zenov

Andrey Zenov
Software Engineer

Anrej Suschevich

Andrej Suschevich
Team Leader

Darya Birukova

Darya Birukova
Marketing Manager


625


Item tags