Building a Custom Spring Boot Starter

Spring Boot has become incredibly popular since it was first released in 2013. Starting from scratch; a developer can have an application communicating with a database through RESTful endpoints that have security configured for them all within the span of an hour. This was a huge improvement over the often days it would take to accomplish similar tasks using just the core Spring Framework.

Spring Boot itself has some core functionality, but much of what Spring Boot application will be doing; communicating with a database, providing REST endpoints, and processing security on incoming requests, are supplied through starters. The Spring team, and third party vendors that integrate with Spring, have created many publicly available starters which can be seen on start.spring.io.

These starters are great, but organizations have domain specific concerns. Organizations typically only use certain database vendors, have specific requirements when implementing security, among many other needs. Here it can be beneficial to create custom Spring Boot Starters that incorporate these constraints and requirements. In this article we will walk through the process of creating a custom Spring Boot starter.

Not Just Another Shared Library

Organizations creating shared libraries to handle cross-cutting concern like data access or security isn’t new. What is new is the Spring team has provided several extension points within Spring Boot that can be used to improve the developer experience, these shared libraries are called “starters”. Improving developer experience can increase the active use of a library which can help in making sure standards are followed across an organization.

Starter Components

There are several extensions points the Spring team has provided for building starters they are:

  • Auto-Configuration
  • Conditionals
  • Metadata for Configuration

We will step through how to use each of these features when building a starter so that it; requires minimal configuration to be used, is flexible for a variety of scenarios, and provides developers with information they need to configure it.

We will use security, my-org-starter-security, as an example for building the custom starter. Security is a common concern for organizations, it is also something that require some amount of configuration, and also might need to behave differently depending on context. This will give us a good opportunity to flex out all the above features in a semi-realistic scenario.

You can find the code example used in this article on my GitHub.

Auto-Configuration

Spring Boot is often described as being “opinionated”, one of the ways this is done is through auto-configuration. Spring components can be automatically (automagically?) loaded into context without requiring developer intervention. This is great for making your starter easier to consume and also used correctly (i.e. ensure required classes/beans are loaded). To have a component be auto-loaded requires following a few steps:

1. Add spring-boot-autoconfigure as a dependency

For auto-configuration to work, and to have the appropriate classes available, the spring-boot-autoconfigure dependency must be on classpath. The dependency can be added directly, or indirectly by including another starter as a dependency. In my example I am using spring-boot-starter as a dependency.


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.2.2.RELEASE</version>
<scope>compile</scope>
</dependency>

2. Create a Configuration class

Create a normal configuration class like you would within a Spring Boot application and have it perform whatever required behavior you need. In this example we will have the @Configuration class load a UserDetailsService and PasswordEncoder into the application context.


@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
public UserDetailsService users(PasswordEncoder encoder) {
UserBuilder users = User.builder().passwordEncoder(encoder::encode);
UserDetails admin = users.username("user").password("password").roles("USER").build();
UserDetails user = users.username("admin").password("password").roles("USER", "ADMIN").build();
return new InMemoryUserDetailsManager(user, admin);
}
}

Full class here.

3. Add spring.factories

Next we need to tell Spring Boot that the class ​SecurityConfig is a candidate for auto-configuration. To do this we need to create a file named spring.factories which needs to be located under the META-INF (typically this is located src/main/resources, like this). Within spring.factories we will add the following:


org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.my.developer.security.SecurityConfig

Now when a developer brings in the my-org-starter-security, the class SecurityConfig will be automatically loaded into the application context.

Conditionals

Auto-configuration is great, but there might be components you want to load only in a certain scenarios. In our hypothetical organization we will have both web based and console based applications. How security works in these scenarios would differ dramatically, however using conditionals we can have only the appropriate classes loaded into the application context depending on the scenario, this saves developers who are using our starter from a lot of headaches.

Spring Boot provides a lot of flexibility when it comes to defining conditionals, however in this example we will use a couple of pre-defined ones @ConditionalOnWebApplication and @ConditionalOnNotWebApplication. Let’s create a couple classes that will only be auto-configured conditionally.

1. Create configuration classes

