Do you want your ad here?

Contact us to get your ad seen by thousands of users every day!

[email protected]

The Proper Way to Define Configuration Properties in Spring

  • January 07, 2025
  • 398 Unique Views
  • 4 min red
Table of Contents
IntroductionThe initial set-up in Spring Boot 2.Spring boot 3: Some of my properties are suddenly empty!The proper way to define your configuration propertiesRead more

Introduction

I recently did a (long overdue) migration from Spring Boot 2 to 3 on one of our larger applications.

Something that surprised me was that classes marked with @ConfigurationProperties had properties that were properly bound when running in Spring Boot 2 but were no longer bound after the upgrade.

Luckily, the automated testing suite caught this issue at run time, but it's obvious that such a thing silently failing can be quite problematic.

To showcase what went wrong and how to write proper classes holding your configuration, I've created following repository.

It's a maven multi-module project with three modules:

  • module-spring2 contains more or less the same set-up we had running in the application I migrated from SB 2.7.x
  • module-spring3-wrong running on SB 3 breaks at run time. Some properties were not bound...
  • module-spring3 is the 'proper' way - but more on that later!

The modules each contain the following:

app:
  url: 'foo'
  username: 'bar'
  required: true
  nested:
    foo: 'foonest'

And you can run the  DemoApp to see what the  ApplicationProperties contain at run time.

The initial set-up in Spring Boot 2.

Let's take a look at the configuration properties in the initial setup of module-spring2:

@Setter
@Getter
@ConfigurationProperties(prefix = "app")
@ToString
@RequiredArgsConstructor
public class ApplicationProperties {

        private String url;
        private String username;
        private boolean required;

        private final NestedApplicationProperties nested;
}

Nothing special here except maybe some lombok magic 🪄

Running the  DemoApp gives us what we expect, though:

ApplicationProperties(
 url=foo,
 username=bar,
 required=true,
 nested=NestedApplicationProperties(foo=foonest)
)

Spring boot 3: Some of my properties are suddenly empty!

Module-spring3-wrong contains the exact same setup we've just seen in module-spring2 but running the  DemoApp gives us:

ApplicationProperties(
url=null,
username=null,
required=false,
nested=NestedApplicationProperties(foo=foonest)
)

url and username now contains a  null value, while  required went from true to false... Not good!

Strangely, the  nested property is still filled in...

So, can you spot what changed from spring boot 2 to spring boot 3?

When delombok'ing the class, it turns out the configuration properties has a constructor like this:

private String url;
private String username;
private boolean required;
private final NestedApplicationProperties nested;
// Delombok'ed.
public ApplicationProperties(NestedApplicationProperties nested) {
    this.nested = nested;
}

That worked fine for Spring Boot 2 because the fields were all still bound via the setters.
But Spring Boot 3 strongly favours constructor binding, and if a single parameterized constructor is found, it will assume you want constructor binding.
(Take a look at the difference in the docs of spring boot 2 and spring boot 3 aswell as the Spring Boot 3 release notes for more information on this)

The ApplicationProperties in our example have a sole constructor which only contains the nested property, so the other properties are simply not bound or have default values.

Normally using Lombok sparsely isn't all that bad, but in this case the sole constructor we made was somewhat hidden by using  @RequiredArgsConstructor , obscuring the problem for me...

The easy solution I first saw was to just replace the  @RequiredArgsConstructor with a  @AllArgsConstructor or mark the fields final. Problem solved.
But while we're busy upgrading, you might as well do some code gardening and look for the more proper and maintainable solution instead of the easy one.

The proper way to define your configuration properties

Let's take a look at the refined ApplicationProperties in module-spring3:

/**
 * Some description.
 * (3)
 * @param url Must be filled in.
 * @param username Must be filled in.
 * @param required is it required?
 * @param nested nested props.
 */
@ConfigurationProperties(prefix = "app")
@Validated

// (1)
public record ApplicationProperties(
        @NotBlank String url, // (2)
        @NotBlank String username, // (2)
        boolean required,
        // (2) & (3)
        @Valid @NestedConfigurationProperty NestedApplicationProperties nested
) {
}

1. Simplify your code & get rid of lombok: use records instead of classes

Since configuration is bound at start-up time and should be immutable, it makes sense to refactor the class to a record.

By doing this, we can get rid of all the Lombok annotations we had before.

  • Getters, setters, and a toString() method are all provided by the record.
  • A record and all its components are also final, and an implicit canonical constructor will be created by the compiler. So no need for the @RequiredArgsConstructor and you won't need to remember to add the final keyword to the field.

2. Acquire 'start-up' time security: validate your configuration

We've added some bean validation to the configuration, too:

  •  @NotBlank on the url and username
  •  @Valid on the nested configuration properties to cascade the validation.

Now, when a property is missing for whatever reason, Spring will fail while wiring up its beans:

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

Description:

Binding to target com.example.ApplicationProperties failed:

    Property: app.username
    Value: "null"
    Reason: must not be blank

    Property: app.url
    Value: ""
    Origin: class path resource [application.yml] - 6:7
    Reason: must not be blank

Action:

Update your application's configuration

Note also that to cascade the validation to the nested properties, we had to add  @Valid , which is in line with what the Bean Validation specification lays out, but which spring boot did not follow up until recently.

To start using bean validation, just add the following dependency:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

3. Bonus: Document your configuration and let your IDE help you.

Spring boot has an annotation processor that can read your configuration at compile-time and generate a JSON file with meta-data describing your configuration.
This metadata is then stored under a /META-INF/spring-configuration-metadata.json as such:

{
  "groups": [
    {
      "name": "app",
      "type": "com.example.ApplicationProperties",
      "sourceType": "com.example.ApplicationProperties"
    },
    {
      "name": "app.nested",
      "type": "com.example.NestedApplicationProperties",
      "sourceType": "com.example.ApplicationProperties",
      "sourceMethod": "nested()"
    }
  ],
  "properties": [
    {
      "name": "app.nested.foo",
      "type": "java.lang.String",
      "sourceType": "com.example.NestedApplicationProperties"
    },
    {
      "name": "app.required",
      "type": "java.lang.Boolean",
      "description": "is it required?",
      "sourceType": "com.example.ApplicationProperties",
      "defaultValue": false
    },
    {
      "name": "app.url",
      "type": "java.lang.String",
      "description": "Must be filled in.",
      "sourceType": "com.example.ApplicationProperties"
    },
    {
      "name": "app.username",
      "type": "java.lang.String",
      "description": "Must be filled in.",
      "sourceType": "com.example.ApplicationProperties"
    }
  ],
  "hints": []
}

Note that we also used the annotation  @NestedConfigurationProperty in the revised example, which provides a hint to the annotation processor to view  com.example.NestedApplicationProperties as a nested type.

Now... the good thing is that your IDE probably supports reading out this JSON file and can give you neat things like:

  • autocompletion
  • error indication (the 'red squiggly' line)
  • 'click through' from the properties file to the Java class
  • descriptions (based upon Javadoc)
Intellij support for spring configuration metadata

Descriptions and autocompletion in IntelliJ.

To start using configuration processing, it's as simple as adding:

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

and enabling annotation processing in your favorite IDE.

Read more


This blog was originally published on my personal blog the 1st of January, 2025.

Do you want your ad here?

Contact us to get your ad seen by thousands of users every day!

[email protected]

Comments (0)

Highlight your code snippets using [code lang="language name"] shortcode. Just insert your code between opening and closing tag: [code lang="java"] code [/code]. Or specify another language.

No comments yet. Be the first.

Subscribe to foojay updates:

https://foojay.io/feed/
Copied to the clipboard