Writing Custom Spring Boot Starters

Alex Antonov

September 2015

 In this article by Alex Antonov, author of the book Spring Boot Cookbook, we will cover the following topics:

  • Understanding Spring Boot autoconfiguration
  • Creating a custom Spring Boot autoconfiguration starter

(For more resources related to this topic, see here.)

Introduction

Its time to take a look behind the scenes and find out the magic behind the Spring Boot autoconfiguration and write some starters of our own as well.

This is a very useful capability to possess, especially for large software enterprises where the presence of a proprietary code is inevitable and it is very helpful to be able to create internal custom starters that would automatically add some of the configuration or functionalities to the applications. Some likely candidates can be custom configuration systems, libraries, and configurations that deal with connecting to databases, using custom connection pools, http clients, servers, and so on. We will go through the internals of Spring Boot autoconfiguration, take a look at how new starters are created, explore conditional initialization and wiring of beans based on various rules, and see that annotations can be a powerful tool, which provides the consumers of the starters more control over dictating what configurations should be used and where.

Understanding Spring Boot autoconfiguration

Spring Boot has a lot of power when it comes to bootstrapping an application and configuring it with exactly the things that are needed, all without much of the glue code that is required of us, the 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 appear in our classpath. Spring Boot detects the presence or absence of specific classes and based on this information, makes some decisions, which are fairly complicated at times, and automatically creates and wires the necessary beans to the application context.

Sounds simple, right?

How to do it…

  1. Conveniently, Spring Boot provides us with an ability to get the AUTO-CONFIGURATION REPORT by simply starting the application with the debug flag. This can be passed to the application either as an environment variable, DEBUG, as a system property, -Ddebug, or as an application property, --debug.
  2. Start the application by running DEBUG=true ./gradlew clean bootRun.
  3. Now, if you look at the console logs, you will see a lot more information printed there that is marked with the DEBUG level log. At the end of the startup log sequence, we will see the AUTO-CONFIGURATION REPORT as follows:
    =========================
    AUTO-CONFIGURATION REPORT
    =========================
    
     
    Positive matches:
    
    -----------------
    …
    DataSourceAutoConfiguration
         - @ConditionalOnClass classes found: javax.sql.DataSource,org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType (OnClassCondition)
    …
    
    Negative matches:
    -----------------
    …
    GsonAutoConfiguration
         - required @ConditionalOnClass classes not found: com.google.gson.Gson (OnClassCondition)


How it works…

As you can see, the amount of information that is printed in the debug mode can be somewhat overwhelming; so I've selected only one example of positive and negative matches each.

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 DataSourceAutoConfiguration:

  • The @ConditionalOnClass classes found tells us that Spring Boot has detected the presence of a particular class, specifically two classes in our case: javax.sql.DataSource and org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType.
  • 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.

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. GsonAutoConfiguration, while available in the classpath as it is a part of the imported spring-boot-autoconfigure artifact, was not included because the required com.google.gson.Gson class was not detected as present in the classpath, thus failing the OnClassCondition.

The implementation of the GsonAutoConfiguration file looks as follows:

@Configuration
@ConditionalOnClass(Gson.class)
public class GsonAutoConfiguration {

@Bean
@ConditionalOnMissingBean
public Gson gson() {
   return new Gson();
}

}

After looking at the code, it is very easy to make the connection between the conditional annotations and report information that is provided by Spring Boot at the start time.

Creating a custom Spring Boot autoconfiguration starter

We have a high-level idea of the process by which Spring Boot decides which configurations to include in the formation of the application context. Now, let's take a stab at creating our own Spring Boot starter artifact, which we can include as an autoconfigurable dependency in our build.

Let's build a simple starter that will create another CommandLineRunner that will take the collection of all the Repository instances and print out the count of the total entries for each.

We will start by adding a child Gradle project to our existing project that will house the codebase for the starter artifact. We will call it db-count-starter.

