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.javapackage com.example; public interface HelloService { void greet(); }
..along with a simple implementation of that interface:
ConsoleHelloService.javapackage 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 onhello-lib
(see itsbuild.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 implementsCommandLineRunner
. 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:
@@ -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
.
-
Comment out the explicit @Bean configuration from
HelloAppApplication.java
-
In
hello-app
's build file, replace the dependency onhello-lib
with a dependency onhello-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:
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 namedMETA-INF
. -
Inside that directory, create the following file:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.HelloAutoConfig
The reason we declare the configuration file in |
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:
@@ -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, |
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 namedHelloProperties
. -
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 tohello
.
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
, openHelloAutoConfig
. -
Autowire
HelloProperties
via constructor. -
In the
helloService()
method, use the helloProperties field to construct theConsoleHelloService
. -
Finally, enable configuration properties detection by annotating this class with
@EnableConfigurationProperties
@@ -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.propertieshello.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:
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:
@@ -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.
@@ -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:
{
"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.
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 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.