The aim of this lab is to gain a deeper understanding of how Spring Boot autoconfiguration functions.

The following lab is inspired from a SpringOnePlatform 2017 presentation by Stéphane Nicoll and Brian Clozel.

For this lab, set aside the demo project you’ve been working with so far, and clone the following github project: https://github.com/eitansuez/boot2-autoconfig-example

$ cd workspace
$ git clone https://github.com/eitansuez/boot2-autoconfig-example.git

Next, import this project into your IDE. This is yet another gradle project, but unlike the previous one, it contains three separate modules.

1. Study the code

This project is kept very simple. Here’s a summary of each module, as they currently stand:

  • hello-lib: defines the following simple interface:

    HelloService.java
    package com.example;
    
    public interface HelloService {
      void greet();
    }

    ..along with a simple implementation of that interface:

    ConsoleHelloService.java
    package com.example;
    
    public final class ConsoleHelloService implements HelloService {
      private final String prefix;
      private final String suffix;
    
      public ConsoleHelloService(String prefix, String suffix) {
        this.prefix = ( prefix == null ? "Hello" : prefix );
        this.suffix = ( suffix == null ? "!" : suffix );
      }
    
      @Override
      public void greet() {
        String message = String.format("%s world%s", prefix, suffix);
        System.out.println(message);
      }
    }
  • hello-app is a simple Spring Boot application with a main class. It doesn’t really do anything yet. Notice that it depends on hello-lib (see its build.gradle file)

  • hello-starter does not yet have any code in it, and is unused at this point in time.

2. Exercise the code

  • In hello-app, add a class that implements CommandLineRunner. Don’t forget to mark the class with the @Component annotation.

  • In this class, autowire an instance of HelloService.

  • In the run method, call the service’s greet() method

Here’s an example:

package com.example;

import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class Runner implements CommandLineRunner {

  private final HelloService service;

  public Runner(HelloService service) {
    this.service = service;
  }

  @Override
  public void run(String... args) {
    service.greet();
  }
}

Run hello-app:

$ gradle :hello-app:bootRun

It should fail with a message similar to this:

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of constructor in com.example.Runner required a bean of type 'com.example.HelloService' that could not be found.

This is easy to fix. All we need to do is expose a Spring Bean that implements the interface:

HelloAppApplication.java
@@ -2,11 +2,19 @@ package com.example;

 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;

 @SpringBootApplication
 public class HelloAppApplication {

  public static void main(String[] args) {
    SpringApplication.run(HelloAppApplication.class, args);
  }

+  @Bean
+  public HelloService helloService() {
+    return new ConsoleHelloService("Hello", "!");
+  }
+
 }

Re-run the application and note the "Hello world!" greeting echo to the console.

3. Configure Bean in the starter

When we use a Spring Boot starter, we often start out without configuring any beans. The starter sets them up for us. For example, spring-boot-starter-data-jpa can automatically configure a DataSource bean.

