AUTO1 Group

Spring 5 candidate component index case study

By Mariusz Sondecki

Mariusz is Senior Software Engineer at AUTO1 Group.

< Back to list
Coding Feb 5

The situation

Since day one at AUTO1, we strongly believe that the microservice architecture helps us run business at scale not only due to the technical benefits it brings but also, if not foremost by allowing many teams to work independently from each other. Today, we’re running more than 250 microservices in production and on average add one more to our platform every week. All of them are powered by the Spring Framework technology stack, which we value very much for its productivity, but at the same time consider it to be a bit heavy in terms of bootstrapping time. The typical startup time of our Spring applications varies between 40 and 80 seconds. Since we do around 30 deployments per day, our engineering teams spend roughly 30 minutes daily waiting for the deployments to be completed. As we value the time of our engineering teams, we wanted to find a way to decrease these numbers.

A new hope

While investigating the startup times of our applications, we have identified classpath component scanning as a potential place to look for improvements. Luckily for us, some of our tech savvy engineers suggested a not so prominent Spring 5.X feature to come at help here - the build time component candidate index. The build time component index is an alternative for regular component scanning, which skips the classpath search operation in favor of using a compile time pre-generated index of component candidates during the spring application context building. However, it comes with a small print from the Spring Framework experts saying that this functionality should have mostly a visible impact on startup times for applications with large amount of beans and that are operating in environments where IO operations are expensive (e.g. remote file systems) or where JVM security managers are in use. Although, neither of this is our use case, we have decided to proceed with the feature evaluation and measurements, as here at AUTO1, we like our decisions to be data driven.

Demystifying the indexer

The key part of the indexer feature is a pre-generated index file located in META-INF/spring.components one per JAR. Every index entry in this file is a fully qualified name of a candidate component as a key and comma separated stereotypes as value. So for example “X=Y, Z” can be read simply as register a candidate component X with following stereotypes Y, Z. Below is an example of a spring.components file:

com.auto1.playground.service.SpringDummyService=org.springframework.stereotype.Component
com.auto1.playground.domain.SpringDummy=javax.persistence.Entity,javax.persistence.Table,javax.persistence.EntityListeners
com.auto1.playground.repository.SpringDummyRepository=org.springframework.stereotype.Component,org.springframework.data.repository.Repository
com.auto1.playground.configuration.LocalConfiguration=org.springframework.stereotype.Component
com.auto1.playground.service.IndexedCustomService=com.auto1.playground.service.IndexedCustomService
com.auto1.playground.Application=org.springframework.stereotype.Component
com.auto1.playground.SpringDummyController=org.springframework.stereotype.Component
com.auto1.playground.service.IndexedCustomExample=com.auto1.playground.annotation.IndexedCustom

The first question that arises here is how to create such a file and keep it up-to-date. The Spring engineers thought about this problem as well and came up with an annotation processor tool called spring indexer that hooks in during the project build phase and generates the file. Making use of it is fairly simple, assuming you are using your favorite build automation tool, in our case maven and want our IDE to keep this file up to date, simply add the spring indexer annotation processor to your pom file:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.5</version>
            <configuration>
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.springframework</groupId>
                        <artifactId>spring-context-indexer</artifactId>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

Once this is done, what happens during project build time is the spring context indexer (precisely speaking the CandidateComponentsIndexer class) while going through the project identifies annotated components and outputs them to META-INF/spring.components. The type of components it includes in the candidate list are:

  1. Classes annotated directly or indirectly with spring @Component annotation (which by itself is meta annotated with @Indexed) and other stereotypes for which @Component is a meta annotation such as @Repository, @Controller, @Service, @Configuration.

  2. Classes and interfaces annotated with annotations from javax package, including CDI annotations (@Named, @ManagedBean), JPA annotations (@Entity, @EntityListeners) or even Servlet annotations (@WebListener)

  3. Any custom classes and interfaces annotated with @Indexed annotation.

It should be noted that applying @Indexed on a class/interface where no other standard stereotype (as from points 1 and 2) is present will not make by itself the bean managed, thus not making them eligible for being autowired, but merely provide a hint that it represents a stereotype to be considered in the index.

Example of the @Indexed annotation usage:

@Indexed
public class IndexedCustomService { }

Or as meta annotated as below:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface IndexedCustom { }

@IndexedCustom
public class IndexedCustomExample { }

In order to make them available in the application context, thus autowirable, we could for example include them in the component scan filters as shown below:

@SpringBootApplication
@ComponentScan(basePackages= {"com.auto1"}, includeFilters = {
  @ComponentScan.Filter(type= ANNOTATION, value = {IndexedCustomExample.class}),
  @ComponentScan.Filter(type= ASSIGNABLE_TYPE, value = {IndexedCustomService.class})
  }
)
public class Application implements WebMvcConfigurer {
    // ...
}

There is one big pitfall that comes with the feature though, that is by default the index is automatically enabled when a META-INF/spring.components file is found on the classpath during application start-up and this in turn, disables completely the regular classpath component scanning. In other words, both the index and classpath scanning cannot be used at the same time. The consequence of this is that if an index is available for some JARs, but is incomplete due to including other JARs that come with potential bean candidates but don’t have the index file generated, those beans will not be discovered, thus application startup might spectacularly fail with a well known error: “X required a bean of type 'Y’ that could not be found”. Fortunately, Spring engineers, as thoughtful as they are, provided a feature flag spring.index.ignore that can be passed either as a system property or set in the spring.properties file at the root of the classpath allowing us to disable the usage of the candidate component index altogether.

Test me one more time

Like mentioned before, the aim of our investigation was to understand if the Spring indexer feature is of any help to us in regards to boosting the application start-up times. In order to make the test fairly reliable and control as many confounding factors as possible, we have decided to run the test firstly, using a “dummy” Spring 5 application (in two flavors - with 120 and 5000 beans) using all the features a production microservice would use, on a bare metal machine i.e. a MacBook Pro late 2017 i7. Secondly, running a real life production service in a less controlled but close to production environment i.e. on a r4.8xlarge EC2 instance with other services running concurrently. The applications themselves were using Spring 5.0.10.RELEASE with Spring Cloud Finchley.SR2 release train. After making a couple of hundred measurements of application bootstrapping times with candidate component index enabled / disabled and verifying in the logs that Spring was indeed using (or not) the component candidate list, we ended up with following averaged results:

Measurements

The expected disappointment

As the above results have consistently shown, there was no improvement or even insignificant worsening (which will be a subject of our analysis in a separate article) of the bootstrapping time of the Spring applications, regardless of the number of candidate beans present, when using the pre-generated candidate component index. This was expected, as explained in the opening section, the conditions for which this feature was designed (slow or expensive IO operations) are simply not met in our case. One more important factor to consider here, is that a great deal of the beans in our Spring applications, is created using bean definition factory methods, which are not subject to the component classpath scanning, thus are not impacted by this feature.

All in all, we deemed this feature not to be useful for our purposes and moved on to the next item on our list.

Stories you might like:
CodingSep 9
By Oleg Osipenko

This blog post explains how you could set up your environment in order to test your fragments in...

CodingJul 2
By Nicholas Peretti

Create forms at scale with Formik and Yup

CodingJun 24
By Wojciech Oroński

Yet another case study of developing serverless apps with PHP.