Custom Spring Boot Starters
Till now, we have covered the basics of Spring Boot in our previous tutorials and ideMockito - Creatntified the power of various Spring Boot starters. In this tutorial we will look at how to define our own Spring Boot starter and also cover the following topics:
- Understanding Spring Boot autoconfiguration
- Creating a custom Spring Boot autoconfiguration starter
- Configuring custom conditional bean instantiations
This is a very useful skill to have, especially for large software enterprise companies where the presence of proprietary code is inevitable, and it is very helpful to be able to create internal custom starters that would automatically provide some of the configuration or functionalities to our apps.
Some likely cases can be custom configuration libraries, and configurations that deal with connecting to databases, using custom connection pools, http clients, servers, and so on. We will cover the internals of Spring Boot autoconfiguration, take a look at how new starters are made, and explore conditional initialization and wiring of beans based on various rules. Further, we’ll see how annotations can be a powerful tool that provide the users of the starters with 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 boilerplate code that is required by the developers. The secret behind this power actually comes from Spring itself or rather from the Java Configuration functionality that is provided.
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.
How to check autoconfiguration?
Conveniently, this is easy as well. We can just start the application with the debug flag. Let’s follow some simple steps to achieve the same:
- This can be passed to the application either as an environment variable, DEBUG, as a system property, -Ddebug, or as an application property, --debug.
- Start the application by running DEBUG=true ./gradlew clean bootRun.
- Now if we look at the logs, we will get more information about what is happening internally whenever our app starts. Here is a demo,
=========================
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)
Clearly, the amount of information that is logged in the debug mode can be somewhat overwhelming, so we demonstrated only one example of positive and negative matches each.
For every line in 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 from being included in the mix.
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 the report information that is provided by Spring Boot at the start time.
The @ConditionalOnClass marker in parenthesis classes found tells us that Spring Boot has detected the presence of a specific class, specifically two classes in our case: javax.sql.DataSource and org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType.
While OnClassCondition is the most common type of detection, Spring Boot make use of many other conditions. For example, OnBeanCondition is used to check the presence of particular bean instances, OnPropertyCondition is used to check the presence, absence, or a particular 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.
Creating Spring Boot starter
We have a slight idea about how Spring Boot adds, positive or negative matches the classes present on its classpath. We’ll take a step forward and try to create an ‘autoconfigured’ starter which will add some classes to classpath automatically.
To make this example easy to understand, we will make use of the example project we’re building from the beginning of our Spring Boot series.
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.
- We will start by creating a new directory named db-count-starter in the root directory of our project.
- 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'
- 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")
}
- Now we are ready to add some code. So, the first step is to create the directory structure, src/main/java/com/discoversdk/dbcount, in the db-count-starter directory in the root of our project.
- 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("com.discoversdk.repository")) {
return repositoryInterface.getSimpleName();
}
}
return "UnknownRepository";
}
}
- With the actual implementation of DbCountRunner in place, we will now have to make 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 snippet:
@Configuration
public class DbCountAutoConfiguration {
@Bean
public DbCountRunner dbCountRunner(Collection<CrudRepository> repositories) {
return new DbCountRunner(repositories);
}
}
- We will have to inform Spring Boot that our newly created JAR artifact contains the autoconfiguration classes. For this, we will have to create a resources/META-INF directory in the db-count-starter/src/main directory in the root of the project.
- In this newly created directory, we will put the file named spring.factories with the following content:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.discoversdk.dbcount.DbCountAutoConfiguration
- 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')
- Start the application by running ./gradlew clean bootRun.
- Once the application is compiled and has run, we should see the following in the console logs:
2015-04-05 INFO com.discoversdk.StartupRunner : Welcome to the Student Catalog System!
2015-04-05 INFO o.t.b.dbcount.DbCountRunner : TeacherRepository has 1 entries
2015-04-05 INFO o.t.b.dbcount.DbCountRunner : ExamRepository has 1 entries
2015-04-05 INFO o.t.b.dbcount.DbCountRunner : StudentRepository has 1 entries
2015-04-05 INFO o.t.b.dbcount.DbCountRunner : SchoolRepository has 0 entries
2015-04-05 INFO com.discoversdk.StudentApplication : Started StudentApplication in 8.528 seconds (JVM running for 9.002)
2015-04-05 INFO com.discoversdk.StudentApplication : Number of students: 1
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.
Configuring custom conditional bean instantiations
In the previous sample, we learned how to make a basic Spring Boot Starter project that we can include in our gradle file. On the includion of dependency, the DbCountRunner bean will be created automatically and added to the application context.
For this sample, we will now improve our starter with a conditional check. This will create the instance of DbCountRunner only if no other bean object of this class is already created and added to the application context
For this, we can follow some simple steps:
- In the DbCountAutoConfiguration class, we will add an @ConditionalOnMissingBean annotation to the dbCountRunner(…) method, as follows:
@Bean
@ConditionalOnMissingBean
public DbCountRunner dbCountRunner(Collection<CrudRepository> repositories) {
return new DbCountRunner(repositories);
}
- We will also have to add a dependency on the spring-boot-autoconfigure artifact to the dependencies section of the db-count-starter/build.gradle file:
compile("org.springframework.boot:spring-boot-autoconfigure:1.2.3.RELEASE")
- Now, let's run the application by running ./gradlew clean bootRun in order to check that we still see the same logs in the console as we did in the last sample.
- If we start the application with the DEBUG switch so as to see the Auto-Configuration Report, which we already learned in the last sample, we will see that our autoconfiguration is in the Positive Matches group, as follows:
DbCountAutoConfiguration#dbCountRunner
- @ConditionalOnMissingBean (types: com.discoversdk.dbcount.DbCountRunner; SearchStrategy: all) found no beans (OnBeanCondition)
- Let's explicitly/manually create an instance of DbCountRunner in our main StudentApplication configuration class and we will also override its run(…) method, just so we can see the difference in the logs:
protected final Log logger = LogFactory.getLog(getClass());
@Bean
public DbCountRunner dbCountRunner(Collection<CrudRepository> repositories) {
return new DbCountRunner(repositories) {
@Override
public void run(String... args) throws Exception {
logger.info("Manually Declared DbCountRunner");
}
};
}
- Start the application by running DEBUG=true ./gradlew clean bootRun.
- If we look at the console logs, we will observe two things: the Auto-Configuration Report will print our autoconfiguration in the Negative Matches group and, instead of the count output for each repository, we will see Manually Declared DbCountRunner:
DbCountAutoConfiguration#dbCountRunner
- @ConditionalOnMissingBean (types: com.discoversdk.dbcount.DbCountRunner; SearchStrategy: all) found the following [dbCountRunner] (OnBeanCondition)
2015-04-05 INFO com.discoversdk.StudentApplication$1 : Manually Declared DbCountRunner
What just happened?
As we learned from the previous sample, Spring Boot will automatically process all the configuration class entries from spring.factories during the application context creation.
Without any extra guidance, everything that is annotated with an @Bean annotation will be used to create a Spring Bean. This functionality is actually a part of the plain old Spring framework Java Configuration.
To enhance this even further, Spring Boot adds an ability to conditionally control the rules for when certain @Configuration or @Bean annotations should be executed and when it is best to ignore them.
In our case, we used the @ConditionalOnMissingBean annotation to instruct Spring Boot to create our DbCountRunner bean only if there is no other bean matching either the class type or bean name already declared elsewhere. As we explicitly created an @Bean entry for DbCountRunner in the StudentApplication configuration, this took precedence and caused OnBeanCondition to detect the existence of the bean, thus instructing Spring Boot not to use DbCountAutoConfiguration during the application context setup.
Recent Stories
Top DiscoverSDK Experts
Compare Products
Select up to three two products to compare by clicking on the compare icon () of each product.
{{compareToolModel.Error}}
{{CommentsModel.TotalCount}} Comments
Your Comment