Skip to content
JackSparrow414
Go back

Creating a Custom Spring Boot Starter

Table of contents

Open Table of contents

Custom Spring Boot Auto-Configuration and Starters

Introduction

In the article on custom Spring Boot properties, we defined properties and configured them in application.yml. Custom properties are often used with auto-configuration. Here we will look at defining our own auto-configuration.

Scenarios

When would we write auto-configuration classes and custom Spring Boot starters?

A large proportion of application development is currently based on Spring Boot.

Official Documentation

Spring Boot provides detailed documentation and an example project for custom auto-configuration. Auto-configuration is generally packaged in a spring-boot-starter JAR for others to use.

Project Structure

Framework starters generally follow one of two approaches:

  1. Framework source and starter are separate. The starter depends on the framework, so users only need the starter dependency rather than adding the framework separately. Alibaba’s Druid data source uses this approach.
  2. Framework source and starter are not independent in this way. Automatic configuration in a Spring Boot application requires two dependencies: the main framework and its starter. MyBatis-Plus uses this approach.

Here we use the first approach: adding the starter makes all framework features available. The example framework is a small custom project named my-app.

The basic conventions for a standard starter structure are:

Frameworks generally have configurable properties. For custom properties:

Implementation

Adding Dependencies

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.mycompany.app</groupId>
  <artifactId>my-app-spring-boot-starter</artifactId>
  <version>1.0-SNAPSHOT</version>

  <name>my-app-spring-boot-starter</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <!-- Add the Spring Boot dependency inside dependencyManagement. For import usage, see the Maven article. -->
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>2.3.3.RELEASE</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
<!-- Dependency for auto-configuration classes; recommended by the official documentation. -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-autoconfigure</artifactId>
    </dependency>
<!-- Dependency for custom properties in application.yml. -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-configuration-processor</artifactId>
    </dependency>
<!-- Add the framework dependency. -->
    <dependency>
      <groupId>com.mycompany.app</groupId>
      <artifactId>my-app</artifactId>
      <version>1.0-SNAPSHOT</version>
    </dependency>
    <!-- Test dependencies. -->
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
      <plugins>
        <!-- Packaging plugin. -->
        <plugin>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-maven-plugin</artifactId>
            <version>2.3.3.RELEASE</version>
        </plugin>
      </plugins>
  </build>
</project>

For questions about spring-boot-dependencies in the POM above, see Using Maven.

Writing spring-configuration-metadata.json

Place the file under resources/META-INF.

{
  "groups": [
    {
      "name": "my-app",
      "type": "com.mycompany.app.autoconfigure.properties.MyAppProperties",
      "sourceType": "com.mycompany.app.autoconfigure.properties.MyAppProperties"
    }
  ],
  "properties": [
    {
      "name": "my-app.enabled",
      "type": "java.lang.Boolean",
      "defaultValue": false,
      "sourceType": "com.mycompany.app.autoconfigure.properties.MyAppProperties"
    },
    {
      "name": "my-app.info",
      "type": "java.lang.String",
      "sourceType": "com.mycompany.app.autoconfigure.properties.MyAppProperties"
    }
  ]
}

The custom property metadata mainly consists of groups and properties. For details, see the official documentation.

Creating a Java Class for Custom Property Binding

As mentioned, starter classes belong under autoconfigure. For classes other than auto-configuration classes, create subpackages. Here we create a properties subpackage and a property-binding class named MyAppProperties, annotated with @ConfigurationProperties. prefix specifies the parent name of the properties, corresponding to the groups entry above.

/**
 * @author jacksparrow414
 * @date 2021/2/3
 */
@ConfigurationProperties(prefix = "my-app")
public class MyAppProperties {

    private Boolean enabled;

    private String info;

    public Boolean getEnabled() {
        return enabled;
    }

    public void setEnabled(final Boolean enabled) {
        this.enabled = enabled;
    }

    public String getInfo() {
        return info;
    }

    public void setInfo(final String info) {
        this.info = info;
    }
}