Like above we will create a couple of standard @Configuration classes, however we will also need to annotate those classes with the appropriate annotations, @ConditionalOnNotWebApplication for NonWebSecurityConfig and @ConditionalOnWebApplication for WebSecurityConfig. The classes look like this:


@Configuration
@EnableConfigurationProperties({ CommandLineSecurityConfigurer.class })
@ConditionalOnNotWebApplication
public class NonWebSecurityConfig {
@Bean
public GrantedAuthority createGrantedAuthority(CommandLineSecurityConfigurer cliSecurityConfigurer) {
return new SimpleGrantedAuthority("ROLE_" + cliSecurityConfigurer.getRequiredRole());
}
}


@Configuration
@ConditionalOnWebApplication
@EnableConfigurationProperties({WebSecurityConfigurer.class})
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private WebSecurityConfigurer webSecurityConfigurer;
@Override
protected void configure(final HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers(webSecurityConfigurer.getAdminEndpoints()).hasRole("ADMIN")
.antMatchers(webSecurityConfigurer.getUserEndpoints()).hasRole("USER")
.antMatchers(webSecurityConfigurer.getUnsecuredEndpoints()).permitAll()
.antMatchers("/login*").permitAll()
.anyRequest().authenticated()
.and()
.formLogin();
}
}

Note: Conditional annotations can also be placed at the method level if needed.

2. Update spring.factories

The spring.factories file will need to be updated with these new classes to mark them as auto-configuration candidates.


org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.ibm.developer.security.SecurityConfig,\
com.ibm.developer.security.WebSecurityConfig,\
com.ibm.developer.security.NonWebSecurityConfig

3. Curate Your POM

I will cover this in-depth in a separate article, but a key element in building a good starter, particularly when using @Conditional, is setting up the POM for your starter correctly. Commonly Conditionals will be looking for the presence (or non-presence) of classes on classpath. In my starter POM I made use of optionals, which made consuming my starter easier for console applications:


