The magic behind the magic: Spring Boot Autoconfiguration

Spring boot is one of the most used and trending frameworks. The ease of use, the strong and lovely community and its powerful features makes Spring Boot one the most popular dev frameworks of 2017

In this post, we'll go behind the scenes and dig into the magic behind the Spring Boot autoconfiguration, make and explore a starter of our own as well.

Why do we need it ?

Spring Boot autoconfiguration represents a way to automatically configure a Spring application based on the dependencies that are present on the classpath, all without much effort required from us, as developers. The secret behind this power actually comes from Spring itself or rather from the Java Configuration functionality that it provides. As we add more starters as dependencies, more and more classes will be added to our classpath. Spring Boot detects the presence or absence of specific classes and based on this information, makes some decisions, which are not simple and fairly complicated at times, and automatically creates and wires the necessary beans to the application context.

This powerful capability is very helpful and open a whole new world of isage. Let's say for example, We can create custom starters that would automatically add some of the configuration or functionalities to our applications.

How it works then ?!

The first step toward understanding how Spring boot autoconfiguration works, is to analyse the output of the AUTO-CONFIGURATION REPORT by enabling the DEBUG mode of our spring boot application. This is a (very) small selection of what I got for my reactive aircraft app:

============================
CONDITIONS EVALUATION REPORT
============================


Positive matches:
-----------------
...
   MongoReactiveDataAutoConfiguration#reactiveMongoDatabaseFactory matched:
      - @ConditionalOnMissingBean (types: org.springframework.data.mongodb.ReactiveMongoDatabaseFactory; SearchStrategy: all) did not find any beans (OnBeanCondition)

   MongoReactiveDataAutoConfiguration#reactiveMongoTemplate matched:
      - @ConditionalOnMissingBean (types: org.springframework.data.mongodb.core.ReactiveMongoTemplate; SearchStrategy: all) did not find any beans (OnBeanCondition)

   MongoReactiveRepositoriesAutoConfiguration matched:
      - @ConditionalOnClass found required classes 'com.mongodb.reactivestreams.client.MongoClient', 'org.springframework.data.mongodb.repository.ReactiveMongoRepository'; @ConditionalOnMissingClass did not find unwanted class (OnClassCondition)
      - @ConditionalOnRepositoryType configured type of 'AUTO' matched required type (OnRepositoryTypeCondition)
      - @ConditionalOnMissingBean (types: org.springframework.data.mongodb.repository.support.ReactiveMongoRepositoryFactoryBean,org.springframework.data.mongodb.repository.config.ReactiveMongoRepositoryConfigurationExtension; SearchStrategy: all) did not find any beans (OnBeanCondition)

   ThymeleafAutoConfiguration.ThymeleafJava8TimeDialect#java8TimeDialect matched:
      - @ConditionalOnMissingBean (types: org.thymeleaf.extras.java8time.dialect.Java8TimeDialect; SearchStrategy: all) did not find any beans (OnBeanCondition)

   ThymeleafAutoConfiguration.ThymeleafReactiveConfiguration matched:
      - found ReactiveWebApplicationContext (OnWebApplicationCondition)
      - @ConditionalOnProperty (spring.thymeleaf.enabled) matched (OnPropertyCondition)

...

Negative matches:
-----------------

   ActiveMQAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required classes 'javax.jms.ConnectionFactory', 'org.apache.activemq.ActiveMQConnectionFactory' (OnClassCondition)

   AopAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required classes 'org.aspectj.lang.annotation.Aspect', 'org.aspectj.lang.reflect.Advice', 'org.aspectj.weaver.AnnotatedElement' (OnClassCondition)

   ArtemisAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required classes 'javax.jms.ConnectionFactory', 'org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory' (OnClassCondition)

...