We’d like to do the same: let’s have the hello-starter dependency automatically contribute the HelloService bean so that we don’t have to in hello-app.

  1. Comment out the explicit @Bean configuration from HelloAppApplication.java

  2. In hello-app 's build file, replace the dependency on hello-lib with a dependency on hello-starter:

    hello-app/build.gradle
    @@ -4,7 +4,7 @@ plugins {
    
     dependencies {
       compile 'org.springframework.boot:spring-boot-starter'
    -  compile project(':hello-lib')
    +  compile project(':hello-starter')
    
       testCompile 'org.springframework.boot:spring-boot-starter-test'
     }

Note that hello-starter has a transitive dependency on hello-lib. This is similar to the way that many Spring Boot starters have dependencies on the library (or libraries) that they configure.

Next, in hello-starter, create a class named HelloAutoConfig in which we’ll configure the HelloService bean:

hello-starter/src/main/java/com/example/HelloAutoConfig.java
package com.example;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class HelloAutoConfig {

  @Bean
  HelloService helloService() {
    return new ConsoleHelloService("Bonjour", ".");
  }

}

Starters contribute auto-configured beans to an application by declaring their configuration classes in a special file named spring.factories.

  • In hello-starter/src/main/resources, create a subdirectory named META-INF.

  • Inside that directory, create the following file:

src/main/resources/META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.HelloAutoConfig

The reason we declare the configuration file in spring.factories is to ensure that it gets loaded by Spring, even if the client application does not have component scanning turned on.

Re-run hello-app. The HelloService bean is now contributed by the starter.

What happens if we were to also configure a HelloService bean in our project? Add back the @Bean annotation in HelloAppApplication. Which bean wins?

The starter’s configuration wins. Let’s fix that.

4. Honor clients with manual configuration

The starter must have a way to detect if a client has already configured the bean, and only contribute its configuration if the bean is absent, or missing. For this, Spring provides the @ConditionalOnMissingBean annotation.

  • In HelloAutoConfig, add the annotation to the method

  • Also, to ensure this class is not loaded when the class HelloService is absent from the classpath, add a second condition at the class level, like this:

hello-starter/src/main/java/com/example/HelloAutoConfig.java
@@ -1,11 +1,15 @@
 package com.example;

+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;

+@ConditionalOnClass(HelloService.class)
 @Configuration
 public class HelloAutoConfig {

+  @ConditionalOnMissingBean
   @Bean
   HelloService helloService() {
     return new ConsoleHelloService("Bonjour", ".");

Re-run the application. The manually-configured bean in the application should now be the one that is loaded in the Spring application context. Conversely, if we comment out the manual configuration once more, the starter will contribute it.

This is precisely how many Spring Boot autoconfiguration libraries function. For example, spring-starter-jdbc will only contribute a jdbcTemplate bean if none is already defined by the client, and on the condition that a dependent bean, DataSource is present. See the class JdbcTemplateAutoConfiguration for details.

5. Expose configuration

The ConsoleHelloService is now auto-contributed by the starter only for clients that don’t want to explicitly define the bean.

It would be nice to generally have the starter contribute the bean, but to also expose the ability to configure that bean in the client.

ConsoleHelloService is configured with both a prefix and a suffix. In the Spring Boot Basics lab, we learned about @ConfigurationProperties, a JavaBean whose properties can automatically bind to configuration properties matching a specific prefix.

Define such a bean for ConsoleHelloService:

  • In hello-starter, create a class named HelloProperties.

  • Add two properties: prefix, and suffix, with initial values "Hello" and "!", respectively.

  • Create getter and setter methods for both properties.

  • Annotate the class with @ConfigurationProperties, and set the configuration prefix to hello.

hello-starter/src/main/java/com/example/HelloProperties.java
package com.example;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix="hello")
public class HelloProperties {
  private String prefix = "Hello";
  private String suffix = "!";

  public String getPrefix() {
    return prefix;
  }

  public void setPrefix(String prefix) {
    this.prefix = prefix;
  }

  public String getSuffix() {
    return suffix;
  }

  public void setSuffix(String suffix) {
    this.suffix = suffix;
  }

}

5.1. Retrofit HelloAutoConfig

Next, we need to make sure that when the service is constructed, it uses the HelloProperties object:

  • In hello-starter, open HelloAutoConfig.

  • Autowire HelloProperties via constructor.

  • In the helloService() method, use the helloProperties field to construct the ConsoleHelloService.

  • Finally, enable configuration properties detection by annotating this class with @EnableConfigurationProperties

HelloAutoConfig.java
@@ -2,17 +2,25 @@ package com.example;

 import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;

 @ConditionalOnClass(HelloService.class)
 @Configuration
+@EnableConfigurationProperties(HelloProperties.class)
 public class HelloAutoConfig {

+  private final HelloProperties helloProperties;
+
+  public HelloAutoConfig(HelloProperties helloProperties) {
+    this.helloProperties = helloProperties;
+  }
+
   @ConditionalOnMissingBean
   @Bean
   HelloService helloService() {
-    return new ConsoleHelloService("Bonjour", ".");
+    return new ConsoleHelloService(helloProperties.getPrefix(), helloProperties.getSuffix());
   }

 }

5.2. Test the change

  • Back in hello-app, configure the hello service properties, as follows:

    hello-app/src/main/resources/application.properties
    hello.prefix=Howdy
    hello.suffix=?
  • In HelloAppApplication, be sure to comment out (or remove) any manually-configured beans.

  • Run the app

It should display the autoconfigured bean, but configured with the properties from the properties file:

console output autoconfig

6. Properties Autocompletion

Did you notice how using configuration properties from Spring Boot’s own starters, such as server.port, we automatically had IDE autocomplete support?

For IDEs to be aware of hello configuration properties, there’s a contract: supply of a metadata file in classpath:META-INF/spring-configuration-metadata.json that informs it of the configuration fields, and their names.

Spring Boot provides a dependency, spring-boot-configuration-processor, that can automatically generate this metadata file.

➜ Edit hello-starter 's build file and add that dependency.

Here’s what the updated build file looks like:

hello-starter/build.gradle
@@ -6,6 +6,8 @@ dependencies {
   compile 'org.springframework.boot:spring-boot-starter'
   compile project(':hello-lib')

+  annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
+
   testCompile 'org.springframework.boot:spring-boot-starter-test'
 }
1 The Spring Boot configuration processor dependency is declared using gradle’s annotationProcessor dependency configuration

➜ Rebuild hello-starter

$ cd hello-starter
$ gradle clean build

Inspect the build artifacts, and verify that, under build/classes/java/main/META-INF there now exists a file named spring-configuration-metadata.json.

We can go further, and add javadoc comments on the HelloProperties fields, and these will be incorporated into the metadata file as well, and appear in autocompletions.

hello-starter/src/main/java/com/example/HelloProperties.java
@@ -4,7 +4,14 @@ import org.springframework.boot.context.properties.ConfigurationProperties;

 @ConfigurationProperties(prefix="hello")
 public class HelloProperties {
+  /**
+   * The prefix for the greeting.
+   */
   private String prefix = "Hello";
+
+  /**
+   * Placed at the end of the greeting.
+   */
   private String suffix = "!";

   public String getPrefix() {

Rebuild hello-starter one more time. Here is the generated JSON file:

build/classes/java/main/META-INF/spring-configuration-metadata.json
{
  "hints": [],
  "groups": [
    {
      "sourceType": "com.example.HelloProperties",
      "name": "hello",
      "type": "com.example.HelloProperties"
    }
  ],
  "properties": [
    {
      "sourceType": "com.example.HelloProperties",
      "defaultValue": "Hello",
      "name": "hello.prefix",
      "description": "The prefix for the greeting.",
      "type": "java.lang.String"
    },
    {
      "sourceType": "com.example.HelloProperties",
      "defaultValue": "!",
      "name": "hello.suffix",
      "description": "Placed at the end of the greeting.",
      "type": "java.lang.String"
    }
  ]
}

6.1. Test the change

In your IDE, navigate back to hello-app 's application.properties file. Key in a hello.prefix and verify that the autocompletion now works with these custom configuration properties.

properties autocompletion

In this multi-module project, I encountered a discrepancy between gradle’s and my IDE’s classpath configurations. In IntelliJ, the module output path for hello-starter is set to out/production/classes whereas gradle’s is build/classes/java/main. I had to hand-edit IntelliJ’s setting to allow the hello-app module to discover the spring-configuration-metada.json file that resides in its dependency.

This issue generally does not surface in a real situation, where each project publishes its artifact to a maven repository from which jar files are downloaded, and in which these metadata files reside.

7. Summary

The aim of this lab was to provide deeper insight into how Spring Boot starters and autoconfiguration works. You wrote your own simple starter, and:

  • Registered a bean with spring.factories,

  • Used @Conditional annotations,

  • Exposed properties for configuration, and

  • Added support for IDE autocompletion of configuration properties.

In our next and final lab, we go back to our original demo application, and explore deployment to Pivotal Cloud Foundry (PCF), and the built-in support for Spring Boot applications that PCF provides.