The fully qualified name of this class corresponds to sourceType in the JSON above.

Creating the Auto-Configuration Class

Spring Boot’s @ConditionalXXX annotations are commonly used in auto-configuration classes. See the conditional annotation documentation.

Goal

The goal is to configure automatically when the project includes my-app but the Spring container has no MyAppLog bean.

Java Class

@ConditionalOnProperty(prefix = "my-app", name = "enabled", havingValue = "true", matchIfMissing = false)
@ConditionalOnClass(MyAppLog.class)
@Configuration
@EnableConfigurationProperties(value = {MyAppProperties.class})
public class MyAppAutoConfiguration {

    @ConditionalOnMissingBean(MyAppLog.class)
    @Bean
    public MyAppLog myAppDefaultLog() {
        return new MyAppLogDefaultImpl();
    }
}

This auto-configuration takes effect when my-app.enabled is true.

Creating spring.factories

After creating the auto-configuration class, register it in spring.factories. The file format is:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.mycompany.app.autoconfigure.MyAppAutoConfiguration

The fully qualified name of Spring Boot’s auto-configuration class = the fully qualified name of the custom auto-configuration class. Separate multiple entries with commas. Spring Boot documentation registering auto-configuration classes through META-INF/spring.factories

Complete Project Structure

Custom starter project with auto-configuration classes, spring.factories, and test directories

Testing

Spring Boot provides ApplicationContextRunner for testing auto-configuration. See the detailed documentation.

The test class is:

public class MyAppAutoConfigurationTest {

    private final ApplicationContextRunner contextRunner = new ApplicationContextRunner();

    @Test
    public void assertMyAppAutoConfiguration() {
        contextRunner
            .withPropertyValues("my-app.enabled=true")
            .withUserConfiguration(MyAppAutoConfiguration.class)
            .run(context -> {
            context.getBean("myAppDefaultLog");
        });
    }
}

When testing our custom property conditions (@ConditionalOnProperty), use withPropertyValues to supply values. The test will not obtain them merely because they are set in the default application.yml.

For other special annotations such as @ConditionalOnMissingClass, Spring Boot also offers a corresponding testing method, withClassLoader. Spring Boot test using FilteredClassLoader to simulate a missing dependency class

Practical Usage

Package my-app-spring-boot-starter and use it in another project.

Adding the Dependency

        <dependency>
            <groupId>com.mycompany.app</groupId>-->
            <artifactId>my-app-spring-boot-starter</artifactId>-->
           <version>1.0-SNAPSHOT</version>-->
        </dependency>

Configuring Properties

Configure application.yml:

my-app:
  enabled: true
  info: "myAppTest"

Starting the Project

Add @EnableAutoConfiguration to the main application class. Newer Spring Boot versions do not require it separately because @SpringBootApplication already includes it.

Start the project. With no externally configured MyAppLog bean, my-app uses its default implementation and prints default Impl.

Overriding Defaults with Custom Configuration

my-app also supports extension: implement MyAppLog and configure it.

@Configuration
@AutoConfigureBefore(MyAppAutoConfiguration.class)
public class CustomMyAppConfiguration {

    @Bean
    public MyAppLog myCustomLog() {
        return new MyCustomLog();
    }
}

This configuration must take effect before auto-configuration. The custom implementation will then be used instead of the default.

Closing

This completes the entire flow: defining custom properties, using them to enable custom auto-configuration, testing it, and using it in an application.

Code Used in the Project

  1. my-app framework source download.
  2. my-app-spring-boot-starter source download.
  3. Usage example in an application.

Usage Notes

Download the source in order, then run the following command for each project in that order:

mvn install

This installs each source project’s dependency locally. Refresh the POMs that reference those dependencies afterward.


Share this post:

Previous Post
Learning and Understanding Maven in Depth
Next Post
Learning RabbitMQ and Spring AMQP (Part 5): Delayed Queues

Comments

Questions, corrections, and experiences are welcome. Sign in with GitHub to comment; both language versions share this discussion.

Comments are available on the live site only.