For each line of the report, Spring Boot tells us why certain configurations have been selected to be included, what they have been positively matched on, or, for the negative matches, what was missing that prevented a particular configuration to be included in the mix. Let’s look at the positive match for MongoReactiveRepositoriesAutoConfiguration:

  • The @ConditionalOnClass classes found tells us that Spring Boot has detected the presence of a particular class, specifically two classes in our case: com.mongodb.reactivestreams.client.MongoClient and org.springframework.data.mongodb.repository.ReactiveMongoRepository.
  • The OnClassCondition indicates the kind of matching that was used. This is supported by the @ConditionalOnClass and @ConditionalOnMissingClass annotations.

While OnClassCondition is the most common kind of detection, Spring Boot also uses many other conditions. For example, OnBeanCondition is used to check the presence or absence of specific bean instances, OnPropertyCondition is used to check the presence, absence, or a specific value of a property as well as any number of the custom conditions that can be defined using the @Conditional annotation and Condition interface implementations. More detail can be found here

The negative matches show us a list of configurations that Spring Boot has evaluated, which means that they do exist in the classpath and were scanned by Spring Boot but didn’t pass the conditions required for their inclusion. ActiveMQAutoConfiguration, while available in the classpath as it is a part of the imported spring-boot-autoconfigure artifact, was not included because the required org.apache.activemq.ActiveMQConnectionFactory class was not detected as present in the classpath, thus failing the OnClassCondition.

Example of Custom Autoconfiguration: Minio starter

Few weeks ago, I started working on a minio starter that autoconfigures a minio client. We'll use it as example to discuss how to make a spring boot starter of our own :)

First thing to do is creating the starter pom which contains the dependencies for the auto-configure module. The naming convention for all the starters which are not managed by the core Spring Boot team should start with the library name followed by the suffix XXX-spring-boot-starter (i.e minio-springboot-starter).

Worth nothing to mention that Spring Boot Autoconfiguration Starters are nothing more than a regular Spring Java Configuration class annotated with the @Configuration annotation and the presence of spring.factories in the classpath in the META-INF directory with the appropriate configuration entries.

With that being said, let's start digging into our starter. First, I've called our autoconfigure module miniospringbootautoconfigure. This module will have three main classes, among others, i.e. MinioProperties which will enable setting custom properties through application.properties file, MinioTemplate to wrap minio API and MinioAutoConfiguration which will create the beans for minio starter.

@Configuration
@EnableConfigurationProperties({ MinioProperties.class })
public class MinioAutoConfiguration {

    @Autowired
    private MinioProperties properties;

    @Bean
    @ConditionalOnMissingBean(MinioTemplate.class)
    @ConditionalOnProperty(name = "minio.url")
    MinioTemplate template(){
        return new MinioTemplate(
                properties.getUrl(),
                properties.getAccessKey(),
                properties.getSecretKey()
        );
    }
}

On application startup, the MinioAutoConfiguration class will run if the property minio.url is present in the classpath. If run successfully, it will populate the Spring application context with MinioTemplate bean by reading the properties via MinioProperties class.

The @ConditionalOnMissingBean annotation will ensure that beans will only be created if they don’t already exist. This allow us to completely override the autoconfigured beans by defining our own in one of the @Configuration classes.

As mentioned above, the next mandatory step is registering the MinioAutoConfiguration as an auto-configuration candidate, by adding it under the key org.springframework.boot.autoconfigure.EnableAutoConfiguration in the standard file resources/META-INF/spring.factories:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=me.aboullaite.minio.MinioAutoConfiguration

What happen is, during the application startup, Spring Boot uses SpringFactoriesLoader, which is a part of Spring Core, in order to get a list of the Spring Java Configurations that are configured for the org.springframework.boot.autoconfigure.EnableAutoConfiguration property key. Under the hood, this call collects all the spring.factories files located in the META-INF directory from all the jars or other entries in the classpath and builds a composite list to be added as application context configurations.

This should gave you an overview of what our minio starter does.
The very final step and in order to use our starter, we need to add its dependency to a spring boot application:

<dependency>
 <groupId>me.aboullaite</groupId>
 <artifactId>minio-springboot-starter</artifactId>
 <version>${minio-starter-version}</version>
</dependency>

And voila! Please feel free to comment the section below for any question, suggestion or update ;)