How to do it…

  1. We will start by creating a new directory named db-count-starter in the root of our project.
  2. As our project has now become what is known as a multiproject build, we will need to create a settings.gradle configuration file in the root of our project with the following content:
    include 'db-count-starter'
  3. We should also create a separate build.gradle configuration file for our subproject in the db-count-starter directory in the root of our project with the following content:
    apply plugin: 'java'
    repositories {
    mavenCentral()
    maven { url "https://repo.spring.io/snapshot" }
    maven { url "https://repo.spring.io/milestone" }
    }
    
    dependencies {
    compile("org.springframework.boot:spring-
       boot:1.2.3.RELEASE")
    compile("org.springframework.data:spring-data-
       commons:1.9.2.RELEASE")
    }
  4. Now we are ready to start coding. So, the first thing is to create the directory structure, src/main/java/org/test/bookpubstarter/dbcount, in the db-count-starter directory in the root of our project.
  5. In the newly created directory, let's add our implementation of the CommandLineRunner file named DbCountRunner.java with the following content:
    public class DbCountRunner implements CommandLineRunner {
       protected final Log logger = LogFactory.getLog(getClass());
    
       private Collection<CrudRepository> repositories;
    
       public DbCountRunner(Collection<CrudRepository> repositories) {
           this.repositories = repositories;
       }
    
       @Override
       public void run(String... args) throws Exception {
           repositories.forEach(crudRepository ->
               logger.info(String.format("%s has %s entries",
                   getRepositoryName(crudRepository.getClass()),
                   crudRepository.count())));
       }
    
       private static String getRepositoryName(Class crudRepositoryClass) {
           for(Class repositoryInterface :
                   crudRepositoryClass.getInterfaces()) {
               if (repositoryInterface.getName().
    startsWith("org.test.bookpub.repository")) {
                   return repositoryInterface.getSimpleName();
               }
           }
           return "UnknownRepository";
       }
    }
  6. With the actual implementation of DbCountRunner in place, we will now need to create the configuration object that will declaratively create an instance during the configuration phase. So, let's create a new class file called DbCountAutoConfiguration.java with the following content:
    @Configuration
    public class DbCountAutoConfiguration {
       @Bean
       public DbCountRunner dbCountRunner(Collection<CrudRepository> repositories) {
           return new DbCountRunner(repositories);
       }
    }
  7. We will also need to tell Spring Boot that our newly created JAR artifact contains the autoconfiguration classes. For this, we will need to create a resources/META-INF directory in the db-count-starter/src/main directory in the root of our project.
  8. In this newly created directory, we will place the file named spring.factories with the following content:
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.test.bookpubstarter.dbcount.DbCountAutoConfiguration
  9. For the purpose of our demo, we will add the dependency to our starter artifact in the main project's build.gradle by adding the following entry in the dependencies section:
    compile project(':db-count-starter')
  10. Start the application by running ./gradlew clean bootRun.
  11. Once the application is complied and has started, we should see the following in the console logs:
    2015-04-05 INFO org.test.bookpub.StartupRunner           : Welcome to the Book Catalog System!
    
    2015-04-05 INFO o.t.b.dbcount.DbCountRunner             : AuthorRepository has 1 entries
    
    2015-04-05 INFO o.t.b.dbcount.DbCountRunner             : PublisherRepository has 1 entries
    
    2015-04-05 INFO o.t.b.dbcount.DbCountRunner             : BookRepository has 1 entries
    
    2015-04-05 INFO o.t.b.dbcount.DbCountRunner             : ReviewerRepository has 0 entries
    
    2015-04-05 INFO org.test.bookpub.BookPubApplication : Started BookPubApplication in 8.528 seconds (JVM running for 9.002)

2015-04-05 INFO org.test.bookpub.StartupRunner           : Number of books: 1
How it works…

Congratulations! You have now built your very own Spring Boot autoconfiguration starter.

First, let's quickly walk through the changes that we made to our Gradle build configuration and then we will examine the starter setup in detail.

As the Spring Boot starter is a separate, independent artifact, just adding more classes to our existing project source tree would not really demonstrate much. To make this separate artifact, we had a few choices: making a separate Gradle configuration in our existing project or creating a completely separate project altogether. The most ideal solution, however, was to just convert our build to Gradle Multi-Project Build by adding a nested project directory and subproject dependency to build.gradle of the root project. By doing this, Gradle actually creates a separate artifact JAR for us but we don't have to publish it anywhere, only include it as a compile project(':db-count-starter') dependency.

For more information about Gradle multi-project builds, you can check out the manual at http://gradle.org/docs/current/userguide/multi_project_builds.html.

Spring Boot Auto-Configuration Starter is 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.

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. In addition to the EnableAutoConfiguration key, we can declare the following automatically initializable other startup implementations in a similar fashion:

  • org.springframework.context.ApplicationContextInitializer
  • org.springframework.context.ApplicationListener
  • org.springframework.boot.SpringApplicationRunListener
  • org.springframework.boot.env.PropertySourceLoader
  • org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider
  • org.springframework.test.contex.TestExecutionListener

Ironically enough, a Spring Boot Starter does not need to depend on the Spring Boot library as its compile time dependency. If we look at the list of class imports in the DbCountAutoConfiguration class, we will not see anything from the org.springframework.boot package. The only reason that we have a dependency declared on Spring Boot is because our implementation of DbCountRunner implements the org.springframework.boot.CommandLineRunner interface.

Summary

Resources for Article:


Further resources on this subject:


You've been reading an excerpt of:

Spring Boot Cookbook

Explore Title