<project xmlns="http://maven.apache.org/POM/4.0.0&quot;
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance&quot;
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"&gt;
<modelVersion>4.0.0</modelVersion>
<groupId>org.my.developer</groupId>
<artifactId>my-org-starter-security</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>my-org-starter-security</name>
<url>http://maven.apache.org</url&gt;
<properties>
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.target>1.8</maven.compiler.target>
<spring.boot.version>2.2.2.RELEASE</spring.boot.version>
<spring.framework.version>5.2.1.RELEASE</spring.framework.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>${spring.boot.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.framework.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${spring.framework.version}</version>
<scope>compile</scope>
</dependency>
<!– Optional Dependencies –>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring.boot.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>${spring.framework.version}</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<archive>
<addMavenDescriptor>false</addMavenDescriptor>
<manifest>
<addDefaultImplementationEntries>false</addDefaultImplementationEntries>
<addDefaultSpecificationEntries>false</addDefaultSpecificationEntries>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>

view raw

pom.xml

hosted with ❤ by GitHub

Metadata for Configuration

Inevitably some amount of configuration is needed. Remembering the exact name for a property or the correct values to supply it with can be difficult. To address this the Spring team provides a mechanism for defining metadata about configuration properties which can provide developers with which properties are available, information about those properties, and hints on valid values.

There are several options for supplying configuration metadata.

1. Add the spring-boot-configuration-processor:

Before you can do anything with metadata you will need to add the spring-boot-configuration-processor to your pom file for the metadata to be generated:


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>2.2.2.RELEASE</version>
<optional>true</optional>
</dependency>

2. Add META-INF/spring-configuration-metadata.json

Under the META-INF folder where we earlier added spring.factories you will need to create another file called spring-configuration-metadata.json which, as its name suggests, Spring will read to generate configuration meta data.

With the initial setup work done, there are several ways to provide configuration metadata. Below at two popular ways of doing this.

Defining metadata in spring-configuration-metadata

Metadata can be defined directly within the spring-configuration-metadata file. Here I am providing metadata on how to configure console security:


{
"groups": [
{
"name": "my-org.cli.security",
"type": "org.my.developer.security.CommandLineSecurityConfigurer"
},
{
"name": "my-org.web.security",
"type": "org.my.developer.security.WebSecurityConfigurer"
}
],
"properties": [
{
"name": "my-org.cli.security.required-role",
"type": "java.lang.String",
"description": "The role a user must have to run the application."
}
],
"hints": [
{
"name": "my-org.cli.security.required-role",
"values": [
{
"value": "USER",
"description": "Standard user role, should only have access to READ functions."
},
{
"value": "ADMIN",
"description": "Administrative user role, should only have access to READ and WRITE functions."
}
]
}
]
}

With this added, going into application.properties hints are provided saying that my-org.cli.security.required-role is a property, providing information on that property, and then with the hints field, valid values and their meaning can also be provided. This metadata makes configuring security for console applications much easier for developers.

Binding Properties to a Class

Within a @Configuration class I could just use a @Value to retrieve the value of my-org.cli.security.required-role. However I can also bind that property to a class as well, this is particularly helpful when dealing with several properties. To do this simply create a POJO and add a @ConfigurationProperties annotation to it. You will also need to give it a prefix that is the same as the groups value you defined in spring-configuration-metadata, so in this case my-org.cli.security.


@ConfigurationProperties(prefix="my-org.cli.security")
public class CommandLineSecurityConfigurer {
/**
* The role a user must have to run the application.
*/
private String requiredRole;
public String getRequiredRole() {
return requiredRole;
}
public void setRequiredRole(String requiredRole) {
this.requiredRole = requiredRole;
}
}

Supply Properties to a Configuration Class

If we want to retrieve the values out of CommandLineSecurityConfigurer we will need to annotate a @Configuration class with @EnableConfigurationProperties({ CommandLineSecurityConfigurer.class }), like I did in NonWebSecurityConfig.

Fields and Javadoc as Metadata

Class fields and Javadoc can also be used to provide metadata for properties. I created a second properties class, WebSecurityConfigurer. In this class I have three properties, userEndpoints, adminEndpoints, unsecuredEndpoints, each with Javadoc attached. Because I use the same group name for the prefix in @ConfigurationProperties as I supplied above in spring-configuration-metadata, Spring will process the fields and Javadoc and generate property metadata from it. As can be seen in the screen shot below the code. Like above, the values supplied in applications.properties will be bound to the fields.


@ConfigurationProperties(prefix = "my-org.web.security")
public class WebSecurityConfigurer {
/**
* Endpoints that require the USER role to access
*/
private String[] userEndpoints = new String[] {};
/**
* Endpoints that require the ADMIN role to access
*/
private String[] adminEndpoints = new String[] {};
/**
* Endpoints that have no security requirements.
*
* Default value: /public**
*/
private String[] unsecuredEndpoints = new String[] {"/public**"};
public String[] getUserEndpoints() {
return userEndpoints;
}
public void setUserEndpoints(String[] userEndpoints) {
this.userEndpoints = userEndpoints;
}
public String[] getAdminEndpoints() {
return adminEndpoints;
}
public void setAdminEndpoints(String[] adminEndpoints) {
this.adminEndpoints = adminEndpoints;
}
public String[] getUnsecuredEndpoints() {
return unsecuredEndpoints;
}
public void setUnsecuredEndpoints(String[] unsecuredEndpoints) {
this.unsecuredEndpoints = unsecuredEndpoints;
}
}

Screen Shot 2019-12-30 at 11.31.52 AM

Providing Sensible Configuration Defaults

Another way Spring Boot is described as being “opinionated” is through providing “sensible defaults”. A common way this is experienced is through the default port number Spring Boot uses of 8080. Providing default property values is super easy, as I have done above by initializing unsecuredEndpoints with "/public**". ​

Conclusion

Starters offer a lot of opportunities for organizations to improve the experience of their developers. This article only scrapes the surface of what is possible. Be sure to check out the provided links for more details on how to create starters as well as the official documentation:

Auto-configuration: https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-developing-auto-configuration

Providing metadata: https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-configuration-metadata.html

You can find the code example used in this article on my GitHub.

3 thoughts on “Building a Custom Spring Boot Starter

Leave a comment